From 11b66c8bf76a296860d98aff72fed00d367b03ea Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 3 Feb 2024 13:46:05 +0100 Subject: [PATCH] Finish PUS service optimizations - Better naming for pool abstractions - Added last unittests for PUS helper services - Introduce new abstraction for PUS schedulers - `StoreAddr` is now a generic u64 - `spacepackets` points to 0.7.0 release --- satrs-book/src/events.md | 2 +- .../src/images/events/event_man_arch.png | Bin 0 -> 118589 bytes satrs-core/Cargo.toml | 4 +- satrs-core/src/error.rs | 49 -- satrs-core/src/lib.rs | 1 - satrs-core/src/pool.rs | 420 ++++++------ satrs-core/src/pus/event_man.rs | 2 +- satrs-core/src/pus/event_srv.rs | 243 +++++-- satrs-core/src/pus/mod.rs | 576 +++++++++++++--- satrs-core/src/pus/scheduler.rs | 635 +++++++++++------- satrs-core/src/pus/scheduler_srv.rs | 343 +++++++--- satrs-core/src/pus/test.rs | 326 +++++---- satrs-core/src/pus/verification.rs | 76 +-- satrs-core/src/tmtc/tm_helper.rs | 49 +- satrs-core/tests/pools.rs | 8 +- satrs-core/tests/pus_verification.rs | 11 +- satrs-example/pyclient/.gitignore | 1 + satrs-example/pyclient/common.py | 4 - satrs-example/pyclient/main.py | 70 +- satrs-example/pyclient/pus_tc.py | 91 ++- satrs-example/pyclient/requirements.txt | 2 +- satrs-example/src/main.rs | 67 +- satrs-example/src/pus/action.rs | 68 +- satrs-example/src/pus/event.rs | 6 +- satrs-example/src/pus/hk.rs | 75 ++- satrs-example/src/pus/mod.rs | 56 +- satrs-example/src/pus/scheduler.rs | 42 +- satrs-example/src/pus/test.rs | 30 +- satrs-example/src/tmtc.rs | 16 +- satrs-example/src/udp.rs | 4 +- 30 files changed, 2124 insertions(+), 1153 deletions(-) create mode 100644 satrs-book/src/images/events/event_man_arch.png delete mode 100644 satrs-core/src/error.rs diff --git a/satrs-book/src/events.md b/satrs-book/src/events.md index 95c5df2..01dbb59 100644 --- a/satrs-book/src/events.md +++ b/satrs-book/src/events.md @@ -13,4 +13,4 @@ event components recommended by this framework do not really need this service. The following images shows how the flow of events could look like in a system where components can generate events, and where other system components might be interested in those events: -![Event flow](../../images/events/event_man_arch.png) +![Event flow](images/events/event_man_arch.png) diff --git a/satrs-book/src/images/events/event_man_arch.png b/satrs-book/src/images/events/event_man_arch.png new file mode 100644 index 0000000000000000000000000000000000000000..6920a18c62b0399f01a7169a1cd7aed74b284ced GIT binary patch literal 118589 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 literal 0 HcmV?d00001 diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index d16f0a0..6730619 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -73,7 +73,7 @@ features = ["all"] optional = true [dependencies.spacepackets] -version = "0.7.0-beta.4" +version = "0.7.0" default-features = false # git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" # rev = "297cfad22637d3b07a1b27abe56d9a607b5b82a7" @@ -123,4 +123,4 @@ doc-images = [] [package.metadata.docs.rs] all-features = true -rustdoc-args = ["--cfg", "doc_cfg"] +rustdoc-args = ["--cfg", "doc_cfg", "--generate-link-to-definition"] diff --git a/satrs-core/src/error.rs b/satrs-core/src/error.rs deleted file mode 100644 index 2144839..0000000 --- a/satrs-core/src/error.rs +++ /dev/null @@ -1,49 +0,0 @@ -pub enum FsrcGroupIds { - Tmtc = 0, -} - -pub struct FsrcErrorRaw { - pub group_id: u8, - pub unique_id: u8, - pub group_name: &'static str, - pub info: &'static str, -} - -pub trait FsrcErrorHandler { - fn error(&mut self, e: FsrcErrorRaw); - fn error_with_one_param(&mut self, e: FsrcErrorRaw, _p1: u32) { - self.error(e); - } - fn error_with_two_params(&mut self, e: FsrcErrorRaw, _p1: u32, _p2: u32) { - self.error(e); - } -} - -impl FsrcErrorRaw { - pub const fn new( - group_id: u8, - unique_id: u8, - group_name: &'static str, - info: &'static str, - ) -> Self { - FsrcErrorRaw { - group_id, - unique_id, - group_name, - info, - } - } -} - -#[derive(Clone, Copy, Default)] -pub struct SimpleStdErrorHandler {} - -#[cfg(feature = "use_std")] -impl FsrcErrorHandler for SimpleStdErrorHandler { - fn error(&mut self, e: FsrcErrorRaw) { - println!( - "Received error from group {} with ID ({},{}): {}", - e.group_name, e.group_id, e.unique_id, e.info - ); - } -} diff --git a/satrs-core/src/lib.rs b/satrs-core/src/lib.rs index 061f728..7418693 100644 --- a/satrs-core/src/lib.rs +++ b/satrs-core/src/lib.rs @@ -24,7 +24,6 @@ extern crate std; #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] pub mod cfdp; pub mod encoding; -pub mod error; #[cfg(feature = "alloc")] #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] pub mod event_man; diff --git a/satrs-core/src/pool.rs b/satrs-core/src/pool.rs index 3e644d3..0111d18 100644 --- a/satrs-core/src/pool.rs +++ b/satrs-core/src/pool.rs @@ -1,23 +1,13 @@ -//! # Pool implementation providing pre-allocated sub-pools with fixed size memory blocks +//! # Pool implementation providing memory pools for packet storage. //! -//! This is a simple memory pool implementation which pre-allocates all sub-pools using a given pool -//! configuration. After the pre-allocation, no dynamic memory allocation will be performed -//! during run-time. This makes the implementation suitable for real-time applications and -//! embedded environments. The pool implementation will also track the size of the data stored -//! inside it. -//! -//! Transactions with the [pool][LocalPool] are done using a special [address][StoreAddr] type. -//! Adding any data to the pool will yield a store address. Modification and read operations are -//! done using a reference to a store address. Deletion will consume the store address. -//! -//! # Example +//! # Example for the [StaticMemoryPool] //! //! ``` -//! use satrs_core::pool::{LocalPool, PoolCfg, PoolProvider}; +//! use satrs_core::pool::{PoolProviderMemInPlace, StaticMemoryPool, StaticPoolConfig}; //! //! // 4 buckets of 4 bytes, 2 of 8 bytes and 1 of 16 bytes -//! let pool_cfg = PoolCfg::new(vec![(4, 4), (2, 8), (1, 16)]); -//! let mut local_pool = LocalPool::new(pool_cfg); +//! let pool_cfg = StaticPoolConfig::new(vec![(4, 4), (2, 8), (1, 16)]); +//! let mut local_pool = StaticMemoryPool::new(pool_cfg); //! let mut addr; //! { //! // Add new data to the pool @@ -77,22 +67,24 @@ #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] pub use alloc_mod::*; use core::fmt::{Display, Formatter}; +use delegate::delegate; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; #[cfg(feature = "std")] use std::error::Error; type NumBlocks = u16; +pub type StoreAddr = u64; /// Simple address type used for transactions with the local pool. #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct StoreAddr { +pub struct StaticPoolAddr { pub(crate) pool_idx: u16, pub(crate) packet_idx: NumBlocks, } -impl StoreAddr { +impl StaticPoolAddr { pub const INVALID_ADDR: u32 = 0xFFFFFFFF; pub fn raw(&self) -> u32 { @@ -100,7 +92,22 @@ impl StoreAddr { } } -impl Display for StoreAddr { +impl From for StoreAddr { + fn from(value: StaticPoolAddr) -> Self { + ((value.pool_idx as u64) << 16) | value.packet_idx as u64 + } +} + +impl From for StaticPoolAddr { + fn from(value: StoreAddr) -> Self { + Self { + pool_idx: ((value >> 16) & 0xff) as u16, + packet_idx: (value & 0xff) as u16, + } + } +} + +impl Display for StaticPoolAddr { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { write!( f, @@ -180,39 +187,158 @@ impl Error for StoreError { } } +/// Generic trait for pool providers where the data can be modified and read in-place. This +/// generally means that a shared pool structure has to be wrapped inside a lock structure. +pub trait PoolProviderMemInPlace { + /// Add new data to the pool. The provider should attempt to reserve a memory block with the + /// appropriate size and then copy the given data to the block. Yields a [StoreAddr] which can + /// be used to access the data stored in the pool + fn add(&mut self, data: &[u8]) -> Result; + + /// The provider should attempt to reserve a free memory block with the appropriate size and + /// then return a mutable reference to it. Yields a [StoreAddr] which can be used to access + /// the data stored in the pool + fn free_element(&mut self, len: usize) -> Result<(StoreAddr, &mut [u8]), StoreError>; + + /// Modify data added previously using a given [StoreAddr] by yielding a mutable reference + /// to it + fn modify(&mut self, addr: &StoreAddr) -> Result<&mut [u8], StoreError>; + + /// Read data by yielding a read-only reference given a [StoreAddr] + fn read(&self, addr: &StoreAddr) -> Result<&[u8], StoreError>; + + /// Delete data inside the pool given a [StoreAddr] + fn delete(&mut self, addr: StoreAddr) -> Result<(), StoreError>; + fn has_element_at(&self, addr: &StoreAddr) -> Result; + + /// Retrieve the length of the data at the given store address. + fn len_of_data(&self, addr: &StoreAddr) -> Result { + if !self.has_element_at(addr)? { + return Err(StoreError::DataDoesNotExist(*addr)); + } + Ok(self.read(addr)?.len()) + } +} + +pub trait PoolProviderMemInPlaceWithGuards: PoolProviderMemInPlace { + /// This function behaves like [PoolProviderMemInPlace::read], but consumes the provided address + /// and returns a RAII conformant guard object. + /// + /// Unless the guard [PoolRwGuard::release] method is called, the data for the + /// given address will be deleted automatically when the guard is dropped. + /// This can prevent memory leaks. Users can read the data and release the guard + /// if the data in the store is valid for further processing. If the data is faulty, no + /// manual deletion is necessary when returning from a processing function prematurely. + fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard; + + /// This function behaves like [PoolProviderMemInPlace::modify], but consumes the provided + /// address and returns a RAII conformant guard object. + /// + /// Unless the guard [PoolRwGuard::release] method is called, the data for the + /// given address will be deleted automatically when the guard is dropped. + /// This can prevent memory leaks. Users can read (and modify) the data and release the guard + /// if the data in the store is valid for further processing. If the data is faulty, no + /// manual deletion is necessary when returning from a processing function prematurely. + fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard; +} + +pub struct PoolGuard<'a, MemProvider: PoolProviderMemInPlace + ?Sized> { + pool: &'a mut MemProvider, + pub addr: StoreAddr, + no_deletion: bool, + deletion_failed_error: Option, +} + +/// This helper object +impl<'a, MemProvider: PoolProviderMemInPlace> PoolGuard<'a, MemProvider> { + pub fn new(pool: &'a mut MemProvider, addr: StoreAddr) -> Self { + Self { + pool, + addr, + no_deletion: false, + deletion_failed_error: None, + } + } + + pub fn read(&self) -> Result<&[u8], StoreError> { + self.pool.read(&self.addr) + } + + /// Releasing the pool guard will disable the automatic deletion of the data when the guard + /// is dropped. + pub fn release(&mut self) { + self.no_deletion = true; + } +} + +impl Drop for PoolGuard<'_, MemProvider> { + fn drop(&mut self) { + if !self.no_deletion { + if let Err(e) = self.pool.delete(self.addr) { + self.deletion_failed_error = Some(e); + } + } + } +} + +pub struct PoolRwGuard<'a, MemProvider: PoolProviderMemInPlace + ?Sized> { + guard: PoolGuard<'a, MemProvider>, +} + +impl<'a, MemProvider: PoolProviderMemInPlace> PoolRwGuard<'a, MemProvider> { + pub fn new(pool: &'a mut MemProvider, addr: StoreAddr) -> Self { + Self { + guard: PoolGuard::new(pool, addr), + } + } + + pub fn modify(&mut self) -> Result<&mut [u8], StoreError> { + self.guard.pool.modify(&self.guard.addr) + } + + delegate!( + to self.guard { + pub fn read(&self) -> Result<&[u8], StoreError>; + /// Releasing the pool guard will disable the automatic deletion of the data when the guard + /// is dropped. + pub fn release(&mut self); + } + ); +} + #[cfg(feature = "alloc")] mod alloc_mod { + use super::{ + PoolGuard, PoolProviderMemInPlace, PoolProviderMemInPlaceWithGuards, PoolRwGuard, + StaticPoolAddr, + }; use crate::pool::{NumBlocks, StoreAddr, StoreError, StoreIdError}; - use alloc::boxed::Box; use alloc::vec; use alloc::vec::Vec; - use delegate::delegate; #[cfg(feature = "std")] use std::sync::{Arc, RwLock}; #[cfg(feature = "std")] - pub type ShareablePoolProvider = Box; - #[cfg(feature = "std")] - pub type SharedPool = Arc>; + pub type SharedStaticMemoryPool = Arc>; type PoolSize = usize; const STORE_FREE: PoolSize = PoolSize::MAX; pub const POOL_MAX_SIZE: PoolSize = STORE_FREE - 1; - /// Configuration structure of the [local pool][LocalPool] + /// Configuration structure of the [static memory pool][StaticMemoryPool] /// /// # Parameters /// /// * `cfg`: Vector of tuples which represent a subpool. The first entry in the tuple specifies the /// number of memory blocks in the subpool, the second entry the size of the blocks #[derive(Clone)] - pub struct PoolCfg { + pub struct StaticPoolConfig { cfg: Vec<(NumBlocks, usize)>, } - impl PoolCfg { + impl StaticPoolConfig { pub fn new(cfg: Vec<(NumBlocks, usize)>) -> Self { - PoolCfg { cfg } + StaticPoolConfig { cfg } } pub fn cfg(&self) -> &Vec<(NumBlocks, usize)> { @@ -228,135 +354,30 @@ mod alloc_mod { } } - pub struct PoolGuard<'a> { - pool: &'a mut LocalPool, - pub addr: StoreAddr, - no_deletion: bool, - deletion_failed_error: Option, - } - - /// This helper object - impl<'a> PoolGuard<'a> { - pub fn new(pool: &'a mut LocalPool, addr: StoreAddr) -> Self { - Self { - pool, - addr, - no_deletion: false, - deletion_failed_error: None, - } - } - - pub fn read(&self) -> Result<&[u8], StoreError> { - self.pool.read(&self.addr) - } - - /// Releasing the pool guard will disable the automatic deletion of the data when the guard - /// is dropped. - pub fn release(&mut self) { - self.no_deletion = true; - } - } - - impl Drop for PoolGuard<'_> { - fn drop(&mut self) { - if !self.no_deletion { - if let Err(e) = self.pool.delete(self.addr) { - self.deletion_failed_error = Some(e); - } - } - } - } - - pub struct PoolRwGuard<'a> { - guard: PoolGuard<'a>, - } - - impl<'a> PoolRwGuard<'a> { - pub fn new(pool: &'a mut LocalPool, addr: StoreAddr) -> Self { - Self { - guard: PoolGuard::new(pool, addr), - } - } - - pub fn modify(&mut self) -> Result<&mut [u8], StoreError> { - self.guard.pool.modify(&self.guard.addr) - } - - delegate!( - to self.guard { - pub fn read(&self) -> Result<&[u8], StoreError>; - /// Releasing the pool guard will disable the automatic deletion of the data when the guard - /// is dropped. - pub fn release(&mut self); - } - ); - } - - pub trait PoolProvider { - /// Add new data to the pool. The provider should attempt to reserve a memory block with the - /// appropriate size and then copy the given data to the block. Yields a [StoreAddr] which can - /// be used to access the data stored in the pool - fn add(&mut self, data: &[u8]) -> Result; - - /// The provider should attempt to reserve a free memory block with the appropriate size and - /// then return a mutable reference to it. Yields a [StoreAddr] which can be used to access - /// the data stored in the pool - fn free_element(&mut self, len: usize) -> Result<(StoreAddr, &mut [u8]), StoreError>; - - /// Modify data added previously using a given [StoreAddr] by yielding a mutable reference - /// to it - fn modify(&mut self, addr: &StoreAddr) -> Result<&mut [u8], StoreError>; - - /// This function behaves like [Self::modify], but consumes the provided address and returns a - /// RAII conformant guard object. - /// - /// Unless the guard [PoolRwGuard::release] method is called, the data for the - /// given address will be deleted automatically when the guard is dropped. - /// This can prevent memory leaks. Users can read (and modify) the data and release the guard - /// if the data in the store is valid for further processing. If the data is faulty, no - /// manual deletion is necessary when returning from a processing function prematurely. - fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard; - - /// Read data by yielding a read-only reference given a [StoreAddr] - fn read(&self, addr: &StoreAddr) -> Result<&[u8], StoreError>; - - /// This function behaves like [Self::read], but consumes the provided address and returns a - /// RAII conformant guard object. - /// - /// Unless the guard [PoolRwGuard::release] method is called, the data for the - /// given address will be deleted automatically when the guard is dropped. - /// This can prevent memory leaks. Users can read the data and release the guard - /// if the data in the store is valid for further processing. If the data is faulty, no - /// manual deletion is necessary when returning from a processing function prematurely. - fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard; - - /// Delete data inside the pool given a [StoreAddr] - fn delete(&mut self, addr: StoreAddr) -> Result<(), StoreError>; - fn has_element_at(&self, addr: &StoreAddr) -> Result; - - /// Retrieve the length of the data at the given store address. - fn len_of_data(&self, addr: &StoreAddr) -> Result { - if !self.has_element_at(addr)? { - return Err(StoreError::DataDoesNotExist(*addr)); - } - Ok(self.read(addr)?.len()) - } - } - - /// Pool implementation providing sub-pools with fixed size memory blocks. More details in - /// the [module documentation][crate::pool] - pub struct LocalPool { - pool_cfg: PoolCfg, + /// Pool implementation providing sub-pools with fixed size memory blocks. + /// + /// This is a simple memory pool implementation which pre-allocates all sub-pools using a given pool + /// configuration. After the pre-allocation, no dynamic memory allocation will be performed + /// during run-time. This makes the implementation suitable for real-time applications and + /// embedded environments. The pool implementation will also track the size of the data stored + /// inside it. + /// + /// Transactions with the [pool][StaticMemoryPool] are done using a generic + /// [address][StoreAddr] type. + /// Adding any data to the pool will yield a store address. Modification and read operations are + /// done using a reference to a store address. Deletion will consume the store address. + pub struct StaticMemoryPool { + pool_cfg: StaticPoolConfig, pool: Vec>, sizes_lists: Vec>, } - impl LocalPool { - /// Create a new local pool from the [given configuration][PoolCfg]. This function will sanitize - /// the given configuration as well. - pub fn new(mut cfg: PoolCfg) -> LocalPool { + impl StaticMemoryPool { + /// Create a new local pool from the [given configuration][StaticPoolConfig]. This function + /// will sanitize the given configuration as well. + pub fn new(mut cfg: StaticPoolConfig) -> StaticMemoryPool { let subpools_num = cfg.sanitize(); - let mut local_pool = LocalPool { + let mut local_pool = StaticMemoryPool { pool_cfg: cfg, pool: Vec::with_capacity(subpools_num), sizes_lists: Vec::with_capacity(subpools_num), @@ -372,39 +393,39 @@ mod alloc_mod { local_pool } - fn addr_check(&self, addr: &StoreAddr) -> Result { + fn addr_check(&self, addr: &StaticPoolAddr) -> Result { self.validate_addr(addr)?; let pool_idx = addr.pool_idx as usize; let size_list = self.sizes_lists.get(pool_idx).unwrap(); let curr_size = size_list[addr.packet_idx as usize]; if curr_size == STORE_FREE { - return Err(StoreError::DataDoesNotExist(*addr)); + return Err(StoreError::DataDoesNotExist(StoreAddr::from(*addr))); } Ok(curr_size) } - fn validate_addr(&self, addr: &StoreAddr) -> Result<(), StoreError> { + fn validate_addr(&self, addr: &StaticPoolAddr) -> Result<(), StoreError> { let pool_idx = addr.pool_idx as usize; if pool_idx >= self.pool_cfg.cfg.len() { return Err(StoreError::InvalidStoreId( StoreIdError::InvalidSubpool(addr.pool_idx), - Some(*addr), + Some(StoreAddr::from(*addr)), )); } if addr.packet_idx >= self.pool_cfg.cfg[addr.pool_idx as usize].0 { return Err(StoreError::InvalidStoreId( StoreIdError::InvalidPacketIdx(addr.packet_idx), - Some(*addr), + Some(StoreAddr::from(*addr)), )); } Ok(()) } - fn reserve(&mut self, data_len: usize) -> Result { + fn reserve(&mut self, data_len: usize) -> Result { let subpool_idx = self.find_subpool(data_len, 0)?; let (slot, size_slot_ref) = self.find_empty(subpool_idx)?; *size_slot_ref = data_len; - Ok(StoreAddr { + Ok(StaticPoolAddr { pool_idx: subpool_idx, packet_idx: slot, }) @@ -422,7 +443,7 @@ mod alloc_mod { Err(StoreError::DataTooLarge(req_size)) } - fn write(&mut self, addr: &StoreAddr, data: &[u8]) -> Result<(), StoreError> { + fn write(&mut self, addr: &StaticPoolAddr, data: &[u8]) -> Result<(), StoreError> { let packet_pos = self.raw_pos(addr).ok_or(StoreError::InternalError(0))?; let subpool = self .pool @@ -449,13 +470,13 @@ mod alloc_mod { Err(StoreError::StoreFull(subpool)) } - fn raw_pos(&self, addr: &StoreAddr) -> Option { + fn raw_pos(&self, addr: &StaticPoolAddr) -> Option { let (_, size) = self.pool_cfg.cfg.get(addr.pool_idx as usize)?; Some(addr.packet_idx as usize * size) } } - impl PoolProvider for LocalPool { + impl PoolProviderMemInPlace for StaticMemoryPool { fn add(&mut self, data: &[u8]) -> Result { let data_len = data.len(); if data_len > POOL_MAX_SIZE { @@ -463,7 +484,7 @@ mod alloc_mod { } let addr = self.reserve(data_len)?; self.write(&addr, data)?; - Ok(addr) + Ok(addr.into()) } fn free_element(&mut self, len: usize) -> Result<(StoreAddr, &mut [u8]), StoreError> { @@ -474,34 +495,29 @@ mod alloc_mod { let raw_pos = self.raw_pos(&addr).unwrap(); let block = &mut self.pool.get_mut(addr.pool_idx as usize).unwrap()[raw_pos..raw_pos + len]; - Ok((addr, block)) + Ok((addr.into(), block)) } fn modify(&mut self, addr: &StoreAddr) -> Result<&mut [u8], StoreError> { - let curr_size = self.addr_check(addr)?; - let raw_pos = self.raw_pos(addr).unwrap(); + let addr = StaticPoolAddr::from(*addr); + let curr_size = self.addr_check(&addr)?; + let raw_pos = self.raw_pos(&addr).unwrap(); let block = &mut self.pool.get_mut(addr.pool_idx as usize).unwrap() [raw_pos..raw_pos + curr_size]; Ok(block) } - fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard { - PoolRwGuard::new(self, addr) - } - fn read(&self, addr: &StoreAddr) -> Result<&[u8], StoreError> { - let curr_size = self.addr_check(addr)?; - let raw_pos = self.raw_pos(addr).unwrap(); + let addr = StaticPoolAddr::from(*addr); + let curr_size = self.addr_check(&addr)?; + let raw_pos = self.raw_pos(&addr).unwrap(); let block = &self.pool.get(addr.pool_idx as usize).unwrap()[raw_pos..raw_pos + curr_size]; Ok(block) } - fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard { - PoolGuard::new(self, addr) - } - fn delete(&mut self, addr: StoreAddr) -> Result<(), StoreError> { + let addr = StaticPoolAddr::from(addr); self.addr_check(&addr)?; let block_size = self.pool_cfg.cfg.get(addr.pool_idx as usize).unwrap().1; let raw_pos = self.raw_pos(&addr).unwrap(); @@ -514,7 +530,8 @@ mod alloc_mod { } fn has_element_at(&self, addr: &StoreAddr) -> Result { - self.validate_addr(addr)?; + let addr = StaticPoolAddr::from(*addr); + self.validate_addr(&addr)?; let pool_idx = addr.pool_idx as usize; let size_list = self.sizes_lists.get(pool_idx).unwrap(); let curr_size = size_list[addr.packet_idx as usize]; @@ -524,34 +541,45 @@ mod alloc_mod { Ok(true) } } + + impl PoolProviderMemInPlaceWithGuards for StaticMemoryPool { + fn modify_with_guard(&mut self, addr: StoreAddr) -> PoolRwGuard { + PoolRwGuard::new(self, addr) + } + + fn read_with_guard(&mut self, addr: StoreAddr) -> PoolGuard { + PoolGuard::new(self, addr) + } + } } #[cfg(test)] mod tests { use crate::pool::{ - LocalPool, PoolCfg, PoolGuard, PoolProvider, PoolRwGuard, StoreAddr, StoreError, - StoreIdError, POOL_MAX_SIZE, + PoolGuard, PoolProviderMemInPlace, PoolProviderMemInPlaceWithGuards, PoolRwGuard, + StaticMemoryPool, StaticPoolAddr, StaticPoolConfig, StoreError, StoreIdError, + POOL_MAX_SIZE, }; use std::vec; - fn basic_small_pool() -> LocalPool { + fn basic_small_pool() -> StaticMemoryPool { // 4 buckets of 4 bytes, 2 of 8 bytes and 1 of 16 bytes - let pool_cfg = PoolCfg::new(vec![(4, 4), (2, 8), (1, 16)]); - LocalPool::new(pool_cfg) + let pool_cfg = StaticPoolConfig::new(vec![(4, 4), (2, 8), (1, 16)]); + StaticMemoryPool::new(pool_cfg) } #[test] fn test_cfg() { // Values where number of buckets is 0 or size is too large should be removed - let mut pool_cfg = PoolCfg::new(vec![(0, 0), (1, 0), (2, POOL_MAX_SIZE)]); + let mut pool_cfg = StaticPoolConfig::new(vec![(0, 0), (1, 0), (2, POOL_MAX_SIZE)]); pool_cfg.sanitize(); assert_eq!(*pool_cfg.cfg(), vec![(1, 0)]); // Entries should be ordered according to bucket size - pool_cfg = PoolCfg::new(vec![(16, 6), (32, 3), (8, 12)]); + pool_cfg = StaticPoolConfig::new(vec![(16, 6), (32, 3), (8, 12)]); pool_cfg.sanitize(); assert_eq!(*pool_cfg.cfg(), vec![(32, 3), (16, 6), (8, 12)]); // Unstable sort is used, so order of entries with same block length should not matter - pool_cfg = PoolCfg::new(vec![(12, 12), (14, 16), (10, 12)]); + pool_cfg = StaticPoolConfig::new(vec![(12, 12), (14, 16), (10, 12)]); pool_cfg.sanitize(); assert!( *pool_cfg.cfg() == vec![(12, 12), (10, 12), (14, 16)] @@ -600,10 +628,10 @@ mod tests { let (addr, buf_ref) = res.unwrap(); assert_eq!( addr, - StoreAddr { + u64::from(StaticPoolAddr { pool_idx: 2, packet_idx: 0 - } + }) ); assert_eq!(buf_ref.len(), 12); } @@ -655,10 +683,13 @@ mod tests { fn test_read_does_not_exist() { let local_pool = basic_small_pool(); // Try to access data which does not exist - let res = local_pool.read(&StoreAddr { - packet_idx: 0, - pool_idx: 0, - }); + let res = local_pool.read( + &StaticPoolAddr { + packet_idx: 0, + pool_idx: 0, + } + .into(), + ); assert!(res.is_err()); assert!(matches!( res.unwrap_err(), @@ -684,10 +715,11 @@ mod tests { #[test] fn test_invalid_pool_idx() { let local_pool = basic_small_pool(); - let addr = StoreAddr { + let addr = StaticPoolAddr { pool_idx: 3, packet_idx: 0, - }; + } + .into(); let res = local_pool.read(&addr); assert!(res.is_err()); let err = res.unwrap_err(); @@ -700,12 +732,12 @@ mod tests { #[test] fn test_invalid_packet_idx() { let local_pool = basic_small_pool(); - let addr = StoreAddr { + let addr = StaticPoolAddr { pool_idx: 2, packet_idx: 1, }; assert_eq!(addr.raw(), 0x00020001); - let res = local_pool.read(&addr); + let res = local_pool.read(&addr.into()); assert!(res.is_err()); let err = res.unwrap_err(); assert!(matches!( diff --git a/satrs-core/src/pus/event_man.rs b/satrs-core/src/pus/event_man.rs index 132e24c..d51135a 100644 --- a/satrs-core/src/pus/event_man.rs +++ b/satrs-core/src/pus/event_man.rs @@ -82,7 +82,7 @@ pub mod heapless_mod { } } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum EventRequest { Enable(Event), Disable(Event), diff --git a/satrs-core/src/pus/event_srv.rs b/satrs-core/src/pus/event_srv.rs index 5f423f0..49a080c 100644 --- a/satrs-core/src/pus/event_srv.rs +++ b/satrs-core/src/pus/event_srv.rs @@ -1,85 +1,64 @@ use crate::events::EventU32; -use crate::pool::{SharedPool, StoreAddr}; use crate::pus::event_man::{EventRequest, EventRequestWithToken}; -use crate::pus::verification::{ - StdVerifReporterWithSender, TcStateAccepted, TcStateToken, VerificationToken, -}; -use crate::pus::{ - EcssTcReceiver, EcssTmSender, PartialPusHandlingError, PusPacketHandlerResult, - PusPacketHandlingError, PusServiceBase, PusServiceHandler, -}; -use alloc::boxed::Box; +use crate::pus::verification::TcStateToken; +use crate::pus::{PartialPusHandlingError, PusPacketHandlerResult, PusPacketHandlingError}; use spacepackets::ecss::event::Subservice; -use spacepackets::ecss::tc::PusTcReader; use spacepackets::ecss::PusPacket; use std::sync::mpsc::Sender; -pub struct PusService5EventHandler { - psb: PusServiceBase, +use super::{EcssTcInMemConverter, PusServiceBase, PusServiceHelper}; + +pub struct PusService5EventHandler { + pub service_helper: PusServiceHelper, event_request_tx: Sender, } -impl PusService5EventHandler { +impl PusService5EventHandler { pub fn new( - tc_receiver: Box, - shared_tc_store: SharedPool, - tm_sender: Box, - tm_apid: u16, - verification_handler: StdVerifReporterWithSender, + service_handler: PusServiceHelper, event_request_tx: Sender, ) -> Self { Self { - psb: PusServiceBase::new( - tc_receiver, - shared_tc_store, - tm_sender, - tm_apid, - verification_handler, - ), + service_helper: service_handler, event_request_tx, } } -} -impl PusServiceHandler for PusService5EventHandler { - fn psb_mut(&mut self) -> &mut PusServiceBase { - &mut self.psb - } - fn psb(&self) -> &PusServiceBase { - &self.psb - } - - fn handle_one_tc( - &mut self, - addr: StoreAddr, - token: VerificationToken, - ) -> Result { - self.copy_tc_to_buf(addr)?; - let (tc, _) = PusTcReader::new(&self.psb.pus_buf)?; + pub fn handle_one_tc(&mut self) -> Result { + let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?; + if possible_packet.is_none() { + return Ok(PusPacketHandlerResult::Empty); + } + let ecss_tc_and_token = possible_packet.unwrap(); + let tc = self + .service_helper + .tc_in_mem_converter + .convert_ecss_tc_in_memory_to_reader(&ecss_tc_and_token.tc_in_memory)?; let subservice = tc.subservice(); let srv = Subservice::try_from(subservice); if srv.is_err() { return Ok(PusPacketHandlerResult::CustomSubservice( tc.subservice(), - token, + ecss_tc_and_token.token, )); } let handle_enable_disable_request = |enable: bool, stamp: [u8; 7]| { if tc.user_data().len() < 4 { return Err(PusPacketHandlingError::NotEnoughAppData( - "At least 4 bytes event ID expected".into(), + "at least 4 bytes event ID expected".into(), )); } let user_data = tc.user_data(); let event_u32 = EventU32::from(u32::from_be_bytes(user_data[0..4].try_into().unwrap())); let start_token = self - .psb + .service_helper + .common .verification_handler .borrow_mut() - .start_success(token, Some(&stamp)) + .start_success(ecss_tc_and_token.token, Some(&stamp)) .map_err(|_| PartialPusHandlingError::Verification); let partial_error = start_token.clone().err(); - let mut token: TcStateToken = token.into(); + let mut token: TcStateToken = ecss_tc_and_token.token.into(); if let Ok(start_token) = start_token { token = start_token.into(); } @@ -107,7 +86,7 @@ impl PusServiceHandler for PusService5EventHandler { Ok(PusPacketHandlerResult::RequestHandled) }; let mut partial_error = None; - let time_stamp = self.psb().get_current_timestamp(&mut partial_error); + let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error); match srv.unwrap() { Subservice::TmInfoReport | Subservice::TmLowSeverityReport @@ -123,7 +102,8 @@ impl PusServiceHandler for PusService5EventHandler { } Subservice::TcReportDisabledList | Subservice::TmDisabledEventsReport => { return Ok(PusPacketHandlerResult::SubserviceNotImplemented( - subservice, token, + subservice, + ecss_tc_and_token.token, )); } } @@ -131,3 +111,170 @@ impl PusServiceHandler for PusService5EventHandler { Ok(PusPacketHandlerResult::RequestHandled) } } + +#[cfg(test)] +mod tests { + use delegate::delegate; + use spacepackets::ecss::event::Subservice; + use spacepackets::util::UnsignedEnum; + use spacepackets::{ + ecss::{ + tc::{PusTcCreator, PusTcSecondaryHeader}, + tm::PusTmReader, + }, + SequenceFlags, SpHeader, + }; + use std::sync::mpsc::{self, Sender}; + + use crate::pus::event_man::EventRequest; + use crate::pus::tests::SimplePusPacketHandler; + use crate::pus::verification::RequestId; + use crate::{ + events::EventU32, + pus::{ + event_man::EventRequestWithToken, + tests::{PusServiceHandlerWithSharedStoreCommon, PusTestHarness, TEST_APID}, + verification::{TcStateAccepted, VerificationToken}, + EcssTcInSharedStoreConverter, PusPacketHandlerResult, PusPacketHandlingError, + }, + }; + + use super::PusService5EventHandler; + + const TEST_EVENT_0: EventU32 = EventU32::const_new(crate::events::Severity::INFO, 5, 25); + + struct Pus5HandlerWithStoreTester { + common: PusServiceHandlerWithSharedStoreCommon, + handler: PusService5EventHandler, + } + + impl Pus5HandlerWithStoreTester { + pub fn new(event_request_tx: Sender) -> Self { + let (common, srv_handler) = PusServiceHandlerWithSharedStoreCommon::new(); + Self { + common, + handler: PusService5EventHandler::new(srv_handler, event_request_tx), + } + } + } + + impl PusTestHarness for Pus5HandlerWithStoreTester { + delegate! { + to self.common { + fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken; + fn read_next_tm(&mut self) -> PusTmReader<'_>; + fn check_no_tm_available(&self) -> bool; + fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId); + } + + } + } + + impl SimplePusPacketHandler for Pus5HandlerWithStoreTester { + delegate! { + to self.handler { + fn handle_one_tc(&mut self) -> Result; + } + } + } + + fn event_test( + test_harness: &mut (impl PusTestHarness + SimplePusPacketHandler), + subservice: Subservice, + expected_event_req: EventRequest, + event_req_receiver: mpsc::Receiver, + ) { + let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap(); + let sec_header = PusTcSecondaryHeader::new_simple(5, subservice as u8); + let mut app_data = [0; 4]; + TEST_EVENT_0 + .write_to_be_bytes(&mut app_data) + .expect("writing test event failed"); + let ping_tc = PusTcCreator::new(&mut sp_header, sec_header, &app_data, true); + let token = test_harness.send_tc(&ping_tc); + let request_id = token.req_id(); + test_harness.handle_one_tc().unwrap(); + test_harness.check_next_verification_tm(1, request_id); + test_harness.check_next_verification_tm(3, request_id); + // Completion TM is not generated for us. + assert!(test_harness.check_no_tm_available()); + let event_request = event_req_receiver + .try_recv() + .expect("no event request received"); + assert_eq!(expected_event_req, event_request.request); + } + + #[test] + fn test_enabling_event_reporting() { + let (event_request_tx, event_request_rx) = mpsc::channel(); + let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx); + event_test( + &mut test_harness, + Subservice::TcEnableEventGeneration, + EventRequest::Enable(TEST_EVENT_0), + event_request_rx, + ); + } + + #[test] + fn test_disabling_event_reporting() { + let (event_request_tx, event_request_rx) = mpsc::channel(); + let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx); + event_test( + &mut test_harness, + Subservice::TcDisableEventGeneration, + EventRequest::Disable(TEST_EVENT_0), + event_request_rx, + ); + } + + #[test] + fn test_empty_tc_queue() { + let (event_request_tx, _) = mpsc::channel(); + let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx); + let result = test_harness.handle_one_tc(); + assert!(result.is_ok()); + let result = result.unwrap(); + if let PusPacketHandlerResult::Empty = result { + } else { + panic!("unexpected result type {result:?}") + } + } + + #[test] + fn test_sending_custom_subservice() { + let (event_request_tx, _) = mpsc::channel(); + let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx); + let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap(); + let sec_header = PusTcSecondaryHeader::new_simple(5, 200); + let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true); + test_harness.send_tc(&ping_tc); + let result = test_harness.handle_one_tc(); + assert!(result.is_ok()); + let result = result.unwrap(); + if let PusPacketHandlerResult::CustomSubservice(subservice, _) = result { + assert_eq!(subservice, 200); + } else { + panic!("unexpected result type {result:?}") + } + } + + #[test] + fn test_sending_invalid_app_data() { + let (event_request_tx, _) = mpsc::channel(); + let mut test_harness = Pus5HandlerWithStoreTester::new(event_request_tx); + let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap(); + let sec_header = + PusTcSecondaryHeader::new_simple(5, Subservice::TcEnableEventGeneration as u8); + let ping_tc = PusTcCreator::new(&mut sp_header, sec_header, &[0, 1, 2], true); + test_harness.send_tc(&ping_tc); + let result = test_harness.handle_one_tc(); + assert!(result.is_err()); + let result = result.unwrap_err(); + if let PusPacketHandlingError::NotEnoughAppData(string) = result { + assert_eq!(string, "at least 4 bytes event ID expected"); + } else { + panic!("unexpected result type {result:?}") + } + } +} diff --git a/satrs-core/src/pus/mod.rs b/satrs-core/src/pus/mod.rs index cc05a92..7c721ee 100644 --- a/satrs-core/src/pus/mod.rs +++ b/satrs-core/src/pus/mod.rs @@ -55,12 +55,6 @@ impl<'tm> From> for PusTmWrapper<'tm> { } } -pub type TcAddrWithToken = (StoreAddr, TcStateToken); - -/// Generic abstraction for a telecommand being sent around after is has been accepted. -/// The actual telecommand is stored inside a pre-allocated pool structure. -pub type AcceptedTc = (StoreAddr, VerificationToken); - /// Generic error type for sending something via a message queue. #[derive(Debug, Copy, Clone)] pub enum GenericSendError { @@ -200,11 +194,75 @@ pub trait EcssTcSenderCore: EcssChannel { fn send_tc(&self, tc: PusTcCreator, token: Option) -> Result<(), EcssTmtcError>; } -pub struct ReceivedTcWrapper { - pub store_addr: StoreAddr, +/// A PUS telecommand packet can be stored in memory using different methods. Right now, +/// storage inside a pool structure like [crate::pool::StaticMemoryPool], and storage inside a +/// `Vec` are supported. +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TcInMemory { + StoreAddr(StoreAddr), + #[cfg(feature = "alloc")] + Vec(alloc::vec::Vec), +} + +impl From for TcInMemory { + fn from(value: StoreAddr) -> Self { + Self::StoreAddr(value) + } +} + +#[cfg(feature = "alloc")] +impl From> for TcInMemory { + fn from(value: alloc::vec::Vec) -> Self { + Self::Vec(value) + } +} + +/// Generic structure for an ECSS PUS Telecommand and its correspoding verification token. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EcssTcAndToken { + pub tc_in_memory: TcInMemory, pub token: Option, } +impl EcssTcAndToken { + pub fn new(tc_in_memory: impl Into, token: impl Into) -> Self { + Self { + tc_in_memory: tc_in_memory.into(), + token: Some(token.into()), + } + } +} + +/// Generic abstraction for a telecommand being sent around after is has been accepted. +pub struct AcceptedEcssTcAndToken { + pub tc_in_memory: TcInMemory, + pub token: VerificationToken, +} + +impl From for EcssTcAndToken { + fn from(value: AcceptedEcssTcAndToken) -> Self { + EcssTcAndToken { + tc_in_memory: value.tc_in_memory, + token: Some(value.token.into()), + } + } +} + +impl TryFrom for AcceptedEcssTcAndToken { + type Error = (); + + fn try_from(value: EcssTcAndToken) -> Result { + if let Some(TcStateToken::Accepted(token)) = value.token { + return Ok(AcceptedEcssTcAndToken { + tc_in_memory: value.tc_in_memory, + token, + }); + } + Err(()) + } +} + #[derive(Debug, Clone)] pub enum TryRecvTmtcError { Error(EcssTmtcError), @@ -231,7 +289,7 @@ impl From for TryRecvTmtcError { /// Generic trait for a user supplied receiver object. pub trait EcssTcReceiverCore: EcssChannel { - fn recv_tc(&self) -> Result; + fn recv_tc(&self) -> Result; } /// Generic trait for objects which can receive ECSS PUS telecommands. This trait is @@ -309,21 +367,23 @@ mod alloc_mod { } #[cfg(feature = "std")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] pub mod std_mod { - use crate::pool::{SharedPool, StoreAddr}; + use crate::pool::{PoolProviderMemInPlaceWithGuards, SharedStaticMemoryPool, StoreAddr}; use crate::pus::verification::{ StdVerifReporterWithSender, TcStateAccepted, VerificationToken, }; use crate::pus::{ - EcssChannel, EcssTcReceiver, EcssTcReceiverCore, EcssTmSender, EcssTmSenderCore, - EcssTmtcError, GenericRecvError, GenericSendError, PusTmWrapper, ReceivedTcWrapper, - TcAddrWithToken, TryRecvTmtcError, + EcssChannel, EcssTcAndToken, EcssTcReceiver, EcssTcReceiverCore, EcssTmSender, + EcssTmSenderCore, EcssTmtcError, GenericRecvError, GenericSendError, PusTmWrapper, + TryRecvTmtcError, }; use crate::tmtc::tm_helper::SharedTmStore; use crate::ChannelId; use alloc::boxed::Box; use alloc::vec::Vec; use crossbeam_channel as cb; + use spacepackets::ecss::tc::PusTcReader; use spacepackets::ecss::tm::PusTmCreator; use spacepackets::ecss::PusError; use spacepackets::time::cds::TimeProvider; @@ -335,6 +395,9 @@ pub mod std_mod { use std::sync::mpsc::TryRecvError; use thiserror::Error; + use super::verification::VerificationReporterWithSender; + use super::{AcceptedEcssTcAndToken, TcInMemory}; + impl From> for EcssTmtcError { fn from(_: mpsc::SendError) -> Self { Self::Send(GenericSendError::RxDisconnected) @@ -411,13 +474,13 @@ pub mod std_mod { } } - pub struct MpscTcInStoreReceiver { + pub struct MpscTcReceiver { id: ChannelId, name: &'static str, - receiver: mpsc::Receiver, + receiver: mpsc::Receiver, } - impl EcssChannel for MpscTcInStoreReceiver { + impl EcssChannel for MpscTcReceiver { fn id(&self) -> ChannelId { self.id } @@ -427,26 +490,22 @@ pub mod std_mod { } } - impl EcssTcReceiverCore for MpscTcInStoreReceiver { - fn recv_tc(&self) -> Result { - let (store_addr, token) = self.receiver.try_recv().map_err(|e| match e { + impl EcssTcReceiverCore for MpscTcReceiver { + fn recv_tc(&self) -> Result { + self.receiver.try_recv().map_err(|e| match e { TryRecvError::Empty => TryRecvTmtcError::Empty, TryRecvError::Disconnected => { TryRecvTmtcError::Error(EcssTmtcError::from(GenericRecvError::TxDisconnected)) } - })?; - Ok(ReceivedTcWrapper { - store_addr, - token: Some(token), }) } } - impl MpscTcInStoreReceiver { + impl MpscTcReceiver { pub fn new( id: ChannelId, name: &'static str, - receiver: mpsc::Receiver, + receiver: mpsc::Receiver, ) -> Self { Self { id, name, receiver } } @@ -459,8 +518,8 @@ pub mod std_mod { #[derive(Clone)] pub struct MpscTmAsVecSender { id: ChannelId, - sender: mpsc::Sender>, name: &'static str, + sender: mpsc::Sender>, } impl From>> for EcssTmtcError { @@ -545,23 +604,23 @@ pub mod std_mod { } } - pub struct CrossbeamTcInStoreReceiver { + pub struct CrossbeamTcReceiver { id: ChannelId, name: &'static str, - receiver: cb::Receiver, + receiver: cb::Receiver, } - impl CrossbeamTcInStoreReceiver { + impl CrossbeamTcReceiver { pub fn new( id: ChannelId, name: &'static str, - receiver: cb::Receiver, + receiver: cb::Receiver, ) -> Self { Self { id, name, receiver } } } - impl EcssChannel for CrossbeamTcInStoreReceiver { + impl EcssChannel for CrossbeamTcReceiver { fn id(&self) -> ChannelId { self.id } @@ -571,17 +630,13 @@ pub mod std_mod { } } - impl EcssTcReceiverCore for CrossbeamTcInStoreReceiver { - fn recv_tc(&self) -> Result { - let (store_addr, token) = self.receiver.try_recv().map_err(|e| match e { + impl EcssTcReceiverCore for CrossbeamTcReceiver { + fn recv_tc(&self) -> Result { + self.receiver.try_recv().map_err(|e| match e { cb::TryRecvError::Empty => TryRecvTmtcError::Empty, cb::TryRecvError::Disconnected => { TryRecvTmtcError::Error(EcssTmtcError::from(GenericRecvError::TxDisconnected)) } - })?; - Ok(ReceivedTcWrapper { - store_addr, - token: Some(token), }) } } @@ -596,8 +651,12 @@ pub mod std_mod { InvalidSubservice(u8), #[error("not enough application data available: {0}")] NotEnoughAppData(String), + #[error("PUS packet too large, does not fit in buffer: {0}")] + PusPacketTooLarge(usize), #[error("invalid application data")] InvalidAppData(String), + #[error("invalid format of TC in memory: {0:?}")] + InvalidTcInMemoryFormat(TcInMemory), #[error("generic ECSS tmtc error: {0}")] EcssTmtc(#[from] EcssTmtcError), #[error("invalid verification token")] @@ -634,42 +693,126 @@ pub mod std_mod { } } - /// Base class for handlers which can handle PUS TC packets. Right now, the verification - /// reporter is constrained to the [StdVerifReporterWithSender] and the service handler - /// relies on TMTC packets being exchanged via a [SharedPool]. + pub trait EcssTcInMemConverter { + fn cache_ecss_tc_in_memory( + &mut self, + possible_packet: &TcInMemory, + ) -> Result<(), PusPacketHandlingError>; + + fn tc_slice_raw(&self) -> &[u8]; + + fn convert_ecss_tc_in_memory_to_reader( + &mut self, + possible_packet: &TcInMemory, + ) -> Result, PusPacketHandlingError> { + self.cache_ecss_tc_in_memory(possible_packet)?; + Ok(PusTcReader::new(self.tc_slice_raw())?.0) + } + } + + /// Converter structure for PUS telecommands which are stored inside a `Vec` structure. + /// Please note that this structure is not able to convert TCs which are stored inside a + /// [SharedStaticMemoryPool]. + #[derive(Default, Clone)] + pub struct EcssTcInVecConverter { + pub pus_tc_raw: Option>, + } + + impl EcssTcInMemConverter for EcssTcInVecConverter { + fn cache_ecss_tc_in_memory( + &mut self, + tc_in_memory: &TcInMemory, + ) -> Result<(), PusPacketHandlingError> { + self.pus_tc_raw = None; + match tc_in_memory { + super::TcInMemory::StoreAddr(_) => { + return Err(PusPacketHandlingError::InvalidTcInMemoryFormat( + tc_in_memory.clone(), + )); + } + super::TcInMemory::Vec(vec) => { + self.pus_tc_raw = Some(vec.clone()); + } + }; + Ok(()) + } + + fn tc_slice_raw(&self) -> &[u8] { + if self.pus_tc_raw.is_none() { + return &[]; + } + self.pus_tc_raw.as_ref().unwrap() + } + } + + /// Converter structure for PUS telecommands which are stored inside + /// [SharedStaticMemoryPool] structure. This is useful if run-time allocation for these + /// packets should be avoided. Please note that this structure is not able to convert TCs which + /// are stored as a `Vec`. + pub struct EcssTcInSharedStoreConverter { + shared_tc_store: SharedStaticMemoryPool, + pus_buf: Vec, + } + + impl EcssTcInSharedStoreConverter { + pub fn new(shared_tc_store: SharedStaticMemoryPool, max_expected_tc_size: usize) -> Self { + Self { + shared_tc_store, + pus_buf: alloc::vec![0; max_expected_tc_size], + } + } + + pub fn copy_tc_to_buf(&mut self, addr: StoreAddr) -> Result<(), PusPacketHandlingError> { + // Keep locked section as short as possible. + let mut tc_pool = self + .shared_tc_store + .write() + .map_err(|_| PusPacketHandlingError::EcssTmtc(EcssTmtcError::StoreLock))?; + let tc_guard = tc_pool.read_with_guard(addr); + let tc_raw = tc_guard.read().unwrap(); + if tc_raw.len() > self.pus_buf.len() { + return Err(PusPacketHandlingError::PusPacketTooLarge(tc_raw.len())); + } + self.pus_buf[0..tc_raw.len()].copy_from_slice(tc_raw); + Ok(()) + } + } + + impl EcssTcInMemConverter for EcssTcInSharedStoreConverter { + fn cache_ecss_tc_in_memory( + &mut self, + tc_in_memory: &TcInMemory, + ) -> Result<(), PusPacketHandlingError> { + match tc_in_memory { + super::TcInMemory::StoreAddr(addr) => { + self.copy_tc_to_buf(*addr)?; + } + super::TcInMemory::Vec(_) => { + return Err(PusPacketHandlingError::InvalidTcInMemoryFormat( + tc_in_memory.clone(), + )); + } + }; + Ok(()) + } + + fn tc_slice_raw(&self) -> &[u8] { + self.pus_buf.as_ref() + } + } + pub struct PusServiceBase { pub tc_receiver: Box, - pub shared_tc_store: SharedPool, pub tm_sender: Box, pub tm_apid: u16, /// The verification handler is wrapped in a [RefCell] to allow the interior mutability /// pattern. This makes writing methods which are not mutable a lot easier. pub verification_handler: RefCell, - pub pus_buf: [u8; 2048], - pub pus_size: usize, } impl PusServiceBase { - pub fn new( - tc_receiver: Box, - shared_tc_store: SharedPool, - tm_sender: Box, - tm_apid: u16, - verification_handler: StdVerifReporterWithSender, - ) -> Self { - Self { - tc_receiver, - shared_tc_store, - tm_apid, - tm_sender, - verification_handler: RefCell::new(verification_handler), - pus_buf: [0; 2048], - pus_size: 0, - } - } - + #[cfg(feature = "std")] pub fn get_current_timestamp( - &self, partial_error: &mut Option, ) -> [u8; 7] { let mut time_stamp: [u8; 7] = [0; 7]; @@ -684,48 +827,73 @@ pub mod std_mod { time_stamp } - pub fn get_current_timestamp_ignore_error(&self) -> [u8; 7] { + #[cfg(feature = "std")] + pub fn get_current_timestamp_ignore_error() -> [u8; 7] { let mut dummy = None; - self.get_current_timestamp(&mut dummy) + Self::get_current_timestamp(&mut dummy) } } - pub trait PusServiceHandler { - fn psb_mut(&mut self) -> &mut PusServiceBase; - fn psb(&self) -> &PusServiceBase; - fn handle_one_tc( - &mut self, - addr: StoreAddr, - token: VerificationToken, - ) -> Result; + /// This is a high-level PUS packet handler helper. + /// + /// It performs some of the boilerplate acitivities involved when handling PUS telecommands and + /// it can be used to implement the handling of PUS telecommands for certain PUS telecommands + /// groups (for example individual services). + /// + /// This base class can handle PUS telecommands backed by different memory storage machanisms + /// by using the [EcssTcInMemConverter] abstraction. This object provides some convenience + /// methods to make the generic parts of TC handling easier. + pub struct PusServiceHelper { + pub common: PusServiceBase, + pub tc_in_mem_converter: TcInMemConverter, + } - fn copy_tc_to_buf(&mut self, addr: StoreAddr) -> Result<(), PusPacketHandlingError> { - // Keep locked section as short as possible. - let psb_mut = self.psb_mut(); - let mut tc_pool = psb_mut - .shared_tc_store - .write() - .map_err(|_| PusPacketHandlingError::EcssTmtc(EcssTmtcError::StoreLock))?; - let tc_guard = tc_pool.read_with_guard(addr); - let tc_raw = tc_guard.read().unwrap(); - psb_mut.pus_buf[0..tc_raw.len()].copy_from_slice(tc_raw); - Ok(()) + impl PusServiceHelper { + pub fn new( + tc_receiver: Box, + tm_sender: Box, + tm_apid: u16, + verification_handler: VerificationReporterWithSender, + tc_in_mem_converter: TcInMemConverter, + ) -> Self { + Self { + common: PusServiceBase { + tc_receiver, + tm_sender, + tm_apid, + verification_handler: RefCell::new(verification_handler), + }, + tc_in_mem_converter, + } } - fn handle_next_packet(&mut self) -> Result { - match self.psb().tc_receiver.recv_tc() { - Ok(ReceivedTcWrapper { store_addr, token }) => { + /// This function can be used to poll the internal [EcssTcReceiver] object for the next + /// telecommand packet. It will return `Ok(None)` if there are not packets available. + /// In any other case, it will perform the acceptance of the ECSS TC packet using the + /// internal [VerificationReporterWithSender] object. It will then return the telecommand + /// and the according accepted token. + pub fn retrieve_and_accept_next_packet( + &mut self, + ) -> Result, PusPacketHandlingError> { + match self.common.tc_receiver.recv_tc() { + Ok(EcssTcAndToken { + tc_in_memory, + token, + }) => { if token.is_none() { return Err(PusPacketHandlingError::InvalidVerificationToken); } let token = token.unwrap(); let accepted_token = VerificationToken::::try_from(token) .map_err(|_| PusPacketHandlingError::InvalidVerificationToken)?; - self.handle_one_tc(store_addr, accepted_token) + Ok(Some(AcceptedEcssTcAndToken { + tc_in_memory, + token: accepted_token, + })) } Err(e) => match e { TryRecvTmtcError::Error(e) => Err(PusPacketHandlingError::EcssTmtc(e)), - TryRecvTmtcError::Empty => Ok(PusPacketHandlerResult::Empty), + TryRecvTmtcError::Empty => Ok(None), }, } } @@ -746,10 +914,35 @@ pub(crate) fn source_buffer_large_enough(cap: usize, len: usize) -> Result<(), E } #[cfg(test)] -pub(crate) mod tests { - use spacepackets::ecss::tm::{GenericPusTmSecondaryHeader, PusTmCreator}; +pub mod tests { + use std::sync::mpsc::TryRecvError; + use std::sync::{mpsc, RwLock}; + + use alloc::boxed::Box; + use alloc::vec; + use spacepackets::ecss::tc::PusTcCreator; + use spacepackets::ecss::tm::{GenericPusTmSecondaryHeader, PusTmCreator, PusTmReader}; + use spacepackets::ecss::{PusPacket, WritablePusPacket}; use spacepackets::CcsdsPacket; + use crate::pool::{ + PoolProviderMemInPlace, SharedStaticMemoryPool, StaticMemoryPool, StaticPoolConfig, + StoreAddr, + }; + use crate::pus::verification::RequestId; + use crate::tmtc::tm_helper::SharedTmStore; + + use super::verification::{ + TcStateAccepted, VerificationReporterCfg, VerificationReporterWithSender, VerificationToken, + }; + use super::{ + EcssTcAndToken, EcssTcInSharedStoreConverter, EcssTcInVecConverter, MpscTcReceiver, + MpscTmAsVecSender, MpscTmInStoreSender, PusPacketHandlerResult, PusPacketHandlingError, + PusServiceHelper, TcInMemory, + }; + + pub const TEST_APID: u16 = 0x101; + #[derive(Debug, Eq, PartialEq, Clone)] pub(crate) struct CommonTmInfo { pub subservice: u8, @@ -759,12 +952,23 @@ pub(crate) mod tests { pub time_stamp: [u8; 7], } + pub trait PusTestHarness { + fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken; + fn read_next_tm(&mut self) -> PusTmReader<'_>; + fn check_no_tm_available(&self) -> bool; + fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId); + } + + pub trait SimplePusPacketHandler { + fn handle_one_tc(&mut self) -> Result; + } + impl CommonTmInfo { pub fn new_from_tm(tm: &PusTmCreator) -> Self { let mut time_stamp = [0; 7]; time_stamp.clone_from_slice(&tm.timestamp()[0..7]); Self { - subservice: tm.subservice(), + subservice: PusPacket::subservice(tm), apid: tm.apid(), msg_counter: tm.msg_counter(), dest_id: tm.dest_id(), @@ -772,4 +976,192 @@ pub(crate) mod tests { } } } + + /// Common fields for a PUS service test harness. + pub struct PusServiceHandlerWithSharedStoreCommon { + pus_buf: [u8; 2048], + tm_buf: [u8; 2048], + tc_pool: SharedStaticMemoryPool, + tm_pool: SharedTmStore, + tc_sender: mpsc::Sender, + tm_receiver: mpsc::Receiver, + verification_handler: VerificationReporterWithSender, + } + + impl PusServiceHandlerWithSharedStoreCommon { + /// This function generates the structure in addition to the PUS service handler + /// [PusServiceHandler] which might be required for a specific PUS service handler. + /// + /// The PUS service handler is instantiated with a [EcssTcInStoreConverter]. + pub fn new() -> (Self, PusServiceHelper) { + let pool_cfg = StaticPoolConfig::new(vec![(16, 16), (8, 32), (4, 64)]); + let tc_pool = StaticMemoryPool::new(pool_cfg.clone()); + let tm_pool = StaticMemoryPool::new(pool_cfg); + let shared_tc_pool = SharedStaticMemoryPool::new(RwLock::new(tc_pool)); + let shared_tm_pool = SharedTmStore::new(tm_pool); + let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::channel(); + let (tm_tx, tm_rx) = mpsc::channel(); + + let verif_sender = + MpscTmInStoreSender::new(0, "verif_sender", shared_tm_pool.clone(), tm_tx.clone()); + let verif_cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap(); + let verification_handler = + VerificationReporterWithSender::new(&verif_cfg, Box::new(verif_sender)); + let test_srv_tm_sender = + MpscTmInStoreSender::new(0, "TEST_SENDER", shared_tm_pool.clone(), tm_tx); + let test_srv_tc_receiver = MpscTcReceiver::new(0, "TEST_RECEIVER", test_srv_tc_rx); + let in_store_converter = + EcssTcInSharedStoreConverter::new(shared_tc_pool.clone(), 2048); + ( + Self { + pus_buf: [0; 2048], + tm_buf: [0; 2048], + tc_pool: shared_tc_pool, + tm_pool: shared_tm_pool, + tc_sender: test_srv_tc_tx, + tm_receiver: tm_rx, + verification_handler: verification_handler.clone(), + }, + PusServiceHelper::new( + Box::new(test_srv_tc_receiver), + Box::new(test_srv_tm_sender), + TEST_APID, + verification_handler, + in_store_converter, + ), + ) + } + pub fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken { + let token = self.verification_handler.add_tc(tc); + let token = self + .verification_handler + .acceptance_success(token, Some(&[0; 7])) + .unwrap(); + let tc_size = tc.write_to_bytes(&mut self.pus_buf).unwrap(); + let mut tc_pool = self.tc_pool.write().unwrap(); + let addr = tc_pool.add(&self.pus_buf[..tc_size]).unwrap(); + drop(tc_pool); + // Send accepted TC to test service handler. + self.tc_sender + .send(EcssTcAndToken::new(addr, token)) + .expect("sending tc failed"); + token + } + + pub fn read_next_tm(&mut self) -> PusTmReader<'_> { + let next_msg = self.tm_receiver.try_recv(); + assert!(next_msg.is_ok()); + let tm_addr = next_msg.unwrap(); + let tm_pool = self.tm_pool.shared_pool.read().unwrap(); + let tm_raw = tm_pool.read(&tm_addr).unwrap(); + self.tm_buf[0..tm_raw.len()].copy_from_slice(tm_raw); + PusTmReader::new(&self.tm_buf, 7).unwrap().0 + } + + pub fn check_no_tm_available(&self) -> bool { + let next_msg = self.tm_receiver.try_recv(); + if let TryRecvError::Empty = next_msg.unwrap_err() { + return true; + } + false + } + + pub fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId) { + let next_msg = self.tm_receiver.try_recv(); + assert!(next_msg.is_ok()); + let tm_addr = next_msg.unwrap(); + let tm_pool = self.tm_pool.shared_pool.read().unwrap(); + let tm_raw = tm_pool.read(&tm_addr).unwrap(); + let tm = PusTmReader::new(tm_raw, 7).unwrap().0; + assert_eq!(PusPacket::service(&tm), 1); + assert_eq!(PusPacket::subservice(&tm), subservice); + assert_eq!(tm.apid(), TEST_APID); + let req_id = + RequestId::from_bytes(tm.user_data()).expect("generating request ID failed"); + assert_eq!(req_id, expected_request_id); + } + } + + pub struct PusServiceHandlerWithVecCommon { + current_tm: Option>, + tc_sender: mpsc::Sender, + tm_receiver: mpsc::Receiver>, + verification_handler: VerificationReporterWithSender, + } + + impl PusServiceHandlerWithVecCommon { + pub fn new() -> (Self, PusServiceHelper) { + let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::channel(); + let (tm_tx, tm_rx) = mpsc::channel(); + + let verif_sender = MpscTmAsVecSender::new(0, "verififcatio-sender", tm_tx.clone()); + let verif_cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap(); + let verification_handler = + VerificationReporterWithSender::new(&verif_cfg, Box::new(verif_sender)); + let test_srv_tm_sender = MpscTmAsVecSender::new(0, "test-sender", tm_tx); + let test_srv_tc_receiver = MpscTcReceiver::new(0, "test-receiver", test_srv_tc_rx); + let in_store_converter = EcssTcInVecConverter::default(); + ( + Self { + current_tm: None, + tc_sender: test_srv_tc_tx, + tm_receiver: tm_rx, + verification_handler: verification_handler.clone(), + }, + PusServiceHelper::new( + Box::new(test_srv_tc_receiver), + Box::new(test_srv_tm_sender), + TEST_APID, + verification_handler, + in_store_converter, + ), + ) + } + + pub fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken { + let token = self.verification_handler.add_tc(tc); + let token = self + .verification_handler + .acceptance_success(token, Some(&[0; 7])) + .unwrap(); + // Send accepted TC to test service handler. + self.tc_sender + .send(EcssTcAndToken::new( + TcInMemory::Vec(tc.to_vec().expect("pus tc conversion to vec failed")), + token, + )) + .expect("sending tc failed"); + token + } + + pub fn read_next_tm(&mut self) -> PusTmReader<'_> { + let next_msg = self.tm_receiver.try_recv(); + assert!(next_msg.is_ok()); + self.current_tm = Some(next_msg.unwrap()); + PusTmReader::new(self.current_tm.as_ref().unwrap(), 7) + .unwrap() + .0 + } + + pub fn check_no_tm_available(&self) -> bool { + let next_msg = self.tm_receiver.try_recv(); + if let TryRecvError::Empty = next_msg.unwrap_err() { + return true; + } + false + } + + pub fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId) { + let next_msg = self.tm_receiver.try_recv(); + assert!(next_msg.is_ok()); + let next_msg = next_msg.unwrap(); + let tm = PusTmReader::new(next_msg.as_slice(), 7).unwrap().0; + assert_eq!(PusPacket::service(&tm), 1); + assert_eq!(PusPacket::subservice(&tm), subservice); + assert_eq!(tm.apid(), TEST_APID); + let req_id = + RequestId::from_bytes(tm.user_data()).expect("generating request ID failed"); + assert_eq!(req_id, expected_request_id); + } + } } diff --git a/satrs-core/src/pus/scheduler.rs b/satrs-core/src/pus/scheduler.rs index 392eb87..d0f6a72 100644 --- a/satrs-core/src/pus/scheduler.rs +++ b/satrs-core/src/pus/scheduler.rs @@ -2,23 +2,25 @@ //! //! The core data structure of this module is the [PusScheduler]. This structure can be used //! to perform the scheduling of telecommands like specified in the ECSS standard. -use core::fmt::Debug; +use core::fmt::{Debug, Display, Formatter}; +use core::time::Duration; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use spacepackets::ecss::scheduling::TimeWindowType; -use spacepackets::ecss::tc::{GenericPusTcSecondaryHeader, IsPusTelecommand}; -use spacepackets::time::CcsdsTimeProvider; +use spacepackets::ecss::tc::{GenericPusTcSecondaryHeader, IsPusTelecommand, PusTcReader}; +use spacepackets::ecss::{PusError, PusPacket}; +use spacepackets::time::{CcsdsTimeProvider, TimeReader, TimestampError, UnixTimestamp}; use spacepackets::CcsdsPacket; #[cfg(feature = "std")] use std::error::Error; -use crate::pool::StoreAddr; +use crate::pool::{PoolProviderMemInPlace, StoreError}; #[cfg(feature = "alloc")] pub use alloc_mod::*; /// This is the request ID as specified in ECSS-E-ST-70-41C 5.4.11.2 of the standard. /// -/// This version of the request ID is used to identify scheduled commands and also contains +/// This version of the request ID is used to identify scheduled commands and also contains /// the source ID found in the secondary header of PUS telecommands. #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -56,17 +58,19 @@ impl RequestId { } } +pub type AddrInStore = u64; + /// This is the format stored internally by the TC scheduler for each scheduled telecommand. -/// It consists of the address of that telecommand in the TC pool and a request ID. +/// It consists of a generic address for that telecommand in the TC pool and a request ID. #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct TcInfo { - addr: StoreAddr, + addr: AddrInStore, request_id: RequestId, } impl TcInfo { - pub fn addr(&self) -> StoreAddr { + pub fn addr(&self) -> AddrInStore { self.addr } @@ -74,7 +78,7 @@ impl TcInfo { self.request_id } - pub fn new(addr: StoreAddr, request_id: RequestId) -> Self { + pub fn new(addr: u64, request_id: RequestId) -> Self { TcInfo { addr, request_id } } } @@ -133,23 +137,172 @@ impl TimeWindow { } } +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ScheduleError { + PusError(PusError), + /// The release time is within the time-margin added on top of the current time. + /// The first parameter is the current time, the second one the time margin, and the third one + /// the release time. + ReleaseTimeInTimeMargin { + current_time: UnixTimestamp, + time_margin: Duration, + release_time: UnixTimestamp, + }, + /// Nested time-tagged commands are not allowed. + NestedScheduledTc, + StoreError(StoreError), + TcDataEmpty, + TimestampError(TimestampError), + WrongSubservice, + WrongService, +} + +impl Display for ScheduleError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + ScheduleError::PusError(e) => { + write!(f, "Pus Error: {e}") + } + ScheduleError::ReleaseTimeInTimeMargin { + current_time, + time_margin, + release_time, + } => { + write!( + f, + "Error: time margin too short, current time: {current_time:?}, time margin: {time_margin:?}, release time: {release_time:?}" + ) + } + ScheduleError::NestedScheduledTc => { + write!(f, "Error: nested scheduling is not allowed") + } + ScheduleError::StoreError(e) => { + write!(f, "Store Error: {e}") + } + ScheduleError::TcDataEmpty => { + write!(f, "Error: empty Tc Data field") + } + ScheduleError::TimestampError(e) => { + write!(f, "Timestamp Error: {e}") + } + ScheduleError::WrongService => { + write!(f, "Error: Service not 11.") + } + ScheduleError::WrongSubservice => { + write!(f, "Error: Subservice not 4.") + } + } + } +} + +impl From for ScheduleError { + fn from(e: PusError) -> Self { + ScheduleError::PusError(e) + } +} + +impl From for ScheduleError { + fn from(e: StoreError) -> Self { + ScheduleError::StoreError(e) + } +} + +impl From for ScheduleError { + fn from(e: TimestampError) -> Self { + ScheduleError::TimestampError(e) + } +} + +#[cfg(feature = "std")] +impl Error for ScheduleError {} + +pub trait PusSchedulerInterface { + type TimeProvider: CcsdsTimeProvider + TimeReader; + + fn reset( + &mut self, + store: &mut (impl PoolProviderMemInPlace + ?Sized), + ) -> Result<(), StoreError>; + + fn is_enabled(&self) -> bool; + + fn enable(&mut self); + + /// A disabled scheduler should still delete commands where the execution time has been reached + /// but should not release them to be executed. + fn disable(&mut self); + + /// Insert a telecommand which was already unwrapped from the outer Service 11 packet and stored + /// inside the telecommand packet pool. + fn insert_unwrapped_and_stored_tc( + &mut self, + time_stamp: UnixTimestamp, + info: TcInfo, + ) -> Result<(), ScheduleError>; + + /// Insert a telecommand based on the fully wrapped time-tagged telecommand. The timestamp + /// provider needs to be supplied via a generic. + fn insert_wrapped_tc( + &mut self, + pus_tc: &(impl IsPusTelecommand + PusPacket + GenericPusTcSecondaryHeader), + pool: &mut (impl PoolProviderMemInPlace + ?Sized), + ) -> Result { + if PusPacket::service(pus_tc) != 11 { + return Err(ScheduleError::WrongService); + } + if PusPacket::subservice(pus_tc) != 4 { + return Err(ScheduleError::WrongSubservice); + } + if pus_tc.user_data().is_empty() { + return Err(ScheduleError::TcDataEmpty); + } + let user_data = pus_tc.user_data(); + let stamp: Self::TimeProvider = TimeReader::from_bytes(user_data)?; + let unix_stamp = stamp.unix_stamp(); + let stamp_len = stamp.len_as_bytes(); + self.insert_unwrapped_tc(unix_stamp, &user_data[stamp_len..], pool) + } + + /// Insert a telecommand which was already unwrapped from the outer Service 11 packet but still + /// needs to be stored inside the telecommand pool. + fn insert_unwrapped_tc( + &mut self, + time_stamp: UnixTimestamp, + tc: &[u8], + pool: &mut (impl PoolProviderMemInPlace + ?Sized), + ) -> Result { + let check_tc = PusTcReader::new(tc)?; + if PusPacket::service(&check_tc.0) == 11 && PusPacket::subservice(&check_tc.0) == 4 { + return Err(ScheduleError::NestedScheduledTc); + } + let req_id = RequestId::from_tc(&check_tc.0); + + match pool.add(tc) { + Ok(addr) => { + let info = TcInfo::new(addr, req_id); + self.insert_unwrapped_and_stored_tc(time_stamp, info)?; + Ok(info) + } + Err(err) => Err(err.into()), + } + } +} + #[cfg(feature = "alloc")] pub mod alloc_mod { use super::*; - use crate::pool::{PoolProvider, StoreAddr, StoreError}; + use crate::pool::{PoolProviderMemInPlace, StoreAddr, StoreError}; use alloc::collections::btree_map::{Entry, Range}; use alloc::collections::BTreeMap; use alloc::vec; use alloc::vec::Vec; - use core::fmt::{Display, Formatter}; use core::time::Duration; use spacepackets::ecss::scheduling::TimeWindowType; - use spacepackets::ecss::tc::{ - GenericPusTcSecondaryHeader, IsPusTelecommand, PusTc, PusTcReader, - }; - use spacepackets::ecss::{PusError, PusPacket}; + use spacepackets::ecss::tc::{PusTc, PusTcReader}; + use spacepackets::ecss::PusPacket; use spacepackets::time::cds::DaysLen24Bits; - use spacepackets::time::{cds, CcsdsTimeProvider, TimeReader, TimestampError, UnixTimestamp}; + use spacepackets::time::{cds, CcsdsTimeProvider, UnixTimestamp}; #[cfg(feature = "std")] use std::time::SystemTimeError; @@ -159,86 +312,14 @@ pub mod alloc_mod { WithStoreDeletion(Result), } - #[derive(Debug, Clone, PartialEq, Eq)] - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - pub enum ScheduleError { - PusError(PusError), - /// The release time is within the time-margin added on top of the current time. - /// The first parameter is the current time, the second one the time margin, and the third one - /// the release time. - ReleaseTimeInTimeMargin(UnixTimestamp, Duration, UnixTimestamp), - /// Nested time-tagged commands are not allowed. - NestedScheduledTc, - StoreError(StoreError), - TcDataEmpty, - TimestampError(TimestampError), - WrongSubservice, - WrongService, - } - - impl Display for ScheduleError { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - match self { - ScheduleError::PusError(e) => { - write!(f, "Pus Error: {e}") - } - ScheduleError::ReleaseTimeInTimeMargin(current_time, margin, timestamp) => { - write!( - f, - "Error: time margin too short, current time: {current_time:?}, time margin: {margin:?}, release time: {timestamp:?}" - ) - } - ScheduleError::NestedScheduledTc => { - write!(f, "Error: nested scheduling is not allowed") - } - ScheduleError::StoreError(e) => { - write!(f, "Store Error: {e}") - } - ScheduleError::TcDataEmpty => { - write!(f, "Error: empty Tc Data field") - } - ScheduleError::TimestampError(e) => { - write!(f, "Timestamp Error: {e}") - } - ScheduleError::WrongService => { - write!(f, "Error: Service not 11.") - } - ScheduleError::WrongSubservice => { - write!(f, "Error: Subservice not 4.") - } - } - } - } - - impl From for ScheduleError { - fn from(e: PusError) -> Self { - ScheduleError::PusError(e) - } - } - - impl From for ScheduleError { - fn from(e: StoreError) -> Self { - ScheduleError::StoreError(e) - } - } - - impl From for ScheduleError { - fn from(e: TimestampError) -> Self { - ScheduleError::TimestampError(e) - } - } - - #[cfg(feature = "std")] - impl Error for ScheduleError {} - /// This is the core data structure for scheduling PUS telecommands with [alloc] support. /// /// It is assumed that the actual telecommand data is stored in a separate TC pool offering - /// a [crate::pool::PoolProvider] API. This data structure just tracks the store addresses and their - /// release times and offers a convenient API to insert and release telecommands and perform - /// other functionality specified by the ECSS standard in section 6.11. The time is tracked - /// as a [spacepackets::time::UnixTimestamp] but the only requirement to the timekeeping of - /// the user is that it is convertible to that timestamp. + /// a [crate::pool::PoolProviderMemInPlace] API. This data structure just tracks the store + /// addresses and their release times and offers a convenient API to insert and release + /// telecommands and perform other functionality specified by the ECSS standard in section 6.11. + /// The time is tracked as a [spacepackets::time::UnixTimestamp] but the only requirement to + /// the timekeeping of the user is that it is convertible to that timestamp. /// /// The standard also specifies that the PUS scheduler can be enabled and disabled. /// A disabled scheduler should still delete commands where the execution time has been reached @@ -292,44 +373,6 @@ pub mod alloc_mod { num_entries } - pub fn is_enabled(&self) -> bool { - self.enabled - } - - pub fn enable(&mut self) { - self.enabled = true; - } - - /// A disabled scheduler should still delete commands where the execution time has been reached - /// but should not release them to be executed. - pub fn disable(&mut self) { - self.enabled = false; - } - - /// This will disable the scheduler and clear the schedule as specified in 6.11.4.4. - /// Be careful with this command as it will delete all the commands in the schedule. - /// - /// The holding store for the telecommands needs to be passed so all the stored telecommands - /// can be deleted to avoid a memory leak. If at last one deletion operation fails, the error - /// will be returned but the method will still try to delete all the commands in the schedule. - pub fn reset( - &mut self, - store: &mut (impl PoolProvider + ?Sized), - ) -> Result<(), StoreError> { - self.enabled = false; - let mut deletion_ok = Ok(()); - for tc_lists in &mut self.tc_map { - for tc in tc_lists.1 { - let res = store.delete(tc.addr); - if res.is_err() { - deletion_ok = res; - } - } - } - self.tc_map.clear(); - deletion_ok - } - pub fn update_time(&mut self, current_time: UnixTimestamp) { self.current_time = current_time; } @@ -346,11 +389,11 @@ pub mod alloc_mod { info: TcInfo, ) -> Result<(), ScheduleError> { if time_stamp < self.current_time + self.time_margin { - return Err(ScheduleError::ReleaseTimeInTimeMargin( - self.current_time, - self.time_margin, - time_stamp, - )); + return Err(ScheduleError::ReleaseTimeInTimeMargin { + current_time: self.current_time, + time_margin: self.time_margin, + release_time: time_stamp, + }); } match self.tc_map.entry(time_stamp) { Entry::Vacant(e) => { @@ -369,7 +412,7 @@ pub mod alloc_mod { &mut self, time_stamp: UnixTimestamp, tc: &[u8], - pool: &mut (impl PoolProvider + ?Sized), + pool: &mut (impl PoolProviderMemInPlace + ?Sized), ) -> Result { let check_tc = PusTcReader::new(tc)?; if PusPacket::service(&check_tc.0) == 11 && PusPacket::subservice(&check_tc.0) == 4 { @@ -387,35 +430,12 @@ pub mod alloc_mod { } } - /// Insert a telecommand based on the fully wrapped time-tagged telecommand. The timestamp - /// provider needs to be supplied via a generic. - pub fn insert_wrapped_tc( - &mut self, - pus_tc: &(impl IsPusTelecommand + PusPacket + GenericPusTcSecondaryHeader), - pool: &mut (impl PoolProvider + ?Sized), - ) -> Result { - if PusPacket::service(pus_tc) != 11 { - return Err(ScheduleError::WrongService); - } - if PusPacket::subservice(pus_tc) != 4 { - return Err(ScheduleError::WrongSubservice); - } - if pus_tc.user_data().is_empty() { - return Err(ScheduleError::TcDataEmpty); - } - let user_data = pus_tc.user_data(); - let stamp: TimeStamp = TimeReader::from_bytes(user_data)?; - let unix_stamp = stamp.unix_stamp(); - let stamp_len = stamp.len_as_bytes(); - self.insert_unwrapped_tc(unix_stamp, &user_data[stamp_len..], pool) - } - /// Insert a telecommand based on the fully wrapped time-tagged telecommand using a CDS /// short timestamp with 16-bit length of days field. pub fn insert_wrapped_tc_cds_short( &mut self, pus_tc: &PusTc, - pool: &mut (impl PoolProvider + ?Sized), + pool: &mut (impl PoolProviderMemInPlace + ?Sized), ) -> Result { self.insert_wrapped_tc::(pus_tc, pool) } @@ -425,7 +445,7 @@ pub mod alloc_mod { pub fn insert_wrapped_tc_cds_long( &mut self, pus_tc: &PusTc, - pool: &mut (impl PoolProvider + ?Sized), + pool: &mut (impl PoolProviderMemInPlace + ?Sized), ) -> Result { self.insert_wrapped_tc::>(pus_tc, pool) } @@ -441,7 +461,7 @@ pub mod alloc_mod { pub fn delete_by_time_filter( &mut self, time_window: TimeWindow, - pool: &mut (impl PoolProvider + ?Sized), + pool: &mut (impl PoolProviderMemInPlace + ?Sized), ) -> Result { let range = self.retrieve_by_time_filter(time_window); let mut del_packets = 0; @@ -471,7 +491,7 @@ pub mod alloc_mod { /// the last deletion will be supplied in addition to the number of deleted commands. pub fn delete_all( &mut self, - pool: &mut (impl PoolProvider + ?Sized), + pool: &mut (impl PoolProviderMemInPlace + ?Sized), ) -> Result { self.delete_by_time_filter(TimeWindow::::new_select_all(), pool) } @@ -518,7 +538,7 @@ pub mod alloc_mod { /// called repeatedly. pub fn delete_by_request_id(&mut self, req_id: &RequestId) -> Option { if let DeletionResult::WithoutStoreDeletion(v) = - self.delete_by_request_id_internal(req_id, None::<&mut dyn PoolProvider>) + self.delete_by_request_id_internal_without_store_deletion(req_id) { return v; } @@ -529,20 +549,19 @@ pub mod alloc_mod { pub fn delete_by_request_id_and_from_pool( &mut self, req_id: &RequestId, - pool: &mut (impl PoolProvider + ?Sized), + pool: &mut (impl PoolProviderMemInPlace + ?Sized), ) -> Result { if let DeletionResult::WithStoreDeletion(v) = - self.delete_by_request_id_internal(req_id, Some(pool)) + self.delete_by_request_id_internal_with_store_deletion(req_id, pool) { return v; } panic!("unexpected deletion result"); } - fn delete_by_request_id_internal( + fn delete_by_request_id_internal_without_store_deletion( &mut self, req_id: &RequestId, - pool: Option<&mut (impl PoolProvider + ?Sized)>, ) -> DeletionResult { let mut idx_found = None; for time_bucket in &mut self.tc_map { @@ -553,24 +572,33 @@ pub mod alloc_mod { } if let Some(idx) = idx_found { let addr = time_bucket.1.remove(idx).addr; - if let Some(pool) = pool { - return match pool.delete(addr) { - Ok(_) => DeletionResult::WithStoreDeletion(Ok(true)), - Err(e) => DeletionResult::WithStoreDeletion(Err(e)), - }; - } return DeletionResult::WithoutStoreDeletion(Some(addr)); } } - if pool.is_none() { - DeletionResult::WithoutStoreDeletion(None) - } else { - DeletionResult::WithStoreDeletion(Ok(false)) - } + DeletionResult::WithoutStoreDeletion(None) } - /// Retrieve all telecommands which should be release based on the current time. - pub fn telecommands_to_release(&self) -> Range<'_, UnixTimestamp, Vec> { - self.tc_map.range(..=self.current_time) + + fn delete_by_request_id_internal_with_store_deletion( + &mut self, + req_id: &RequestId, + pool: &mut (impl PoolProviderMemInPlace + ?Sized), + ) -> DeletionResult { + let mut idx_found = None; + for time_bucket in &mut self.tc_map { + for (idx, tc_info) in time_bucket.1.iter().enumerate() { + if &tc_info.request_id == req_id { + idx_found = Some(idx); + } + } + if let Some(idx) = idx_found { + let addr = time_bucket.1.remove(idx).addr; + return match pool.delete(addr) { + Ok(_) => DeletionResult::WithStoreDeletion(Ok(true)), + Err(e) => DeletionResult::WithStoreDeletion(Err(e)), + }; + } + } + DeletionResult::WithStoreDeletion(Ok(false)) } #[cfg(feature = "std")] @@ -582,29 +610,31 @@ pub mod alloc_mod { /// Utility method which calls [Self::telecommands_to_release] and then calls a releaser /// closure for each telecommand which should be released. This function will also delete - /// the telecommands from the holding store after calling the release closure, if the scheduler - /// is disabled. + /// the telecommands from the holding store after calling the release closure if the user + /// returns [true] from the release closure. /// /// # Arguments /// /// * `releaser` - Closure where the first argument is whether the scheduler is enabled and - /// the second argument is the telecommand information also containing the store address. - /// This closure should return whether the command should be deleted if the scheduler is - /// disabled to prevent memory leaks. - /// * `store` - The holding store of the telecommands. - pub fn release_telecommands bool>( + /// the second argument is the telecommand information also containing the store + /// address. This closure should return whether the command should be deleted. Please + /// note that returning false might lead to memory leaks if the TC is not cleared from + /// the store in some other way. + /// * `tc_store` - The holding store of the telecommands. + pub fn release_telecommands bool>( &mut self, mut releaser: R, - tc_store: &mut (impl PoolProvider + ?Sized), + tc_store: &mut (impl PoolProviderMemInPlace + ?Sized), ) -> Result { let tcs_to_release = self.telecommands_to_release(); let mut released_tcs = 0; let mut store_error = Ok(()); for tc in tcs_to_release { for info in tc.1 { - let should_delete = releaser(self.enabled, info); + let tc = tc_store.read(&info.addr).map_err(|e| (released_tcs, e))?; + let should_delete = releaser(self.enabled, info, tc); released_tcs += 1; - if should_delete && !self.is_enabled() { + if should_delete { let res = tc_store.delete(info.addr); if res.is_err() { store_error = res; @@ -617,13 +647,112 @@ pub mod alloc_mod { .map(|_| released_tcs) .map_err(|e| (released_tcs, e)) } + + /// This utility method is similar to [Self::release_telecommands] but will not perform + /// store deletions and thus does not require a mutable reference of the TC store. + /// + /// It will returns a [Vec] of [TcInfo]s to transfer the list of released + /// telecommands to the user. The user should take care of deleting those telecommands + /// from the holding store to prevent memory leaks. + pub fn release_telecommands_no_deletion( + &mut self, + mut releaser: R, + tc_store: &(impl PoolProviderMemInPlace + ?Sized), + ) -> Result, (Vec, StoreError)> { + let tcs_to_release = self.telecommands_to_release(); + let mut released_tcs = Vec::new(); + for tc in tcs_to_release { + for info in tc.1 { + let tc = tc_store + .read(&info.addr) + .map_err(|e| (released_tcs.clone(), e))?; + releaser(self.is_enabled(), info, tc); + released_tcs.push(*info); + } + } + self.tc_map.retain(|k, _| k > &self.current_time); + Ok(released_tcs) + } + + /// Retrieve all telecommands which should be release based on the current time. + pub fn telecommands_to_release(&self) -> Range<'_, UnixTimestamp, Vec> { + self.tc_map.range(..=self.current_time) + } + } + + impl PusSchedulerInterface for PusScheduler { + type TimeProvider = cds::TimeProvider; + + /// This will disable the scheduler and clear the schedule as specified in 6.11.4.4. + /// Be careful with this command as it will delete all the commands in the schedule. + /// + /// The holding store for the telecommands needs to be passed so all the stored telecommands + /// can be deleted to avoid a memory leak. If at last one deletion operation fails, the error + /// will be returned but the method will still try to delete all the commands in the schedule. + fn reset( + &mut self, + store: &mut (impl PoolProviderMemInPlace + ?Sized), + ) -> Result<(), StoreError> { + self.enabled = false; + let mut deletion_ok = Ok(()); + for tc_lists in &mut self.tc_map { + for tc in tc_lists.1 { + let res = store.delete(tc.addr); + if res.is_err() { + deletion_ok = res; + } + } + } + self.tc_map.clear(); + deletion_ok + } + + fn is_enabled(&self) -> bool { + self.enabled + } + + fn enable(&mut self) { + self.enabled = true; + } + + /// A disabled scheduler should still delete commands where the execution time has been reached + /// but should not release them to be executed. + fn disable(&mut self) { + self.enabled = false; + } + + fn insert_unwrapped_and_stored_tc( + &mut self, + time_stamp: UnixTimestamp, + info: TcInfo, + ) -> Result<(), ScheduleError> { + if time_stamp < self.current_time + self.time_margin { + return Err(ScheduleError::ReleaseTimeInTimeMargin { + current_time: self.current_time, + time_margin: self.time_margin, + release_time: time_stamp, + }); + } + match self.tc_map.entry(time_stamp) { + Entry::Vacant(e) => { + e.insert(vec![info]); + } + Entry::Occupied(mut v) => { + v.get_mut().push(info); + } + } + Ok(()) + } } } #[cfg(test)] mod tests { use super::*; - use crate::pool::{LocalPool, PoolCfg, PoolProvider, StoreAddr, StoreError}; + use crate::pool::{ + PoolProviderMemInPlace, StaticMemoryPool, StaticPoolAddr, StaticPoolConfig, StoreAddr, + StoreError, + }; use alloc::collections::btree_map::Range; use spacepackets::ecss::tc::{PusTcCreator, PusTcReader, PusTcSecondaryHeader}; use spacepackets::ecss::WritablePusPacket; @@ -694,7 +823,7 @@ mod tests { } fn ping_tc_to_store( - pool: &mut LocalPool, + pool: &mut StaticMemoryPool, buf: &mut [u8], seq_count: u16, app_data: Option<&'static [u8]>, @@ -718,7 +847,7 @@ mod tests { #[test] fn reset() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); @@ -769,10 +898,10 @@ mod tests { .insert_unwrapped_and_stored_tc( UnixTimestamp::new_only_seconds(100), TcInfo::new( - StoreAddr { + StoreAddr::from(StaticPoolAddr { pool_idx: 0, packet_idx: 1, - }, + }), RequestId { seq_count: 1, apid: 0, @@ -786,10 +915,10 @@ mod tests { .insert_unwrapped_and_stored_tc( UnixTimestamp::new_only_seconds(100), TcInfo::new( - StoreAddr { + StoreAddr::from(StaticPoolAddr { pool_idx: 0, packet_idx: 2, - }, + }), RequestId { seq_count: 2, apid: 1, @@ -803,10 +932,11 @@ mod tests { .insert_unwrapped_and_stored_tc( UnixTimestamp::new_only_seconds(300), TcInfo::new( - StoreAddr { + StaticPoolAddr { pool_idx: 0, packet_idx: 2, - }, + } + .into(), RequestId { source_id: 10, seq_count: 20, @@ -869,7 +999,7 @@ mod tests { } #[test] fn release_basic() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); @@ -886,7 +1016,7 @@ mod tests { .expect("insertion failed"); let mut i = 0; - let mut test_closure_1 = |boolvar: bool, tc_info: &TcInfo| { + let mut test_closure_1 = |boolvar: bool, tc_info: &TcInfo, _tc: &[u8]| { common_check(boolvar, &tc_info.addr, vec![tc_info_0.addr()], &mut i); true }; @@ -905,10 +1035,11 @@ mod tests { .release_telecommands(&mut test_closure_1, &mut pool) .expect("deletion failed"); assert_eq!(released, 1); - assert!(pool.has_element_at(&tc_info_0.addr()).unwrap()); + // TC is deleted. + assert!(!pool.has_element_at(&tc_info_0.addr()).unwrap()); // test 3, late timestamp, release 1 overdue tc - let mut test_closure_2 = |boolvar: bool, tc_info: &TcInfo| { + let mut test_closure_2 = |boolvar: bool, tc_info: &TcInfo, _tc: &[u8]| { common_check(boolvar, &tc_info.addr, vec![tc_info_1.addr()], &mut i); true }; @@ -919,7 +1050,8 @@ mod tests { .release_telecommands(&mut test_closure_2, &mut pool) .expect("deletion failed"); assert_eq!(released, 1); - assert!(pool.has_element_at(&tc_info_1.addr()).unwrap()); + // TC is deleted. + assert!(!pool.has_element_at(&tc_info_1.addr()).unwrap()); //test 4: no tcs left scheduler @@ -932,7 +1064,7 @@ mod tests { #[test] fn release_multi_with_same_time() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); @@ -949,7 +1081,7 @@ mod tests { .expect("insertion failed"); let mut i = 0; - let mut test_closure = |boolvar: bool, store_addr: &TcInfo| { + let mut test_closure = |boolvar: bool, store_addr: &TcInfo, _tc: &[u8]| { common_check( boolvar, &store_addr.addr, @@ -974,8 +1106,8 @@ mod tests { .release_telecommands(&mut test_closure, &mut pool) .expect("deletion failed"); assert_eq!(released, 2); - assert!(pool.has_element_at(&tc_info_0.addr()).unwrap()); - assert!(pool.has_element_at(&tc_info_1.addr()).unwrap()); + assert!(!pool.has_element_at(&tc_info_0.addr()).unwrap()); + assert!(!pool.has_element_at(&tc_info_1.addr()).unwrap()); //test 3: no tcs left released = scheduler @@ -989,7 +1121,7 @@ mod tests { #[test] fn release_with_scheduler_disabled() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); @@ -1008,7 +1140,7 @@ mod tests { .expect("insertion failed"); let mut i = 0; - let mut test_closure_1 = |boolvar: bool, tc_info: &TcInfo| { + let mut test_closure_1 = |boolvar: bool, tc_info: &TcInfo, _tc: &[u8]| { common_check_disabled(boolvar, &tc_info.addr, vec![tc_info_0.addr()], &mut i); true }; @@ -1030,7 +1162,7 @@ mod tests { assert!(!pool.has_element_at(&tc_info_0.addr()).unwrap()); // test 3, late timestamp, release 1 overdue tc - let mut test_closure_2 = |boolvar: bool, tc_info: &TcInfo| { + let mut test_closure_2 = |boolvar: bool, tc_info: &TcInfo, _tc: &[u8]| { common_check_disabled(boolvar, &tc_info.addr, vec![tc_info_1.addr()], &mut i); true }; @@ -1057,7 +1189,7 @@ mod tests { let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut buf: [u8; 32] = [0; 32]; let tc_info_0 = ping_tc_to_store(&mut pool, &mut buf, 0, None); @@ -1072,7 +1204,7 @@ mod tests { assert!(pool.has_element_at(&tc_info_0.addr()).unwrap()); let data = pool.read(&tc_info_0.addr()).unwrap(); - let check_tc = PusTcReader::new(&data).expect("incorrect Pus tc raw data"); + let check_tc = PusTcReader::new(data).expect("incorrect Pus tc raw data"); assert_eq!(check_tc.0, base_ping_tc_simple_ctor(0, None)); assert_eq!(scheduler.num_scheduled_telecommands(), 1); @@ -1082,7 +1214,7 @@ mod tests { let mut addr_vec = Vec::new(); let mut i = 0; - let mut test_closure = |boolvar: bool, tc_info: &TcInfo| { + let mut test_closure = |boolvar: bool, tc_info: &TcInfo, _tc: &[u8]| { common_check(boolvar, &tc_info.addr, vec![info.addr], &mut i); // check that tc remains unchanged addr_vec.push(tc_info.addr); @@ -1094,7 +1226,7 @@ mod tests { .unwrap(); let data = pool.read(&addr_vec[0]).unwrap(); - let check_tc = PusTcReader::new(&data).expect("incorrect Pus tc raw data"); + let check_tc = PusTcReader::new(data).expect("incorrect Pus tc raw data"); assert_eq!(check_tc.0, base_ping_tc_simple_ctor(0, None)); } @@ -1103,7 +1235,7 @@ mod tests { let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut buf: [u8; 32] = [0; 32]; let tc = scheduled_tc(UnixTimestamp::new_only_seconds(100), &mut buf); @@ -1118,7 +1250,7 @@ mod tests { assert!(pool.has_element_at(&info.addr).unwrap()); let data = pool.read(&info.addr).unwrap(); - let check_tc = PusTcReader::new(&data).expect("incorrect Pus tc raw data"); + let check_tc = PusTcReader::new(data).expect("incorrect Pus tc raw data"); assert_eq!(check_tc.0, base_ping_tc_simple_ctor(0, None)); assert_eq!(scheduler.num_scheduled_telecommands(), 1); @@ -1128,7 +1260,7 @@ mod tests { let mut addr_vec = Vec::new(); let mut i = 0; - let mut test_closure = |boolvar: bool, tc_info: &TcInfo| { + let mut test_closure = |boolvar: bool, tc_info: &TcInfo, _tc: &[u8]| { common_check(boolvar, &tc_info.addr, vec![info.addr], &mut i); // check that tc remains unchanged addr_vec.push(tc_info.addr); @@ -1140,7 +1272,7 @@ mod tests { .unwrap(); let data = pool.read(&addr_vec[0]).unwrap(); - let check_tc = PusTcReader::new(&data).expect("incorrect PUS tc raw data"); + let check_tc = PusTcReader::new(data).expect("incorrect PUS tc raw data"); assert_eq!(check_tc.0, base_ping_tc_simple_ctor(0, None)); } @@ -1149,7 +1281,7 @@ mod tests { let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut buf: [u8; 32] = [0; 32]; let tc = wrong_tc_service(UnixTimestamp::new_only_seconds(100), &mut buf); @@ -1170,7 +1302,7 @@ mod tests { let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut buf: [u8; 32] = [0; 32]; let tc = wrong_tc_subservice(UnixTimestamp::new_only_seconds(100), &mut buf); @@ -1190,7 +1322,7 @@ mod tests { fn insert_wrapped_tc_faulty_app_data() { let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let tc = invalid_time_tagged_cmd(); let insert_res = scheduler.insert_wrapped_tc::(&tc, &mut pool); assert!(insert_res.is_err()); @@ -1205,7 +1337,7 @@ mod tests { fn insert_doubly_wrapped_time_tagged_cmd() { let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut buf: [u8; 64] = [0; 64]; let tc = double_wrapped_time_tagged_tc(UnixTimestamp::new_only_seconds(50), &mut buf); let insert_res = scheduler.insert_wrapped_tc::(&tc, &mut pool); @@ -1241,7 +1373,7 @@ mod tests { let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut buf: [u8; 32] = [0; 32]; @@ -1250,9 +1382,13 @@ mod tests { assert!(insert_res.is_err()); let err = insert_res.unwrap_err(); match err { - ScheduleError::ReleaseTimeInTimeMargin(curr_time, margin, release_time) => { - assert_eq!(curr_time, UnixTimestamp::new_only_seconds(0)); - assert_eq!(margin, Duration::from_secs(5)); + ScheduleError::ReleaseTimeInTimeMargin { + current_time, + time_margin, + release_time, + } => { + assert_eq!(current_time, UnixTimestamp::new_only_seconds(0)); + assert_eq!(time_margin, Duration::from_secs(5)); assert_eq!(release_time, UnixTimestamp::new_only_seconds(4)); } _ => panic!("unexepcted error {err}"), @@ -1261,7 +1397,7 @@ mod tests { #[test] fn test_store_error_propagation_release() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); let mut buf: [u8; 32] = [0; 32]; @@ -1271,7 +1407,7 @@ mod tests { .expect("insertion failed"); let mut i = 0; - let test_closure_1 = |boolvar: bool, tc_info: &TcInfo| { + let test_closure_1 = |boolvar: bool, tc_info: &TcInfo, _tc: &[u8]| { common_check_disabled(boolvar, &tc_info.addr, vec![tc_info_0.addr()], &mut i); true }; @@ -1284,7 +1420,8 @@ mod tests { let release_res = scheduler.release_telecommands(test_closure_1, &mut pool); assert!(release_res.is_err()); let err = release_res.unwrap_err(); - assert_eq!(err.0, 1); + // TC could not even be read.. + assert_eq!(err.0, 0); match err.1 { StoreError::DataDoesNotExist(addr) => { assert_eq!(tc_info_0.addr(), addr); @@ -1295,7 +1432,7 @@ mod tests { #[test] fn test_store_error_propagation_reset() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); let mut buf: [u8; 32] = [0; 32]; @@ -1319,7 +1456,7 @@ mod tests { #[test] fn test_delete_by_req_id_simple_retrieve_addr() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); let mut buf: [u8; 32] = [0; 32]; @@ -1338,7 +1475,7 @@ mod tests { #[test] fn test_delete_by_req_id_simple_delete_all() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); let mut buf: [u8; 32] = [0; 32]; @@ -1357,7 +1494,7 @@ mod tests { #[test] fn test_delete_by_req_id_complex() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); let mut buf: [u8; 32] = [0; 32]; @@ -1386,7 +1523,7 @@ mod tests { let del_res = scheduler.delete_by_request_id_and_from_pool(&tc_info_2.request_id(), &mut pool); assert!(del_res.is_ok()); - assert_eq!(del_res.unwrap(), true); + assert!(del_res.unwrap()); assert!(!pool.has_element_at(&tc_info_2.addr()).unwrap()); assert_eq!(scheduler.num_scheduled_telecommands(), 1); @@ -1394,7 +1531,7 @@ mod tests { let addr_1 = scheduler.delete_by_request_id_and_from_pool(&tc_info_1.request_id(), &mut pool); assert!(addr_1.is_ok()); - assert_eq!(addr_1.unwrap(), true); + assert!(addr_1.unwrap()); assert!(!pool.has_element_at(&tc_info_1.addr()).unwrap()); assert_eq!(scheduler.num_scheduled_telecommands(), 0); } @@ -1404,7 +1541,7 @@ mod tests { let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); - let mut pool = LocalPool::new(PoolCfg::new(vec![(1, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(1, 64)])); let mut buf: [u8; 32] = [0; 32]; // Store is full after this. @@ -1424,7 +1561,7 @@ mod tests { } fn insert_command_with_release_time( - pool: &mut LocalPool, + pool: &mut StaticMemoryPool, scheduler: &mut PusScheduler, seq_count: u16, release_secs: u64, @@ -1443,7 +1580,7 @@ mod tests { #[test] fn test_time_window_retrieval_select_all() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); let tc_info_0 = insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50); @@ -1474,7 +1611,7 @@ mod tests { #[test] fn test_time_window_retrieval_select_from_stamp() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); let _ = insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50); @@ -1505,7 +1642,7 @@ mod tests { #[test] fn test_time_window_retrieval_select_to_time() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); let tc_info_0 = insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50); @@ -1536,7 +1673,7 @@ mod tests { #[test] fn test_time_window_retrieval_select_from_time_to_time() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); let _ = insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50); @@ -1571,7 +1708,7 @@ mod tests { #[test] fn test_deletion_all() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50); @@ -1598,7 +1735,7 @@ mod tests { #[test] fn test_deletion_from_start_time() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50); @@ -1619,7 +1756,7 @@ mod tests { #[test] fn test_deletion_to_end_time() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); let cmd_0_to_delete = insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50); @@ -1641,7 +1778,7 @@ mod tests { #[test] fn test_deletion_from_start_time_to_end_time() { - let mut pool = LocalPool::new(PoolCfg::new(vec![(10, 32), (5, 64)])); + let mut pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(10, 32), (5, 64)])); let mut scheduler = PusScheduler::new(UnixTimestamp::new_only_seconds(0), Duration::from_secs(5)); let cmd_out_of_range_0 = insert_command_with_release_time(&mut pool, &mut scheduler, 0, 50); diff --git a/satrs-core/src/pus/scheduler_srv.rs b/satrs-core/src/pus/scheduler_srv.rs index 46a2bcd..2225c03 100644 --- a/satrs-core/src/pus/scheduler_srv.rs +++ b/satrs-core/src/pus/scheduler_srv.rs @@ -1,141 +1,130 @@ -use crate::pool::{SharedPool, StoreAddr}; -use crate::pus::scheduler::PusScheduler; -use crate::pus::verification::{StdVerifReporterWithSender, TcStateAccepted, VerificationToken}; -use crate::pus::{ - EcssTcReceiver, EcssTmSender, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, - PusServiceHandler, -}; -use spacepackets::ecss::tc::PusTcReader; +use super::scheduler::PusSchedulerInterface; +use super::{EcssTcInMemConverter, PusServiceBase, PusServiceHelper}; +use crate::pool::PoolProviderMemInPlace; +use crate::pus::{PusPacketHandlerResult, PusPacketHandlingError}; +use alloc::string::ToString; use spacepackets::ecss::{scheduling, PusPacket}; use spacepackets::time::cds::TimeProvider; -use std::boxed::Box; /// This is a helper class for [std] environments to handle generic PUS 11 (scheduling service) -/// packets. This handler is constrained to using the [PusScheduler], but is able to process -/// the most important PUS requests for a scheduling service. +/// packets. This handler is able to handle the most important PUS requests for a scheduling +/// service which provides the [PusSchedulerInterface]. /// /// Please note that this class does not do the regular periodic handling like releasing any /// telecommands inside the scheduler. The user can retrieve the wrapped scheduler via the /// [Self::scheduler] and [Self::scheduler_mut] function and then use the scheduler API to release /// telecommands when applicable. -pub struct PusService11SchedHandler { - psb: PusServiceBase, - scheduler: PusScheduler, +pub struct PusService11SchedHandler< + TcInMemConverter: EcssTcInMemConverter, + Scheduler: PusSchedulerInterface, +> { + pub service_helper: PusServiceHelper, + scheduler: Scheduler, } -impl PusService11SchedHandler { - pub fn new( - tc_receiver: Box, - shared_tc_store: SharedPool, - tm_sender: Box, - tm_apid: u16, - verification_handler: StdVerifReporterWithSender, - scheduler: PusScheduler, - ) -> Self { +impl + PusService11SchedHandler +{ + pub fn new(service_helper: PusServiceHelper, scheduler: Scheduler) -> Self { Self { - psb: PusServiceBase::new( - tc_receiver, - shared_tc_store, - tm_sender, - tm_apid, - verification_handler, - ), + service_helper, scheduler, } } - pub fn scheduler_mut(&mut self) -> &mut PusScheduler { + pub fn scheduler_mut(&mut self) -> &mut Scheduler { &mut self.scheduler } - pub fn scheduler(&self) -> &PusScheduler { + pub fn scheduler(&self) -> &Scheduler { &self.scheduler } -} -impl PusServiceHandler for PusService11SchedHandler { - fn psb_mut(&mut self) -> &mut PusServiceBase { - &mut self.psb - } - fn psb(&self) -> &PusServiceBase { - &self.psb - } - - fn handle_one_tc( + pub fn handle_one_tc( &mut self, - addr: StoreAddr, - token: VerificationToken, + sched_tc_pool: &mut (impl PoolProviderMemInPlace + ?Sized), ) -> Result { - self.copy_tc_to_buf(addr)?; - let (tc, _) = PusTcReader::new(&self.psb.pus_buf)?; - let subservice = tc.subservice(); - let std_service = scheduling::Subservice::try_from(subservice); - if std_service.is_err() { + let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?; + if possible_packet.is_none() { + return Ok(PusPacketHandlerResult::Empty); + } + let ecss_tc_and_token = possible_packet.unwrap(); + let tc = self + .service_helper + .tc_in_mem_converter + .convert_ecss_tc_in_memory_to_reader(&ecss_tc_and_token.tc_in_memory)?; + let subservice = PusPacket::subservice(&tc); + let standard_subservice = scheduling::Subservice::try_from(subservice); + if standard_subservice.is_err() { return Ok(PusPacketHandlerResult::CustomSubservice( - tc.subservice(), - token, + subservice, + ecss_tc_and_token.token, )); } let mut partial_error = None; - let time_stamp = self.psb().get_current_timestamp(&mut partial_error); - match std_service.unwrap() { + let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error); + match standard_subservice.unwrap() { scheduling::Subservice::TcEnableScheduling => { let start_token = self - .psb + .service_helper + .common .verification_handler .get_mut() - .start_success(token, Some(&time_stamp)) + .start_success(ecss_tc_and_token.token, Some(&time_stamp)) .expect("Error sending start success"); self.scheduler.enable(); if self.scheduler.is_enabled() { - self.psb + self.service_helper + .common .verification_handler .get_mut() .completion_success(start_token, Some(&time_stamp)) .expect("Error sending completion success"); } else { - panic!("Failed to enable scheduler"); + return Err(PusPacketHandlingError::Other( + "failed to enabled scheduler".to_string(), + )); } } scheduling::Subservice::TcDisableScheduling => { let start_token = self - .psb + .service_helper + .common .verification_handler .get_mut() - .start_success(token, Some(&time_stamp)) + .start_success(ecss_tc_and_token.token, Some(&time_stamp)) .expect("Error sending start success"); self.scheduler.disable(); if !self.scheduler.is_enabled() { - self.psb + self.service_helper + .common .verification_handler .get_mut() .completion_success(start_token, Some(&time_stamp)) .expect("Error sending completion success"); } else { - panic!("Failed to disable scheduler"); + return Err(PusPacketHandlingError::Other( + "failed to disable scheduler".to_string(), + )); } } scheduling::Subservice::TcResetScheduling => { let start_token = self - .psb + .service_helper + .common .verification_handler .get_mut() - .start_success(token, Some(&time_stamp)) + .start_success(ecss_tc_and_token.token, Some(&time_stamp)) .expect("Error sending start success"); - let mut pool = self - .psb - .shared_tc_store - .write() - .expect("Locking pool failed"); - self.scheduler - .reset(pool.as_mut()) + .reset(sched_tc_pool) .expect("Error resetting TC Pool"); - self.psb + self.service_helper + .common .verification_handler .get_mut() .completion_success(start_token, Some(&time_stamp)) @@ -143,31 +132,30 @@ impl PusServiceHandler for PusService11SchedHandler { } scheduling::Subservice::TcInsertActivity => { let start_token = self - .psb + .service_helper + .common .verification_handler .get_mut() - .start_success(token, Some(&time_stamp)) + .start_success(ecss_tc_and_token.token, Some(&time_stamp)) .expect("error sending start success"); - let mut pool = self - .psb - .shared_tc_store - .write() - .expect("locking pool failed"); + // let mut pool = self.sched_tc_pool.write().expect("locking pool failed"); self.scheduler - .insert_wrapped_tc::(&tc, pool.as_mut()) + .insert_wrapped_tc::(&tc, sched_tc_pool) .expect("insertion of activity into pool failed"); - self.psb + self.service_helper + .common .verification_handler .get_mut() .completion_success(start_token, Some(&time_stamp)) .expect("sending completion success failed"); } _ => { + // Treat unhandled standard subservices as custom subservices for now. return Ok(PusPacketHandlerResult::CustomSubservice( - tc.subservice(), - token, + subservice, + ecss_tc_and_token.token, )); } } @@ -176,9 +164,192 @@ impl PusServiceHandler for PusService11SchedHandler { partial_error, )); } - Ok(PusPacketHandlerResult::CustomSubservice( - tc.subservice(), - token, - )) + Ok(PusPacketHandlerResult::RequestHandled) + } +} + +#[cfg(test)] +mod tests { + use crate::pool::{StaticMemoryPool, StaticPoolConfig}; + use crate::pus::tests::TEST_APID; + use crate::pus::{ + scheduler::{self, PusSchedulerInterface, TcInfo}, + tests::{PusServiceHandlerWithSharedStoreCommon, PusTestHarness}, + verification::{RequestId, TcStateAccepted, VerificationToken}, + EcssTcInSharedStoreConverter, + }; + use alloc::collections::VecDeque; + use delegate::delegate; + use spacepackets::ecss::scheduling::Subservice; + use spacepackets::ecss::tc::PusTcSecondaryHeader; + use spacepackets::ecss::WritablePusPacket; + use spacepackets::time::TimeWriter; + use spacepackets::SpHeader; + use spacepackets::{ + ecss::{tc::PusTcCreator, tm::PusTmReader}, + time::cds, + }; + + use super::PusService11SchedHandler; + + struct Pus11HandlerWithStoreTester { + common: PusServiceHandlerWithSharedStoreCommon, + handler: PusService11SchedHandler, + sched_tc_pool: StaticMemoryPool, + } + + impl Pus11HandlerWithStoreTester { + pub fn new() -> Self { + let test_scheduler = TestScheduler::default(); + let pool_cfg = StaticPoolConfig::new(alloc::vec![(16, 16), (8, 32), (4, 64)]); + let sched_tc_pool = StaticMemoryPool::new(pool_cfg.clone()); + let (common, srv_handler) = PusServiceHandlerWithSharedStoreCommon::new(); + Self { + common, + handler: PusService11SchedHandler::new(srv_handler, test_scheduler), + sched_tc_pool, + } + } + } + + impl PusTestHarness for Pus11HandlerWithStoreTester { + delegate! { + to self.common { + fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken; + fn read_next_tm(&mut self) -> PusTmReader<'_>; + fn check_no_tm_available(&self) -> bool; + fn check_next_verification_tm(&self, subservice: u8, expected_request_id: RequestId); + } + } + } + + #[derive(Default)] + pub struct TestScheduler { + reset_count: u32, + enabled: bool, + enabled_count: u32, + disabled_count: u32, + inserted_tcs: VecDeque, + } + + impl PusSchedulerInterface for TestScheduler { + type TimeProvider = cds::TimeProvider; + + fn reset( + &mut self, + _store: &mut (impl crate::pool::PoolProviderMemInPlace + ?Sized), + ) -> Result<(), crate::pool::StoreError> { + self.reset_count += 1; + Ok(()) + } + + fn is_enabled(&self) -> bool { + self.enabled + } + + fn enable(&mut self) { + self.enabled_count += 1; + self.enabled = true; + } + + fn disable(&mut self) { + self.disabled_count += 1; + self.enabled = false; + } + + fn insert_unwrapped_and_stored_tc( + &mut self, + _time_stamp: spacepackets::time::UnixTimestamp, + info: crate::pus::scheduler::TcInfo, + ) -> Result<(), crate::pus::scheduler::ScheduleError> { + self.inserted_tcs.push_back(info); + Ok(()) + } + } + + fn generic_subservice_send( + test_harness: &mut Pus11HandlerWithStoreTester, + subservice: Subservice, + ) { + let mut reply_header = SpHeader::tm_unseg(TEST_APID, 0, 0).unwrap(); + let tc_header = PusTcSecondaryHeader::new_simple(11, subservice as u8); + let enable_scheduling = PusTcCreator::new(&mut reply_header, tc_header, &[0; 7], true); + let token = test_harness.send_tc(&enable_scheduling); + + let request_id = token.req_id(); + test_harness + .handler + .handle_one_tc(&mut test_harness.sched_tc_pool) + .unwrap(); + test_harness.check_next_verification_tm(1, request_id); + test_harness.check_next_verification_tm(3, request_id); + test_harness.check_next_verification_tm(7, request_id); + } + + #[test] + fn test_scheduling_enabling_tc() { + let mut test_harness = Pus11HandlerWithStoreTester::new(); + test_harness.handler.scheduler_mut().disable(); + assert!(!test_harness.handler.scheduler().is_enabled()); + generic_subservice_send(&mut test_harness, Subservice::TcEnableScheduling); + assert!(test_harness.handler.scheduler().is_enabled()); + assert_eq!(test_harness.handler.scheduler().enabled_count, 1); + } + + #[test] + fn test_scheduling_disabling_tc() { + let mut test_harness = Pus11HandlerWithStoreTester::new(); + test_harness.handler.scheduler_mut().enable(); + assert!(test_harness.handler.scheduler().is_enabled()); + generic_subservice_send(&mut test_harness, Subservice::TcDisableScheduling); + assert!(!test_harness.handler.scheduler().is_enabled()); + assert_eq!(test_harness.handler.scheduler().disabled_count, 1); + } + + #[test] + fn test_reset_scheduler_tc() { + let mut test_harness = Pus11HandlerWithStoreTester::new(); + generic_subservice_send(&mut test_harness, Subservice::TcResetScheduling); + assert_eq!(test_harness.handler.scheduler().reset_count, 1); + } + + #[test] + fn test_insert_activity_tc() { + let mut test_harness = Pus11HandlerWithStoreTester::new(); + let mut reply_header = SpHeader::tm_unseg(TEST_APID, 0, 0).unwrap(); + let mut sec_header = PusTcSecondaryHeader::new_simple(17, 1); + let ping_tc = PusTcCreator::new(&mut reply_header, sec_header, &[], true); + let req_id_ping_tc = scheduler::RequestId::from_tc(&ping_tc); + let stamper = cds::TimeProvider::from_now_with_u16_days().expect("time provider failed"); + let mut sched_app_data: [u8; 64] = [0; 64]; + let mut written_len = stamper.write_to_bytes(&mut sched_app_data).unwrap(); + let ping_raw = ping_tc.to_vec().expect("generating raw tc failed"); + sched_app_data[written_len..written_len + ping_raw.len()].copy_from_slice(&ping_raw); + written_len += ping_raw.len(); + reply_header = SpHeader::tm_unseg(TEST_APID, 1, 0).unwrap(); + sec_header = PusTcSecondaryHeader::new_simple(11, Subservice::TcInsertActivity as u8); + let enable_scheduling = PusTcCreator::new( + &mut reply_header, + sec_header, + &sched_app_data[..written_len], + true, + ); + let token = test_harness.send_tc(&enable_scheduling); + + let request_id = token.req_id(); + test_harness + .handler + .handle_one_tc(&mut test_harness.sched_tc_pool) + .unwrap(); + test_harness.check_next_verification_tm(1, request_id); + test_harness.check_next_verification_tm(3, request_id); + test_harness.check_next_verification_tm(7, request_id); + let tc_info = test_harness + .handler + .scheduler_mut() + .inserted_tcs + .pop_front() + .unwrap(); + assert_eq!(tc_info.request_id(), req_id_ping_tc); } } diff --git a/satrs-core/src/pus/test.rs b/satrs-core/src/pus/test.rs index c205178..bdaed69 100644 --- a/satrs-core/src/pus/test.rs +++ b/satrs-core/src/pus/test.rs @@ -1,67 +1,45 @@ -use crate::pool::{SharedPool, StoreAddr}; -use crate::pus::verification::{StdVerifReporterWithSender, TcStateAccepted, VerificationToken}; use crate::pus::{ - EcssTcReceiver, EcssTmSender, PartialPusHandlingError, PusPacketHandlerResult, - PusPacketHandlingError, PusServiceBase, PusServiceHandler, PusTmWrapper, + PartialPusHandlingError, PusPacketHandlerResult, PusPacketHandlingError, PusTmWrapper, }; -use spacepackets::ecss::tc::PusTcReader; use spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader}; use spacepackets::ecss::PusPacket; use spacepackets::SpHeader; -use std::boxed::Box; + +use super::{EcssTcInMemConverter, PusServiceBase, PusServiceHelper}; /// This is a helper class for [std] environments to handle generic PUS 17 (test service) packets. /// This handler only processes ping requests and generates a ping reply for them accordingly. -pub struct PusService17TestHandler { - psb: PusServiceBase, +pub struct PusService17TestHandler { + pub service_helper: PusServiceHelper, } -impl PusService17TestHandler { - pub fn new( - tc_receiver: Box, - shared_tc_store: SharedPool, - tm_sender: Box, - tm_apid: u16, - verification_handler: StdVerifReporterWithSender, - ) -> Self { - Self { - psb: PusServiceBase::new( - tc_receiver, - shared_tc_store, - tm_sender, - tm_apid, - verification_handler, - ), +impl PusService17TestHandler { + pub fn new(service_helper: PusServiceHelper) -> Self { + Self { service_helper } + } + + pub fn handle_one_tc(&mut self) -> Result { + let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?; + if possible_packet.is_none() { + return Ok(PusPacketHandlerResult::Empty); } - } -} - -impl PusServiceHandler for PusService17TestHandler { - fn psb_mut(&mut self) -> &mut PusServiceBase { - &mut self.psb - } - fn psb(&self) -> &PusServiceBase { - &self.psb - } - - fn handle_one_tc( - &mut self, - addr: StoreAddr, - token: VerificationToken, - ) -> Result { - self.copy_tc_to_buf(addr)?; - let (tc, _) = PusTcReader::new(&self.psb.pus_buf)?; + let ecss_tc_and_token = possible_packet.unwrap(); + let tc = self + .service_helper + .tc_in_mem_converter + .convert_ecss_tc_in_memory_to_reader(&ecss_tc_and_token.tc_in_memory)?; if tc.service() != 17 { return Err(PusPacketHandlingError::WrongService(tc.service())); } if tc.subservice() == 1 { let mut partial_error = None; - let time_stamp = self.psb().get_current_timestamp(&mut partial_error); + let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error); let result = self - .psb + .service_helper + .common .verification_handler .get_mut() - .start_success(token, Some(&time_stamp)) + .start_success(ecss_tc_and_token.token, Some(&time_stamp)) .map_err(|_| PartialPusHandlingError::Verification); let start_token = if let Ok(result) = result { Some(result) @@ -70,11 +48,13 @@ impl PusServiceHandler for PusService17TestHandler { None }; // Sequence count will be handled centrally in TM funnel. - let mut reply_header = SpHeader::tm_unseg(self.psb.tm_apid, 0, 0).unwrap(); + let mut reply_header = + SpHeader::tm_unseg(self.service_helper.common.tm_apid, 0, 0).unwrap(); let tc_header = PusTmSecondaryHeader::new_simple(17, 2, &time_stamp); let ping_reply = PusTmCreator::new(&mut reply_header, tc_header, &[], true); let result = self - .psb + .service_helper + .common .tm_sender .send_tm(PusTmWrapper::Direct(ping_reply)) .map_err(PartialPusHandlingError::TmSend); @@ -84,7 +64,8 @@ impl PusServiceHandler for PusService17TestHandler { if let Some(start_token) = start_token { if self - .psb + .service_helper + .common .verification_handler .get_mut() .completion_success(start_token, Some(&time_stamp)) @@ -98,121 +79,194 @@ impl PusServiceHandler for PusService17TestHandler { partial_error, )); }; - return Ok(PusPacketHandlerResult::RequestHandled); + } else { + return Ok(PusPacketHandlerResult::CustomSubservice( + tc.subservice(), + ecss_tc_and_token.token, + )); } - Ok(PusPacketHandlerResult::CustomSubservice( - tc.subservice(), - token, - )) + Ok(PusPacketHandlerResult::RequestHandled) } } #[cfg(test)] mod tests { - use crate::pool::{LocalPool, PoolCfg, SharedPool}; - use crate::pus::test::PusService17TestHandler; - use crate::pus::verification::{ - RequestId, StdVerifReporterWithSender, VerificationReporterCfg, + use crate::pus::tests::{ + PusServiceHandlerWithSharedStoreCommon, PusServiceHandlerWithVecCommon, PusTestHarness, + SimplePusPacketHandler, TEST_APID, }; - use crate::pus::{MpscTcInStoreReceiver, MpscTmInStoreSender, PusServiceHandler}; - use crate::tmtc::tm_helper::SharedTmStore; + use crate::pus::verification::RequestId; + use crate::pus::verification::{TcStateAccepted, VerificationToken}; + use crate::pus::{ + EcssTcInSharedStoreConverter, EcssTcInVecConverter, PusPacketHandlerResult, + PusPacketHandlingError, + }; + use delegate::delegate; use spacepackets::ecss::tc::{PusTcCreator, PusTcSecondaryHeader}; use spacepackets::ecss::tm::PusTmReader; - use spacepackets::ecss::{PusPacket, WritablePusPacket}; + use spacepackets::ecss::PusPacket; use spacepackets::{SequenceFlags, SpHeader}; - use std::boxed::Box; - use std::sync::{mpsc, RwLock}; - use std::vec; - const TEST_APID: u16 = 0x101; + use super::PusService17TestHandler; - #[test] - fn test_basic_ping_processing() { - let mut pus_buf: [u8; 64] = [0; 64]; - let pool_cfg = PoolCfg::new(vec![(16, 16), (8, 32), (4, 64)]); - let tc_pool = LocalPool::new(pool_cfg.clone()); - let tm_pool = LocalPool::new(pool_cfg); - let tc_pool_shared = SharedPool::new(RwLock::new(Box::new(tc_pool))); - let shared_tm_store = SharedTmStore::new(Box::new(tm_pool)); - let tm_pool_shared = shared_tm_store.clone_backing_pool(); - let (test_srv_tc_tx, test_srv_tc_rx) = mpsc::channel(); - let (tm_tx, tm_rx) = mpsc::channel(); - let verif_sender = - MpscTmInStoreSender::new(0, "verif_sender", shared_tm_store.clone(), tm_tx.clone()); - let verif_cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap(); - let mut verification_handler = - StdVerifReporterWithSender::new(&verif_cfg, Box::new(verif_sender)); - let test_srv_tm_sender = MpscTmInStoreSender::new(0, "TEST_SENDER", shared_tm_store, tm_tx); - let test_srv_tc_receiver = MpscTcInStoreReceiver::new(0, "TEST_RECEIVER", test_srv_tc_rx); - let mut pus_17_handler = PusService17TestHandler::new( - Box::new(test_srv_tc_receiver), - tc_pool_shared.clone(), - Box::new(test_srv_tm_sender), - TEST_APID, - verification_handler.clone(), - ); + struct Pus17HandlerWithStoreTester { + common: PusServiceHandlerWithSharedStoreCommon, + handler: PusService17TestHandler, + } + + impl Pus17HandlerWithStoreTester { + pub fn new() -> Self { + let (common, srv_handler) = PusServiceHandlerWithSharedStoreCommon::new(); + let pus_17_handler = PusService17TestHandler::new(srv_handler); + Self { + common, + handler: pus_17_handler, + } + } + } + + impl PusTestHarness for Pus17HandlerWithStoreTester { + delegate! { + to self.common { + fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken; + fn read_next_tm(&mut self) -> PusTmReader<'_>; + fn check_no_tm_available(&self) -> bool; + fn check_next_verification_tm( + &self, + subservice: u8, + expected_request_id: RequestId + ); + } + } + } + impl SimplePusPacketHandler for Pus17HandlerWithStoreTester { + delegate! { + to self.handler { + fn handle_one_tc(&mut self) -> Result; + } + } + } + + struct Pus17HandlerWithVecTester { + common: PusServiceHandlerWithVecCommon, + handler: PusService17TestHandler, + } + + impl Pus17HandlerWithVecTester { + pub fn new() -> Self { + let (common, srv_handler) = PusServiceHandlerWithVecCommon::new(); + Self { + common, + handler: PusService17TestHandler::new(srv_handler), + } + } + } + + impl PusTestHarness for Pus17HandlerWithVecTester { + delegate! { + to self.common { + fn send_tc(&mut self, tc: &PusTcCreator) -> VerificationToken; + fn read_next_tm(&mut self) -> PusTmReader<'_>; + fn check_no_tm_available(&self) -> bool; + fn check_next_verification_tm( + &self, + subservice: u8, + expected_request_id: RequestId, + ); + } + } + } + impl SimplePusPacketHandler for Pus17HandlerWithVecTester { + delegate! { + to self.handler { + fn handle_one_tc(&mut self) -> Result; + } + } + } + + fn ping_test(test_harness: &mut (impl PusTestHarness + SimplePusPacketHandler)) { // Create a ping TC, verify acceptance. let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap(); let sec_header = PusTcSecondaryHeader::new_simple(17, 1); let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true); - let token = verification_handler.add_tc(&ping_tc); - let token = verification_handler - .acceptance_success(token, None) - .unwrap(); - let tc_size = ping_tc.write_to_bytes(&mut pus_buf).unwrap(); - let mut tc_pool = tc_pool_shared.write().unwrap(); - let addr = tc_pool.add(&pus_buf[..tc_size]).unwrap(); - drop(tc_pool); - // Send accepted TC to test service handler. - test_srv_tc_tx.send((addr, token.into())).unwrap(); - let result = pus_17_handler.handle_next_packet(); + let token = test_harness.send_tc(&ping_tc); + let request_id = token.req_id(); + let result = test_harness.handle_one_tc(); assert!(result.is_ok()); // We should see 4 replies in the TM queue now: Acceptance TM, Start TM, ping reply and // Completion TM - let mut next_msg = tm_rx.try_recv(); - assert!(next_msg.is_ok()); - let mut tm_addr = next_msg.unwrap(); - let tm_pool = tm_pool_shared.read().unwrap(); - let tm_raw = tm_pool.read(&tm_addr).unwrap(); - let (tm, _) = PusTmReader::new(&tm_raw, 0).unwrap(); - assert_eq!(tm.service(), 1); - assert_eq!(tm.subservice(), 1); - let req_id = RequestId::from_bytes(tm.user_data()).expect("generating request ID failed"); - assert_eq!(req_id, token.req_id()); // Acceptance TM - next_msg = tm_rx.try_recv(); - assert!(next_msg.is_ok()); - tm_addr = next_msg.unwrap(); - let tm_raw = tm_pool.read(&tm_addr).unwrap(); - // Is generated with CDS short timestamp. - let (tm, _) = PusTmReader::new(&tm_raw, 7).unwrap(); - assert_eq!(tm.service(), 1); - assert_eq!(tm.subservice(), 3); - let req_id = RequestId::from_bytes(tm.user_data()).expect("generating request ID failed"); - assert_eq!(req_id, token.req_id()); + test_harness.check_next_verification_tm(1, request_id); + + // Start TM + test_harness.check_next_verification_tm(3, request_id); // Ping reply - next_msg = tm_rx.try_recv(); - assert!(next_msg.is_ok()); - tm_addr = next_msg.unwrap(); - let tm_raw = tm_pool.read(&tm_addr).unwrap(); - // Is generated with CDS short timestamp. - let (tm, _) = PusTmReader::new(&tm_raw, 7).unwrap(); + let tm = test_harness.read_next_tm(); assert_eq!(tm.service(), 17); assert_eq!(tm.subservice(), 2); assert!(tm.user_data().is_empty()); // TM completion - next_msg = tm_rx.try_recv(); - assert!(next_msg.is_ok()); - tm_addr = next_msg.unwrap(); - let tm_raw = tm_pool.read(&tm_addr).unwrap(); - // Is generated with CDS short timestamp. - let (tm, _) = PusTmReader::new(&tm_raw, 7).unwrap(); - assert_eq!(tm.service(), 1); - assert_eq!(tm.subservice(), 7); - let req_id = RequestId::from_bytes(tm.user_data()).expect("generating request ID failed"); - assert_eq!(req_id, token.req_id()); + test_harness.check_next_verification_tm(7, request_id); + } + + #[test] + fn test_basic_ping_processing_using_store() { + let mut test_harness = Pus17HandlerWithStoreTester::new(); + ping_test(&mut test_harness); + } + + #[test] + fn test_basic_ping_processing_using_vec() { + let mut test_harness = Pus17HandlerWithVecTester::new(); + ping_test(&mut test_harness); + } + + #[test] + fn test_empty_tc_queue() { + let mut test_harness = Pus17HandlerWithStoreTester::new(); + let result = test_harness.handle_one_tc(); + assert!(result.is_ok()); + let result = result.unwrap(); + if let PusPacketHandlerResult::Empty = result { + } else { + panic!("unexpected result type {result:?}") + } + } + + #[test] + fn test_sending_unsupported_service() { + let mut test_harness = Pus17HandlerWithStoreTester::new(); + let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap(); + let sec_header = PusTcSecondaryHeader::new_simple(3, 1); + let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true); + test_harness.send_tc(&ping_tc); + let result = test_harness.handle_one_tc(); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let PusPacketHandlingError::WrongService(num) = error { + assert_eq!(num, 3); + } else { + panic!("unexpected error type {error}") + } + } + + #[test] + fn test_sending_custom_subservice() { + let mut test_harness = Pus17HandlerWithStoreTester::new(); + let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap(); + let sec_header = PusTcSecondaryHeader::new_simple(17, 200); + let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true); + test_harness.send_tc(&ping_tc); + let result = test_harness.handle_one_tc(); + assert!(result.is_ok()); + let result = result.unwrap(); + if let PusPacketHandlerResult::CustomSubservice(subservice, _) = result { + assert_eq!(subservice, 200); + } else { + panic!("unexpected result type {result:?}") + } } } diff --git a/satrs-core/src/pus/verification.rs b/satrs-core/src/pus/verification.rs index ff38b89..6407bf3 100644 --- a/satrs-core/src/pus/verification.rs +++ b/satrs-core/src/pus/verification.rs @@ -15,7 +15,7 @@ //! ``` //! use std::sync::{Arc, mpsc, RwLock}; //! use std::time::Duration; -//! use satrs_core::pool::{LocalPool, PoolCfg, PoolProvider, SharedPool}; +//! use satrs_core::pool::{PoolProviderMemInPlaceWithGuards, StaticMemoryPool, StaticPoolConfig}; //! use satrs_core::pus::verification::{VerificationReporterCfg, VerificationReporterWithSender}; //! use satrs_core::seq_count::SeqCountProviderSimple; //! use satrs_core::pus::MpscTmInStoreSender; @@ -28,9 +28,9 @@ //! const EMPTY_STAMP: [u8; 7] = [0; 7]; //! const TEST_APID: u16 = 0x02; //! -//! let pool_cfg = PoolCfg::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]); -//! let tm_pool = LocalPool::new(pool_cfg.clone()); -//! let shared_tm_store = SharedTmStore::new(Box::new(tm_pool)); +//! let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]); +//! let tm_pool = StaticMemoryPool::new(pool_cfg.clone()); +//! let shared_tm_store = SharedTmStore::new(tm_pool); //! let tm_store = shared_tm_store.clone_backing_pool(); //! let (verif_tx, verif_rx) = mpsc::channel(); //! let sender = MpscTmInStoreSender::new(0, "Test Sender", shared_tm_store, verif_tx); @@ -208,6 +208,8 @@ impl WasAtLeastAccepted for TcStateAccepted {} impl WasAtLeastAccepted for TcStateStarted {} impl WasAtLeastAccepted for TcStateCompleted {} +/// Token wrapper to model all possible verification tokens. These tokens are used to +/// enforce the correct order for the verification steps when doing verification reporting. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum TcStateToken { None(VerificationToken), @@ -1323,7 +1325,7 @@ mod std_mod { #[cfg(test)] mod tests { - use crate::pool::{LocalPool, PoolCfg}; + use crate::pool::{PoolProviderMemInPlaceWithGuards, StaticMemoryPool, StaticPoolConfig}; use crate::pus::tests::CommonTmInfo; use crate::pus::verification::{ EcssTmSenderCore, EcssTmtcError, FailParams, FailParamsWithStep, RequestId, TcStateNone, @@ -1447,12 +1449,11 @@ mod tests { fn base_init(api_sel: bool) -> (TestBase<'static>, VerificationToken) { let mut reporter = base_reporter(); let (tc, req_id) = base_tc_init(None); - let init_tok; - if api_sel { - init_tok = reporter.add_tc_with_req_id(req_id); + let init_tok = if api_sel { + reporter.add_tc_with_req_id(req_id) } else { - init_tok = reporter.add_tc(&tc); - } + reporter.add_tc(&tc) + }; (TestBase { vr: reporter, tc }, init_tok) } @@ -1475,7 +1476,7 @@ mod tests { time_stamp: EMPTY_STAMP, }, additional_data: None, - req_id: req_id.clone(), + req_id: *req_id, }; let mut service_queue = sender.service_queue.borrow_mut(); assert_eq!(service_queue.len(), 1); @@ -1485,9 +1486,8 @@ mod tests { #[test] fn test_mpsc_verif_send_sync() { - let pool = LocalPool::new(PoolCfg::new(vec![(8, 8)])); - let tm_store = Box::new(pool); - let shared_tm_store = SharedTmStore::new(tm_store); + let pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![(8, 8)])); + let shared_tm_store = SharedTmStore::new(pool); let (tx, _) = mpsc::channel(); let mpsc_verif_sender = MpscTmInStoreSender::new(0, "verif_sender", shared_tm_store, tx); is_send(&mpsc_verif_sender); @@ -1505,7 +1505,7 @@ mod tests { fn test_basic_acceptance_success() { let (b, tok) = base_init(false); let mut sender = TestSender::default(); - b.vr.acceptance_success(tok, &mut sender, Some(&EMPTY_STAMP)) + b.vr.acceptance_success(tok, &sender, Some(&EMPTY_STAMP)) .expect("Sending acceptance success failed"); acceptance_check(&mut sender, &tok.req_id); } @@ -1605,7 +1605,7 @@ mod tests { #[test] fn test_basic_acceptance_failure_with_fail_data() { let (b, tok) = base_init(false); - let mut sender = TestSender::default(); + let sender = TestSender::default(); let fail_code = EcssEnumU8::new(10); let fail_data = EcssEnumU32::new(12); let mut fail_data_raw = [0; 4]; @@ -1615,7 +1615,7 @@ mod tests { &fail_code, Some(fail_data_raw.as_slice()), ); - b.vr.acceptance_failure(tok, &mut sender, fail_params) + b.vr.acceptance_failure(tok, &sender, fail_params) .expect("Sending acceptance success failed"); let cmp_info = TmInfo { common: CommonTmInfo { @@ -1784,8 +1784,7 @@ mod tests { .rep() .start_success(accepted_token, &mut sender, Some(&[0, 1, 0, 1, 0, 1, 0])) .expect("Sending start success failed"); - let mut empty = b - .rep() + b.rep() .step_success( &started_token, &mut sender, @@ -1793,16 +1792,13 @@ mod tests { EcssEnumU8::new(0), ) .expect("Sending step 0 success failed"); - assert_eq!(empty, ()); - empty = - b.vr.step_success( - &started_token, - &mut sender, - Some(&EMPTY_STAMP), - EcssEnumU8::new(1), - ) - .expect("Sending step 1 success failed"); - assert_eq!(empty, ()); + b.vr.step_success( + &started_token, + &mut sender, + Some(&EMPTY_STAMP), + EcssEnumU8::new(1), + ) + .expect("Sending step 1 success failed"); assert_eq!(sender.service_queue.borrow().len(), 4); step_success_check(&mut sender, tok.req_id); } @@ -1818,16 +1814,12 @@ mod tests { .helper .start_success(accepted_token, Some(&[0, 1, 0, 1, 0, 1, 0])) .expect("Sending start success failed"); - let mut empty = b - .helper + b.helper .step_success(&started_token, Some(&EMPTY_STAMP), EcssEnumU8::new(0)) .expect("Sending step 0 success failed"); - assert_eq!(empty, ()); - empty = b - .helper + b.helper .step_success(&started_token, Some(&EMPTY_STAMP), EcssEnumU8::new(1)) .expect("Sending step 1 success failed"); - assert_eq!(empty, ()); let sender: &mut TestSender = b.helper.sender.downcast_mut().unwrap(); assert_eq!(sender.service_queue.borrow().len(), 4); step_success_check(sender, tok.req_id); @@ -2122,10 +2114,8 @@ mod tests { let started_token = b.vr.start_success(accepted_token, &mut sender, Some(&[0, 1, 0, 1, 0, 1, 0])) .expect("Sending start success failed"); - let empty = - b.vr.completion_success(started_token, &mut sender, Some(&EMPTY_STAMP)) - .expect("Sending completion success failed"); - assert_eq!(empty, ()); + b.vr.completion_success(started_token, &mut sender, Some(&EMPTY_STAMP)) + .expect("Sending completion success failed"); completion_success_check(&mut sender, tok.req_id); } @@ -2140,11 +2130,9 @@ mod tests { .helper .start_success(accepted_token, Some(&[0, 1, 0, 1, 0, 1, 0])) .expect("Sending start success failed"); - let empty = b - .helper + b.helper .completion_success(started_token, Some(&EMPTY_STAMP)) .expect("Sending completion success failed"); - assert_eq!(empty, ()); let sender: &mut TestSender = b.helper.sender.downcast_mut().unwrap(); completion_success_check(sender, tok.req_id); } @@ -2152,8 +2140,8 @@ mod tests { #[test] // TODO: maybe a bit more extensive testing, all I have time for right now fn test_seq_count_increment() { - let pool_cfg = PoolCfg::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]); - let tm_pool = Box::new(LocalPool::new(pool_cfg.clone())); + let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]); + let tm_pool = StaticMemoryPool::new(pool_cfg.clone()); let shared_tm_store = SharedTmStore::new(tm_pool); let shared_tm_pool = shared_tm_store.clone_backing_pool(); let (verif_tx, verif_rx) = mpsc::channel(); diff --git a/satrs-core/src/tmtc/tm_helper.rs b/satrs-core/src/tmtc/tm_helper.rs index 6dca7ee..fc0a8e9 100644 --- a/satrs-core/src/tmtc/tm_helper.rs +++ b/satrs-core/src/tmtc/tm_helper.rs @@ -8,7 +8,9 @@ pub use std_mod::*; #[cfg(feature = "std")] pub mod std_mod { - use crate::pool::{ShareablePoolProvider, SharedPool, StoreAddr}; + use crate::pool::{ + PoolProviderMemInPlace, SharedStaticMemoryPool, StaticMemoryPool, StoreAddr, + }; use crate::pus::EcssTmtcError; use spacepackets::ecss::tm::PusTmCreator; use spacepackets::ecss::WritablePusPacket; @@ -16,22 +18,25 @@ pub mod std_mod { #[derive(Clone)] pub struct SharedTmStore { - pool: SharedPool, + pub shared_pool: SharedStaticMemoryPool, } impl SharedTmStore { - pub fn new(backing_pool: ShareablePoolProvider) -> Self { + pub fn new(shared_pool: StaticMemoryPool) -> Self { Self { - pool: Arc::new(RwLock::new(backing_pool)), + shared_pool: Arc::new(RwLock::new(shared_pool)), } } - pub fn clone_backing_pool(&self) -> SharedPool { - self.pool.clone() + pub fn clone_backing_pool(&self) -> SharedStaticMemoryPool { + self.shared_pool.clone() } pub fn add_pus_tm(&self, pus_tm: &PusTmCreator) -> Result { - let mut pg = self.pool.write().map_err(|_| EcssTmtcError::StoreLock)?; + let mut pg = self + .shared_pool + .write() + .map_err(|_| EcssTmtcError::StoreLock)?; let (addr, buf) = pg.free_element(pus_tm.len_written())?; pus_tm .write_to_bytes(buf) @@ -91,3 +96,33 @@ impl PusTmWithCdsShortHelper { PusTmCreator::new(&mut reply_header, tc_header, source_data, true) } } + +#[cfg(test)] +mod tests { + use spacepackets::{ecss::PusPacket, time::cds::TimeProvider, CcsdsPacket}; + + use super::PusTmWithCdsShortHelper; + + #[test] + fn test_helper_with_stamper() { + let mut pus_tm_helper = PusTmWithCdsShortHelper::new(0x123); + let stamper = TimeProvider::new_with_u16_days(0, 0); + let tm = pus_tm_helper.create_pus_tm_with_stamper(17, 1, &[1, 2, 3, 4], &stamper, 25); + assert_eq!(tm.service(), 17); + assert_eq!(tm.subservice(), 1); + assert_eq!(tm.user_data(), &[1, 2, 3, 4]); + assert_eq!(tm.seq_count(), 25); + assert_eq!(tm.timestamp(), [64, 0, 0, 0, 0, 0, 0]) + } + + #[test] + fn test_helper_from_now() { + let mut pus_tm_helper = PusTmWithCdsShortHelper::new(0x123); + let tm = pus_tm_helper.create_pus_tm_timestamp_now(17, 1, &[1, 2, 3, 4], 25); + assert_eq!(tm.service(), 17); + assert_eq!(tm.subservice(), 1); + assert_eq!(tm.user_data(), &[1, 2, 3, 4]); + assert_eq!(tm.seq_count(), 25); + assert_eq!(tm.timestamp().len(), 7); + } +} diff --git a/satrs-core/tests/pools.rs b/satrs-core/tests/pools.rs index 375d624..cce2114 100644 --- a/satrs-core/tests/pools.rs +++ b/satrs-core/tests/pools.rs @@ -1,4 +1,6 @@ -use satrs_core::pool::{LocalPool, PoolCfg, PoolGuard, PoolProvider, StoreAddr}; +use satrs_core::pool::{ + PoolGuard, PoolProviderMemInPlace, StaticMemoryPool, StaticPoolConfig, StoreAddr, +}; use std::ops::DerefMut; use std::sync::mpsc; use std::sync::mpsc::{Receiver, Sender}; @@ -9,8 +11,8 @@ const DUMMY_DATA: [u8; 4] = [0, 1, 2, 3]; #[test] fn threaded_usage() { - let pool_cfg = PoolCfg::new(vec![(16, 6), (32, 3), (8, 12)]); - let shared_pool = Arc::new(RwLock::new(LocalPool::new(pool_cfg))); + let pool_cfg = StaticPoolConfig::new(vec![(16, 6), (32, 3), (8, 12)]); + let shared_pool = Arc::new(RwLock::new(StaticMemoryPool::new(pool_cfg))); let shared_clone = shared_pool.clone(); let (tx, rx): (Sender, Receiver) = mpsc::channel(); let jh0 = thread::spawn(move || { diff --git a/satrs-core/tests/pus_verification.rs b/satrs-core/tests/pus_verification.rs index 9c9fc63..a62b7a5 100644 --- a/satrs-core/tests/pus_verification.rs +++ b/satrs-core/tests/pus_verification.rs @@ -1,7 +1,10 @@ #[cfg(feature = "crossbeam")] pub mod crossbeam_test { use hashbrown::HashMap; - use satrs_core::pool::{LocalPool, PoolCfg, PoolProvider}; + use satrs_core::pool::{ + PoolProviderMemInPlace, PoolProviderMemInPlaceWithGuards, StaticMemoryPool, + StaticPoolConfig, + }; use satrs_core::pus::verification::{ FailParams, RequestId, VerificationReporterCfg, VerificationReporterWithSender, }; @@ -33,9 +36,9 @@ pub mod crossbeam_test { // each reporter have an own sequence count provider. let cfg = VerificationReporterCfg::new(TEST_APID, 1, 2, 8).unwrap(); // Shared pool object to store the verification PUS telemetry - let pool_cfg = PoolCfg::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]); - let shared_tm_store = SharedTmStore::new(Box::new(LocalPool::new(pool_cfg.clone()))); - let shared_tc_pool_0 = Arc::new(RwLock::new(LocalPool::new(pool_cfg))); + let pool_cfg = StaticPoolConfig::new(vec![(10, 32), (10, 64), (10, 128), (10, 1024)]); + let shared_tm_store = SharedTmStore::new(StaticMemoryPool::new(pool_cfg.clone())); + let shared_tc_pool_0 = Arc::new(RwLock::new(StaticMemoryPool::new(pool_cfg))); let shared_tc_pool_1 = shared_tc_pool_0.clone(); let (tx, rx) = crossbeam_channel::bounded(10); let sender = diff --git a/satrs-example/pyclient/.gitignore b/satrs-example/pyclient/.gitignore index 894ba5d..d994678 100644 --- a/satrs-example/pyclient/.gitignore +++ b/satrs-example/pyclient/.gitignore @@ -6,3 +6,4 @@ __pycache__ !/.idea/runConfigurations /seqcnt.txt +/.tmtc-history.txt diff --git a/satrs-example/pyclient/common.py b/satrs-example/pyclient/common.py index c2d0777..8f57e54 100644 --- a/satrs-example/pyclient/common.py +++ b/satrs-example/pyclient/common.py @@ -44,10 +44,6 @@ class AcsHkIds(enum.IntEnum): MGM_SET = 1 -class HkOpCodes: - GENERATE_ONE_SHOT = ["0", "oneshot"] - - def make_addressable_id(target_id: int, unique_id: int) -> bytes: byte_string = bytearray(struct.pack("!I", target_id)) byte_string.extend(struct.pack("!I", unique_id)) diff --git a/satrs-example/pyclient/main.py b/satrs-example/pyclient/main.py index c749e26..66a41e4 100755 --- a/satrs-example/pyclient/main.py +++ b/satrs-example/pyclient/main.py @@ -4,6 +4,8 @@ import logging import sys import time from typing import Optional +from prompt_toolkit.history import History +from prompt_toolkit.history import FileHistory import tmtccmd from spacepackets.ecss import PusTelemetry, PusVerificator @@ -11,16 +13,16 @@ from spacepackets.ecss.pus_17_test import Service17Tm from spacepackets.ecss.pus_1_verification import UnpackParams, Service1Tm from spacepackets.ccsds.time import CdsShortTimestamp -from tmtccmd import CcsdsTmtcBackend, TcHandlerBase, ProcedureParamsWrapper +from tmtccmd import TcHandlerBase, ProcedureParamsWrapper from tmtccmd.core.base import BackendRequest from tmtccmd.pus import VerificationWrapper from tmtccmd.tmtc import CcsdsTmHandler, SpecificApidHandlerBase from tmtccmd.com import ComInterface from tmtccmd.config import ( + CmdTreeNode, default_json_path, SetupParams, HookBase, - TmtcDefinitionWrapper, params_to_procedure_conversion, ) from tmtccmd.config import PreArgsParsingWrapper, SetupWrapper @@ -39,12 +41,11 @@ from tmtccmd.tmtc import ( DefaultPusQueueHelper, QueueWrapper, ) -from tmtccmd.util import FileSeqCountProvider, PusFileSeqCountProvider +from spacepackets.seqcount import FileSeqCountProvider, PusFileSeqCountProvider from tmtccmd.util.obj_id import ObjectIdDictT import pus_tc -import tc_definitions from common import EXAMPLE_PUS_APID, TM_PACKET_IDS, EventU32 _LOGGER = logging.getLogger() @@ -54,25 +55,29 @@ class SatRsConfigHook(HookBase): def __init__(self, json_cfg_path: str): super().__init__(json_cfg_path=json_cfg_path) - def assign_communication_interface(self, com_if_key: str) -> Optional[ComInterface]: + def get_communication_interface(self, com_if_key: str) -> Optional[ComInterface]: from tmtccmd.config.com import ( create_com_interface_default, create_com_interface_cfg_default, ) + assert self.cfg_path is not None cfg = create_com_interface_cfg_default( com_if_key=com_if_key, json_cfg_path=self.cfg_path, space_packet_ids=TM_PACKET_IDS, ) + assert cfg is not None return create_com_interface_default(cfg) - def get_tmtc_definitions(self) -> TmtcDefinitionWrapper: - return tc_definitions.tc_definitions() + def get_command_definitions(self) -> CmdTreeNode: + """This function should return the root node of the command definition tree.""" + return pus_tc.create_cmd_definition_tree() - def perform_mode_operation(self, tmtc_backend: CcsdsTmtcBackend, mode: int): - _LOGGER.info("Mode operation hook was called") - pass + def get_cmd_history(self) -> Optional[History]: + """Optionlly return a history class for the past command paths which will be used + when prompting a command path from the user in CLI mode.""" + return FileHistory(".tmtc-history.txt") def get_object_ids(self) -> ObjectIdDictT: from tmtccmd.config.objects import get_core_object_ids @@ -94,15 +99,12 @@ class PusHandler(SpecificApidHandlerBase): def handle_tm(self, packet: bytes, _user_args: any): try: - tm_packet = PusTelemetry.unpack( - packet, time_reader=CdsShortTimestamp.empty() - ) + pus_tm = PusTelemetry.unpack(packet, time_reader=CdsShortTimestamp.empty()) except ValueError as e: _LOGGER.warning("Could not generate PUS TM object from raw data") _LOGGER.warning(f"Raw Packet: [{packet.hex(sep=',')}], REPR: {packet!r}") raise e - service = tm_packet.service - dedicated_handler = False + service = pus_tm.service if service == 1: tm_packet = Service1Tm.unpack( data=packet, params=UnpackParams(CdsShortTimestamp.empty(), 1, 2) @@ -119,8 +121,7 @@ class PusHandler(SpecificApidHandlerBase): else: self.verif_wrapper.log_to_console(tm_packet, res) self.verif_wrapper.log_to_file(tm_packet, res) - dedicated_handler = True - if service == 3: + elif service == 3: _LOGGER.info("No handling for HK packets implemented") _LOGGER.info(f"Raw packet: 0x[{packet.hex(sep=',')}]") pus_tm = PusTelemetry.unpack(packet, time_reader=CdsShortTimestamp.empty()) @@ -129,8 +130,7 @@ class PusHandler(SpecificApidHandlerBase): raise ValueError("No addressable ID in HK packet") json_str = pus_tm.source_data[8:] _LOGGER.info(json_str) - dedicated_handler = True - if service == 5: + elif service == 5: tm_packet = PusTelemetry.unpack( packet, time_reader=CdsShortTimestamp.empty() ) @@ -139,11 +139,10 @@ class PusHandler(SpecificApidHandlerBase): _LOGGER.info(f"Received event packet. Event: {event_u32}") if event_u32.group_id == 0 and event_u32.unique_id == 0: _LOGGER.info("Received test event") - if service == 17: + elif service == 17: tm_packet = Service17Tm.unpack( packet, time_reader=CdsShortTimestamp.empty() ) - dedicated_handler = True if tm_packet.subservice == 2: self.file_logger.info("Received Ping Reply TM[17,2]") _LOGGER.info("Received Ping Reply TM[17,2]") @@ -154,17 +153,14 @@ class PusHandler(SpecificApidHandlerBase): _LOGGER.info( f"Received Test Packet with unknown subservice {tm_packet.subservice}" ) - if tm_packet is None: + else: _LOGGER.info( f"The service {service} is not implemented in Telemetry Factory" ) tm_packet = PusTelemetry.unpack( packet, time_reader=CdsShortTimestamp.empty() ) - self.raw_logger.log_tm(tm_packet) - if not dedicated_handler and tm_packet is not None: - pass - # self.printer.handle_long_tm_print(packet_if=tm_packet, info_if=tm_packet) + self.raw_logger.log_tm(pus_tm) class TcHandler(TcHandlerBase): @@ -196,22 +192,18 @@ class TcHandler(TcHandlerBase): log_entry = entry_helper.to_log_entry() _LOGGER.info(log_entry.log_str) - def queue_finished_cb(self, helper: ProcedureWrapper): - if helper.proc_type == TcProcedureType.DEFAULT: - def_proc = helper.to_def_procedure() - _LOGGER.info( - f"Queue handling finished for service {def_proc.service} and " - f"op code {def_proc.op_code}" - ) + def queue_finished_cb(self, info: ProcedureWrapper): + if info.proc_type == TcProcedureType.DEFAULT: + def_proc = info.to_def_procedure() + _LOGGER.info(f"Queue handling finished for command {def_proc.cmd_path}") - def feed_cb(self, helper: ProcedureWrapper, wrapper: FeedWrapper): + def feed_cb(self, info: ProcedureWrapper, wrapper: FeedWrapper): q = self.queue_helper q.queue_wrapper = wrapper.queue_wrapper - if helper.proc_type == TcProcedureType.DEFAULT: - def_proc = helper.to_def_procedure() - service = def_proc.service - op_code = def_proc.op_code - pus_tc.pack_pus_telecommands(q, service, op_code) + if info.proc_type == TcProcedureType.DEFAULT: + def_proc = info.to_def_procedure() + assert def_proc.cmd_path is not None + pus_tc.pack_pus_telecommands(q, def_proc.cmd_path) def main(): diff --git a/satrs-example/pyclient/pus_tc.py b/satrs-example/pyclient/pus_tc.py index 9996e5b..f73b755 100644 --- a/satrs-example/pyclient/pus_tc.py +++ b/satrs-example/pyclient/pus_tc.py @@ -1,50 +1,85 @@ import datetime +import logging from spacepackets.ccsds import CdsShortTimestamp from spacepackets.ecss import PusTelecommand -from tmtccmd.config import CoreServiceList +from tmtccmd.config import CmdTreeNode from tmtccmd.tmtc import DefaultPusQueueHelper from tmtccmd.pus.s11_tc_sched import create_time_tagged_cmd from tmtccmd.pus.tc.s3_fsfw_hk import create_request_one_hk_command from common import ( EXAMPLE_PUS_APID, - HkOpCodes, make_addressable_id, RequestTargetId, AcsHkIds, ) +_LOGGER = logging.getLogger(__name__) -def pack_pus_telecommands(q: DefaultPusQueueHelper, service: str, op_code: str): - if ( - service == CoreServiceList.SERVICE_17 - or service == CoreServiceList.SERVICE_17_ALT - ): - if op_code == "ping": + +def create_cmd_definition_tree() -> CmdTreeNode: + + root_node = CmdTreeNode.root_node() + + test_node = CmdTreeNode("test", "Test Node") + test_node.add_child(CmdTreeNode("ping", "Send PUS ping TC")) + test_node.add_child(CmdTreeNode("trigger_event", "Send PUS test to trigger event")) + root_node.add_child(test_node) + + scheduler_node = CmdTreeNode("scheduler", "Scheduler Node") + scheduler_node.add_child( + CmdTreeNode( + "schedule_ping_10_secs_ahead", "Schedule Ping to execute in 10 seconds" + ) + ) + root_node.add_child(scheduler_node) + + acs_node = CmdTreeNode("acs", "ACS Subsystem Node") + mgm_node = CmdTreeNode("mgms", "MGM devices node") + mgm_node.add_child(CmdTreeNode("one_shot_hk", "Request one shot HK")) + acs_node.add_child(mgm_node) + root_node.add_child(acs_node) + + return root_node + + +def pack_pus_telecommands(q: DefaultPusQueueHelper, cmd_path: str): + # It should always be at least the root path "/", so we split of the empty portion left of it. + cmd_path_list = cmd_path.split("/")[1:] + if len(cmd_path_list) == 0: + _LOGGER.warning("empty command path") + return + if cmd_path_list[0] == "test": + assert len(cmd_path_list) >= 2 + if cmd_path_list[1] == "ping": q.add_log_cmd("Sending PUS ping telecommand") return q.add_pus_tc(PusTelecommand(service=17, subservice=1)) - elif op_code == "trigger_event": + elif cmd_path_list[1] == "trigger_event": q.add_log_cmd("Triggering test event") return q.add_pus_tc(PusTelecommand(service=17, subservice=128)) - if service == CoreServiceList.SERVICE_11: - q.add_log_cmd("Sending PUS scheduled TC telecommand") - crt_time = CdsShortTimestamp.from_now() - time_stamp = crt_time + datetime.timedelta(seconds=10) - time_stamp = time_stamp.pack() - return q.add_pus_tc( - create_time_tagged_cmd( - time_stamp, - PusTelecommand(service=17, subservice=1), - apid=EXAMPLE_PUS_APID, - ) - ) - if service == CoreServiceList.SERVICE_3: - if op_code in HkOpCodes.GENERATE_ONE_SHOT: - q.add_log_cmd("Sending HK one shot request") - q.add_pus_tc( - create_request_one_hk_command( - make_addressable_id(RequestTargetId.ACS, AcsHkIds.MGM_SET) + if cmd_path_list[0] == "scheduler": + assert len(cmd_path_list) >= 2 + if cmd_path_list[1] == "schedule_ping_10_secs_ahead": + q.add_log_cmd("Sending PUS scheduled TC telecommand") + crt_time = CdsShortTimestamp.from_now() + time_stamp = crt_time + datetime.timedelta(seconds=10) + time_stamp = time_stamp.pack() + return q.add_pus_tc( + create_time_tagged_cmd( + time_stamp, + PusTelecommand(service=17, subservice=1), + apid=EXAMPLE_PUS_APID, ) ) - pass + if cmd_path_list[0] == "acs": + assert len(cmd_path_list) >= 2 + if cmd_path_list[1] == "mgm": + assert len(cmd_path_list) >= 3 + if cmd_path_list[2] == "one_shot_hk": + q.add_log_cmd("Sending HK one shot request") + q.add_pus_tc( + create_request_one_hk_command( + make_addressable_id(RequestTargetId.ACS, AcsHkIds.MGM_SET) + ) + ) diff --git a/satrs-example/pyclient/requirements.txt b/satrs-example/pyclient/requirements.txt index 485c76a..b3f6f2a 100644 --- a/satrs-example/pyclient/requirements.txt +++ b/satrs-example/pyclient/requirements.txt @@ -1,2 +1,2 @@ -tmtccmd == 7.0.0 +tmtccmd == 8.0.0rc1 # -e git+https://github.com/robamu-org/tmtccmd@97e5e51101a08b21472b3ddecc2063359f7e307a#egg=tmtccmd diff --git a/satrs-example/src/main.rs b/satrs-example/src/main.rs index 59d1566..ed1ef32 100644 --- a/satrs-example/src/main.rs +++ b/satrs-example/src/main.rs @@ -29,7 +29,7 @@ use satrs_core::event_man::{ }; use satrs_core::events::EventU32; use satrs_core::hk::HkRequest; -use satrs_core::pool::{LocalPool, PoolCfg}; +use satrs_core::pool::{PoolProviderMemInPlace, StaticMemoryPool, StaticPoolConfig}; use satrs_core::pus::event_man::{ DefaultPusMgmtBackendProvider, EventReporter, EventRequest, EventRequestWithToken, PusEventDispatcher, @@ -42,7 +42,9 @@ use satrs_core::pus::test::PusService17TestHandler; use satrs_core::pus::verification::{ TcStateStarted, VerificationReporterCfg, VerificationReporterWithSender, VerificationToken, }; -use satrs_core::pus::{MpscTcInStoreReceiver, MpscTmInStoreSender}; +use satrs_core::pus::{ + EcssTcInSharedStoreConverter, MpscTcReceiver, MpscTmInStoreSender, PusServiceHelper, +}; use satrs_core::seq_count::{CcsdsSimpleSeqCountProvider, SequenceCountProviderCore}; use satrs_core::spacepackets::ecss::tm::{PusTmCreator, PusTmZeroCopyWriter}; use satrs_core::spacepackets::{ @@ -66,7 +68,7 @@ use std::time::Duration; fn main() { setup_logger().expect("setting up logging with fern failed"); println!("Running OBSW example"); - let tm_pool = LocalPool::new(PoolCfg::new(vec![ + let tm_pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![ (30, 32), (15, 64), (15, 128), @@ -74,9 +76,9 @@ fn main() { (15, 1024), (15, 2048), ])); - let shared_tm_store = SharedTmStore::new(Box::new(tm_pool)); + let shared_tm_store = SharedTmStore::new(tm_pool); let tm_store_event = shared_tm_store.clone(); - let tc_pool = LocalPool::new(PoolCfg::new(vec![ + let tc_pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![ (30, 32), (15, 64), (15, 128), @@ -85,8 +87,16 @@ fn main() { (15, 2048), ])); let tc_store = TcStore { - pool: Arc::new(RwLock::new(Box::new(tc_pool))), + pool: Arc::new(RwLock::new(tc_pool)), }; + let sched_tc_pool = StaticMemoryPool::new(StaticPoolConfig::new(vec![ + (30, 32), + (15, 64), + (15, 128), + (15, 256), + (15, 1024), + (15, 2048), + ])); let seq_count_provider = CcsdsSimpleSeqCountProvider::new(); let mut msg_counter_map: HashMap = HashMap::new(); @@ -172,18 +182,18 @@ fn main() { shared_tm_store.clone(), tm_funnel_tx.clone(), ); - let test_srv_receiver = MpscTcInStoreReceiver::new( + let test_srv_receiver = MpscTcReceiver::new( TcReceiverId::PusTest as ChannelId, "PUS_17_TC_RECV", pus_test_rx, ); - let pus17_handler = PusService17TestHandler::new( + let pus17_handler = PusService17TestHandler::new(PusServiceHelper::new( Box::new(test_srv_receiver), - tc_store.pool.clone(), Box::new(test_srv_tm_sender), PUS_APID, verif_reporter.clone(), - ); + EcssTcInSharedStoreConverter::new(tc_store.pool.clone(), 2048), + )); let mut pus_17_wrapper = Service17CustomWrapper { pus17_handler, test_srv_event_sender, @@ -195,7 +205,7 @@ fn main() { shared_tm_store.clone(), tm_funnel_tx.clone(), ); - let sched_srv_receiver = MpscTcInStoreReceiver::new( + let sched_srv_receiver = MpscTcReceiver::new( TcReceiverId::PusSched as ChannelId, "PUS_11_TC_RECV", pus_sched_rx, @@ -203,15 +213,18 @@ fn main() { let scheduler = PusScheduler::new_with_current_init_time(Duration::from_secs(5)) .expect("Creating PUS Scheduler failed"); let pus_11_handler = PusService11SchedHandler::new( - Box::new(sched_srv_receiver), - tc_store.pool.clone(), - Box::new(sched_srv_tm_sender), - PUS_APID, - verif_reporter.clone(), + PusServiceHelper::new( + Box::new(sched_srv_receiver), + Box::new(sched_srv_tm_sender), + PUS_APID, + verif_reporter.clone(), + EcssTcInSharedStoreConverter::new(tc_store.pool.clone(), 2048), + ), scheduler, ); let mut pus_11_wrapper = Pus11Wrapper { pus_11_handler, + sched_tc_pool, tc_source_wrapper, }; @@ -221,17 +234,19 @@ fn main() { shared_tm_store.clone(), tm_funnel_tx.clone(), ); - let event_srv_receiver = MpscTcInStoreReceiver::new( + let event_srv_receiver = MpscTcReceiver::new( TcReceiverId::PusEvent as ChannelId, "PUS_5_TC_RECV", pus_event_rx, ); let pus_5_handler = PusService5EventHandler::new( - Box::new(event_srv_receiver), - tc_store.pool.clone(), - Box::new(event_srv_tm_sender), - PUS_APID, - verif_reporter.clone(), + PusServiceHelper::new( + Box::new(event_srv_receiver), + Box::new(event_srv_tm_sender), + PUS_APID, + verif_reporter.clone(), + EcssTcInSharedStoreConverter::new(tc_store.pool.clone(), 2048), + ), event_request_tx, ); let mut pus_5_wrapper = Pus5Wrapper { pus_5_handler }; @@ -242,17 +257,17 @@ fn main() { shared_tm_store.clone(), tm_funnel_tx.clone(), ); - let action_srv_receiver = MpscTcInStoreReceiver::new( + let action_srv_receiver = MpscTcReceiver::new( TcReceiverId::PusAction as ChannelId, "PUS_8_TC_RECV", pus_action_rx, ); let pus_8_handler = PusService8ActionHandler::new( Box::new(action_srv_receiver), - tc_store.pool.clone(), Box::new(action_srv_tm_sender), PUS_APID, verif_reporter.clone(), + EcssTcInSharedStoreConverter::new(tc_store.pool.clone(), 2048), request_map.clone(), ); let mut pus_8_wrapper = Pus8Wrapper { pus_8_handler }; @@ -264,13 +279,13 @@ fn main() { tm_funnel_tx.clone(), ); let hk_srv_receiver = - MpscTcInStoreReceiver::new(TcReceiverId::PusHk as ChannelId, "PUS_8_TC_RECV", pus_hk_rx); + MpscTcReceiver::new(TcReceiverId::PusHk as ChannelId, "PUS_8_TC_RECV", pus_hk_rx); let pus_3_handler = PusService3HkHandler::new( Box::new(hk_srv_receiver), - tc_store.pool.clone(), Box::new(hk_srv_tm_sender), PUS_APID, verif_reporter.clone(), + EcssTcInSharedStoreConverter::new(tc_store.pool.clone(), 2048), request_map, ); let mut pus_3_wrapper = Pus3Wrapper { pus_3_handler }; diff --git a/satrs-example/src/pus/action.rs b/satrs-example/src/pus/action.rs index d280e69..3b8cc8d 100644 --- a/satrs-example/src/pus/action.rs +++ b/satrs-example/src/pus/action.rs @@ -1,12 +1,11 @@ use crate::requests::{ActionRequest, Request, RequestWithToken}; use log::{error, warn}; -use satrs_core::pool::{SharedPool, StoreAddr}; use satrs_core::pus::verification::{ - FailParams, StdVerifReporterWithSender, TcStateAccepted, VerificationToken, + FailParams, TcStateAccepted, VerificationReporterWithSender, VerificationToken, }; use satrs_core::pus::{ - EcssTcReceiver, EcssTmSender, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, - PusServiceHandler, + EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcReceiver, EcssTmSender, + PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, PusServiceHelper, }; use satrs_core::spacepackets::ecss::tc::PusTcReader; use satrs_core::spacepackets::ecss::PusPacket; @@ -14,34 +13,32 @@ use satrs_example::{tmtc_err, TargetIdWithApid}; use std::collections::HashMap; use std::sync::mpsc::Sender; -pub struct PusService8ActionHandler { - psb: PusServiceBase, +pub struct PusService8ActionHandler { + service_helper: PusServiceHelper, request_handlers: HashMap>, } -impl PusService8ActionHandler { +impl PusService8ActionHandler { pub fn new( tc_receiver: Box, - shared_tc_pool: SharedPool, tm_sender: Box, tm_apid: u16, - verification_handler: StdVerifReporterWithSender, + verification_handler: VerificationReporterWithSender, + tc_in_mem_converter: TcInMemConverter, request_handlers: HashMap>, ) -> Self { Self { - psb: PusServiceBase::new( + service_helper: PusServiceHelper::new( tc_receiver, - shared_tc_pool, tm_sender, tm_apid, verification_handler, + tc_in_mem_converter, ), request_handlers, } } -} -impl PusService8ActionHandler { fn handle_action_request_with_id( &self, token: VerificationToken, @@ -50,7 +47,8 @@ impl PusService8ActionHandler { ) -> Result<(), PusPacketHandlingError> { let user_data = tc.user_data(); if user_data.len() < 8 { - self.psb() + self.service_helper + .common .verification_handler .borrow_mut() .start_failure( @@ -79,7 +77,8 @@ impl PusService8ActionHandler { } else { let mut fail_data: [u8; 4] = [0; 4]; fail_data.copy_from_slice(&target_id.target.to_be_bytes()); - self.psb() + self.service_helper + .common .verification_handler .borrow_mut() .start_failure( @@ -97,37 +96,32 @@ impl PusService8ActionHandler { } Ok(()) } -} -impl PusServiceHandler for PusService8ActionHandler { - fn psb_mut(&mut self) -> &mut PusServiceBase { - &mut self.psb - } - fn psb(&self) -> &PusServiceBase { - &self.psb - } - - fn handle_one_tc( - &mut self, - addr: StoreAddr, - token: VerificationToken, - ) -> Result { - self.copy_tc_to_buf(addr)?; - let (tc, _) = PusTcReader::new(&self.psb().pus_buf).unwrap(); + fn handle_one_tc(&mut self) -> Result { + let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?; + if possible_packet.is_none() { + return Ok(PusPacketHandlerResult::Empty); + } + let ecss_tc_and_token = possible_packet.unwrap(); + self.service_helper + .tc_in_mem_converter + .cache_ecss_tc_in_memory(&ecss_tc_and_token.tc_in_memory)?; + let tc = PusTcReader::new(self.service_helper.tc_in_mem_converter.tc_slice_raw())?.0; let subservice = tc.subservice(); let mut partial_error = None; - let time_stamp = self.psb().get_current_timestamp(&mut partial_error); + let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error); match subservice { 128 => { - self.handle_action_request_with_id(token, &tc, &time_stamp)?; + self.handle_action_request_with_id(ecss_tc_and_token.token, &tc, &time_stamp)?; } _ => { let fail_data = [subservice]; - self.psb_mut() + self.service_helper + .common .verification_handler .get_mut() .start_failure( - token, + ecss_tc_and_token.token, FailParams::new( Some(&time_stamp), &tmtc_err::INVALID_PUS_SUBSERVICE, @@ -148,12 +142,12 @@ impl PusServiceHandler for PusService8ActionHandler { } pub struct Pus8Wrapper { - pub(crate) pus_8_handler: PusService8ActionHandler, + pub(crate) pus_8_handler: PusService8ActionHandler, } impl Pus8Wrapper { pub fn handle_next_packet(&mut self) -> bool { - match self.pus_8_handler.handle_next_packet() { + match self.pus_8_handler.handle_one_tc() { Ok(result) => match result { PusPacketHandlerResult::RequestHandled => {} PusPacketHandlerResult::RequestHandledPartialSuccess(e) => { diff --git a/satrs-example/src/pus/event.rs b/satrs-example/src/pus/event.rs index 0f2654e..08aa786 100644 --- a/satrs-example/src/pus/event.rs +++ b/satrs-example/src/pus/event.rs @@ -1,14 +1,14 @@ use log::{error, warn}; use satrs_core::pus::event_srv::PusService5EventHandler; -use satrs_core::pus::{PusPacketHandlerResult, PusServiceHandler}; +use satrs_core::pus::{EcssTcInSharedStoreConverter, PusPacketHandlerResult}; pub struct Pus5Wrapper { - pub pus_5_handler: PusService5EventHandler, + pub pus_5_handler: PusService5EventHandler, } impl Pus5Wrapper { pub fn handle_next_packet(&mut self) -> bool { - match self.pus_5_handler.handle_next_packet() { + match self.pus_5_handler.handle_one_tc() { Ok(result) => match result { PusPacketHandlerResult::RequestHandled => {} PusPacketHandlerResult::RequestHandledPartialSuccess(e) => { diff --git a/satrs-example/src/pus/hk.rs b/satrs-example/src/pus/hk.rs index a00f5ef..ef373c4 100644 --- a/satrs-example/src/pus/hk.rs +++ b/satrs-example/src/pus/hk.rs @@ -1,72 +1,63 @@ use crate::requests::{Request, RequestWithToken}; use log::{error, warn}; use satrs_core::hk::{CollectionIntervalFactor, HkRequest}; -use satrs_core::pool::{SharedPool, StoreAddr}; -use satrs_core::pus::verification::{ - FailParams, StdVerifReporterWithSender, TcStateAccepted, VerificationToken, -}; +use satrs_core::pus::verification::{FailParams, StdVerifReporterWithSender}; use satrs_core::pus::{ - EcssTcReceiver, EcssTmSender, PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, - PusServiceHandler, + EcssTcInMemConverter, EcssTcInSharedStoreConverter, EcssTcReceiver, EcssTmSender, + PusPacketHandlerResult, PusPacketHandlingError, PusServiceBase, PusServiceHelper, }; -use satrs_core::spacepackets::ecss::tc::PusTcReader; use satrs_core::spacepackets::ecss::{hk, PusPacket}; use satrs_example::{hk_err, tmtc_err, TargetIdWithApid}; use std::collections::HashMap; use std::sync::mpsc::Sender; -pub struct PusService3HkHandler { - psb: PusServiceBase, +pub struct PusService3HkHandler { + psb: PusServiceHelper, request_handlers: HashMap>, } -impl PusService3HkHandler { +impl PusService3HkHandler { pub fn new( tc_receiver: Box, - shared_tc_pool: SharedPool, tm_sender: Box, tm_apid: u16, verification_handler: StdVerifReporterWithSender, + tc_in_mem_converter: TcInMemConverter, request_handlers: HashMap>, ) -> Self { Self { - psb: PusServiceBase::new( + psb: PusServiceHelper::new( tc_receiver, - shared_tc_pool, tm_sender, tm_apid, verification_handler, + tc_in_mem_converter, ), request_handlers, } } -} -impl PusServiceHandler for PusService3HkHandler { - fn psb_mut(&mut self) -> &mut PusServiceBase { - &mut self.psb - } - fn psb(&self) -> &PusServiceBase { - &self.psb - } - - fn handle_one_tc( - &mut self, - addr: StoreAddr, - token: VerificationToken, - ) -> Result { - self.copy_tc_to_buf(addr)?; - let (tc, _) = PusTcReader::new(&self.psb().pus_buf).unwrap(); + fn handle_one_tc(&mut self) -> Result { + let possible_packet = self.psb.retrieve_and_accept_next_packet()?; + if possible_packet.is_none() { + return Ok(PusPacketHandlerResult::Empty); + } + let ecss_tc_and_token = possible_packet.unwrap(); + let tc = self + .psb + .tc_in_mem_converter + .convert_ecss_tc_in_memory_to_reader(&ecss_tc_and_token.tc_in_memory)?; let subservice = tc.subservice(); let mut partial_error = None; - let time_stamp = self.psb().get_current_timestamp(&mut partial_error); + let time_stamp = PusServiceBase::get_current_timestamp(&mut partial_error); let user_data = tc.user_data(); if user_data.is_empty() { self.psb + .common .verification_handler .borrow_mut() .start_failure( - token, + ecss_tc_and_token.token, FailParams::new(Some(&time_stamp), &tmtc_err::NOT_ENOUGH_APP_DATA, None), ) .expect("Sending start failure TM failed"); @@ -81,9 +72,13 @@ impl PusServiceHandler for PusService3HkHandler { &hk_err::UNIQUE_ID_MISSING }; self.psb + .common .verification_handler .borrow_mut() - .start_failure(token, FailParams::new(Some(&time_stamp), err, None)) + .start_failure( + ecss_tc_and_token.token, + FailParams::new(Some(&time_stamp), err, None), + ) .expect("Sending start failure TM failed"); return Err(PusPacketHandlingError::NotEnoughAppData( "Expected at least 8 bytes of app data".into(), @@ -93,10 +88,11 @@ impl PusServiceHandler for PusService3HkHandler { let unique_id = u32::from_be_bytes(tc.user_data()[0..4].try_into().unwrap()); if !self.request_handlers.contains_key(&target_id) { self.psb + .common .verification_handler .borrow_mut() .start_failure( - token, + ecss_tc_and_token.token, FailParams::new(Some(&time_stamp), &hk_err::UNKNOWN_TARGET_ID, None), ) .expect("Sending start failure TM failed"); @@ -107,7 +103,11 @@ impl PusServiceHandler for PusService3HkHandler { let send_request = |target: TargetIdWithApid, request: HkRequest| { let sender = self.request_handlers.get(&target).unwrap(); sender - .send(RequestWithToken::new(target, Request::Hk(request), token)) + .send(RequestWithToken::new( + target, + Request::Hk(request), + ecss_tc_and_token.token, + )) .unwrap_or_else(|_| panic!("Sending HK request {request:?} failed")); }; if subservice == hk::Subservice::TcEnableHkGeneration as u8 { @@ -119,10 +119,11 @@ impl PusServiceHandler for PusService3HkHandler { } else if subservice == hk::Subservice::TcModifyHkCollectionInterval as u8 { if user_data.len() < 12 { self.psb + .common .verification_handler .borrow_mut() .start_failure( - token, + ecss_tc_and_token.token, FailParams::new( Some(&time_stamp), &hk_err::COLLECTION_INTERVAL_MISSING, @@ -147,12 +148,12 @@ impl PusServiceHandler for PusService3HkHandler { } pub struct Pus3Wrapper { - pub(crate) pus_3_handler: PusService3HkHandler, + pub(crate) pus_3_handler: PusService3HkHandler, } impl Pus3Wrapper { pub fn handle_next_packet(&mut self) -> bool { - match self.pus_3_handler.handle_next_packet() { + match self.pus_3_handler.handle_one_tc() { Ok(result) => match result { PusPacketHandlerResult::RequestHandled => {} PusPacketHandlerResult::RequestHandledPartialSuccess(e) => { diff --git a/satrs-example/src/pus/mod.rs b/satrs-example/src/pus/mod.rs index edc7cc3..a545b6b 100644 --- a/satrs-example/src/pus/mod.rs +++ b/satrs-example/src/pus/mod.rs @@ -1,8 +1,7 @@ use crate::tmtc::MpscStoreAndSendError; use log::warn; -use satrs_core::pool::StoreAddr; use satrs_core::pus::verification::{FailParams, StdVerifReporterWithSender}; -use satrs_core::pus::{PusPacketHandlerResult, TcAddrWithToken}; +use satrs_core::pus::{EcssTcAndToken, PusPacketHandlerResult, TcInMemory}; use satrs_core::spacepackets::ecss::tc::PusTcReader; use satrs_core::spacepackets::ecss::PusServiceId; use satrs_core::spacepackets::time::cds::TimeProvider; @@ -17,11 +16,11 @@ pub mod scheduler; pub mod test; pub struct PusTcMpscRouter { - pub test_service_receiver: Sender, - pub event_service_receiver: Sender, - pub sched_service_receiver: Sender, - pub hk_service_receiver: Sender, - pub action_service_receiver: Sender, + pub test_service_receiver: Sender, + pub event_service_receiver: Sender, + pub sched_service_receiver: Sender, + pub hk_service_receiver: Sender, + pub action_service_receiver: Sender, } pub struct PusReceiver { @@ -70,7 +69,7 @@ impl PusReceiver { impl PusReceiver { pub fn handle_tc_packet( &mut self, - store_addr: StoreAddr, + tc_in_memory: TcInMemory, service: u8, pus_tc: &PusTcReader, ) -> Result { @@ -84,22 +83,33 @@ impl PusReceiver { match service { Ok(standard_service) => match standard_service { PusServiceId::Test => { - self.pus_router - .test_service_receiver - .send((store_addr, accepted_token.into()))?; + self.pus_router.test_service_receiver.send(EcssTcAndToken { + tc_in_memory, + token: Some(accepted_token.into()), + })? + } + PusServiceId::Housekeeping => { + self.pus_router.hk_service_receiver.send(EcssTcAndToken { + tc_in_memory, + token: Some(accepted_token.into()), + })? + } + PusServiceId::Event => { + self.pus_router + .event_service_receiver + .send(EcssTcAndToken { + tc_in_memory, + token: Some(accepted_token.into()), + })? + } + PusServiceId::Scheduling => { + self.pus_router + .sched_service_receiver + .send(EcssTcAndToken { + tc_in_memory, + token: Some(accepted_token.into()), + })? } - PusServiceId::Housekeeping => self - .pus_router - .hk_service_receiver - .send((store_addr, accepted_token.into()))?, - PusServiceId::Event => self - .pus_router - .event_service_receiver - .send((store_addr, accepted_token.into()))?, - PusServiceId::Scheduling => self - .pus_router - .sched_service_receiver - .send((store_addr, accepted_token.into()))?, _ => { let result = self.verif_reporter.start_failure( accepted_token, diff --git a/satrs-example/src/pus/scheduler.rs b/satrs-example/src/pus/scheduler.rs index 35c84b8..75f3494 100644 --- a/satrs-example/src/pus/scheduler.rs +++ b/satrs-example/src/pus/scheduler.rs @@ -1,50 +1,54 @@ use crate::tmtc::PusTcSource; use log::{error, info, warn}; -use satrs_core::pus::scheduler::TcInfo; +use satrs_core::pool::{PoolProviderMemInPlace, StaticMemoryPool}; +use satrs_core::pus::scheduler::{PusScheduler, TcInfo}; use satrs_core::pus::scheduler_srv::PusService11SchedHandler; -use satrs_core::pus::{PusPacketHandlerResult, PusServiceHandler}; +use satrs_core::pus::{EcssTcInSharedStoreConverter, PusPacketHandlerResult}; pub struct Pus11Wrapper { - pub pus_11_handler: PusService11SchedHandler, + pub pus_11_handler: PusService11SchedHandler, + pub sched_tc_pool: StaticMemoryPool, pub tc_source_wrapper: PusTcSource, } impl Pus11Wrapper { pub fn release_tcs(&mut self) { - let releaser = |enabled: bool, info: &TcInfo| -> bool { + let releaser = |enabled: bool, _info: &TcInfo, tc: &[u8]| -> bool { if enabled { + // Transfer TC from scheduler TC pool to shared TC pool. + let released_tc_addr = self + .tc_source_wrapper + .tc_store + .pool + .write() + .expect("locking pool failed") + .add(tc) + .expect("adding TC to shared pool failed"); + self.tc_source_wrapper .tc_source - .send(info.addr()) + .send(released_tc_addr) .expect("sending TC to TC source failed"); } true }; - let mut pool = self - .tc_source_wrapper - .tc_store - .pool - .write() - .expect("error locking pool"); - self.pus_11_handler .scheduler_mut() .update_time_from_now() .unwrap(); - if let Ok(released_tcs) = self + let released_tcs = self .pus_11_handler .scheduler_mut() - .release_telecommands(releaser, pool.as_mut()) - { - if released_tcs > 0 { - info!("{released_tcs} TC(s) released from scheduler"); - } + .release_telecommands(releaser, &mut self.sched_tc_pool) + .expect("releasing TCs failed"); + if released_tcs > 0 { + info!("{released_tcs} TC(s) released from scheduler"); } } pub fn handle_next_packet(&mut self) -> bool { - match self.pus_11_handler.handle_next_packet() { + match self.pus_11_handler.handle_one_tc(&mut self.sched_tc_pool) { Ok(result) => match result { PusPacketHandlerResult::RequestHandled => {} PusPacketHandlerResult::RequestHandledPartialSuccess(e) => { diff --git a/satrs-example/src/pus/test.rs b/satrs-example/src/pus/test.rs index 047cbdb..a52d111 100644 --- a/satrs-example/src/pus/test.rs +++ b/satrs-example/src/pus/test.rs @@ -1,24 +1,24 @@ use log::{info, warn}; -use satrs_core::events::EventU32; use satrs_core::params::Params; use satrs_core::pus::test::PusService17TestHandler; use satrs_core::pus::verification::FailParams; -use satrs_core::pus::{PusPacketHandlerResult, PusServiceHandler}; +use satrs_core::pus::{EcssTcInMemConverter, PusPacketHandlerResult}; use satrs_core::spacepackets::ecss::tc::PusTcReader; use satrs_core::spacepackets::ecss::PusPacket; use satrs_core::spacepackets::time::cds::TimeProvider; use satrs_core::spacepackets::time::TimeWriter; +use satrs_core::{events::EventU32, pus::EcssTcInSharedStoreConverter}; use satrs_example::{tmtc_err, TEST_EVENT}; use std::sync::mpsc::Sender; pub struct Service17CustomWrapper { - pub pus17_handler: PusService17TestHandler, + pub pus17_handler: PusService17TestHandler, pub test_srv_event_sender: Sender<(EventU32, Option)>, } impl Service17CustomWrapper { pub fn handle_next_packet(&mut self) -> bool { - let res = self.pus17_handler.handle_next_packet(); + let res = self.pus17_handler.handle_one_tc(); if res.is_err() { warn!("PUS17 handler failed with error {:?}", res.unwrap_err()); return true; @@ -38,9 +38,13 @@ impl Service17CustomWrapper { warn!("PUS17: Subservice {subservice} not implemented") } PusPacketHandlerResult::CustomSubservice(subservice, token) => { - let psb_mut = self.pus17_handler.psb_mut(); - let buf = psb_mut.pus_buf; - let (tc, _) = PusTcReader::new(&buf).unwrap(); + let (tc, _) = PusTcReader::new( + self.pus17_handler + .service_helper + .tc_in_mem_converter + .tc_slice_raw(), + ) + .unwrap(); let time_stamper = TimeProvider::from_now_with_u16_days().unwrap(); let mut stamp_buf: [u8; 7] = [0; 7]; time_stamper.write_to_bytes(&mut stamp_buf).unwrap(); @@ -49,12 +53,17 @@ impl Service17CustomWrapper { self.test_srv_event_sender .send((TEST_EVENT.into(), None)) .expect("Sending test event failed"); - let start_token = psb_mut + let start_token = self + .pus17_handler + .service_helper + .common .verification_handler .get_mut() .start_success(token, Some(&stamp_buf)) .expect("Error sending start success"); - psb_mut + self.pus17_handler + .service_helper + .common .verification_handler .get_mut() .completion_success(start_token, Some(&stamp_buf)) @@ -62,7 +71,8 @@ impl Service17CustomWrapper { } else { let fail_data = [tc.subservice()]; self.pus17_handler - .psb_mut() + .service_helper + .common .verification_handler .get_mut() .start_failure( diff --git a/satrs-example/src/tmtc.rs b/satrs-example/src/tmtc.rs index 8af701a..b4a584d 100644 --- a/satrs-example/src/tmtc.rs +++ b/satrs-example/src/tmtc.rs @@ -1,12 +1,11 @@ use log::warn; -use satrs_core::pus::ReceivesEcssPusTc; +use satrs_core::pus::{EcssTcAndToken, ReceivesEcssPusTc}; use satrs_core::spacepackets::SpHeader; use std::sync::mpsc::{Receiver, SendError, Sender, TryRecvError}; use thiserror::Error; use crate::pus::PusReceiver; -use satrs_core::pool::{SharedPool, StoreAddr, StoreError}; -use satrs_core::pus::TcAddrWithToken; +use satrs_core::pool::{PoolProviderMemInPlace, SharedStaticMemoryPool, StoreAddr, StoreError}; use satrs_core::spacepackets::ecss::tc::PusTcReader; use satrs_core::spacepackets::ecss::PusPacket; use satrs_core::tmtc::tm_helper::SharedTmStore; @@ -35,14 +34,14 @@ pub enum MpscStoreAndSendError { #[error("Store error: {0}")] Store(#[from] StoreError), #[error("TC send error: {0}")] - TcSend(#[from] SendError), + TcSend(#[from] SendError), #[error("TMTC send error: {0}")] TmTcSend(#[from] SendError), } #[derive(Clone)] pub struct TcStore { - pub pool: SharedPool, + pub pool: SharedStaticMemoryPool, } impl TcStore { @@ -103,7 +102,6 @@ impl TmtcTask { } pub fn periodic_operation(&mut self) { - //while self.poll_tc() {} self.poll_tc(); } @@ -123,7 +121,11 @@ impl TmtcTask { match PusTcReader::new(&self.tc_buf) { Ok((pus_tc, _)) => { self.pus_receiver - .handle_tc_packet(addr, pus_tc.service(), &pus_tc) + .handle_tc_packet( + satrs_core::pus::TcInMemory::StoreAddr(addr), + pus_tc.service(), + &pus_tc, + ) .ok(); true } diff --git a/satrs-example/src/udp.rs b/satrs-example/src/udp.rs index e3ca9f6..3853fd3 100644 --- a/satrs-example/src/udp.rs +++ b/satrs-example/src/udp.rs @@ -3,7 +3,7 @@ use std::{net::SocketAddr, sync::mpsc::Receiver}; use log::{info, warn}; use satrs_core::{ hal::std::udp_server::{ReceiveResult, UdpTcServer}, - pool::{SharedPool, StoreAddr}, + pool::{PoolProviderMemInPlaceWithGuards, SharedStaticMemoryPool, StoreAddr}, tmtc::CcsdsError, }; @@ -12,7 +12,7 @@ use crate::tmtc::MpscStoreAndSendError; pub struct UdpTmtcServer { pub udp_tc_server: UdpTcServer>, pub tm_rx: Receiver, - pub tm_store: SharedPool, + pub tm_store: SharedStaticMemoryPool, } impl UdpTmtcServer { pub fn periodic_operation(&mut self) {