first step in unifying the examples: one example folder

One example folder for all examples, including std and embedded examples
This commit is contained in:
Robin Mueller committed 2026-09-29 13:39:39 +02:00
1 parent bb8decfc74
commit 7cc2ff3ff9
130 files changed
+399 -368

No files matched your search

+1
View File
@@ -0,0 +1 @@
/output.log
+19
View File
@@ -0,0 +1,19 @@
Change Log
=======
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
# [unreleased]
# [v0.1.1] 2024-02-21
satrs v0.2.0-rc.0
satrs-mib v0.1.1
# [v0.1.0] 2024-02-13
satrs v0.1.1
satrs-mib v0.1.0
+38
View File
@@ -0,0 +1,38 @@
[package]
name = "example-std"
version = "0.1.1"
edition = "2024"
default-run = "example-std"
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
[dependencies]
fern = "0.7"
chrono = "0.4"
log = "0.4"
crossbeam-channel = "0.5"
delegate = "0.13"
zerocopy = "0.8"
csv = "1"
num_enum = "0.7"
thiserror = "2"
lazy_static = "1"
strum = { version = "0.28", features = ["derive"] }
derive-new = "0.7"
cfg-if = "1"
arbitrary-int = "2"
bitbybit = "2"
postcard = { version = "1", features = ["alloc"] }
ctrlc = "3"
serde = { version = "1", features = ["derive"] }
satrs = { path = "../../satrs", features = ["test_util"] }
types = { path = "../types" }
minisim-types = { path = "../minisim-types" }
satrs-mib = { path = "../../satrs-mib" }
[features]
# default = ["heap_tmtc"]
# heap_tmtc = []
[dev-dependencies]
env_logger = "0.11"
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+1
View File
@@ -0,0 +1 @@
This software contains code developed at the University of Stuttgart's Institute of Space Systems.
+57
View File
@@ -0,0 +1,57 @@
sat-rs example
======
This crate contains an example application which simulates an on-board software.
It uses various components provided by the sat-rs framework to do this. As such, it shows how
a more complex real on-board software could be built from these components. It is recommended to
read the dedicated
[example chapters](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/example.html) inside
the sat-rs book.
The application opens a UDP and a TCP server on port 7301 to receive telecommands.
You can run the application using `cargo run`.
# Features
The example has the `heap_tmtc` feature which is enabled by default. With this feature enabled,
TMTC packets are exchanged using the heap as the backing memory instead of pre-allocated static
stores.
You can run the application without this feature using
```sh
cargo run --no-default-features
```
# Interacting with the sat-rs example
The `client` crate is a command line client which sends telecommands to the example application
and prints the received telemetry. For example, you can ping the application or switch MGM 0
to normal mode like this:
```sh
cargo run -p client -- --ping
cargo run -p client -- mgm0 -m normal
```
Use `cargo run -p client -- --help` to list all available commands.
## Adding the mini simulator application
This example application features a few device handlers. The
[`minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/minisim)
can be used to simulate the physical devices managed by these device handlers.
The example application will attempt communication with the mini simulator on UDP port 7303.
If this works, the device handlers will use communication interfaces dedicated to the communication
with the mini simulator. Otherwise, they will be replaced by dummy interfaces which either
return constant values or behave like ideal devices.
In summary, you can use the following command command to run the mini-simulator first:
```sh
cargo run -p minisim
```
and then start the example using `cargo run -p example-std`.
+118
View File
@@ -0,0 +1,118 @@
use std::sync::mpsc::{Receiver, SyncSender, TryRecvError};
use types::acs::ctrl::{Mode, request::ModeRequest, response::ModeReport};
/// Helper component for communication with a parent component, which is usually an assembly
/// or a subsystem.
pub struct ModeLeafHelper {
pub request_rx: Receiver<ModeRequest>,
pub report_tx: SyncSender<ModeReport>,
}
/// Dummy ACS controller. It has no actual control law and reaches any commanded mode
/// immediately, which is sufficient to exercise the mode commanding of its parent subsystem.
pub struct Controller {
mode: Mode,
mode_leaf_helper: ModeLeafHelper,
}
impl Controller {
pub fn new(mode_leaf_helper: ModeLeafHelper) -> Self {
Self {
mode: Mode::Passive,
mode_leaf_helper,
}
}
#[allow(dead_code)]
#[inline]
pub fn mode(&self) -> Mode {
self.mode
}
pub fn periodic_operation(&mut self) {
self.handle_mode_leaf_handling();
}
fn handle_mode_leaf_handling(&mut self) {
loop {
match self.mode_leaf_helper.request_rx.try_recv() {
Ok(request) => match request {
ModeRequest::SetMode(mode) => {
log::info!("ACS controller: transitioning to mode {:?}", mode);
self.mode = mode;
self.report_mode();
}
ModeRequest::ReadMode => self.report_mode(),
},
Err(e) => match e {
TryRecvError::Empty => break,
TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
},
}
}
}
fn report_mode(&self) {
self.mode_leaf_helper
.report_tx
.send(ModeReport::Mode(self.mode))
.expect("failed to send mode report to parent");
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use super::*;
struct ControllerTestbench {
request_tx: SyncSender<ModeRequest>,
report_rx: Receiver<ModeReport>,
controller: Controller,
}
impl ControllerTestbench {
fn new() -> Self {
let (request_tx, request_rx) = mpsc::sync_channel(5);
let (report_tx, report_rx) = mpsc::sync_channel(5);
Self {
request_tx,
report_rx,
controller: Controller::new(ModeLeafHelper {
request_rx,
report_tx,
}),
}
}
}
#[test]
fn test_initial_mode() {
let testbench = ControllerTestbench::new();
assert_eq!(testbench.controller.mode(), Mode::Passive);
}
#[test]
fn test_set_mode() {
let mut testbench = ControllerTestbench::new();
testbench
.request_tx
.send(ModeRequest::SetMode(Mode::Safe))
.unwrap();
testbench.controller.periodic_operation();
assert_eq!(testbench.controller.mode(), Mode::Safe);
let report = testbench.report_rx.try_recv().expect("no mode report sent");
assert_eq!(report, ModeReport::Mode(Mode::Safe));
}
#[test]
fn test_read_mode() {
let mut testbench = ControllerTestbench::new();
testbench.request_tx.send(ModeRequest::ReadMode).unwrap();
testbench.controller.periodic_operation();
let report = testbench.report_rx.try_recv().expect("no mode report sent");
assert_eq!(report, ModeReport::Mode(Mode::Passive));
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,662 @@
use std::{sync::mpsc, time::Duration};
use example_std::{ModeHelper, TmtcQueues};
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use types::{
ComponentId, DeviceMode,
acs::mgm_assembly::{Mode, request, response},
};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
pub struct ParentQueueHelper {
pub request_rx: mpsc::Receiver<request::ModeRequest>,
pub report_tx: mpsc::SyncSender<response::ModeResponse>,
}
/// Helper component for communication with a parent component, which is usually as assembly.
pub struct ChildrenQueueHelper {
pub request_tx_queues: [mpsc::SyncSender<types::acs::mgm::request::ModeRequest>; 2],
pub report_rx_queues: [mpsc::Receiver<types::acs::mgm::response::ModeResponse>; 2],
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum TransitionState {
#[default]
Idle,
AwaitingReplies,
}
#[derive(Debug, Default, Copy, Clone)]
pub struct MgmInfo {
reply_received: bool,
mode: Option<DeviceMode>,
}
/// MGM assembly component.
pub struct Assembly {
mode_helper: ModeHelper<Mode, TransitionState>,
/// This boolean is used for the distinction between transitions commanded by the parent
/// or by ground, and transitions which were commanded autonomously as part of children
/// mode keeping.
mode_keeping_transition: bool,
tmtc_queues: TmtcQueues,
mgm_modes: [MgmInfo; 2],
parent_queues: ParentQueueHelper,
pub(crate) children_queues: ChildrenQueueHelper,
event_tx: mpsc::SyncSender<types::acs::mgm_assembly::Event>,
}
impl Assembly {
pub const ID: ComponentId = ComponentId::AcsMgmAssembly;
pub fn new(
parent_queues: ParentQueueHelper,
children_queues: ChildrenQueueHelper,
tmtc_queues: TmtcQueues,
mode_timeout: Duration,
event_tx: mpsc::SyncSender<types::acs::mgm_assembly::Event>,
) -> Self {
Self {
mode_helper: ModeHelper::new(Mode::NoModeKeeping, mode_timeout),
mode_keeping_transition: false,
tmtc_queues,
mgm_modes: [MgmInfo::default(); 2],
parent_queues,
children_queues,
event_tx,
}
}
pub fn periodic_operation(&mut self) {
self.handle_telecommands();
self.handle_parent_mode_queue();
self.handle_children_mode_queues();
if self.mode_helper.transition_active() {
self.handle_mode_transition();
}
}
pub fn handle_telecommands(&mut self) {
loop {
match self.tmtc_queues.tc_rx.try_recv() {
Ok(packet) => {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
match postcard::from_bytes::<types::acs::mgm_assembly::request::Request>(
&packet.payload,
) {
Ok(request) => match request {
types::acs::mgm_assembly::request::Request::Ping => {
self.send_telemetry(Some(tc_id), response::Response::Ok)
}
types::acs::mgm_assembly::request::Request::Mode(request) => {
match request {
request::ModeRequest::SetMode(assembly_mode) => {
self.start_transition(false, assembly_mode, Some(tc_id))
}
request::ModeRequest::ReadMode => self.send_telemetry(
Some(tc_id),
response::Response::Mode(response::ModeResponse::Mode(
self.mode(),
)),
),
}
}
},
Err(e) => {
log::warn!("failed to deserialize request: {}", e);
}
}
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
},
}
}
}
pub fn send_telemetry(
&self,
tc_id: Option<CcsdsPacketIdAndPsc>,
response: types::acs::mgm_assembly::response::Response,
) {
match pack_ccsds_tm_packet_for_now(Self::ID, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
pub fn handle_parent_mode_queue(&mut self) {
loop {
match self.parent_queues.request_rx.try_recv() {
Ok(request) => match request {
request::ModeRequest::SetMode(assembly_mode) => match assembly_mode {
Mode::Device(_device_mode) => {
self.start_transition(false, assembly_mode, None);
}
Mode::NoModeKeeping => {
self.mode_helper.current = Mode::NoModeKeeping;
}
},
request::ModeRequest::ReadMode => self
.parent_queues
.report_tx
.send(response::ModeResponse::Mode(self.mode_helper.current))
.unwrap(),
},
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("packet sender disconnected")
}
},
}
}
}
pub fn handle_children_mode_queues(&mut self) {
let mut mode_report_received = false;
for (idx, rx) in self.children_queues.report_rx_queues.iter_mut().enumerate() {
loop {
match rx.try_recv() {
Ok(report) => match report {
types::acs::mgm::response::ModeResponse::Mode(device_mode) => {
self.mgm_modes[idx].mode = Some(device_mode);
self.mgm_modes[idx].reply_received = true;
mode_report_received = true;
}
types::acs::mgm::response::ModeResponse::SetModeTimeout => {
// Ignore, handle this with our own timeout.
log::warn!("MGM {} mode timeout", idx);
}
},
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("packet sender disconnected")
}
},
}
}
}
if !mode_report_received {
return;
}
// Transition is active, check for completion.
if self.mode_helper.transition_active()
&& self.mgm_modes.iter().all(|i| i.reply_received)
&& let Mode::Device(device_mode) = self.mode_helper.target.unwrap()
{
// If at least one child reached the correct mode, we are done.
if self.mgm_modes.iter().any(|i| i.mode == Some(device_mode)) {
self.handle_mode_reached(true);
} else {
let report = if self.mode_keeping_transition {
response::ModeResponse::CanNotKeepMode(self.mgm_modes.map(|info| info.mode))
} else {
response::ModeResponse::WrongMode(self.mgm_modes.map(|info| info.mode))
};
self.handle_mode_transition_failure(report);
}
}
// Mode keeping active: Check children modes.
if let Mode::Device(device_mode) = self.mode_helper.current
&& self
.mgm_modes
.iter()
.all(|info| info.mode != Some(device_mode))
{
// Children lost mode. Try to command them back to the correct
// mode.
self.start_transition(true, self.mode_helper.current, None);
}
}
pub fn handle_mode_transition(&mut self) {
if self.mode_helper.target.is_none() {
self.handle_mode_reached(true);
return;
}
let target = self.mode_helper.target.unwrap();
let device_mode = match target {
Mode::Device(device_mode) => device_mode,
Mode::NoModeKeeping => {
self.handle_mode_reached(true);
return;
}
};
if self.mode_helper.transition_state == TransitionState::Idle {
self.command_children(device_mode);
self.mode_helper.transition_state = TransitionState::AwaitingReplies;
}
if self.mode_helper.transition_state == TransitionState::AwaitingReplies
&& self.mode_helper.timed_out()
{
let report = if self.mode_keeping_transition {
response::ModeResponse::CanNotKeepMode(self.mgm_modes.map(|info| info.mode))
} else {
response::ModeResponse::SetModeTimeout(self.mgm_modes.map(|info| info.mode))
};
self.handle_mode_transition_failure(report);
}
}
pub fn handle_mode_reached(&mut self, success: bool) {
let tc_commander = self.mode_helper.finish(success);
self.announce_mode();
if tc_commander.is_some() {
self.send_telemetry(tc_commander, response::Response::Ok);
}
self.parent_queues
.report_tx
.send(response::ModeResponse::Mode(self.mode_helper.current))
.unwrap();
}
pub fn handle_mode_transition_failure(&mut self, report: response::ModeResponse) {
if self.mode_helper.tc_commander.is_some() {
self.send_telemetry(
self.mode_helper.tc_commander,
response::Response::Mode(response::ModeResponse::SetModeTimeout(
self.mgm_modes.map(|info| info.mode),
)),
);
}
self.parent_queues.report_tx.send(report).unwrap();
self.mode_helper.finish(false);
}
pub fn command_children(&self, mode: DeviceMode) {
for tx in &self.children_queues.request_tx_queues {
tx.send(types::acs::mgm::request::ModeRequest::SetMode(mode))
.unwrap();
}
}
pub fn start_transition(
&mut self,
mode_keeping: bool,
target: Mode,
tc_id: Option<CcsdsPacketIdAndPsc>,
) {
self.mode_keeping_transition = mode_keeping;
self.mode_helper.tc_commander = tc_id;
self.mgm_modes
.iter_mut()
.for_each(|m| m.reply_received = false);
self.mode_helper.start(target);
}
fn announce_mode(&self) {
log::info!(
"{:?} announcing mode: {:?}",
Self::ID,
self.mode_helper.current
);
if let Err(e) = self
.event_tx
.send(types::acs::mgm_assembly::Event::ModeChanged(
self.mode_helper.current,
))
{
log::warn!("{:?}: failed to send mode changed event: {}", Self::ID, e);
}
}
#[inline]
pub fn mode(&self) -> Mode {
self.mode_helper.current
}
#[inline]
#[cfg(test)]
fn mode_transition_active(&self) -> bool {
self.mode_helper.transition_active()
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc::TryRecvError;
use arbitrary_int::u11;
use satrs::spacepackets::SpacePacketHeader;
use types::{
Apid, Message, MessageType, TcHeader,
acs::mgm_assembly,
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
};
use super::*;
pub struct Testbench {
subsystem_req_tx: mpsc::SyncSender<request::ModeRequest>,
subsystem_report_rx: mpsc::Receiver<response::ModeResponse>,
mgm_request_rx: [mpsc::Receiver<types::acs::mgm::request::ModeRequest>; 2],
mgm_report_tx: [mpsc::SyncSender<types::acs::mgm::response::ModeResponse>; 2],
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
event_rx: mpsc::Receiver<mgm_assembly::Event>,
assembly: Assembly,
}
impl Testbench {
pub fn new() -> Self {
let (subsystem_req_tx, subsystem_req_rx) = mpsc::sync_channel(5);
let (subsystem_report_tx, subsystem_report_rx) = mpsc::sync_channel(5);
let (mgm_0_mode_request_tx, mgm_0_mode_request_rx) = mpsc::sync_channel(5);
let (mgm_1_mode_request_tx, mgm_1_mode_request_rx) = mpsc::sync_channel(5);
let (mgm_0_mode_report_tx, mgm_0_mode_report_rx) = mpsc::sync_channel(5);
let (mgm_1_mode_report_tx, mgm_1_mode_report_rx) = mpsc::sync_channel(5);
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
let (event_tx, event_rx) = mpsc::sync_channel(5);
Self {
subsystem_req_tx,
subsystem_report_rx,
mgm_request_rx: [mgm_0_mode_request_rx, mgm_1_mode_request_rx],
mgm_report_tx: [mgm_0_mode_report_tx, mgm_1_mode_report_tx],
tc_tx,
tm_rx,
event_rx,
assembly: Assembly::new(
ParentQueueHelper {
request_rx: subsystem_req_rx,
report_tx: subsystem_report_tx,
},
ChildrenQueueHelper {
request_tx_queues: [mgm_0_mode_request_tx, mgm_1_mode_request_tx],
report_rx_queues: [mgm_0_mode_report_rx, mgm_1_mode_report_rx],
},
TmtcQueues { tc_rx, tm_tx },
Duration::from_millis(20),
event_tx,
),
}
}
pub fn assert_all_queues_empty(&self) {
assert!(
matches!(self.tm_rx.try_recv().unwrap_err(), TryRecvError::Empty),
"TM queue not empty"
);
assert!(
matches!(
self.subsystem_report_rx.try_recv().unwrap_err(),
TryRecvError::Empty
),
"subsystem report queue not empty"
);
for rx in self.mgm_request_rx.iter() {
assert!(
matches!(rx.try_recv().unwrap_err(), TryRecvError::Empty),
"mgm request queue not empty"
)
}
}
}
pub fn create_request_tc(
request: types::acs::mgm_assembly::request::Request,
) -> types::ccsds::CcsdsTcPacketOwned {
types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(Assembly::ID, request.message_type()),
request,
)
}
#[test]
fn basic_test() {
let mut tb = Testbench::new();
tb.assert_all_queues_empty();
tb.assembly.periodic_operation();
tb.assert_all_queues_empty();
assert_eq!(tb.assembly.mode(), Mode::NoModeKeeping);
}
#[test]
fn test_tc_commanded_transition() {
let mut tb = Testbench::new();
tb.tc_tx
.send(create_request_tc(mgm_assembly::request::Request::Mode(
request::ModeRequest::SetMode(Mode::Device(DeviceMode::Normal)),
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Confirm the mode is set.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Normal,
))
.unwrap();
}
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
let response = tb.tm_rx.try_recv().unwrap();
assert_eq!(response.tm_header.sender_id, Assembly::ID);
assert_eq!(response.tm_header.message_type, MessageType::Verification);
let response: response::Response = postcard::from_bytes(&response.payload).unwrap();
assert_eq!(response, response::Response::Ok);
let event = tb.event_rx.try_recv().expect("expected mode changed event");
assert!(matches!(
event,
mgm_assembly::Event::ModeChanged(Mode::Device(DeviceMode::Normal))
));
}
#[test]
fn test_parent_commanded_transition() {
let mut tb = Testbench::new();
tb.subsystem_req_tx
.send(request::ModeRequest::SetMode(Mode::Device(
DeviceMode::Normal,
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Confirm the mode is set.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Normal,
))
.unwrap();
}
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
);
}
#[test]
fn test_one_mgm_is_sufficient() {
let mut tb = Testbench::new();
tb.subsystem_req_tx
.send(request::ModeRequest::SetMode(Mode::Device(
DeviceMode::Normal,
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// One device is sufficient.
tb.mgm_report_tx[0]
.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Normal,
))
.unwrap();
tb.mgm_report_tx[1]
.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Off,
))
.unwrap();
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
);
}
#[test]
fn test_mode_commanding_fails() {
let mut tb = Testbench::new();
tb.subsystem_req_tx
.send(request::ModeRequest::SetMode(Mode::Device(
DeviceMode::Normal,
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Confirm the mode is set.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Off,
))
.unwrap();
}
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::NoModeKeeping);
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::WrongMode([Some(DeviceMode::Off), Some(DeviceMode::Off)])
);
}
#[test]
fn test_mode_keeping_fails() {
let mut tb = Testbench::new();
tb.subsystem_req_tx
.send(request::ModeRequest::SetMode(Mode::Device(
DeviceMode::Normal,
)))
.unwrap();
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Confirm the mode is set.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Normal,
))
.unwrap();
}
tb.assembly.periodic_operation();
assert!(!tb.assembly.mode_transition_active());
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
);
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Off,
))
.unwrap();
}
// This should start mode keeping.
tb.assembly.periodic_operation();
assert!(tb.assembly.mode_transition_active());
for rx in tb.mgm_request_rx.iter() {
let request = rx.try_recv().unwrap();
assert_eq!(
request,
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
);
}
// Let the mode keeping fail.
for tx in tb.mgm_report_tx.iter() {
tx.send(types::acs::mgm::response::ModeResponse::Mode(
DeviceMode::Off,
))
.unwrap();
}
tb.assembly.periodic_operation();
let report = tb.subsystem_report_rx.try_recv().unwrap();
assert_eq!(
report,
response::ModeResponse::CanNotKeepMode([Some(DeviceMode::Off), Some(DeviceMode::Off)])
);
}
}
+876
View File
@@ -0,0 +1,876 @@
use std::collections::VecDeque;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use example_std::{HkHelperSingleSet, TmtcQueues};
use minisim_types::acs::mgt as sim_mgt;
use minisim_types::{SimReply, SimRequest, SimRequestWithTime};
use satrs::fdir::{FaultCounterStd, RecoveryEvent};
use satrs::health::HealthTableMapSync;
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use types::acs::mgt::{
self, HkSet,
request::{ModeRequest, Request},
response::{ModeResponse, Response},
};
use types::pcdu::SwitchId;
use types::{ComponentId, DeviceMode, HealthRequest, HkRequestType};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
use crate::device_fdir::{DeviceFdir, FdirEvent};
use crate::device_mode::{ModeTransitionEvent, SwitchAndModeHelper};
use crate::eps::PowerSwitchHelper;
// The handler blocks while waiting for a reply, so this must be well below the cycle time
// of the ACS thread.
pub const REPLY_TIMEOUT: Duration = Duration::from_millis(50);
pub const REPLY_FAULT_THRESHOLD: u32 = 3;
pub const REPLY_FAULT_DECREMENT_AFTER: Duration = Duration::from_secs(30);
/// Interface for ideal device which never fails.
#[derive(Default)]
pub struct DummyInterface {
dipole: mgt::Dipole,
torque_end: Option<Instant>,
}
impl DummyInterface {
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
let reply = match sim_mgt::Request::from_frame(frame).ok()? {
sim_mgt::Request::ApplyTorque { duration, dipole } => {
self.dipole = dipole;
self.torque_end = Some(Instant::now() + duration);
sim_mgt::Reply::Ack
}
sim_mgt::Request::RequestHk => {
let torquing = self.torque_end.is_some_and(|end| Instant::now() < end);
sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: if torquing {
self.dipole
} else {
mgt::Dipole::default()
},
torquing,
})
}
};
Some(reply.to_frame())
}
}
/// Records all sent frames and returns injected reply frames.
#[derive(Default)]
pub struct TestInterface {
pub sent_frames: Vec<Vec<u8>>,
pub replies: VecDeque<Vec<u8>>,
}
pub struct SimInterface {
pub sim_request_tx: mpsc::Sender<SimRequestWithTime>,
pub sim_reply_rx: mpsc::Receiver<SimReply>,
}
impl SimInterface {
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
// Replies which arrived after a previous timeout must not be mistaken for this reply.
while self.sim_reply_rx.try_recv().is_ok() {}
if let Err(e) = self
.sim_request_tx
.send(SimRequestWithTime::new_with_epoch_time(SimRequest::Mgt(
frame.to_vec(),
)))
{
log::error!("failed to send MGT SIM request: {e}");
return None;
}
match self.sim_reply_rx.recv_timeout(REPLY_TIMEOUT).ok()? {
SimReply::Mgt(frame) => Some(frame),
sim_reply => {
log::warn!("unexpected MGT SIM reply: {sim_reply:?}");
None
}
}
}
}
/// Frame based transport to the device. The handler implements the protocol on top of it.
pub enum MgtCommunication {
Dummy(DummyInterface),
Sim(SimInterface),
#[allow(dead_code)]
Test(TestInterface),
}
impl MgtCommunication {
/// Sends a request frame and blocks until the reply frame arrives. Returns [None] if there
/// was no reply in time.
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
match self {
MgtCommunication::Dummy(dummy) => dummy.transfer(frame),
MgtCommunication::Sim(sim) => sim.transfer(frame),
MgtCommunication::Test(test) => {
test.sent_frames.push(frame.to_vec());
test.replies.pop_front()
}
}
}
}
/// Helper component for communication with a parent component, which is usually an assembly
/// or a subsystem.
pub struct ModeLeafHelper {
pub request_rx: mpsc::Receiver<ModeRequest>,
pub report_tx: mpsc::SyncSender<ModeResponse>,
}
/// Magnetorquer (MGT) device handler.
///
/// The device is powered through the PCDU and only accepts torque commands in normal mode.
/// In normal mode, the device HK is polled every cycle and cached as the HK set of the handler.
/// Every request is answered with exactly one reply. A missing, invalid or unexpected reply is
/// a fault which is handled by the FDIR.
///
/// This device handler includes several components beyond the scope of commanding the device:
///
/// - The [MgtCommunication] structure models different communication interfaces to the
/// physical device.
/// - The device manages and commands its own power switch using the [SwitchAndModeHelper].
/// - The device FDIR is integrated directly into the device handler using the [DeviceFdir]
/// helper.
/// - Periodic HK is generated using the [HkHelperSingleSet] helper.
/// - The device is a mode leaf in the ACS tree and has a [ModeLeafHelper] for this.
pub struct MgtHandler {
tmtc_queues: TmtcQueues,
pub com: MgtCommunication,
hk_set: HkSet,
hk_helper: HkHelperSingleSet,
switch_and_mode_helper: SwitchAndModeHelper<DeviceMode>,
mode_leaf_helper: ModeLeafHelper,
fdir: DeviceFdir,
event_tx: mpsc::SyncSender<mgt::Event>,
}
impl MgtHandler {
pub fn new(
tmtc_queues: TmtcQueues,
switch_helper: PowerSwitchHelper,
com: MgtCommunication,
mode_leaf_helper: ModeLeafHelper,
mode_timeout: Duration,
health_table: HealthTableMapSync,
event_tx: mpsc::SyncSender<mgt::Event>,
) -> Self {
Self {
tmtc_queues,
com,
hk_set: HkSet::default(),
hk_helper: HkHelperSingleSet::new(false, Duration::from_millis(200)),
switch_and_mode_helper: SwitchAndModeHelper::new(
DeviceMode::Off,
mode_timeout,
switch_helper,
SwitchId::Mgt,
),
mode_leaf_helper,
fdir: DeviceFdir::new(
"MGT",
ComponentId::AcsMgt,
health_table,
FaultCounterStd::new(REPLY_FAULT_THRESHOLD, REPLY_FAULT_DECREMENT_AFTER),
),
event_tx,
}
}
#[inline]
pub fn mode(&self) -> DeviceMode {
self.switch_and_mode_helper.mode()
}
pub fn periodic_operation(&mut self) {
self.handle_telecommands();
self.handle_mode_leaf_handling();
self.fdir
.periodic_operation(&mut self.switch_and_mode_helper);
self.handle_fdir_events();
if let Some(event) = self.switch_and_mode_helper.handle_mode_transition() {
match event {
ModeTransitionEvent::Reached(tc_commander) => {
self.handle_mode_reached(tc_commander)
}
ModeTransitionEvent::Failed(tc_commander) => {
self.handle_mode_transition_failure(tc_commander)
}
ModeTransitionEvent::PowerCycleDone => {
self.fdir.handle_power_cycle_done();
self.handle_fdir_events();
}
ModeTransitionEvent::PowerCycleFailed { restore_mode } => {
self.fdir
.handle_power_cycle_failed(&mut self.switch_and_mode_helper, restore_mode);
self.handle_fdir_events();
}
}
}
if self.ready_for_commanding() {
self.poll_hk();
}
if self.hk_helper.needs_generation() {
self.send_telemetry(None, Response::Hk(self.hk_set));
}
}
fn poll_hk(&mut self) {
match self.transfer(sim_mgt::Request::RequestHk) {
Some(sim_mgt::Reply::Hk(hk)) => {
self.fdir.register_success();
self.hk_set = HkSet {
valid: true,
dipole: hk.dipole,
torquing: hk.torquing,
};
}
reply => self.register_reply_fault(reply),
}
}
/// Returns [None] if there was no reply in time or the reply frame was invalid.
fn transfer(&mut self, request: sim_mgt::Request) -> Option<sim_mgt::Reply> {
let frame = self.com.transfer(&request.to_frame())?;
sim_mgt::Reply::from_frame(&frame)
.inspect_err(|e| log::warn!("MGT: invalid reply frame {frame:02x?}: {e}"))
.ok()
}
fn register_reply_fault(&mut self, reply: Option<sim_mgt::Reply>) {
log::warn!("MGT: missing or unexpected reply {reply:?}");
self.hk_set.valid = false;
self.fdir.register_fault(&mut self.switch_and_mode_helper);
self.handle_fdir_events();
}
fn handle_fdir_events(&mut self) {
while let Some(event) = self.fdir.next_event() {
let event = match event {
FdirEvent::FaultThresholdExceeded => mgt::Event::ReplyFaultThresholdExceeded,
FdirEvent::Recovery(recovery_event) => {
// The device is power cycled or switched off.
if matches!(
recovery_event,
RecoveryEvent::Started | RecoveryEvent::ThresholdExceeded
) {
self.hk_set = HkSet::default();
}
mgt::Event::Recovery(recovery_event)
}
};
self.send_event(event);
}
}
fn ready_for_commanding(&self) -> bool {
self.mode() == DeviceMode::Normal && self.switch_and_mode_helper.target().is_none()
}
fn handle_telecommands(&mut self) {
while let Ok(packet) = self.tmtc_queues.tc_rx.try_recv() {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
let request = match postcard::from_bytes::<Request>(&packet.payload) {
Ok(request) => request,
Err(e) => {
log::warn!("MGT: failed to deserialize request: {}", e);
continue;
}
};
log::info!(
"MGT: received request {:?} with TC ID {:#010x}",
request,
tc_id.raw()
);
match request {
Request::Ping => self.send_telemetry(Some(tc_id), Response::Ok),
Request::Hk(hk_request) => self.handle_hk_request(tc_id, hk_request),
Request::Mode(ModeRequest::SetMode(mode)) => {
self.start_transition(mode, Some(tc_id))
}
Request::Mode(ModeRequest::ReadMode) => self.send_telemetry(
Some(tc_id),
Response::Mode(ModeResponse::Mode(
self.switch_and_mode_helper.reported_mode(),
)),
),
Request::ApplyTorque { dipole, duration } => {
self.handle_torque_command(tc_id, dipole, duration)
}
Request::Health(HealthRequest::SetHealth(health)) => {
log::info!("MGT: setting health to {health:?} via ground command");
self.fdir.set_health(health);
self.send_telemetry(Some(tc_id), Response::Ok);
}
}
}
}
fn handle_mode_leaf_handling(&mut self) {
while let Ok(request) = self.mode_leaf_helper.request_rx.try_recv() {
match request {
ModeRequest::SetMode(mode) => self.start_transition(mode, None),
ModeRequest::ReadMode => self.report_mode_to_parent(),
}
}
}
fn handle_hk_request(&mut self, tc_id: CcsdsPacketIdAndPsc, hk_request: HkRequestType) {
match hk_request {
HkRequestType::OneShot => self.send_telemetry(Some(tc_id), Response::Hk(self.hk_set)),
HkRequestType::EnablePeriodic(opt_interval) => {
self.hk_helper.enabled = true;
if let Some(interval) = opt_interval {
self.hk_helper.frequency = interval;
}
}
HkRequestType::DisablePeriodic => self.hk_helper.enabled = false,
HkRequestType::ModifyInterval(interval) => self.hk_helper.frequency = interval,
_ => log::warn!("MGT: unhandled HK request"),
}
}
fn handle_torque_command(
&mut self,
tc_id: CcsdsPacketIdAndPsc,
dipole: mgt::Dipole,
duration: Duration,
) {
if !self.ready_for_commanding() {
log::warn!("MGT: rejecting torque command, device not in normal mode");
self.send_telemetry(Some(tc_id), Response::NotInNormalMode);
return;
}
match self.transfer(sim_mgt::Request::ApplyTorque { duration, dipole }) {
Some(sim_mgt::Reply::Ack) => {
self.fdir.register_success();
self.send_telemetry(Some(tc_id), Response::Ok);
}
reply => {
self.register_reply_fault(reply);
self.send_telemetry(Some(tc_id), Response::ReplyTimeout);
}
}
}
fn start_transition(
&mut self,
target_mode: DeviceMode,
tc_commander: Option<CcsdsPacketIdAndPsc>,
) {
log::info!("MGT: transitioning to mode {:?}", target_mode);
self.fdir.handle_mode_command(&self.switch_and_mode_helper);
if target_mode == DeviceMode::Off {
self.hk_set = HkSet::default();
}
self.switch_and_mode_helper
.start_transition(target_mode, tc_commander);
}
fn handle_mode_reached(&mut self, tc_commander: Option<CcsdsPacketIdAndPsc>) {
log::info!("MGT: mode {:?} reached", self.mode());
self.send_event(mgt::Event::ModeChanged(self.mode()));
if tc_commander.is_some() {
self.send_telemetry(tc_commander, Response::Ok);
}
self.report_mode_to_parent();
}
fn handle_mode_transition_failure(&mut self, tc_commander: Option<CcsdsPacketIdAndPsc>) {
if tc_commander.is_some() {
self.send_telemetry(tc_commander, Response::Mode(ModeResponse::SetModeTimeout));
}
self.mode_leaf_helper
.report_tx
.send(ModeResponse::SetModeTimeout)
.unwrap();
}
fn report_mode_to_parent(&self) {
self.mode_leaf_helper
.report_tx
.send(ModeResponse::Mode(
self.switch_and_mode_helper.reported_mode(),
))
.unwrap();
}
fn send_event(&self, event: mgt::Event) {
if let Err(e) = self.event_tx.send(event) {
log::warn!("MGT: failed to send event {:?}: {}", event, e);
}
}
fn send_telemetry(&self, tc_id: Option<CcsdsPacketIdAndPsc>, response: Response) {
match pack_ccsds_tm_packet_for_now(ComponentId::AcsMgt, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
log::warn!("MGT: failed to send TM packet: {}", e);
}
}
Err(e) => log::warn!("MGT: failed to pack TM packet: {}", e),
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use arbitrary_int::u11;
use satrs::health::{HealthState, HealthTableProvider};
use satrs::spacepackets::SpacePacketHeader;
use types::{
Apid, Message as _, TcHeader,
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
pcdu::{SwitchRequest, SwitchState, SwitchStateBinary},
};
use crate::device_fdir::RECOVERY_THRESHOLD;
use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet};
use super::*;
impl TestInterface {
fn sent_requests(&self) -> Vec<sim_mgt::Request> {
self.sent_frames
.iter()
.map(|frame| sim_mgt::Request::from_frame(frame).unwrap())
.collect()
}
fn push_reply(&mut self, reply: sim_mgt::Reply) {
self.replies.push_back(reply.to_frame());
}
}
struct MgtTestbench {
parent_request_tx: mpsc::SyncSender<ModeRequest>,
parent_report_rx: mpsc::Receiver<ModeResponse>,
shared_switch_set: SharedSwitchSet,
switch_rx: mpsc::Receiver<SwitchRequest>,
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
event_rx: mpsc::Receiver<mgt::Event>,
health_table: HealthTableMapSync,
handler: MgtHandler,
}
impl MgtTestbench {
fn new() -> Self {
let (parent_request_tx, request_rx) = mpsc::sync_channel(5);
let (report_tx, parent_report_rx) = mpsc::sync_channel(5);
let (tc_tx, tc_rx) = mpsc::sync_channel(10);
let (tm_tx, tm_rx) = mpsc::sync_channel(10);
let (switch_tx, switch_rx) = mpsc::sync_channel(10);
let (event_tx, event_rx) = mpsc::sync_channel(20);
let mut switch_map = SwitchMap::new();
switch_map.insert(SwitchId::Mgt, SwitchState::Off);
let shared_switch_set = SharedSwitchSet::new(Mutex::new(SwitchSet::new(switch_map)));
let health_table = HealthTableMapSync::default();
let mut handler = MgtHandler::new(
TmtcQueues { tc_rx, tm_tx },
PowerSwitchHelper::new(switch_tx, shared_switch_set.clone()),
MgtCommunication::Test(TestInterface::default()),
ModeLeafHelper {
request_rx,
report_tx,
},
Duration::from_millis(100),
health_table.clone(),
event_tx,
);
handler.fdir.recovery_off_duration = Duration::ZERO;
Self {
parent_request_tx,
parent_report_rx,
shared_switch_set,
switch_rx,
tc_tx,
tm_rx,
event_rx,
health_table,
handler,
}
}
fn send_tc(&self, request: Request) {
self.tc_tx
.send(CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(ComponentId::AcsMgt, request.message_type()),
request,
))
.unwrap();
}
fn next_response(&self) -> Response {
let tm = self.tm_rx.try_recv().expect("no TM generated");
assert_eq!(tm.tm_header.sender_id, ComponentId::AcsMgt);
postcard::from_bytes(&tm.payload).expect("invalid MGT response")
}
/// Completes the power switch handshake for a commanded switch-on.
fn switch_to_normal(&mut self) {
self.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Normal)));
self.handler.periodic_operation();
self.set_switch_state(SwitchState::On);
self.handler.periodic_operation();
assert_eq!(self.handler.mode(), DeviceMode::Normal);
assert_eq!(self.next_response(), Response::Ok);
}
fn test_interface(&mut self) -> &mut TestInterface {
match &mut self.handler.com {
MgtCommunication::Test(test) => test,
_ => panic!("unexpected MGT interface"),
}
}
fn set_switch_state(&self, state: SwitchState) {
self.shared_switch_set
.lock()
.unwrap()
.set_switch_state(SwitchId::Mgt, state);
}
fn health(&self) -> Option<HealthState> {
self.health_table.health(ComponentId::AcsMgt.into())
}
fn drain_events(&self) -> Vec<mgt::Event> {
self.event_rx.try_iter().collect()
}
/// No replies are injected, so every HK poll is a fault. The poll in the cycle which
/// reached normal mode already registered the first fault.
fn exceed_reply_fault_threshold(&mut self) {
for _ in 0..REPLY_FAULT_THRESHOLD {
self.handler.periodic_operation();
}
}
/// Drives a started power cycle recovery to completion, completing both power switch
/// handshakes.
fn complete_power_cycle(&mut self) {
self.handler.periodic_operation();
self.set_switch_state(SwitchState::Off);
self.handler.periodic_operation();
assert_eq!(self.handler.mode(), DeviceMode::Off);
self.handler.periodic_operation();
self.set_switch_state(SwitchState::On);
self.handler.periodic_operation();
assert_eq!(self.handler.mode(), DeviceMode::Normal);
}
}
#[test]
fn test_initial_state_no_polling() {
let mut testbench = MgtTestbench::new();
testbench.handler.periodic_operation();
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
assert!(testbench.test_interface().sent_frames.is_empty());
}
#[test]
fn test_switch_to_normal() {
let mut testbench = MgtTestbench::new();
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Normal)));
testbench.handler.periodic_operation();
let switch_request = testbench.switch_rx.try_recv().expect("no switch request");
assert_eq!(switch_request.switch_id, SwitchId::Mgt);
assert_eq!(switch_request.target_state, SwitchStateBinary::On);
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
testbench.set_switch_state(SwitchState::On);
testbench.handler.periodic_operation();
assert_eq!(testbench.handler.mode(), DeviceMode::Normal);
assert_eq!(testbench.next_response(), Response::Ok);
assert!(matches!(
testbench.event_rx.try_recv(),
Ok(mgt::Event::ModeChanged(DeviceMode::Normal))
));
assert_eq!(
testbench.parent_report_rx.try_recv(),
Ok(ModeResponse::Mode(DeviceMode::Normal))
);
}
#[test]
fn test_mode_command_from_parent() {
let mut testbench = MgtTestbench::new();
testbench
.parent_request_tx
.send(ModeRequest::SetMode(DeviceMode::Normal))
.unwrap();
testbench.handler.periodic_operation();
testbench.set_switch_state(SwitchState::On);
testbench.handler.periodic_operation();
assert_eq!(
testbench.parent_report_rx.try_recv(),
Ok(ModeResponse::Mode(DeviceMode::Normal))
);
// Commanded by the parent, so no TC response is expected.
assert!(testbench.tm_rx.try_recv().is_err());
}
#[test]
fn test_torque_command_rejected_when_off() {
let mut testbench = MgtTestbench::new();
testbench.send_tc(Request::ApplyTorque {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
duration: Duration::from_millis(100),
});
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::NotInNormalMode);
assert!(testbench.test_interface().sent_frames.is_empty());
}
#[test]
fn test_torque_command_forwarded_in_normal_mode() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.test_interface().sent_frames.clear();
testbench.test_interface().push_reply(sim_mgt::Reply::Ack);
testbench.send_tc(Request::ApplyTorque {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
duration: Duration::from_millis(100),
});
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::Ok);
assert_eq!(
testbench.test_interface().sent_requests().first(),
Some(&sim_mgt::Request::ApplyTorque {
duration: Duration::from_millis(100),
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
})
);
}
#[test]
fn test_torque_command_without_ack() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.send_tc(Request::ApplyTorque {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
duration: Duration::from_millis(100),
});
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::ReplyTimeout);
}
#[test]
fn test_hk_polling_updates_hk_set() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
assert!(
testbench
.test_interface()
.sent_requests()
.contains(&sim_mgt::Request::RequestHk)
);
testbench
.test_interface()
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
torquing: true,
}));
testbench.handler.periodic_operation();
testbench.send_tc(Request::Hk(HkRequestType::OneShot));
testbench.handler.periodic_operation();
assert_eq!(
testbench.next_response(),
Response::Hk(HkSet {
valid: true,
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
torquing: true,
})
);
}
#[test]
fn test_switch_off() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.drain_events();
while testbench.parent_report_rx.try_recv().is_ok() {}
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Off)));
testbench.handler.periodic_operation();
let switch_request = testbench
.switch_rx
.try_iter()
.last()
.expect("no switch request");
assert_eq!(switch_request.target_state, SwitchStateBinary::Off);
testbench.set_switch_state(SwitchState::Off);
testbench.handler.periodic_operation();
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
assert_eq!(testbench.next_response(), Response::Ok);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Off)]
));
assert_eq!(
testbench.parent_report_rx.try_recv(),
Ok(ModeResponse::Mode(DeviceMode::Off))
);
}
#[test]
fn test_hk_set_invalid_after_switch_off() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench
.test_interface()
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
}));
testbench.handler.periodic_operation();
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Off)));
testbench.send_tc(Request::Hk(HkRequestType::OneShot));
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::Hk(HkSet::default()));
}
#[test]
fn test_periodic_hk() {
let mut testbench = MgtTestbench::new();
testbench.send_tc(Request::Hk(HkRequestType::EnablePeriodic(Some(
Duration::ZERO,
))));
testbench.handler.periodic_operation();
assert!(matches!(testbench.next_response(), Response::Hk(_)));
let tm = testbench.tm_rx.try_recv();
assert!(tm.is_err(), "only one HK packet per cycle expected");
testbench.send_tc(Request::Hk(HkRequestType::DisablePeriodic));
testbench.handler.periodic_operation();
assert!(testbench.tm_rx.try_recv().is_err());
}
#[test]
fn test_valid_reply_no_fault() {
let mut testbench = MgtTestbench::new();
testbench
.test_interface()
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
}));
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 0);
assert!(testbench.handler.hk_set.valid);
}
#[test]
fn test_missing_reply_registers_fault() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 1);
assert!(!testbench.handler.hk_set.valid);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Normal)]
));
}
#[test]
fn test_unexpected_reply_registers_fault() {
let mut testbench = MgtTestbench::new();
testbench.test_interface().push_reply(sim_mgt::Reply::Ack);
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 1);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Normal)]
));
}
#[test]
fn test_invalid_reply_registers_fault() {
let mut testbench = MgtTestbench::new();
testbench.test_interface().replies.push_back(vec![0xff]);
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 1);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Normal)]
));
}
#[test]
fn test_reply_fault_threshold_starts_recovery() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.drain_events();
while testbench.parent_report_rx.try_recv().is_ok() {}
testbench.exceed_reply_fault_threshold();
assert_eq!(testbench.health(), Some(HealthState::NeedsRecovery));
assert!(matches!(
testbench.drain_events().as_slice(),
[
mgt::Event::ReplyFaultThresholdExceeded,
mgt::Event::Recovery(RecoveryEvent::Started)
]
));
testbench.complete_power_cycle();
assert_eq!(testbench.health(), Some(HealthState::Healthy));
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::Recovery(RecoveryEvent::Done)]
));
// The power cycle is hidden from the parent.
assert!(testbench.parent_report_rx.try_recv().is_err());
}
#[test]
fn test_unresponsive_device_marked_faulty() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
for _ in 0..RECOVERY_THRESHOLD {
testbench.exceed_reply_fault_threshold();
assert_eq!(testbench.health(), Some(HealthState::NeedsRecovery));
testbench.complete_power_cycle();
}
testbench.drain_events();
testbench.exceed_reply_fault_threshold();
assert_eq!(testbench.health(), Some(HealthState::Faulty));
assert!(matches!(
testbench.drain_events().as_slice(),
[
mgt::Event::ReplyFaultThresholdExceeded,
mgt::Event::Recovery(RecoveryEvent::ThresholdExceeded)
]
));
assert_eq!(
testbench.handler.switch_and_mode_helper.target(),
Some(DeviceMode::Off)
);
}
#[test]
fn test_set_health() {
let mut testbench = MgtTestbench::new();
testbench.send_tc(Request::Health(HealthRequest::SetHealth(
HealthState::Faulty,
)));
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::Ok);
assert_eq!(testbench.health(), Some(HealthState::Faulty));
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod ctrl;
pub mod mgm;
pub mod mgm_assembly;
pub mod mgt;
pub mod subsystem;
+406
View File
@@ -0,0 +1,406 @@
#![allow(dead_code)]
use std::{
sync::mpsc::{self, Receiver, SyncSender},
time::Duration,
};
use example_std::{ModeHelper, TmtcQueues};
use satrs::{
mode_tree::{
ModeStoreProvider, ModeStoreVec, SequenceModeTables, SequenceTableEntry,
SequenceTableMapTable, SequenceTablesMapValue, TargetModeTables,
},
spacepackets::CcsdsPacketIdAndPsc,
subsystem::{
ModeCommandingResult, ModeRaw, ModeResponse, ModeSetRequest, ModeTreeHelperError,
SubsystemCommandingHelper, SubsystemHelperResult,
},
};
use types::{
ComponentId,
acs::subsystem::{Mode, response},
};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum TransitionState {
#[default]
Idle,
AwaitingReplies,
}
fn build_sequence_tables() -> SequenceModeTables {
let mut seq_tables = SequenceModeTables::default();
let mut off_table = SequenceTablesMapValue::new("OFF");
let mut off_step_0 = SequenceTableMapTable::new("OFF_STEP_0");
off_step_0.add_entry(SequenceTableEntry::new(
"OFF_CTRL_PASSIVE",
ComponentId::AcsController as satrs::ComponentId,
types::acs::ctrl::Mode::Passive.into(),
true,
));
off_table.add_sequence_table(off_step_0);
let mut off_step_1 = SequenceTableMapTable::new("OFF_STEP_1");
off_step_1.add_entry(SequenceTableEntry::new(
"OFF_MGM_ASSY_OFF",
ComponentId::AcsMgmAssembly as satrs::ComponentId,
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Off).into(),
false,
));
off_step_1.add_entry(SequenceTableEntry::new(
"OFF_MGT_OFF",
ComponentId::AcsMgt as satrs::ComponentId,
types::DeviceMode::Off.into(),
false,
));
off_table.add_sequence_table(off_step_1);
seq_tables.0.insert(Mode::Off as ModeRaw, off_table);
let mut safe_table = SequenceTablesMapValue::new("SAFE");
let mut safe_step_0 = SequenceTableMapTable::new("SAFE_STEP_0");
safe_step_0.add_entry(SequenceTableEntry::new(
"SAFE_MGM_ASSY_NORMAL",
ComponentId::AcsMgmAssembly as satrs::ComponentId,
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Normal).into(),
false,
));
safe_step_0.add_entry(SequenceTableEntry::new(
"SAFE_MGT_NORMAL",
ComponentId::AcsMgt as satrs::ComponentId,
types::DeviceMode::Normal.into(),
false,
));
safe_table.add_sequence_table(safe_step_0);
let mut safe_step_1 = SequenceTableMapTable::new("SAFE_STEP_1");
safe_step_1.add_entry(SequenceTableEntry::new(
"SAFE_CTRL_SAFE",
ComponentId::AcsController as satrs::ComponentId,
types::acs::ctrl::Mode::Safe.into(),
false,
));
safe_table.add_sequence_table(safe_step_1);
seq_tables.0.insert(Mode::Safe as ModeRaw, safe_table);
seq_tables
}
fn ctrl_response_to_mode_response(
response: types::acs::ctrl::response::ModeReport,
) -> ModeResponse {
let sender_id = ComponentId::AcsController as satrs::ComponentId;
match response {
types::acs::ctrl::response::ModeReport::Mode(mode) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: mode.into(),
success: true,
},
types::acs::ctrl::response::ModeReport::WrongMode(_) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: 0,
success: false,
},
}
}
fn mgm_assy_response_to_mode_response(
response: types::acs::mgm_assembly::response::ModeResponse,
) -> ModeResponse {
let sender_id = ComponentId::AcsMgmAssembly as satrs::ComponentId;
match response {
types::acs::mgm_assembly::response::ModeResponse::Mode(mode) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: mode.into(),
success: true,
},
types::acs::mgm_assembly::response::ModeResponse::SetModeTimeout(_)
| types::acs::mgm_assembly::response::ModeResponse::WrongMode(_)
| types::acs::mgm_assembly::response::ModeResponse::CanNotKeepMode(_) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: 0,
success: false,
},
}
}
fn mgt_response_to_mode_response(
response: types::acs::mgt::response::ModeResponse,
) -> ModeResponse {
let sender_id = ComponentId::AcsMgt as satrs::ComponentId;
match response {
types::acs::mgt::response::ModeResponse::Mode(mode) => ModeResponse {
request_id: 0,
sender_id,
reported_mode: mode.into(),
success: true,
},
types::acs::mgt::response::ModeResponse::SetModeTimeout => ModeResponse {
request_id: 0,
sender_id,
reported_mode: 0,
success: false,
},
}
}
#[derive(Debug)]
pub struct ModeRequestSenders {
pub mode_request_ctrl: SyncSender<types::acs::ctrl::request::ModeRequest>,
pub mode_request_mgm_assy: SyncSender<types::acs::mgm_assembly::request::ModeRequest>,
pub mode_request_mgt: SyncSender<types::acs::mgt::request::ModeRequest>,
}
#[derive(Debug)]
pub struct ModeReportReceivers {
pub mode_response_ctrl: Receiver<types::acs::ctrl::response::ModeReport>,
pub mode_response_mgm_assy: Receiver<types::acs::mgm_assembly::response::ModeResponse>,
pub mode_response_mgt: Receiver<types::acs::mgt::response::ModeResponse>,
}
#[derive(Debug)]
pub struct Subsystem {
mode_helper: ModeHelper<types::acs::subsystem::Mode, TransitionState>,
mode_request_senders: ModeRequestSenders,
mode_report_receivers: ModeReportReceivers,
tmtc_queues: TmtcQueues,
subsystem_helper: SubsystemCommandingHelper,
}
impl Subsystem {
pub const ID: ComponentId = ComponentId::AcsSubsystem;
pub fn new(
mode_request_senders: ModeRequestSenders,
mode_report_receivers: ModeReportReceivers,
tmtc_queues: TmtcQueues,
) -> Self {
let mut mode_store_vec = ModeStoreVec::default();
mode_store_vec
.add_component(
ComponentId::AcsMgmAssembly as satrs::ComponentId,
types::acs::mgm_assembly::Mode::NoModeKeeping.into(),
)
.unwrap();
mode_store_vec
.add_component(
ComponentId::AcsController as satrs::ComponentId,
types::acs::ctrl::Mode::Passive.into(),
)
.unwrap();
mode_store_vec
.add_component(
ComponentId::AcsMgt as satrs::ComponentId,
types::DeviceMode::Off.into(),
)
.unwrap();
let target_tables = TargetModeTables::default();
let sequence_tables = build_sequence_tables();
Self {
mode_helper: ModeHelper::new(
types::acs::subsystem::Mode::Off,
Duration::from_millis(2000),
),
mode_request_senders,
mode_report_receivers,
tmtc_queues,
subsystem_helper: SubsystemCommandingHelper::new(
mode_store_vec,
target_tables,
sequence_tables,
),
}
}
pub fn periodic_operation(&mut self) {
self.handle_telecommands();
let mut mode_requests = Vec::new();
while let Ok(response) = self.mode_report_receivers.mode_response_ctrl.try_recv() {
let result = self
.subsystem_helper
.state_machine(Some(ctrl_response_to_mode_response(response)), |request| {
mode_requests.push(request)
});
self.handle_state_machine_result(result);
}
while let Ok(response) = self.mode_report_receivers.mode_response_mgm_assy.try_recv() {
let result = self.subsystem_helper.state_machine(
Some(mgm_assy_response_to_mode_response(response)),
|request| mode_requests.push(request),
);
self.handle_state_machine_result(result);
}
while let Ok(response) = self.mode_report_receivers.mode_response_mgt.try_recv() {
let result = self
.subsystem_helper
.state_machine(Some(mgt_response_to_mode_response(response)), |request| {
mode_requests.push(request)
});
self.handle_state_machine_result(result);
}
let result = self
.subsystem_helper
.state_machine(None, |request| mode_requests.push(request));
self.handle_state_machine_result(result);
for request in mode_requests {
self.handle_mode_set_request(request);
}
}
fn handle_state_machine_result(
&mut self,
result: Result<SubsystemHelperResult, ModeTreeHelperError>,
) {
match result {
Ok(result) => match result {
SubsystemHelperResult::Idle => (),
SubsystemHelperResult::TargetKeeping => (),
SubsystemHelperResult::ModeCommanding(mode_commanding_result) => {
match mode_commanding_result {
ModeCommandingResult::Done => {
let target_mode_typed =
self.subsystem_helper.seq_exec_helper.target_mode();
let target_mode = target_mode_typed.map(Mode::try_from);
log::info!(
"ACS SS: mode commanding for target mode {:?} done",
target_mode
);
self.finish_mode_transition();
}
ModeCommandingResult::StepDone => log::info!("mode commanding step done"),
ModeCommandingResult::AwaitingSuccessCheck => {
log::info!("ACS SS: mode commanding awaiting success check")
}
}
}
},
Err(e) => {
log::error!("mode tree helper error: {}", e);
self.mode_helper.finish(false);
}
}
}
fn finish_mode_transition(&mut self) {
let tc_commander = self.mode_helper.finish(true);
if let Some(requestor) = tc_commander {
self.send_telemetry(
Some(requestor),
response::Response::Mode(response::ModeResponse::Mode(self.mode_helper.current)),
);
}
}
fn handle_mode_set_request(&mut self, request: ModeSetRequest) {
let target_id = ComponentId::try_from(request.target_id);
if let Ok(target_id) = target_id {
match target_id {
ComponentId::AcsMgmAssembly => self
.mode_request_senders
.mode_request_mgm_assy
.send(types::acs::mgm_assembly::request::ModeRequest::SetMode(
types::acs::mgm_assembly::Mode::try_from(request.mode).unwrap(),
))
.unwrap(),
ComponentId::AcsController => self
.mode_request_senders
.mode_request_ctrl
.send(types::acs::ctrl::request::ModeRequest::SetMode(
types::acs::ctrl::Mode::try_from(request.mode).unwrap(),
))
.unwrap(),
ComponentId::AcsMgt => self
.mode_request_senders
.mode_request_mgt
.send(types::acs::mgt::request::ModeRequest::SetMode(
types::DeviceMode::try_from(request.mode).unwrap(),
))
.unwrap(),
_ => {
log::error!("invalid target ID {:?} for mode command", target_id);
}
}
}
}
pub fn handle_telecommands(&mut self) {
loop {
match self.tmtc_queues.tc_rx.try_recv() {
Ok(packet) => {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
match postcard::from_bytes::<types::acs::subsystem::request::Request>(
&packet.payload,
) {
Ok(request) => match request {
types::acs::subsystem::request::Request::Ping => {
self.send_telemetry(Some(tc_id), response::Response::Ok)
}
types::acs::subsystem::request::Request::Mode(mode_request) => {
self.handle_mode_request(Some(tc_id), mode_request);
}
},
Err(e) => {
log::warn!("failed to deserialize request: {}", e);
}
}
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
},
}
}
}
pub fn handle_mode_request(
&mut self,
tc_id: Option<CcsdsPacketIdAndPsc>,
mode_request: types::acs::subsystem::request::ModeRequest,
) {
match mode_request {
types::acs::subsystem::request::ModeRequest::SetMode(target_mode) => {
self.mode_helper.start(target_mode);
self.mode_helper.tc_commander = tc_id;
if let Err(e) = self
.subsystem_helper
.start_command_sequence(target_mode as ModeRaw)
{
log::error!("error starting command sequence: {}", e);
}
}
types::acs::subsystem::request::ModeRequest::ReadMode => {
self.send_telemetry(
None,
response::Response::Mode(response::ModeResponse::Mode(
self.mode_helper.current,
)),
);
}
}
}
pub fn send_telemetry(
&self,
tc_id: Option<CcsdsPacketIdAndPsc>,
response: types::acs::subsystem::response::Response,
) {
match crate::ccsds::pack_ccsds_tm_packet_for_now(Self::ID, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
}
+34
View File
@@ -0,0 +1,34 @@
use arbitrary_int::u11;
use satrs::spacepackets::{
CcsdsPacketIdAndPsc, SpHeader,
time::{StdTimestampError, cds::CdsTime},
};
use serde::Serialize;
use types::{Apid, ComponentId, Message, TmHeader, ccsds::CcsdsTmPacketOwned};
#[derive(Debug, thiserror::Error)]
pub enum CcsdsTmCreationError {
#[error("postcard error: {0}")]
Postcard(#[from] postcard::Error),
#[error("timestamp error: {0}")]
Time(#[from] StdTimestampError),
}
pub fn pack_ccsds_tm_packet_for_now(
sender_id: ComponentId,
tc_id: Option<CcsdsPacketIdAndPsc>,
payload: &(impl Serialize + Message),
) -> Result<CcsdsTmPacketOwned, CcsdsTmCreationError> {
let now = CdsTime::now_with_u16_days()?;
let sp_header = SpHeader::new_from_apid(u11::new(Apid::Tmtc as u16));
let tm_header = TmHeader::new(
sender_id,
ComponentId::Ground,
payload.message_type(),
tc_id,
&now,
);
Ok(CcsdsTmPacketOwned::new_with_serde_payload(
sp_header, &tm_header, payload,
)?)
}
+175
View File
@@ -0,0 +1,175 @@
use arbitrary_int::u11;
use lazy_static::lazy_static;
use satrs::{
legacy::res_code::ResultU16,
spacepackets::{PacketId, PacketType},
};
use satrs_mib::res_code::ResultU16Info;
use satrs_mib::resultcode;
use std::{collections::HashSet, net::Ipv4Addr};
use strum::IntoEnumIterator as _;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use satrs::pool::{StaticMemoryPool, StaticPoolConfig};
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum CustomPusServiceId {
Mode = 200,
Health = 201,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum RequestTargetId {
AcsSubsystem = 1,
}
pub const AOCS_APID: u16 = 1;
#[derive(Debug)]
pub enum GroupId {
Tmtc = 0,
Hk = 1,
Mode = 2,
}
pub const OBSW_SERVER_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
pub const SERVER_PORT: u16 = 7301;
lazy_static! {
pub static ref PACKET_ID_VALIDATOR: HashSet<PacketId> = {
let mut set = HashSet::new();
for id in types::Apid::iter() {
set.insert(PacketId::new(PacketType::Tc, true, u11::new(id as u16)));
}
set
};
pub static ref APID_VALIDATOR: HashSet<u16> = {
let mut set = HashSet::new();
for id in types::Apid::iter() {
set.insert(id as u16);
}
set
};
}
pub mod tmtc_err {
use super::*;
#[resultcode]
pub const INVALID_PUS_SERVICE: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 0);
#[resultcode]
pub const INVALID_PUS_SUBSERVICE: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 1);
#[resultcode]
pub const PUS_SERVICE_NOT_IMPLEMENTED: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 2);
#[resultcode]
pub const PUS_SUBSERVICE_NOT_IMPLEMENTED: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 3);
#[resultcode]
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 4);
#[resultcode]
pub const ROUTING_ERROR: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 5);
#[resultcode(info = "Request timeout for targeted PUS request. P1: Request ID. P2: Target ID")]
pub const REQUEST_TIMEOUT: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 6);
#[resultcode(
info = "Not enough data inside the TC application data field. Optionally includes: \
8 bytes of failure data containing 2 failure parameters, \
P1 (u32 big endian): Expected data length, P2: Found data length"
)]
pub const NOT_ENOUGH_APP_DATA: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 2);
pub const TMTC_RESULTS: &[ResultU16Info] = &[
INVALID_PUS_SERVICE_EXT,
INVALID_PUS_SUBSERVICE_EXT,
PUS_SERVICE_NOT_IMPLEMENTED_EXT,
UNKNOWN_TARGET_ID_EXT,
ROUTING_ERROR_EXT,
NOT_ENOUGH_APP_DATA_EXT,
];
}
pub mod hk_err {
use super::*;
#[resultcode]
pub const TARGET_ID_MISSING: ResultU16 = ResultU16::new(GroupId::Hk as u8, 0);
#[resultcode]
pub const UNIQUE_ID_MISSING: ResultU16 = ResultU16::new(GroupId::Hk as u8, 1);
#[resultcode]
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::new(GroupId::Hk as u8, 2);
#[resultcode]
pub const COLLECTION_INTERVAL_MISSING: ResultU16 = ResultU16::new(GroupId::Hk as u8, 3);
pub const HK_ERR_RESULTS: &[ResultU16Info] = &[
TARGET_ID_MISSING_EXT,
UNKNOWN_TARGET_ID_EXT,
UNKNOWN_TARGET_ID_EXT,
COLLECTION_INTERVAL_MISSING_EXT,
];
}
pub mod mode_err {
use super::*;
#[resultcode]
pub const WRONG_MODE: ResultU16 = ResultU16::new(GroupId::Mode as u8, 0);
}
pub mod components {
use satrs::ComponentId;
pub const NO_SENDER: ComponentId = ComponentId::MAX;
}
pub mod pool {
use super::*;
pub fn create_static_pools() -> (StaticMemoryPool, StaticMemoryPool) {
(
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
],
true,
)),
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
],
true,
)),
)
}
pub fn create_sched_tc_pool() -> StaticMemoryPool {
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
vec![
(30, 32),
(15, 64),
(15, 128),
(15, 256),
(15, 1024),
(15, 2048),
],
true,
))
}
}
pub mod tasks {
pub const FREQ_MS_UDP_TMTC: u64 = 200;
pub const FREQ_MS_AOCS: u64 = 200;
pub const FREQ_MS_CONTROLLER: u64 = 200;
pub const SIM_CLIENT_IDLE_DELAY_MS: u64 = 5;
}
+88
View File
@@ -0,0 +1,88 @@
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use types::{
ComponentId,
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
control,
};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
pub struct Controller {
pub tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
pub tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
pub event_ctrl_tx: std::sync::mpsc::SyncSender<control::Event>,
}
impl Controller {
pub fn new(
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
event_ctrl_tx: std::sync::mpsc::SyncSender<control::Event>,
) -> Self {
Self {
tc_rx,
tm_tx,
event_ctrl_tx,
}
}
pub fn periodic_operation(&mut self) {
self.handle_telecommands();
}
pub fn handle_telecommands(&mut self) {
loop {
match self.tc_rx.try_recv() {
Ok(packet) => {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
match postcard::from_bytes::<control::request::Request>(&packet.payload) {
Ok(request) => {
log::info!(
"received request {:?} with TC ID {:#010x}",
request,
tc_id.raw()
);
match request {
control::request::Request::Ping => self
.send_telemetry(Some(tc_id), control::response::Response::Ok),
control::request::Request::TestEvent => {
self.event_ctrl_tx.send(control::Event::TestEvent).unwrap();
self.send_telemetry(
Some(tc_id),
control::response::Response::Ok,
);
}
}
}
Err(e) => {
log::warn!("failed to deserialize request: {}", e);
}
}
}
Err(e) => match e {
std::sync::mpsc::TryRecvError::Empty => break,
std::sync::mpsc::TryRecvError::Disconnected => {
log::warn!("packet sender disconnected")
}
},
}
}
}
pub fn send_telemetry(
&self,
tc_id: Option<CcsdsPacketIdAndPsc>,
response: control::response::Response,
) {
match pack_ccsds_tm_packet_for_now(ComponentId::Controller, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
}
+200
View File
@@ -0,0 +1,200 @@
use std::collections::VecDeque;
use std::time::Duration;
use satrs::fdir::{FaultCounterStd, FaultResponse, RecoveryEvent, RecoveryFdir};
use satrs::health::{HealthState, HealthTableMapSync};
use types::{ComponentId, DeviceMode};
use crate::device_mode::SwitchAndModeHelper;
// The component is marked faulty if it would be recovered more than RECOVERY_THRESHOLD times
// before the counter is decremented again.
pub const RECOVERY_THRESHOLD: u32 = 2;
pub const RECOVERY_DECREMENT_AFTER: Duration = Duration::from_secs(60);
/// Time the device stays unpowered during a power cycle, so it can fully discharge.
pub const RECOVERY_OFF_DURATION: Duration = Duration::from_millis(500);
/// Generic FDIR events. The device handler maps them to its own event type.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum FdirEvent {
FaultThresholdExceeded,
Recovery(RecoveryEvent),
}
/// Fault counting and power cycle recovery for device handlers which own the power switch of
/// their device.
///
/// The handler detects faults itself and reports them with [Self::register_fault]. This helper
/// then decides whether the device is power cycled, or marked faulty and switched off, and drives
/// the [SwitchAndModeHelper] of the handler accordingly. The handler retrieves the resulting
/// events with [Self::next_event].
pub struct DeviceFdir {
name: &'static str,
fault_counter: FaultCounterStd,
recovery: RecoveryFdir<HealthTableMapSync>,
pub recovery_off_duration: Duration,
events: VecDeque<FdirEvent>,
}
impl DeviceFdir {
pub fn new(
name: &'static str,
component_id: ComponentId,
health_table: HealthTableMapSync,
fault_counter: FaultCounterStd,
) -> Self {
Self {
name,
fault_counter,
recovery: RecoveryFdir::new(
component_id.into(),
health_table,
RECOVERY_THRESHOLD,
RECOVERY_DECREMENT_AFTER,
),
recovery_off_duration: RECOVERY_OFF_DURATION,
events: VecDeque::new(),
}
}
#[cfg(test)]
pub fn fault_count(&self) -> u32 {
self.fault_counter.fault_count()
}
pub fn set_health(&mut self, health: HealthState) {
self.recovery.set_health(health);
}
pub fn next_event(&mut self) -> Option<FdirEvent> {
self.events.pop_front()
}
/// Should be called once per cycle, before the mode transition is handled. Starts a power
/// cycle if the health was set to [HealthState::NeedsRecovery] by the FDIR or by ground.
pub fn periodic_operation(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
self.recovery.periodic_operation();
self.check_needs_recovery(modes);
}
pub fn register_success(&mut self) {
self.fault_counter.try_decrement();
}
/// If the fault threshold is exceeded, the device is power cycled. If it was power cycled
/// too often, the component is marked faulty and commanded off instead.
pub fn register_fault(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
if !self.fault_counter.increment_and_check() {
return;
}
match self.recovery.handle_fault() {
FaultResponse::Ignored => {
log::info!(
"{}: fault threshold exceeded, but component is already faulty, \
recovering or externally controlled",
self.name
);
}
FaultResponse::Recover => {
log::warn!(
"{}: fault threshold exceeded, power cycling device",
self.name
);
self.events.push_back(FdirEvent::FaultThresholdExceeded);
self.check_needs_recovery(modes);
}
FaultResponse::SetFaulty => {
log::error!(
"{}: fault threshold exceeded after too many recoveries, marking \
component faulty",
self.name
);
self.events.push_back(FdirEvent::FaultThresholdExceeded);
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::ThresholdExceeded));
self.switch_off_faulty_device(modes);
}
}
}
/// Mode commands from ground or the parent abort a running recovery. Must be called before
/// the commanded transition is started.
pub fn handle_mode_command(&mut self, modes: &SwitchAndModeHelper<DeviceMode>) {
if modes.power_cycle_active() {
log::warn!("{}: mode command aborts power cycle recovery", self.name);
// Otherwise, the recovery would restart right away.
self.recovery.recovery_done();
}
}
pub fn handle_power_cycle_done(&mut self) {
log::info!("{}: power cycle recovery done", self.name);
// Faults registered while the device was switched off do not count anymore.
self.fault_counter.clear();
self.recovery.recovery_done();
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::Done));
}
/// A failed power cycle costs a recovery attempt like any other fault.
pub fn handle_power_cycle_failed(
&mut self,
modes: &mut SwitchAndModeHelper<DeviceMode>,
restore_mode: DeviceMode,
) {
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::Failed));
match self.recovery.recovery_failed() {
FaultResponse::Recover => {
log::warn!("{}: power cycle recovery failed, retrying", self.name);
self.start_recovery(modes, restore_mode);
}
FaultResponse::SetFaulty => {
log::error!(
"{}: power cycle recovery failed too often, marking component faulty",
self.name
);
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::ThresholdExceeded));
self.switch_off_faulty_device(modes);
}
// Ground changed the health during the recovery and is in charge now.
FaultResponse::Ignored => (),
}
}
fn check_needs_recovery(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
if modes.power_cycle_active() || modes.target().is_some() || !self.recovery.needs_recovery()
{
return;
}
if modes.mode() == DeviceMode::Off {
// Nothing to power cycle, the next switch-on is a fresh start anyway.
log::info!("{}: device is off, no recovery required", self.name);
self.recovery.recovery_done();
return;
}
let restore_mode = modes.mode();
self.start_recovery(modes, restore_mode);
}
fn start_recovery(
&mut self,
modes: &mut SwitchAndModeHelper<DeviceMode>,
restore_mode: DeviceMode,
) {
log::warn!("{}: starting power cycle recovery", self.name);
modes.start_power_cycle(restore_mode, self.recovery_off_duration);
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::Started));
}
fn switch_off_faulty_device(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
// Do not restart an already pending Off transition, which would reset the transition
// state machine before it can finish.
if modes.target() != Some(DeviceMode::Off) {
log::warn!("{}: commanding device off due to fault", self.name);
modes.start_transition(DeviceMode::Off, None);
}
}
}
+504
View File
@@ -0,0 +1,504 @@
use std::time::{Duration, Instant};
use types::pcdu::SwitchId;
use crate::eps::PowerSwitchHelper;
/// This is a helper trait required to make [SwitchAndModeHelper] generic.
///
/// It allows distinguish a powered-off state from one or more powered-on states, so
/// [`SwitchAndModeHelper`] knows which way to drive the switch for a given target mode.
pub trait PowerSwitchedMode: Copy + PartialEq {
const OFF: Self;
fn requires_power(&self) -> bool;
}
impl PowerSwitchedMode for types::DeviceMode {
const OFF: Self = types::DeviceMode::Off;
fn requires_power(&self) -> bool {
*self != types::DeviceMode::Off
}
}
#[derive(Default, Debug, PartialEq, Eq)]
enum SwitchTransitionState {
#[default]
Idle,
PowerSwitching,
Done,
}
/// Outcome of a single power switch transition.
enum SwitchOutcome {
Reached(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
Failed(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
}
/// Dedicated states for power cycling a device.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PowerCycleState<Mode> {
Idle,
SwitchingOff { restore_mode: Mode },
WaitingOff { restore_mode: Mode, since: Instant },
SwitchingOn { restore_mode: Mode },
}
/// Outcome of a pending mode transition, once [`SwitchAndModeHelper::handle_mode_transition`]
/// has driven it to completion. Carries back whichever TC commanded the transition, if any, so
/// the caller can reply to it -- what that reply looks like is handler-specific, so this stays
/// out of the helper.
pub enum ModeTransitionEvent<Mode> {
/// The target mode was reached.
Reached(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
/// The target mode could not be reached.
Failed(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
/// The power cycle completed and the mode before the power cycle was restored.
PowerCycleDone,
/// Power switching failed during the power cycle. The power cycle is not hidden anymore,
/// so [SwitchAndModeHelper::reported_mode] returns the actual mode again. `restore_mode` is
/// the mode the power cycle should have restored, which can be used to retry it.
PowerCycleFailed { restore_mode: Mode },
}
/// Drives the on/off power-switch commanding state machine (Idle -> PowerSwitching -> Done)
/// shared by every device handler that owns a single power switch of its own.
///
/// Handler-specific reactions (sending telemetry, invalidating cached sensor data, reporting to
/// a parent) are not this helper's concern: [`Self::handle_mode_transition`] just reports when a
/// transition finishes (or fails) and leaves what to do about it to the caller.
pub struct SwitchAndModeHelper<Mode: PowerSwitchedMode> {
mode_helper: example_std::ModeHelper<Mode, SwitchTransitionState>,
switch_helper: PowerSwitchHelper,
switch_id: SwitchId,
power_cycle_state: PowerCycleState<Mode>,
power_cycle_off_duration: Duration,
}
impl<Mode: PowerSwitchedMode> SwitchAndModeHelper<Mode> {
pub fn new(
init_mode: Mode,
timeout: Duration,
switch_helper: PowerSwitchHelper,
switch_id: SwitchId,
) -> Self {
Self {
mode_helper: example_std::ModeHelper::new(init_mode, timeout),
switch_helper,
switch_id,
power_cycle_state: PowerCycleState::Idle,
power_cycle_off_duration: Duration::ZERO,
}
}
#[inline]
pub fn mode(&self) -> Mode {
self.mode_helper.current
}
#[inline]
pub fn target(&self) -> Option<Mode> {
self.mode_helper.target
}
/// Mode which should be reported to other components. A power cycle is hidden from them,
/// so this is the mode which is restored after the power cycle while one is active.
pub fn reported_mode(&self) -> Mode {
match self.power_cycle_state {
PowerCycleState::SwitchingOff { restore_mode }
| PowerCycleState::WaitingOff { restore_mode, .. }
| PowerCycleState::SwitchingOn { restore_mode } => restore_mode,
PowerCycleState::Idle => self.mode(),
}
}
#[inline]
pub fn power_cycle_active(&self) -> bool {
self.power_cycle_state != PowerCycleState::Idle
}
/// Starts a new transition, aborting a running power cycle.
pub fn start_transition(
&mut self,
target_mode: Mode,
tc_commander: Option<satrs::spacepackets::CcsdsPacketIdAndPsc>,
) {
self.power_cycle_state = PowerCycleState::Idle;
self.start_transition_internal(target_mode, tc_commander);
}
/// Switches the device off, keeps it off for `off_duration` and then switches it to
/// `restore_mode`. Reaching the intermediate off mode does not generate an event.
pub fn start_power_cycle(&mut self, restore_mode: Mode, off_duration: Duration) {
self.power_cycle_state = PowerCycleState::SwitchingOff { restore_mode };
self.power_cycle_off_duration = off_duration;
self.start_transition_internal(Mode::OFF, None);
}
fn start_transition_internal(
&mut self,
target_mode: Mode,
tc_commander: Option<satrs::spacepackets::CcsdsPacketIdAndPsc>,
) {
self.mode_helper.tc_commander = tc_commander;
self.mode_helper.start(target_mode);
}
/// This is the main API that the periodic handler of a device handler should call.
///
/// It handles the switch commanding and returns relevant events.
pub fn handle_mode_transition(&mut self) -> Option<ModeTransitionEvent<Mode>> {
// The most probable case: Nothing to do.
if self.target().is_none() && !self.power_cycle_active() {
return None;
}
// Handle this as an extra step so the switch transition after this can proceed.
self.handle_waiting_for_off_when_power_cycling();
// Core logic: Command the switches, check whether target switch state was reached.
// Note the ?: if a switch transition is on-going, we might do an early return.
let outcome = self.handle_switch_transition()?;
// Regular mode transition without power cycling.
if self.power_cycle_state == PowerCycleState::Idle {
return Some(match outcome {
SwitchOutcome::Reached(tc_commander) => ModeTransitionEvent::Reached(tc_commander),
SwitchOutcome::Failed(tc_commander) => ModeTransitionEvent::Failed(tc_commander),
});
}
// Power cycling, where a bit more logic is required.
// Handle the error case first.
if let SwitchOutcome::Failed(_) = outcome {
let restore_mode = self.reported_mode();
self.power_cycle_state = PowerCycleState::Idle;
return Some(ModeTransitionEvent::PowerCycleFailed { restore_mode });
}
// At this point: The switching was succesfull, so we only match on the
// power cycle state.
match self.power_cycle_state {
// No switching going on for thse cases.
PowerCycleState::Idle | PowerCycleState::WaitingOff { .. } => None,
PowerCycleState::SwitchingOff { restore_mode } => {
self.power_cycle_state = PowerCycleState::WaitingOff {
restore_mode,
since: Instant::now(),
};
None
}
PowerCycleState::SwitchingOn { .. } => {
// Power is back and we are done.
self.power_cycle_state = PowerCycleState::Idle;
Some(ModeTransitionEvent::PowerCycleDone)
}
}
}
fn handle_waiting_for_off_when_power_cycling(&mut self) {
if let PowerCycleState::WaitingOff {
restore_mode,
since,
} = self.power_cycle_state
&& since.elapsed() >= self.power_cycle_off_duration
{
self.power_cycle_state = PowerCycleState::SwitchingOn { restore_mode };
self.start_transition_internal(restore_mode, None);
}
}
fn handle_switch_transition(&mut self) -> Option<SwitchOutcome> {
let target_mode = self.mode_helper.target?;
let switch_target_on = target_mode.requires_power();
if self.mode_helper.transition_state == SwitchTransitionState::Idle {
let result = if switch_target_on {
self.switch_helper.send_switch_on_cmd(self.switch_id)
} else {
self.switch_helper.send_switch_off_cmd(self.switch_id)
};
if result.is_err() {
// Could not send switch command.. still continue with transition.
log::error!(
"failed to send switch {} command",
if switch_target_on { "on" } else { "off" }
);
}
self.mode_helper.transition_state = SwitchTransitionState::PowerSwitching;
}
if self.mode_helper.transition_state == SwitchTransitionState::PowerSwitching {
if self.switch_helper.is_switch_on(self.switch_id) == switch_target_on {
log::info!("switch is {}", if switch_target_on { "on" } else { "off" });
self.mode_helper.transition_state = SwitchTransitionState::Done;
} else if self.mode_helper.timed_out() {
return Some(SwitchOutcome::Failed(self.mode_helper.finish(false)));
}
}
if self.mode_helper.transition_state == SwitchTransitionState::Done {
return Some(SwitchOutcome::Reached(self.mode_helper.finish(true)));
}
None
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex, mpsc};
use arbitrary_int::u11;
use satrs::spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
use types::{
DeviceMode,
pcdu::{SwitchRequest, SwitchState, SwitchStateBinary},
};
use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet};
use super::*;
const TIMEOUT: Duration = Duration::from_millis(50);
struct Testbench {
helper: SwitchAndModeHelper<DeviceMode>,
switch_rx: mpsc::Receiver<SwitchRequest>,
shared_switch_set: SharedSwitchSet,
}
impl Testbench {
fn new() -> Self {
let (switch_tx, switch_rx) = mpsc::sync_channel(10);
let mut switch_map = SwitchMap::new();
switch_map.insert(SwitchId::Mgm0, SwitchState::Off);
let shared_switch_set: SharedSwitchSet =
Arc::new(Mutex::new(SwitchSet::new(switch_map)));
Self {
helper: SwitchAndModeHelper::new(
DeviceMode::Off,
TIMEOUT,
PowerSwitchHelper::new(switch_tx, shared_switch_set.clone()),
SwitchId::Mgm0,
),
switch_rx,
shared_switch_set,
}
}
fn set_switch_state(&self, state: SwitchState) {
self.shared_switch_set
.lock()
.unwrap()
.set_switch_state(SwitchId::Mgm0, state);
}
fn switch_requests(&self) -> Vec<SwitchStateBinary> {
self.switch_rx
.try_iter()
.map(|req| req.target_state)
.collect()
}
/// Drives a transition to `Normal` to completion.
fn switch_to_normal(&mut self) {
self.helper.start_transition(DeviceMode::Normal, None);
self.set_switch_state(SwitchState::On);
assert!(matches!(
self.helper.handle_mode_transition(),
Some(ModeTransitionEvent::Reached(None))
));
self.switch_requests();
}
/// Starts a power cycle from `Normal` and drives it until the device is off.
fn power_cycle_until_off(&mut self, off_duration: Duration) {
self.switch_to_normal();
self.helper
.start_power_cycle(DeviceMode::Normal, off_duration);
assert!(self.helper.handle_mode_transition().is_none());
assert_eq!(self.switch_requests(), [SwitchStateBinary::Off]);
self.set_switch_state(SwitchState::Off);
assert!(self.helper.handle_mode_transition().is_none());
assert_eq!(self.helper.mode(), DeviceMode::Off);
}
}
fn tc_id() -> CcsdsPacketIdAndPsc {
CcsdsPacketIdAndPsc::new_from_ccsds_packet(&SpacePacketHeader::new_from_apid(u11::new(1)))
}
#[test]
fn test_no_transition() {
let mut tb = Testbench::new();
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.target(), None);
assert!(tb.helper.handle_mode_transition().is_none());
assert!(tb.switch_requests().is_empty());
assert!(!tb.helper.power_cycle_active());
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
}
#[test]
fn test_switch_on() {
let mut tb = Testbench::new();
tb.helper
.start_transition(DeviceMode::Normal, Some(tc_id()));
assert!(tb.helper.handle_mode_transition().is_none());
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
tb.set_switch_state(SwitchState::On);
match tb.helper.handle_mode_transition() {
Some(ModeTransitionEvent::Reached(Some(id))) => assert_eq!(id, tc_id()),
_ => panic!("expected mode reached event with TC commander"),
}
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
assert_eq!(tb.helper.target(), None);
assert!(tb.switch_requests().is_empty());
}
#[test]
fn test_switch_off() {
let mut tb = Testbench::new();
tb.switch_to_normal();
tb.helper.start_transition(DeviceMode::Off, None);
assert!(tb.helper.handle_mode_transition().is_none());
assert_eq!(tb.switch_requests(), [SwitchStateBinary::Off]);
tb.set_switch_state(SwitchState::Off);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::Reached(None))
));
assert_eq!(tb.helper.mode(), DeviceMode::Off);
}
#[test]
fn test_switch_already_in_target_state() {
let mut tb = Testbench::new();
tb.set_switch_state(SwitchState::On);
tb.helper.start_transition(DeviceMode::On, None);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::Reached(None))
));
// The switch command is still sent.
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
assert_eq!(tb.helper.mode(), DeviceMode::On);
}
#[test]
fn test_switch_timeout() {
let mut tb = Testbench::new();
tb.helper
.start_transition(DeviceMode::Normal, Some(tc_id()));
assert!(tb.helper.handle_mode_transition().is_none());
std::thread::sleep(TIMEOUT);
match tb.helper.handle_mode_transition() {
Some(ModeTransitionEvent::Failed(Some(id))) => assert_eq!(id, tc_id()),
_ => panic!("expected mode failed event with TC commander"),
}
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.target(), None);
}
#[test]
fn test_power_cycle() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::ZERO);
assert!(tb.helper.power_cycle_active());
// The off duration elapsed, so switching on starts right away.
assert!(tb.helper.handle_mode_transition().is_none());
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
tb.set_switch_state(SwitchState::On);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::PowerCycleDone)
));
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
assert!(!tb.helper.power_cycle_active());
}
#[test]
fn test_power_cycle_reports_restored_mode() {
let mut tb = Testbench::new();
tb.switch_to_normal();
tb.helper
.start_power_cycle(DeviceMode::Normal, Duration::from_secs(60));
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
tb.helper.handle_mode_transition();
tb.set_switch_state(SwitchState::Off);
tb.helper.handle_mode_transition();
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
}
#[test]
fn test_power_cycle_reports_restored_mode_while_switching_on() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::ZERO);
assert!(tb.helper.handle_mode_transition().is_none());
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
}
#[test]
fn test_power_cycle_waits_off_duration() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::from_secs(60));
for _ in 0..3 {
assert!(tb.helper.handle_mode_transition().is_none());
}
assert!(tb.switch_requests().is_empty());
assert_eq!(tb.helper.target(), None);
assert!(tb.helper.power_cycle_active());
}
#[test]
fn test_power_cycle_switch_off_timeout() {
let mut tb = Testbench::new();
tb.switch_to_normal();
tb.helper
.start_power_cycle(DeviceMode::Normal, Duration::ZERO);
assert!(tb.helper.handle_mode_transition().is_none());
std::thread::sleep(TIMEOUT);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::PowerCycleFailed {
restore_mode: DeviceMode::Normal
})
));
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
assert!(!tb.helper.power_cycle_active());
}
#[test]
fn test_power_cycle_switch_on_timeout() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::ZERO);
assert!(tb.helper.handle_mode_transition().is_none());
std::thread::sleep(TIMEOUT);
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::PowerCycleFailed {
restore_mode: DeviceMode::Normal
})
));
assert_eq!(tb.helper.mode(), DeviceMode::Off);
assert!(!tb.helper.power_cycle_active());
// The failed power cycle is not hidden anymore.
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
}
#[test]
fn test_transition_aborts_power_cycle() {
let mut tb = Testbench::new();
tb.power_cycle_until_off(Duration::from_secs(60));
tb.helper.start_transition(DeviceMode::On, Some(tc_id()));
assert!(!tb.helper.power_cycle_active());
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
tb.set_switch_state(SwitchState::On);
// A regular transition event instead of a power cycle event.
assert!(matches!(
tb.helper.handle_mode_transition(),
Some(ModeTransitionEvent::Reached(Some(_)))
));
assert_eq!(tb.helper.mode(), DeviceMode::On);
}
}
+119
View File
@@ -0,0 +1,119 @@
use derive_new::new;
use std::{cell::RefCell, collections::VecDeque, sync::mpsc, time::Duration};
use types::pcdu::{SwitchId, SwitchRequest, SwitchState, SwitchStateBinary};
use satrs::{queue::GenericSendError, request::MessageMetadata};
use thiserror::Error;
use crate::eps::pcdu::SwitchMapWrapper;
use self::pcdu::SharedSwitchSet;
pub mod pcdu;
#[derive(new, Clone)]
pub struct PowerSwitchHelper {
switcher_tx: mpsc::SyncSender<SwitchRequest>,
shared_switch_set: SharedSwitchSet,
}
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum SwitchCommandingError {
#[error("send error: {0}")]
Send(#[from] GenericSendError),
}
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
pub enum SwitchInfoError {
/// This is a configuration error which should not occur.
#[error("switch ID not in map")]
SwitchIdNotInMap(SwitchId),
#[error("switch set invalid")]
SwitchSetInvalid,
}
impl PowerSwitchHelper {
pub fn send_switch_on_cmd(&self, switch_id: SwitchId) -> Result<(), GenericSendError> {
self.switcher_tx
.send(SwitchRequest::new(switch_id, SwitchStateBinary::On))?;
Ok(())
}
pub fn send_switch_off_cmd(&self, switch_id: SwitchId) -> Result<(), GenericSendError> {
self.switcher_tx
.send(SwitchRequest::new(switch_id, SwitchStateBinary::Off))?;
Ok(())
}
pub fn switch_state(&self, switch_id: SwitchId) -> Result<SwitchState, SwitchInfoError> {
let switch_set = self
.shared_switch_set
.lock()
.expect("failed to lock switch set");
if !switch_set.valid {
return Err(SwitchInfoError::SwitchSetInvalid);
}
if let Some(state) = switch_set.switch_map.get(&switch_id) {
return Ok(*state);
}
Err(SwitchInfoError::SwitchIdNotInMap(switch_id))
}
#[allow(dead_code)]
fn switch_delay_ms(&self) -> Duration {
// Here, we could set device specific switch delays theoretically. Set it to this value
// for now.
Duration::from_millis(1000)
}
pub fn is_switch_on(&self, switch_id: SwitchId) -> bool {
if let Ok(state) = self.switch_state(switch_id) {
state == SwitchState::On
} else {
false
}
}
}
#[allow(dead_code)]
#[derive(new)]
pub struct SwitchRequestInfo {
pub requestor_info: MessageMetadata,
pub switch_id: SwitchId,
pub target_state: SwitchStateBinary,
}
// Test switch helper which can be used for unittests.
#[allow(dead_code)]
pub struct TestSwitchHelper {
pub switch_requests: RefCell<VecDeque<SwitchRequestInfo>>,
pub switch_info_requests: RefCell<VecDeque<SwitchId>>,
#[allow(dead_code)]
pub switch_delay_request_count: u32,
pub next_switch_delay: Duration,
pub switch_map: RefCell<SwitchMapWrapper>,
pub switch_map_valid: bool,
}
impl Default for TestSwitchHelper {
fn default() -> Self {
Self {
switch_requests: Default::default(),
switch_info_requests: Default::default(),
switch_delay_request_count: Default::default(),
next_switch_delay: Duration::from_millis(1000),
switch_map: Default::default(),
switch_map_valid: true,
}
}
}
#[allow(dead_code)]
impl TestSwitchHelper {
// Helper function which can be used to force a switch to another state for test purposes.
pub fn set_switch_state(&mut self, switch: SwitchId, state: SwitchState) {
self.switch_map.get_mut().0.insert(switch, state);
}
}
+773
View File
@@ -0,0 +1,773 @@
use std::{
cell::RefCell,
collections::{HashMap, VecDeque},
sync::{Arc, Mutex, mpsc},
};
use derive_new::new;
use example_std::TimestampHelper;
use minisim_types::{
SimReply, SimRequestWithTime,
eps::{PcduReply, PcduRequest},
};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator as _;
use types::{
ComponentId, DeviceMode,
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
pcdu::{
self, SwitchId, SwitchMapBinary, SwitchMapBinaryWrapper, SwitchRequest, SwitchState,
SwitchStateBinary, SwitchesBitfield,
},
};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SwitchSet {
pub valid: bool,
pub switch_map: SwitchMap,
}
impl SwitchSet {
pub fn new(switch_map: SwitchMap) -> Self {
Self {
valid: true,
switch_map,
}
}
pub fn new_with_init_switches_unknown() -> Self {
let wrapper = SwitchMapWrapper::default();
Self::new(wrapper.0)
}
pub fn as_bitfield(&self) -> Option<SwitchesBitfield> {
for entry in SwitchId::iter() {
if !self.switch_map.contains_key(&entry) {
return None;
}
}
Some(
SwitchesBitfield::builder()
.with_magnetorquer(*self.switch_map.get(&SwitchId::Mgt).unwrap() == SwitchState::On)
.with_mgm1(*self.switch_map.get(&SwitchId::Mgm1).unwrap() == SwitchState::On)
.with_mgm0(*self.switch_map.get(&SwitchId::Mgm0).unwrap() == SwitchState::On)
.build(),
)
}
#[allow(dead_code)]
pub fn set_switch_state(&mut self, switch_id: SwitchId, state: SwitchState) -> bool {
if !self.switch_map.contains_key(&switch_id) {
return false;
}
*self.switch_map.get_mut(&switch_id).unwrap() = state;
true
}
}
pub type SwitchMap = HashMap<SwitchId, SwitchState>;
pub struct SwitchMapWrapper(pub SwitchMap);
impl Default for SwitchMapWrapper {
fn default() -> Self {
let mut switch_map = SwitchMap::default();
for entry in SwitchId::iter() {
switch_map.insert(entry, SwitchState::Unknown);
}
Self(switch_map)
}
}
impl SwitchMapWrapper {
#[allow(dead_code)]
pub fn new_with_init_switches_off() -> Self {
let mut switch_map = SwitchMap::default();
for entry in SwitchId::iter() {
switch_map.insert(entry, SwitchState::Off);
}
Self(switch_map)
}
pub fn from_binary_switch_map_ref(switch_map: &SwitchMapBinary) -> Self {
Self(
switch_map
.iter()
.map(|(key, value)| (*key, SwitchState::from(*value)))
.collect(),
)
}
}
pub type SharedSwitchSet = Arc<Mutex<SwitchSet>>;
pub trait SerialInterface {
type Error: core::fmt::Debug;
/// Send some data via the serial interface.
fn send(&self, data: &[u8]) -> Result<(), Self::Error>;
/// Receive all replies received on the serial interface so far. This function takes a closure
/// and call its for each received packet, passing the received packet into it.
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
f: ReplyHandler,
) -> Result<(), Self::Error>;
}
#[derive(new)]
pub struct SerialInterfaceToSim {
pub sim_request_tx: mpsc::Sender<SimRequestWithTime>,
pub sim_reply_rx: mpsc::Receiver<SimReply>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[repr(u32)]
pub enum SetId {
SwitcherSet = 0,
}
impl SerialInterface for SerialInterfaceToSim {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
let request: PcduRequest = postcard::from_bytes(data).expect("expected a PCDU request");
self.sim_request_tx
.send(SimRequestWithTime::new_with_epoch_time(request))
.expect("failed to send request to simulation");
Ok(())
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
mut f: ReplyHandler,
) -> Result<(), Self::Error> {
loop {
match self.sim_reply_rx.try_recv() {
Ok(reply) => {
let reply = postcard::to_allocvec(&reply).unwrap();
f(&reply);
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("sim reply sender has disconnected");
break;
}
},
}
}
Ok(())
}
}
#[derive(Default)]
pub struct SerialInterfaceDummy {
// Need interior mutability here for both fields.
pub switch_map: RefCell<SwitchMapBinaryWrapper>,
pub reply_deque: RefCell<VecDeque<SimReply>>,
}
impl SerialInterface for SerialInterfaceDummy {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
let pcdu_req: PcduRequest = postcard::from_bytes(data).unwrap();
let switch_map_mut = &mut self.switch_map.borrow_mut().0;
match pcdu_req {
PcduRequest::SwitchDevice { switch, state } => {
switch_map_mut
.insert(switch, state)
.expect("switch map capacity exceeded");
}
PcduRequest::RequestSwitchInfo => {
let mut reply_deque_mut = self.reply_deque.borrow_mut();
reply_deque_mut.push_back(SimReply::from(PcduReply::SwitchInfo(
switch_map_mut.clone(),
)));
}
};
Ok(())
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
mut f: ReplyHandler,
) -> Result<(), Self::Error> {
if self.reply_queue_empty() {
return Ok(());
}
loop {
let reply = self.next_reply_serialized();
f(&reply);
if self.reply_queue_empty() {
break;
}
}
Ok(())
}
}
impl SerialInterfaceDummy {
fn next_reply_serialized(&self) -> Vec<u8> {
let mut reply_deque_mut = self.reply_deque.borrow_mut();
let next_reply = reply_deque_mut.pop_front().unwrap();
postcard::to_allocvec(&next_reply).unwrap()
}
fn reply_queue_empty(&self) -> bool {
self.reply_deque.borrow().is_empty()
}
}
pub enum SerialSimInterfaceWrapper {
Dummy(SerialInterfaceDummy),
Sim(SerialInterfaceToSim),
}
impl SerialInterface for SerialSimInterfaceWrapper {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
match self {
SerialSimInterfaceWrapper::Dummy(dummy) => dummy.send(data),
SerialSimInterfaceWrapper::Sim(sim) => sim.send(data),
}
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
f: ReplyHandler,
) -> Result<(), Self::Error> {
match self {
SerialSimInterfaceWrapper::Dummy(dummy) => dummy.try_recv_replies(f),
SerialSimInterfaceWrapper::Sim(sim) => sim.try_recv_replies(f),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum OpCode {
RegularOp = 0,
PollAndRecvReplies = 1,
}
/// Example PCDU device handler.
#[allow(clippy::too_many_arguments)]
pub struct PcduHandler<ComInterface: SerialInterface> {
dev_str: &'static str,
switch_request_rx: mpsc::Receiver<SwitchRequest>,
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
tm_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
pub com_interface: ComInterface,
shared_switch_map: Arc<Mutex<SwitchSet>>,
mode: DeviceMode,
stamp_helper: TimestampHelper,
event_tx: mpsc::SyncSender<pcdu::Event>,
}
impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
pub fn new(
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
switch_request_rx: mpsc::Receiver<SwitchRequest>,
com_interface: ComInterface,
shared_switch_map: Arc<Mutex<SwitchSet>>,
init_mode: DeviceMode,
event_tx: mpsc::SyncSender<pcdu::Event>,
) -> Self {
Self {
dev_str: "PCDU",
tc_rx,
switch_request_rx,
tm_tx,
com_interface,
shared_switch_map,
stamp_helper: TimestampHelper::default(),
// Start in normal mode by default. Assume that the PCDU itself is on by default.
mode: init_mode,
event_tx,
}
}
pub fn periodic_operation(&mut self, op_code: OpCode) {
match op_code {
OpCode::RegularOp => {
self.stamp_helper.update_from_now();
// Handle requests.
self.handle_telecommands();
self.handle_switch_requests();
// Poll the switch states and/or telemetry regularly here.
if self.mode() == DeviceMode::Normal || self.mode() == DeviceMode::On {
self.handle_periodic_commands();
}
}
OpCode::PollAndRecvReplies => {
self.poll_and_handle_replies();
}
}
}
#[inline]
pub fn mode(&self) -> DeviceMode {
self.mode
}
pub fn handle_telecommands(&mut self) {
loop {
match self.tc_rx.try_recv() {
Ok(packet) => {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
match postcard::from_bytes::<pcdu::request::Request>(&packet.payload) {
Ok(request) => {
log::info!(
"received request {:?} with TC ID {:#010x}",
request,
tc_id.raw()
);
match request {
pcdu::request::Request::Ping => {
self.send_tm(Some(tc_id), pcdu::response::Response::Ok)
}
pcdu::request::Request::GetSwitches => self.send_tm(
Some(tc_id),
pcdu::response::Response::Switches(
self.shared_switch_map
.lock()
.unwrap()
.as_bitfield()
.expect("could not build switches response"),
),
),
pcdu::request::Request::EnableSwitches(switches) => {
self.handle_switches_bitfield_request(
switches,
SwitchStateBinary::On,
);
}
pcdu::request::Request::DisableSwitches(switches) => {
self.handle_switches_bitfield_request(
switches,
SwitchStateBinary::Off,
);
}
pcdu::request::Request::Mode(device_mode) => {
self.switch_mode(tc_id, device_mode)
}
}
}
Err(e) => {
log::warn!("failed to deserialize request: {}", e);
}
}
}
Err(e) => match e {
std::sync::mpsc::TryRecvError::Empty => break,
std::sync::mpsc::TryRecvError::Disconnected => {
log::warn!("packet sender disconnected")
}
},
}
}
}
pub fn handle_switches_bitfield_request(
&mut self,
switches: SwitchesBitfield,
state: SwitchStateBinary,
) {
if switches.mgm0() {
self.handle_device_switching(SwitchId::Mgm0, state);
}
if switches.mgm1() {
self.handle_device_switching(SwitchId::Mgm1, state);
}
if switches.magnetorquer() {
self.handle_device_switching(SwitchId::Mgt, state);
}
}
pub fn send_tm(&self, tc_id: Option<CcsdsPacketIdAndPsc>, response: pcdu::response::Response) {
match pack_ccsds_tm_packet_for_now(ComponentId::EpsPcdu, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
fn switch_mode(&mut self, requestor: CcsdsPacketIdAndPsc, mode: DeviceMode) {
log::info!("{}: transitioning to mode {:?}", self.dev_str, mode);
self.mode = mode;
if self.mode() == DeviceMode::Off {
self.shared_switch_map.lock().unwrap().valid = false;
}
log::info!("{} announcing mode: {:?}", self.dev_str, self.mode);
self.send_telemetry(Some(requestor), pcdu::response::Response::Ok);
}
pub fn send_telemetry(
&self,
tc_id: Option<CcsdsPacketIdAndPsc>,
response: pcdu::response::Response,
) {
match pack_ccsds_tm_packet_for_now(ComponentId::EpsPcdu, tc_id, &response) {
Ok(packet) => {
if let Err(e) = self.tm_tx.send(packet) {
log::warn!("failed to send TM packet: {}", e);
}
}
Err(e) => {
log::warn!("failed to pack TM packet: {}", e);
}
}
}
pub fn handle_periodic_commands(&self) {
let pcdu_req = PcduRequest::RequestSwitchInfo;
let pcdu_req_ser = postcard::to_allocvec(&pcdu_req).unwrap();
if let Err(_e) = self.com_interface.send(&pcdu_req_ser) {
log::warn!("polling PCDU switch info failed");
if let Err(e) = self.event_tx.send(pcdu::Event::SerialCommError) {
log::warn!("failed to send comm error event: {}", e);
}
}
}
/*
pub fn handle_mode_requests(&mut self) {
loop {
// TODO: Only allow one set mode request per cycle?
match self.mode_node.try_recv_mode_request() {
Ok(opt_msg) => {
if let Some(msg) = opt_msg {
let result = self.handle_mode_request(msg);
// TODO: Trigger event?
if result.is_err() {
log::warn!(
"{}: mode request failed with error {:?}",
self.dev_str,
result.err().unwrap()
);
}
} else {
break;
}
}
Err(e) => match e {
satrs::queue::GenericReceiveError::Empty => {
break;
}
satrs::queue::GenericReceiveError::TxDisconnected(_) => {
log::warn!("{}: failed to receive mode request: {:?}", self.dev_str, e);
}
},
}
}
}
*/
pub fn handle_device_switching(&mut self, switch_id: SwitchId, state: SwitchStateBinary) {
let pcdu_req = PcduRequest::SwitchDevice {
switch: switch_id,
state,
};
let pcdu_req_ser = postcard::to_allocvec(&pcdu_req).unwrap();
self.com_interface
.send(&pcdu_req_ser)
.expect("failed to send switch request to PCDU");
}
pub fn handle_switch_requests(&mut self) {
loop {
match self.switch_request_rx.try_recv() {
Ok(switch_req) => {
self.handle_device_switching(switch_req.switch_id(), switch_req.target_state());
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("switch request receiver has disconnected");
break;
}
},
};
}
}
pub fn poll_and_handle_replies(&mut self) {
if let Err(e) = self.com_interface.try_recv_replies(|reply| {
let sim_reply: SimReply = postcard::from_bytes(reply).expect("invalid reply format");
let SimReply::Pcdu(pcdu_reply) = sim_reply else {
log::warn!("unexpected PCDU SIM reply: {sim_reply:?}");
return;
};
match pcdu_reply {
PcduReply::SwitchInfo(switch_info) => {
let switch_map_wrapper =
SwitchMapWrapper::from_binary_switch_map_ref(&switch_info);
let mut shared_switch_map = self
.shared_switch_map
.lock()
.expect("failed to lock switch map");
shared_switch_map.switch_map = switch_map_wrapper.0;
shared_switch_map.valid = true;
}
}
}) {
log::warn!("receiving PCDU replies failed: {e:?}");
}
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use arbitrary_int::u11;
use satrs::spacepackets::SpacePacketHeader;
use types::{
Apid, TcHeader,
pcdu::{SwitchMapBinary, SwitchStateBinary},
};
use super::*;
pub fn create_request_tc(
request: types::pcdu::request::Request,
) -> types::ccsds::CcsdsTcPacketOwned {
types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Eps as u16)),
TcHeader::new(ComponentId::EpsPcdu, request.message_type()),
request,
)
}
#[derive(Default)]
pub struct SerialInterfaceTest {
pub inner: SerialInterfaceDummy,
pub send_queue: RefCell<VecDeque<Vec<u8>>>,
pub reply_queue: RefCell<VecDeque<Vec<u8>>>,
/// Makes the next `send` call fail, to exercise comm-error handling.
pub fail_next_send: RefCell<bool>,
}
impl SerialInterface for SerialInterfaceTest {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
if self.fail_next_send.replace(false) {
return Err(());
}
let mut send_queue_mut = self.send_queue.borrow_mut();
send_queue_mut.push_back(data.to_vec());
self.inner.send(data)
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
mut f: ReplyHandler,
) -> Result<(), Self::Error> {
if self.inner.reply_queue_empty() {
return Ok(());
}
loop {
let reply = self.inner.next_reply_serialized();
self.reply_queue.borrow_mut().push_back(reply.clone());
f(&reply);
if self.inner.reply_queue_empty() {
break;
}
}
Ok(())
}
}
#[allow(dead_code)]
pub struct PcduTestbench {
pub mode_request_tx: mpsc::SyncSender<types::pcdu::request::Request>,
pub mode_reply_rx_to_parent: mpsc::Receiver<types::pcdu::response::Response>,
pub tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
pub tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
pub switch_request_tx: mpsc::Sender<SwitchRequest>,
pub event_rx: mpsc::Receiver<pcdu::Event>,
pub handler: PcduHandler<SerialInterfaceTest>,
}
impl PcduTestbench {
pub fn new() -> Self {
let (mode_request_tx, _mode_request_rx) = mpsc::sync_channel(5);
let (_mode_reply_tx_to_parent, mode_reply_rx_to_parent) = mpsc::sync_channel(5);
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
let (switch_request_tx, switch_reqest_rx) = mpsc::channel();
let (event_tx, event_rx) = mpsc::sync_channel(5);
let shared_switch_map =
Arc::new(Mutex::new(SwitchSet::new_with_init_switches_unknown()));
let handler = PcduHandler::new(
tc_rx,
tm_tx.clone(),
switch_reqest_rx,
SerialInterfaceTest::default(),
shared_switch_map,
DeviceMode::Off,
event_tx,
);
Self {
mode_request_tx,
mode_reply_rx_to_parent,
tc_tx,
tm_rx,
switch_request_tx,
event_rx,
handler,
}
}
pub fn verify_switch_info_req_was_sent(&self, expected_queue_len: usize) {
// Check that there is now communication happening.
let mut send_queue_mut = self.handler.com_interface.send_queue.borrow_mut();
assert_eq!(send_queue_mut.len(), expected_queue_len);
let packet_sent = send_queue_mut.pop_front().unwrap();
drop(send_queue_mut);
let pcdu_req: PcduRequest = postcard::from_bytes(&packet_sent).unwrap();
assert_eq!(pcdu_req, PcduRequest::RequestSwitchInfo);
}
pub fn verify_switch_req_was_sent(
&self,
expected_queue_len: usize,
switch_id: SwitchId,
target_state: SwitchStateBinary,
) {
// Check that there is now communication happening.
let mut send_queue_mut = self.handler.com_interface.send_queue.borrow_mut();
assert_eq!(send_queue_mut.len(), expected_queue_len);
let packet_sent = send_queue_mut.pop_front().unwrap();
drop(send_queue_mut);
let pcdu_req: PcduRequest = postcard::from_bytes(&packet_sent).unwrap();
assert_eq!(
pcdu_req,
PcduRequest::SwitchDevice {
switch: switch_id,
state: target_state
}
)
}
pub fn verify_switch_reply_received(
&self,
expected_queue_len: usize,
expected_map: SwitchMapBinary,
) {
// Check that a switch reply was read back.
let mut reply_received_mut = self.handler.com_interface.reply_queue.borrow_mut();
assert_eq!(reply_received_mut.len(), expected_queue_len);
let reply_received = reply_received_mut.pop_front().unwrap();
let sim_reply: SimReply = postcard::from_bytes(&reply_received).unwrap();
assert_eq!(
sim_reply,
SimReply::Pcdu(PcduReply::SwitchInfo(expected_map))
);
}
}
#[test]
fn test_periodic_command_send_failure_sends_event() {
let testbench = PcduTestbench::new();
*testbench.handler.com_interface.fail_next_send.borrow_mut() = true;
testbench.handler.handle_periodic_commands();
let event = testbench
.event_rx
.try_recv()
.expect("expected comm error event");
assert!(matches!(event, pcdu::Event::SerialCommError));
}
#[test]
fn test_basic_handler() {
let mut testbench = PcduTestbench::new();
assert_eq!(testbench.handler.com_interface.send_queue.borrow().len(), 0);
assert_eq!(
testbench.handler.com_interface.reply_queue.borrow().len(),
0
);
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
testbench.handler.periodic_operation(OpCode::RegularOp);
testbench
.handler
.periodic_operation(OpCode::PollAndRecvReplies);
// Handler is OFF, no changes expected.
assert_eq!(testbench.handler.com_interface.send_queue.borrow().len(), 0);
assert_eq!(
testbench.handler.com_interface.reply_queue.borrow().len(),
0
);
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
}
#[test]
fn test_normal_mode() {
let mut testbench = PcduTestbench::new();
testbench
.tc_tx
.send(create_request_tc(pcdu::request::Request::Mode(
DeviceMode::Normal,
)))
.unwrap();
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
assert!(switch_map_shared.valid);
drop(switch_map_shared);
testbench.handler.periodic_operation(OpCode::RegularOp);
testbench
.handler
.periodic_operation(OpCode::PollAndRecvReplies);
// Check correctness of mode.
assert_eq!(testbench.handler.mode(), DeviceMode::Normal);
testbench.verify_switch_info_req_was_sent(1);
testbench.verify_switch_reply_received(1, SwitchMapBinaryWrapper::default().0);
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
assert!(switch_map_shared.valid);
drop(switch_map_shared);
}
#[test]
fn test_switch_request_handling() {
let mut testbench = PcduTestbench::new();
testbench
.tc_tx
.send(create_request_tc(pcdu::request::Request::Mode(
DeviceMode::Normal,
)))
.unwrap();
testbench
.switch_request_tx
.send(SwitchRequest::new(SwitchId::Mgm0, SwitchStateBinary::On))
.expect("failed to send switch request");
testbench.handler.periodic_operation(OpCode::RegularOp);
testbench
.handler
.periodic_operation(OpCode::PollAndRecvReplies);
testbench.verify_switch_req_was_sent(2, SwitchId::Mgm0, SwitchStateBinary::On);
testbench.verify_switch_info_req_was_sent(1);
let mut switch_map = SwitchMapBinaryWrapper::default().0;
*switch_map
.get_mut(&SwitchId::Mgm0)
.expect("switch state setting failed") = SwitchStateBinary::On;
testbench.verify_switch_reply_received(1, switch_map);
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
assert!(switch_map_shared.valid);
drop(switch_map_shared);
}
}
+340
View File
@@ -0,0 +1,340 @@
use std::collections::HashSet;
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use types::{
ComponentId, Event, EventId, Message,
acs::{mgm, mgm_assembly, mgt},
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
control,
event_manager::{request::Request, response::Response},
pcdu, tmtc,
};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
pub struct EventManager {
pub tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
pub ctrl_rx: std::sync::mpsc::Receiver<control::Event>,
/// Shared by all MGM instances, which is why the sender ID is part of the message.
pub mgm_rx: std::sync::mpsc::Receiver<(ComponentId, mgm::Event)>,
pub mgm_assembly_rx: std::sync::mpsc::Receiver<mgm_assembly::Event>,
pub mgt_rx: std::sync::mpsc::Receiver<mgt::Event>,
pub pcdu_rx: std::sync::mpsc::Receiver<pcdu::Event>,
/// Shared by all TC sources, which is why the sender ID is part of the message.
pub tc_source_rx: std::sync::mpsc::Receiver<(ComponentId, tmtc::Event)>,
pub tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
/// Senders with all their event TM generation disabled.
disabled_components: HashSet<ComponentId>,
/// Individual (sender, event ID) pairs with their TM generation disabled.
disabled_events: HashSet<(ComponentId, u16)>,
}
impl EventManager {
#[allow(clippy::too_many_arguments)]
pub fn new(
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
ctrl_rx: std::sync::mpsc::Receiver<control::Event>,
mgm_rx: std::sync::mpsc::Receiver<(ComponentId, mgm::Event)>,
mgm_assembly_rx: std::sync::mpsc::Receiver<mgm_assembly::Event>,
mgt_rx: std::sync::mpsc::Receiver<mgt::Event>,
pcdu_rx: std::sync::mpsc::Receiver<pcdu::Event>,
tc_source_rx: std::sync::mpsc::Receiver<(ComponentId, tmtc::Event)>,
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
) -> Self {
Self {
tc_rx,
ctrl_rx,
mgm_rx,
mgm_assembly_rx,
mgt_rx,
pcdu_rx,
tc_source_rx,
tm_tx,
disabled_components: HashSet::new(),
disabled_events: HashSet::new(),
}
}
/// Silences all event TM generation for `sender_id`, regardless of the specific event.
fn disable_component(&mut self, sender_id: ComponentId) {
self.disabled_components.insert(sender_id);
}
fn enable_component(&mut self, sender_id: ComponentId) {
self.disabled_components.remove(&sender_id);
}
/// Silences TM generation for one specific event of `sender_id`, leaving its other events
/// unaffected.
fn disable_event(&mut self, sender_id: ComponentId, event_id: u16) {
self.disabled_events.insert((sender_id, event_id));
}
fn enable_event(&mut self, sender_id: ComponentId, event_id: u16) {
self.disabled_events.remove(&(sender_id, event_id));
}
fn tm_generation_enabled(&self, sender_id: ComponentId, event_id: u16) -> bool {
!self.disabled_components.contains(&sender_id)
&& !self.disabled_events.contains(&(sender_id, event_id))
}
pub fn periodic_operation(&mut self) {
// Telecommands first, so filter changes already apply to the events of this cycle.
self.handle_telecommands();
while let Ok(event) = self.ctrl_rx.try_recv() {
self.event_to_tm(ComponentId::Controller, &Event::ControllerEvent(event));
}
while let Ok((sender_id, event)) = self.mgm_rx.try_recv() {
self.event_to_tm(sender_id, &event);
}
while let Ok(event) = self.mgm_assembly_rx.try_recv() {
self.event_to_tm(ComponentId::AcsMgmAssembly, &event);
}
while let Ok(event) = self.mgt_rx.try_recv() {
self.event_to_tm(ComponentId::AcsMgt, &event);
}
while let Ok(event) = self.pcdu_rx.try_recv() {
self.event_to_tm(ComponentId::EpsPcdu, &event);
}
while let Ok((sender_id, event)) = self.tc_source_rx.try_recv() {
self.event_to_tm(sender_id, &event);
}
}
fn handle_telecommands(&mut self) {
while let Ok(packet) = self.tc_rx.try_recv() {
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
let request = match postcard::from_bytes::<Request>(&packet.payload) {
Ok(request) => request,
Err(e) => {
log::warn!("failed to deserialize event manager request: {}", e);
continue;
}
};
log::info!(
"received request {:?} with TC ID {:#010x}",
request,
tc_id.raw()
);
match request {
Request::EnableComponent(sender_id) => self.enable_component(sender_id),
Request::DisableComponent(sender_id) => self.disable_component(sender_id),
Request::EnableEvent {
sender_id,
event_id,
} => self.enable_event(sender_id, event_id),
Request::DisableEvent {
sender_id,
event_id,
} => self.disable_event(sender_id, event_id),
}
self.send_tm(ComponentId::EventManager, Some(tc_id), &Response::Ok);
}
}
pub fn event_to_tm(
&mut self,
sender_id: ComponentId,
event: &(impl serde::Serialize + Message + EventId + core::fmt::Debug),
) {
let tm_enabled = self.tm_generation_enabled(sender_id, event.event_id());
log::info!(
"event {:?} (ID {}) from {:?}{}",
event,
event.event_id(),
sender_id,
if tm_enabled { "" } else { ", TM disabled" }
);
if !tm_enabled {
return;
}
self.send_tm(sender_id, None, event);
}
fn send_tm(
&self,
sender_id: ComponentId,
tc_id: Option<CcsdsPacketIdAndPsc>,
payload: &(impl serde::Serialize + Message),
) {
match pack_ccsds_tm_packet_for_now(sender_id, tc_id, payload) {
Ok(packet) => {
if let Err(e) = self.tm_tx.send(packet) {
log::warn!("error sending TM packet: {:?}", e);
}
}
Err(e) => {
log::warn!("error packing TM packet: {:?}", e);
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc::{self, TryRecvError};
use arbitrary_int::u11;
use satrs::spacepackets::SpacePacketHeader;
use types::{Apid, MessageType, TcHeader, acs::mgm};
use super::*;
struct Testbench {
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
mgm_event_tx: mpsc::SyncSender<(ComponentId, mgm::Event)>,
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
event_manager: EventManager,
}
impl Testbench {
fn new() -> Self {
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
let (_ctrl_tx, ctrl_rx) = mpsc::sync_channel(5);
let (mgm_event_tx, mgm_rx) = mpsc::sync_channel(5);
let (_mgm_assembly_tx, mgm_assembly_rx) = mpsc::sync_channel(5);
let (_mgt_tx, mgt_rx) = mpsc::sync_channel(5);
let (_pcdu_tx, pcdu_rx) = mpsc::sync_channel(5);
let (_tc_source_tx, tc_source_rx) = mpsc::sync_channel(5);
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
Self {
tc_tx,
mgm_event_tx,
tm_rx,
event_manager: EventManager::new(
tc_rx,
ctrl_rx,
mgm_rx,
mgm_assembly_rx,
mgt_rx,
pcdu_rx,
tc_source_rx,
tm_tx,
),
}
}
fn send_request(&self, request: Request) {
self.tc_tx
.send(CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(ComponentId::EventManager, MessageType::Event),
request,
))
.unwrap();
}
}
#[test]
fn test_events_enabled_by_default() {
let mut tb = Testbench::new();
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected event TM");
}
#[test]
fn test_disabled_component_silences_all_its_events() {
let mut tb = Testbench::new();
tb.event_manager.disable_component(ComponentId::AcsMgm0);
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.mgm_event_tx
.send((
ComponentId::AcsMgm0,
mgm::Event::ModeChanged(types::DeviceMode::Normal),
))
.unwrap();
tb.event_manager.periodic_operation();
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
}
#[test]
fn test_disabled_component_does_not_affect_other_components() {
let mut tb = Testbench::new();
tb.event_manager.disable_component(ComponentId::AcsMgm1);
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected event TM");
}
#[test]
fn test_disabled_event_only_silences_that_specific_event() {
let mut tb = Testbench::new();
tb.event_manager.disable_event(
ComponentId::AcsMgm0,
mgm::Event::SpiFaultThresholdExceeded.event_id(),
);
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.mgm_event_tx
.send((
ComponentId::AcsMgm0,
mgm::Event::ModeChanged(types::DeviceMode::Normal),
))
.unwrap();
tb.event_manager.periodic_operation();
let tm = tb.tm_rx.try_recv().expect("expected event TM");
let event: mgm::Event = postcard::from_bytes(&tm.payload).unwrap();
assert!(matches!(event, mgm::Event::ModeChanged(_)));
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
}
#[test]
fn test_re_enabling_a_component_restores_its_events() {
let mut tb = Testbench::new();
tb.event_manager.disable_component(ComponentId::AcsMgm0);
tb.event_manager.enable_component(ComponentId::AcsMgm0);
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected event TM");
}
#[test]
fn test_disable_component_request() {
let mut tb = Testbench::new();
tb.send_request(Request::DisableComponent(ComponentId::AcsMgm0));
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
let tm = tb.tm_rx.try_recv().expect("expected request response TM");
assert_eq!(tm.tm_header.sender_id, ComponentId::EventManager);
assert!(tm.tm_header.tc_id.is_some());
let response: Response = postcard::from_bytes(&tm.payload).unwrap();
assert_eq!(response, Response::Ok);
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
}
#[test]
fn test_disable_event_request() {
let mut tb = Testbench::new();
tb.send_request(Request::DisableEvent {
sender_id: ComponentId::AcsMgm0,
event_id: mgm::Event::SpiFaultThresholdExceeded.event_id(),
});
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected request response TM");
tb.send_request(Request::EnableEvent {
sender_id: ComponentId::AcsMgm0,
event_id: mgm::Event::SpiFaultThresholdExceeded.event_id(),
});
tb.mgm_event_tx
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
.unwrap();
tb.event_manager.periodic_operation();
tb.tm_rx.try_recv().expect("expected request response TM");
tb.tm_rx.try_recv().expect("expected event TM");
}
}
@@ -0,0 +1,4 @@
//! This module contains all component related to the direct interface of the example.
pub mod sim_client_udp;
pub mod tcp;
pub mod udp;
@@ -0,0 +1,402 @@
use std::{
collections::HashMap,
net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket},
sync::mpsc,
time::Duration,
};
use minisim_types::{ComponentId, SimReply, SimRequestWithTime, udp::SIM_CTRL_PORT};
use minisim_types::{SimCtrlReply, SimCtrlRequest};
use satrs::HandlingStatus;
struct SimReplyMap(pub HashMap<ComponentId, mpsc::Sender<SimReply>>);
pub fn create_sim_client(
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
) -> Option<SimClientUdp> {
match SimClientUdp::new(
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, SIM_CTRL_PORT)),
sim_request_rx,
) {
Ok(sim_client) => {
log::info!("simulator client connection success");
return Some(sim_client);
}
Err(e) => {
log::warn!("sim client creation error: {e}");
}
}
None
}
#[derive(thiserror::Error, Debug)]
pub enum SimClientCreationError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("timeout when trying to connect to sim UDP server")]
Timeout,
#[error("invalid ping reply when trying connection to UDP sim server: {0}")]
InvalidReply(#[from] postcard::Error),
#[error("invalid sim reply, not pong reply as expected: {0:?}")]
ReplyIsNotPong(Box<SimReply>),
}
pub struct SimClientUdp {
udp_client: UdpSocket,
simulator_addr: SocketAddr,
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
reply_map: SimReplyMap,
reply_buf: [u8; 4096],
}
impl SimClientUdp {
pub fn new(
simulator_addr: SocketAddr,
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
) -> Result<Self, SimClientCreationError> {
let mut reply_buf: [u8; 4096] = [0; 4096];
let mut udp_client = UdpSocket::bind("127.0.0.1:0")?;
udp_client.set_read_timeout(Some(Duration::from_millis(100)))?;
Self::attempt_connection(&mut udp_client, simulator_addr, &mut reply_buf)?;
udp_client.set_nonblocking(true)?;
Ok(Self {
udp_client,
simulator_addr,
sim_request_rx,
reply_map: SimReplyMap(HashMap::new()),
reply_buf,
})
}
pub fn attempt_connection(
udp_client: &mut UdpSocket,
simulator_addr: SocketAddr,
reply_buf: &mut [u8],
) -> Result<(), SimClientCreationError> {
let sim_req = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
let sim_req_raw = postcard::to_allocvec(&sim_req).expect("failed to serialize SimRequest");
udp_client.send_to(&sim_req_raw, simulator_addr)?;
match udp_client.recv(reply_buf) {
Ok(reply_len) => {
let sim_reply: SimReply = postcard::from_bytes(&reply_buf[0..reply_len])?;
match sim_reply {
SimReply::SimCtrl(SimCtrlReply::Pong) => Ok(()),
_ => Err(SimClientCreationError::ReplyIsNotPong(Box::new(sim_reply))),
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::TimedOut
|| e.kind() == std::io::ErrorKind::WouldBlock
{
Err(SimClientCreationError::Timeout)
} else {
Err(SimClientCreationError::Io(e))
}
}
}
}
pub fn operation(&mut self) -> HandlingStatus {
let mut no_sim_requests_handled = true;
let mut no_data_from_udp_server_received = true;
loop {
match self.sim_request_rx.try_recv() {
Ok(request) => {
let request_raw =
postcard::to_allocvec(&request).expect("failed to serialize SimRequest");
if let Err(e) = self.udp_client.send_to(&request_raw, self.simulator_addr) {
log::error!("error sending data to UDP SIM server: {e}");
break;
} else {
no_sim_requests_handled = false;
}
}
Err(e) => match e {
mpsc::TryRecvError::Empty => {
break;
}
mpsc::TryRecvError::Disconnected => {
log::warn!("SIM request sender disconnected");
break;
}
},
}
}
loop {
match self.udp_client.recv(&mut self.reply_buf) {
Ok(recvd_bytes) => {
no_data_from_udp_server_received = false;
let sim_reply_result: postcard::Result<SimReply> =
postcard::from_bytes(&self.reply_buf[0..recvd_bytes]);
match sim_reply_result {
Ok(sim_reply) => {
if let Some(sender) = self.reply_map.0.get(&sim_reply.component()) {
sender.send(sim_reply).expect("failed to send SIM reply");
} else {
log::warn!(
"no recipient for SIM reply from component {:?}",
sim_reply.component()
);
}
}
Err(e) => {
log::warn!("failed to deserialize SIM reply: {e}");
}
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut
{
break;
}
log::error!("error receiving data from UDP SIM server: {e}");
break;
}
}
}
if no_sim_requests_handled && no_data_from_udp_server_received {
return HandlingStatus::Empty;
}
HandlingStatus::HandledOne
}
pub fn add_reply_recipient(
&mut self,
component: ComponentId,
reply_sender: mpsc::Sender<SimReply>,
) {
self.reply_map.0.insert(component, reply_sender);
}
}
#[cfg(test)]
pub mod tests {
use std::{
net::{SocketAddr, UdpSocket},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc,
},
time::Duration,
};
use minisim_types::{
ComponentId, SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
eps::{PcduReply, PcduRequest},
};
use types::pcdu::SwitchMapBinary;
use super::SimClientUdp;
struct UdpSimTestServer {
udp_server: UdpSocket,
request_tx: mpsc::Sender<SimRequestWithTime>,
reply_rx: mpsc::Receiver<SimReply>,
last_sender: Option<SocketAddr>,
stop_signal: Arc<AtomicBool>,
recv_buf: [u8; 1024],
}
impl UdpSimTestServer {
pub fn new(
request_tx: mpsc::Sender<SimRequestWithTime>,
reply_rx: mpsc::Receiver<SimReply>,
stop_signal: Arc<AtomicBool>,
) -> Self {
let udp_server = UdpSocket::bind("127.0.0.1:0").expect("creating UDP server failed");
udp_server
.set_nonblocking(true)
.expect("failed to set UDP server to non-blocking");
Self {
udp_server,
request_tx,
reply_rx,
last_sender: None,
stop_signal,
recv_buf: [0; 1024],
}
}
pub fn operation(&mut self) {
loop {
let mut no_sim_replies_handled = true;
let mut no_data_received = true;
if self.stop_signal.load(Ordering::Relaxed) {
break;
}
if let Some(last_sender) = self.last_sender {
loop {
match self.reply_rx.try_recv() {
Ok(sim_reply) => {
let sim_reply_raw = postcard::to_allocvec(&sim_reply)
.expect("failed to serialize SimReply");
self.udp_server
.send_to(&sim_reply_raw, last_sender)
.expect("failed to send reply to client from UDP server");
no_sim_replies_handled = false;
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
panic!("reply sender disconnected")
}
},
}
}
}
loop {
match self.udp_server.recv_from(&mut self.recv_buf) {
Ok((read_bytes, from)) => {
let sim_request: SimRequestWithTime =
postcard::from_bytes(&self.recv_buf[0..read_bytes])
.expect("failed to deserialize SimRequest");
// For a ping, we perform the reply handling here directly
if sim_request.request == SimRequest::SimCtrl(SimCtrlRequest::Ping) {
no_data_received = false;
self.last_sender = Some(from);
let sim_reply = SimReply::from(SimCtrlReply::Pong);
let sim_reply_raw = postcard::to_allocvec(&sim_reply)
.expect("failed to serialize SimReply");
self.udp_server
.send_to(&sim_reply_raw, from)
.expect("failed to send reply to client from UDP server");
}
// Forward each SIM request for testing purposes.
self.request_tx
.send(sim_request)
.expect("failed to send request");
}
Err(e) => {
if e.kind() != std::io::ErrorKind::WouldBlock
&& e.kind() != std::io::ErrorKind::TimedOut
{
panic!("UDP server error: {}", e);
}
break;
}
}
}
if no_sim_replies_handled && no_data_received {
std::thread::sleep(Duration::from_millis(5));
}
}
}
pub fn local_addr(&self) -> SocketAddr {
self.udp_server.local_addr().unwrap()
}
}
#[test]
fn basic_connection_test() {
let (server_sim_request_tx, server_sim_request_rx) = mpsc::channel();
let (_server_sim_reply_tx, server_sim_reply_rx) = mpsc::channel();
let stop_signal = Arc::new(AtomicBool::new(false));
let mut udp_server = UdpSimTestServer::new(
server_sim_request_tx,
server_sim_reply_rx,
stop_signal.clone(),
);
let server_addr = udp_server.local_addr();
let (_client_sim_req_tx, client_sim_req_rx) = mpsc::channel();
// Need to spawn the simulator UDP server before calling the client constructor.
let jh0 = std::thread::spawn(move || {
udp_server.operation();
});
// Creating the client also performs the connection test.
SimClientUdp::new(server_addr, client_sim_req_rx).unwrap();
let sim_request = server_sim_request_rx
.recv_timeout(Duration::from_millis(50))
.expect("no SIM request received");
assert_eq!(
sim_request.request,
SimRequest::SimCtrl(SimCtrlRequest::Ping)
);
// Stop the server.
stop_signal.store(true, Ordering::Relaxed);
jh0.join().unwrap();
}
#[test]
fn basic_request_reply_test() {
let (server_sim_request_tx, server_sim_request_rx) = mpsc::channel();
let (server_sim_reply_tx, sever_sim_reply_rx) = mpsc::channel();
let stop_signal = Arc::new(AtomicBool::new(false));
let mut udp_server = UdpSimTestServer::new(
server_sim_request_tx,
sever_sim_reply_rx,
stop_signal.clone(),
);
let server_addr = udp_server.local_addr();
let (client_sim_req_tx, client_sim_req_rx) = mpsc::channel();
let (client_pcdu_reply_tx, client_pcdu_reply_rx) = mpsc::channel();
// Need to spawn the simulator UDP server before calling the client constructor.
let jh0 = std::thread::spawn(move || {
udp_server.operation();
});
// Creating the client also performs the connection test.
let mut client = SimClientUdp::new(server_addr, client_sim_req_rx).unwrap();
client.add_reply_recipient(ComponentId::Pcdu, client_pcdu_reply_tx);
let sim_request = server_sim_request_rx
.recv_timeout(Duration::from_millis(50))
.expect("no SIM request received");
assert_eq!(
sim_request.request,
SimRequest::SimCtrl(SimCtrlRequest::Ping)
);
let pcdu_req = PcduRequest::RequestSwitchInfo;
client_sim_req_tx
.send(SimRequestWithTime::new_with_epoch_time(pcdu_req))
.expect("send failed");
client.operation();
// Check that the request arrives properly at the server.
let sim_request = server_sim_request_rx
.recv_timeout(Duration::from_millis(50))
.expect("no SIM request received");
assert_eq!(
sim_request.request,
SimRequest::Pcdu(PcduRequest::RequestSwitchInfo)
);
// We inject the reply ourselves.
let pcdu_reply = PcduReply::SwitchInfo(SwitchMapBinary::default());
server_sim_reply_tx
.send(SimReply::from(pcdu_reply.clone()))
.expect("sending PCDU reply failed");
// Now we verify that the reply is sent by the UDP server back to the client, and then
// forwarded by the clients internal map.
let mut pcdu_reply_received = false;
for _ in 0..3 {
client.operation();
match client_pcdu_reply_rx.try_recv() {
Ok(sim_reply) => {
assert_eq!(sim_reply.component(), ComponentId::Pcdu);
assert_eq!(sim_reply, SimReply::Pcdu(pcdu_reply.clone()));
pcdu_reply_received = true;
break;
}
Err(e) => match e {
mpsc::TryRecvError::Empty => std::thread::sleep(Duration::from_millis(10)),
mpsc::TryRecvError::Disconnected => panic!("reply sender disconnected"),
},
}
}
if !pcdu_reply_received {
panic!("no reply received");
}
// Stop the server.
stop_signal.store(true, Ordering::Relaxed);
jh0.join().unwrap();
}
}
+142
View File
@@ -0,0 +1,142 @@
use std::time::Duration;
use std::{
collections::{HashSet, VecDeque},
sync::{Arc, Mutex},
};
use log::{info, warn};
use satrs::hal::std::tcp_spacepackets_server::CcsdsPacketParser;
use satrs::{
encoding::ccsds::{SpValidity, SpacePacketValidator},
hal::std::tcp_server::{HandledConnectionHandler, ServerConfig, TcpSpacepacketsServer},
spacepackets::{CcsdsPacket, PacketId},
tmtc::PacketSource,
};
use crate::tmtc::sender::TmTcSender;
#[derive(Default)]
pub struct ConnectionFinishedHandler {}
pub struct SimplePacketValidator {
pub valid_ids: HashSet<PacketId>,
}
impl SpacePacketValidator for SimplePacketValidator {
fn validate(
&self,
sp_header: &satrs::spacepackets::SpHeader,
_raw_buf: &[u8],
) -> satrs::encoding::ccsds::SpValidity {
if self.valid_ids.contains(&sp_header.packet_id()) {
return SpValidity::Valid;
}
log::warn!("ignoring space packet with header {sp_header:?}");
// We could perform a CRC check.. but lets keep this simple and assume that TCP ensures
// data integrity.
SpValidity::Skip
}
}
impl HandledConnectionHandler for ConnectionFinishedHandler {
fn handled_connection(&mut self, info: satrs::hal::std::tcp_server::HandledConnectionInfo) {
info!(
"Served {} TMs and {} TCs for client {:?}",
info.num_sent_tms, info.num_received_tcs, info.addr
);
}
}
#[derive(Default, Clone)]
pub struct SyncTcpTmSource {
tm_queue: Arc<Mutex<VecDeque<Vec<u8>>>>,
max_packets_stored: usize,
pub silent_packet_overwrite: bool,
}
impl SyncTcpTmSource {
pub fn new(max_packets_stored: usize) -> Self {
Self {
tm_queue: Arc::default(),
max_packets_stored,
silent_packet_overwrite: true,
}
}
pub fn add_tm(&mut self, tm: &[u8]) {
let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failec");
if tm_queue.len() > self.max_packets_stored {
if !self.silent_packet_overwrite {
warn!("TPC TM source is full, deleting oldest packet");
}
tm_queue.pop_front();
}
tm_queue.push_back(tm.to_vec());
}
}
impl PacketSource for SyncTcpTmSource {
type Error = ();
fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error> {
let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failed");
if !tm_queue.is_empty() {
let next_vec = tm_queue.front().unwrap();
if buffer.len() < next_vec.len() {
panic!(
"provided buffer too small, must be at least {} bytes",
next_vec.len()
);
}
let next_vec = tm_queue.pop_front().unwrap();
buffer[0..next_vec.len()].copy_from_slice(&next_vec);
if next_vec.len() > 9 {
let service = next_vec[7];
let subservice = next_vec[8];
info!("Sending PUS TM[{service},{subservice}]")
} else {
info!("Sending PUS TM");
}
return Ok(next_vec.len());
}
Ok(0)
}
}
pub type TcpServer<ReceivesTc> = TcpSpacepacketsServer<
SyncTcpTmSource,
ReceivesTc,
SimplePacketValidator,
ConnectionFinishedHandler,
>;
pub struct TcpTask(pub TcpServer<TmTcSender>);
impl TcpTask {
pub fn new(
cfg: ServerConfig,
tm_source: SyncTcpTmSource,
tc_sender: TmTcSender,
valid_ids: HashSet<PacketId>,
) -> Result<Self, std::io::Error> {
Ok(Self(TcpSpacepacketsServer::new(
cfg,
tm_source,
CcsdsPacketParser::new(cfg.id, 2048, tc_sender, SimplePacketValidator { valid_ids }),
ConnectionFinishedHandler::default(),
None,
)?))
}
pub fn periodic_operation(&mut self) {
let result = self
.0
.handle_all_connections(Some(Duration::from_millis(400)));
match result {
Ok(_conn_result) => (),
Err(e) => {
warn!("TCP server error: {e:?}");
}
}
}
}
+223
View File
@@ -0,0 +1,223 @@
#![allow(dead_code)]
use std::collections::VecDeque;
use std::net::{SocketAddr, UdpSocket};
use std::sync::{Arc, Mutex, mpsc};
use log::warn;
use satrs::HandlingStatus;
use satrs::hal::std::udp_server::{ReceiveResult, UdpTcServer};
use satrs::queue::GenericSendError;
use types::ccsds::CcsdsTmPacketOwned;
use crate::tmtc::sender::TmTcSender;
pub trait UdpTmHandlerProvider {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr);
}
pub struct UdpTmHandlerWithChannel {
pub tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
}
impl UdpTmHandlerProvider for UdpTmHandlerWithChannel {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
while let Ok(tm) = self.tm_rx.try_recv() {
log::debug!("sending TM from sender {:?}", tm.tm_header.sender_id);
let result = socket.send_to(&tm.to_vec(), recv_addr);
if let Err(e) = result {
warn!("Sending TM with UDP socket failed: {e}")
}
}
}
}
#[derive(Default, Debug, Clone)]
pub struct TestTmHandler {
addrs_to_send_to: Arc<Mutex<VecDeque<SocketAddr>>>,
}
impl UdpTmHandlerProvider for TestTmHandler {
fn send_tm_to_udp_client(&mut self, _socket: &UdpSocket, recv_addr: &SocketAddr) {
self.addrs_to_send_to.lock().unwrap().push_back(*recv_addr);
}
}
pub enum UdpTmHandler {
Normal(UdpTmHandlerWithChannel),
Test(TestTmHandler),
}
impl From<UdpTmHandlerWithChannel> for UdpTmHandler {
fn from(handler: UdpTmHandlerWithChannel) -> Self {
UdpTmHandler::Normal(handler)
}
}
impl From<TestTmHandler> for UdpTmHandler {
fn from(handler: TestTmHandler) -> Self {
UdpTmHandler::Test(handler)
}
}
impl UdpTmHandlerProvider for UdpTmHandler {
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
match self {
UdpTmHandler::Normal(handler) => handler.send_tm_to_udp_client(socket, recv_addr),
UdpTmHandler::Test(handler) => handler.send_tm_to_udp_client(socket, recv_addr),
}
}
}
pub struct UdpTmtcServer {
pub udp_tc_server: UdpTcServer<TmTcSender, GenericSendError>,
pub tm_handler: UdpTmHandler,
}
impl UdpTmtcServer {
pub fn periodic_operation(&mut self) {
loop {
if self.poll_tc_server() == HandlingStatus::Empty {
break;
}
}
if let Some(recv_addr) = self.udp_tc_server.last_sender() {
self.tm_handler
.send_tm_to_udp_client(&self.udp_tc_server.socket, &recv_addr);
}
}
fn poll_tc_server(&mut self) -> HandlingStatus {
match self.udp_tc_server.try_recv_tc() {
Ok(_) => HandlingStatus::HandledOne,
Err(e) => {
match e {
ReceiveResult::NothingReceived => (),
ReceiveResult::Io(e) => {
warn!("IO error {e}");
}
ReceiveResult::Send(send_error) => {
warn!("send error {send_error:?}");
}
}
HandlingStatus::Empty
}
}
}
}
#[cfg(test)]
mod tests {
use std::net::IpAddr;
use std::net::Ipv4Addr;
use arbitrary_int::traits::Integer as _;
use arbitrary_int::u14;
use example_std::config::OBSW_SERVER_ADDR;
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use satrs::{
ComponentId,
spacepackets::{
SpHeader,
ecss::{WritablePusPacket, tc::PusTcCreator},
},
};
use types::Apid;
use crate::tmtc::sender::MockSender;
use super::*;
const UDP_SERVER_ID: ComponentId = 0x05;
#[test]
fn test_basic() {
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), 0);
let test_receiver = TmTcSender::Mock(MockSender::default());
let udp_tc_server =
UdpTcServer::new(UDP_SERVER_ID, sock_addr, 2048, test_receiver).unwrap();
let tm_handler = TestTmHandler::default();
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
let mut udp_dyn_server = UdpTmtcServer {
udp_tc_server,
tm_handler: tm_handler.into(),
};
udp_dyn_server.periodic_operation();
let queue = udp_dyn_server
.udp_tc_server
.tc_sender
.get_mock_sender()
.unwrap()
.0
.borrow();
assert!(queue.is_empty());
assert!(tm_handler_calls.lock().unwrap().is_empty());
}
#[test]
fn test_transactions() {
let sock_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
let test_receiver = TmTcSender::Mock(MockSender::default());
let udp_tc_server =
UdpTcServer::new(UDP_SERVER_ID, sock_addr, 2048, test_receiver).unwrap();
let server_addr = udp_tc_server.socket.local_addr().unwrap();
let tm_handler = TestTmHandler::default();
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
let mut udp_dyn_server = UdpTmtcServer {
udp_tc_server,
tm_handler: tm_handler.into(),
};
let sph = SpHeader::new_for_unseg_tc(Apid::Tmtc.raw_value(), u14::ZERO, 0);
let ping_tc = PusTcCreator::new_simple(
sph,
MessageTypeId::new(17, 1),
&[],
CreatorConfig::default(),
)
.to_vec()
.unwrap();
let client = UdpSocket::bind("127.0.0.1:0").expect("Connecting to UDP server failed");
let client_addr = client.local_addr().unwrap();
println!("{}", server_addr);
client.send_to(&ping_tc, server_addr).unwrap();
udp_dyn_server.periodic_operation();
{
let mut queue = udp_dyn_server
.udp_tc_server
.tc_sender
.get_mock_sender()
.unwrap()
.0
.borrow_mut();
assert!(!queue.is_empty());
let packet_with_sender = queue.pop_front().unwrap();
assert_eq!(packet_with_sender.packet, ping_tc);
assert_eq!(packet_with_sender.sender_id, UDP_SERVER_ID);
}
{
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
assert!(!tm_handler_calls.is_empty());
assert_eq!(tm_handler_calls.len(), 1);
let received_addr = tm_handler_calls.pop_front().unwrap();
assert_eq!(received_addr, client_addr);
}
udp_dyn_server.periodic_operation();
let queue = udp_dyn_server
.udp_tc_server
.tc_sender
.get_mock_sender()
.unwrap()
.0
.borrow();
assert!(queue.is_empty());
drop(queue);
// Still tries to send to the same client.
{
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
assert!(!tm_handler_calls.is_empty());
assert_eq!(tm_handler_calls.len(), 1);
let received_addr = tm_handler_calls.pop_front().unwrap();
assert_eq!(received_addr, client_addr);
}
}
}
+293
View File
@@ -0,0 +1,293 @@
use arbitrary_int::traits::Integer as _;
use arbitrary_int::u11;
use satrs::event_man_legacy::{EventMessageU32, EventRoutingError};
use satrs::pus::event::EventTmHook;
use satrs::pus::verification::VerificationReporter;
use satrs::pus::EcssTmSender;
use satrs::request::UniqueApidTargetId;
use satrs::{
event_man_legacy::{EventManagerWithBoundedMpsc, EventSendProvider, EventU32SenderMpscBounded},
pus::{
event_man::{
DefaultPusEventU32TmCreator, EventReporter, EventRequest, EventRequestWithToken,
},
verification::{TcStateStarted, VerificationReportingProvider, VerificationToken},
},
spacepackets::time::cds::CdsTime,
};
use example_std::ids::generic_pus::PUS_EVENT_MANAGEMENT;
use crate::update_time;
// This helper sets the APID of the event sender for the PUS telemetry.
#[derive(Default)]
pub struct EventApidSetter {
pub next_apid: u11,
}
impl EventTmHook for EventApidSetter {
fn modify_tm(&self, tm: &mut satrs::spacepackets::ecss::tm::PusTmCreator) {
tm.set_apid(self.next_apid);
}
}
/*
/// The PUS event handler subscribes for all events and converts them into ECSS PUS 5 event
/// packets. It also handles the verification completion of PUS event service requests.
pub struct PusEventHandler<TmSender: EcssTmSender> {
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
pus_event_tm_creator: DefaultPusEventU32TmCreator<EventApidSetter>,
pus_event_man_rx: mpsc::Receiver<EventMessageU32>,
tm_sender: TmSender,
time_provider: CdsTime,
timestamp: [u8; 7],
small_data_buf: [u8; 64],
verif_handler: VerificationReporter,
}
impl<TmSender: EcssTmSender> PusEventHandler<TmSender> {
pub fn new(
tm_sender: TmSender,
verif_handler: VerificationReporter,
event_manager: &mut EventManagerWithBoundedMpsc,
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
) -> Self {
let event_queue_cap = 30;
let (pus_event_man_tx, pus_event_man_rx) = mpsc::sync_channel(event_queue_cap);
// All events sent to the manager are routed to the PUS event manager, which generates PUS event
// telemetry for each event.
let event_reporter = EventReporter::new_with_hook(
PUS_EVENT_MANAGEMENT.raw(),
u11::ZERO,
0,
128,
EventApidSetter::default(),
);
let pus_event_dispatcher =
DefaultPusEventU32TmCreator::new_with_default_backend(event_reporter);
let pus_event_man_send_provider = EventU32SenderMpscBounded::new(
PUS_EVENT_MANAGEMENT.raw(),
pus_event_man_tx,
event_queue_cap,
);
event_manager.subscribe_all(pus_event_man_send_provider.target_id());
event_manager.add_sender(pus_event_man_send_provider);
Self {
event_request_rx,
pus_event_tm_creator: pus_event_dispatcher,
pus_event_man_rx,
time_provider: CdsTime::new_with_u16_days(0, 0),
timestamp: [0; 7],
small_data_buf: [0; 64],
verif_handler,
tm_sender,
}
}
pub fn handle_event_requests(&mut self) {
let report_completion = |event_req: EventRequestWithToken, timestamp: &[u8]| {
let started_token: VerificationToken<TcStateStarted> = event_req
.token
.try_into()
.expect("expected start verification token");
self.verif_handler
.completion_success(&self.tm_sender, started_token, timestamp)
.expect("Sending completion success failed");
};
loop {
// handle event requests
match self.event_request_rx.try_recv() {
Ok(event_req) => match event_req.request {
EventRequest::Enable(event) => {
self.pus_event_tm_creator
.enable_tm_for_event(&event)
.expect("Enabling TM failed");
update_time(&mut self.time_provider, &mut self.timestamp);
report_completion(event_req, &self.timestamp);
}
EventRequest::Disable(event) => {
self.pus_event_tm_creator
.disable_tm_for_event(&event)
.expect("Disabling TM failed");
update_time(&mut self.time_provider, &mut self.timestamp);
report_completion(event_req, &self.timestamp);
}
},
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("all event request senders have disconnected");
break;
}
},
}
}
}
pub fn generate_pus_event_tm(&mut self) {
loop {
// Perform the generation of PUS event packets
match self.pus_event_man_rx.try_recv() {
Ok(event_msg) => {
// We use the TM modification hook to set the sender APID for each event.
self.pus_event_tm_creator.reporter.tm_hook.next_apid =
UniqueApidTargetId::from(event_msg.sender_id()).apid;
update_time(&mut self.time_provider, &mut self.timestamp);
let generation_result = self
.pus_event_tm_creator
.generate_pus_event_tm_generic_with_generic_params(
&self.tm_sender,
&self.timestamp,
event_msg.event(),
&mut self.small_data_buf,
event_msg.params(),
)
.expect("Sending TM as event failed");
if !generation_result.params_were_propagated {
log::warn!(
"Event TM parameters were not propagated: {:?}",
event_msg.params()
);
}
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("All event senders have disconnected");
break;
}
},
}
}
}
}
pub struct EventHandler<TmSender: EcssTmSender> {
pub pus_event_handler: PusEventHandler<TmSender>,
event_manager: EventManagerWithBoundedMpsc,
}
impl<TmSender: EcssTmSender> EventHandler<TmSender> {
pub fn new(
tm_sender: TmSender,
event_rx: mpsc::Receiver<EventMessageU32>,
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
) -> Self {
let mut event_manager = EventManagerWithBoundedMpsc::new(event_rx);
let pus_event_handler = PusEventHandler::new(
tm_sender,
create_verification_reporter(PUS_EVENT_MANAGEMENT.id(), PUS_EVENT_MANAGEMENT.apid),
&mut event_manager,
event_request_rx,
);
Self {
pus_event_handler,
event_manager,
}
}
#[allow(dead_code)]
pub fn event_manager(&mut self) -> &mut EventManagerWithBoundedMpsc {
&mut self.event_manager
}
pub fn periodic_operation(&mut self) {
self.pus_event_handler.handle_event_requests();
self.try_event_routing();
self.pus_event_handler.generate_pus_event_tm();
}
pub fn try_event_routing(&mut self) {
let error_handler = |event_msg: &EventMessageU32, error: EventRoutingError| {
self.routing_error_handler(event_msg, error)
};
// Perform the event routing.
self.event_manager.try_event_handling(error_handler);
}
pub fn routing_error_handler(&self, event_msg: &EventMessageU32, error: EventRoutingError) {
log::warn!("event routing error for event {event_msg:?}: {error:?}");
}
}
#[cfg(test)]
mod tests {
use arbitrary_int::u21;
use satrs::{
events_legacy::EventU32,
pus::verification::VerificationReporterConfig,
spacepackets::ecss::{tm::PusTmReader, PusPacket},
tmtc::PacketAsVec,
};
use super::*;
const TEST_CREATOR_ID: UniqueApidTargetId = UniqueApidTargetId::new(u11::new(1), u21::new(2));
const TEST_EVENT: EventU32 = EventU32::new(satrs::events_legacy::Severity::Info, 1, 1);
pub struct EventManagementTestbench {
pub event_tx: mpsc::SyncSender<EventMessageU32>,
pub event_manager: EventManagerWithBoundedMpsc,
pub tm_receiver: mpsc::Receiver<PacketAsVec>,
pub pus_event_handler: PusEventHandler<mpsc::Sender<PacketAsVec>>,
}
impl EventManagementTestbench {
pub fn new() -> Self {
let (event_tx, event_rx) = mpsc::sync_channel(10);
let (_event_req_tx, event_req_rx) = mpsc::sync_channel(10);
let (tm_sender, tm_receiver) = mpsc::channel();
let verif_reporter_cfg = VerificationReporterConfig::new(u11::new(0x05), 2, 2, 128);
let verif_reporter =
VerificationReporter::new(PUS_EVENT_MANAGEMENT.id(), &verif_reporter_cfg);
let mut event_manager = EventManagerWithBoundedMpsc::new(event_rx);
let pus_event_handler = PusEventHandler::<mpsc::Sender<PacketAsVec>>::new(
tm_sender,
verif_reporter,
&mut event_manager,
event_req_rx,
);
Self {
event_tx,
tm_receiver,
event_manager,
pus_event_handler,
}
}
}
#[test]
fn test_basic_event_generation() {
let mut testbench = EventManagementTestbench::new();
testbench
.event_tx
.send(EventMessageU32::new(
TEST_CREATOR_ID.id(),
EventU32::new(satrs::events_legacy::Severity::Info, 1, 1),
))
.expect("failed to send event");
testbench.pus_event_handler.handle_event_requests();
testbench.event_manager.try_event_handling(|_, _| {});
testbench.pus_event_handler.generate_pus_event_tm();
let tm_packet = testbench
.tm_receiver
.try_recv()
.expect("failed to receive TM packet");
assert_eq!(tm_packet.sender_id, PUS_EVENT_MANAGEMENT.id());
let tm_reader = PusTmReader::new(&tm_packet.packet, 7).expect("failed to create TM reader");
assert_eq!(tm_reader.apid(), TEST_CREATOR_ID.apid);
assert_eq!(tm_reader.user_data().len(), 4);
let event_read_back = EventU32::from_be_bytes(tm_reader.user_data().try_into().unwrap());
assert_eq!(event_read_back, TEST_EVENT);
}
#[test]
fn test_basic_event_disabled() {
// TODO: Add test.
}
}
*/
+74
View File
@@ -0,0 +1,74 @@
use arbitrary_int::traits::Integer as _;
use derive_new::new;
use satrs::hk::UniqueId;
use satrs::request::UniqueApidTargetId;
use satrs::spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader};
use satrs::spacepackets::ecss::{hk, CreatorConfig, MessageTypeId};
use satrs::spacepackets::{ByteConversionError, SpHeader};
#[derive(Debug, new, Copy, Clone)]
pub struct HkUniqueId {
target_id: UniqueApidTargetId,
set_id: UniqueId,
}
impl HkUniqueId {
#[allow(dead_code)]
pub fn target_id(&self) -> UniqueApidTargetId {
self.target_id
}
#[allow(dead_code)]
pub fn set_id(&self) -> UniqueId {
self.set_id
}
#[allow(dead_code)]
pub fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
if buf.len() < 8 {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: 8,
});
}
buf[0..4].copy_from_slice(&self.target_id.unique_id.as_u32().to_be_bytes());
buf[4..8].copy_from_slice(&self.set_id.to_be_bytes());
Ok(8)
}
}
#[derive(new)]
pub struct PusHkHelper {
component_id: UniqueApidTargetId,
}
impl PusHkHelper {
pub fn generate_hk_report_packet<
'a,
'b,
HkWriter: FnMut(&mut [u8]) -> Result<usize, ByteConversionError>,
>(
&self,
timestamp: &'a [u8],
set_id: u32,
hk_data_writer: &mut HkWriter,
buf: &'b mut [u8],
) -> Result<PusTmCreator<'a, 'b>, ByteConversionError> {
let sec_header = PusTmSecondaryHeader::new(
MessageTypeId::new(3, hk::MessageSubtypeId::TmHkPacket as u8),
0,
0,
timestamp,
);
buf[0..4].copy_from_slice(&self.component_id.unique_id.as_u32().to_be_bytes());
buf[4..8].copy_from_slice(&set_id.to_be_bytes());
let (_, second_half) = buf.split_at_mut(8);
let hk_data_len = hk_data_writer(second_half)?;
Ok(PusTmCreator::new(
SpHeader::new_from_apid(self.component_id.apid),
sec_header,
&buf[0..8 + hk_data_len],
CreatorConfig::default(),
))
}
}
@@ -0,0 +1,725 @@
use log::warn;
use satrs::action::{ActionRequest, ActionRequestVariant};
use satrs::pus::action::{
ActionReplyPus, ActionReplyVariant, ActivePusActionRequestStd, DefaultActiveActionRequestMap,
};
use satrs::pus::verification::{
handle_completion_failure_with_generic_params, handle_step_failure_with_generic_params,
FailParamHelper, FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
VerificationReportingProvider, VerificationToken,
};
use satrs::pus::{
ActiveRequest, EcssTcAndToken, EcssTcCacher, EcssTmSender, EcssTmtcError,
GenericConversionError, MpscTcReceiver, PusPacketHandlingError, PusReplyHandler,
PusServiceHelper, PusTcToRequestConverter,
};
use satrs::request::{GenericMessage, UniqueApidTargetId};
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::{EcssEnumU16, PusPacket, PusServiceId};
use example_std::config::tmtc_err;
use example_std::ids;
use example_std::ids::generic_pus::PUS_ACTION;
use std::sync::mpsc;
use std::time::Duration;
use crate::requests::GenericRequestRouter;
use crate::tmtc::sender::TmTcSender;
use super::{
create_verification_reporter, generic_pus_request_timeout_handler, HandlingStatus,
PusTargetedRequestService, TargetedPusService,
};
pub struct ActionReplyHandler {
fail_data_buf: [u8; 128],
}
impl Default for ActionReplyHandler {
fn default() -> Self {
Self {
fail_data_buf: [0; 128],
}
}
}
impl PusReplyHandler<ActivePusActionRequestStd, ActionReplyPus> for ActionReplyHandler {
type Error = EcssTmtcError;
fn handle_unrequested_reply(
&mut self,
reply: &GenericMessage<ActionReplyPus>,
_tm_sender: &impl EcssTmSender,
) -> Result<(), Self::Error> {
warn!("received unexpected reply for service 8: {reply:?}");
Ok(())
}
fn handle_reply(
&mut self,
reply: &GenericMessage<ActionReplyPus>,
active_request: &ActivePusActionRequestStd,
tm_sender: &(impl EcssTmSender + ?Sized),
verification_handler: &impl VerificationReportingProvider,
timestamp: &[u8],
) -> Result<bool, Self::Error> {
let verif_token: VerificationToken<TcStateStarted> = active_request
.token()
.try_into()
.expect("invalid token state");
let remove_entry = match &reply.message.variant {
ActionReplyVariant::CompletionFailed { error_code, params } => {
let error_propagated = handle_completion_failure_with_generic_params(
tm_sender,
verif_token,
verification_handler,
FailParamHelper {
error_code,
params: params.as_ref(),
timestamp,
small_data_buf: &mut self.fail_data_buf,
},
)?;
if !error_propagated {
log::warn!(
"error params for completion failure were not propated: {:?}",
params.as_ref()
);
}
true
}
ActionReplyVariant::StepFailed {
error_code,
step,
params,
} => {
let error_propagated = handle_step_failure_with_generic_params(
tm_sender,
verif_token,
verification_handler,
FailParamHelper {
error_code,
params: params.as_ref(),
timestamp,
small_data_buf: &mut self.fail_data_buf,
},
&EcssEnumU16::new(*step),
)?;
if !error_propagated {
log::warn!(
"error params for completion failure were not propated: {:?}",
params.as_ref()
);
}
true
}
ActionReplyVariant::Completed => {
verification_handler.completion_success(tm_sender, verif_token, timestamp)?;
true
}
ActionReplyVariant::StepSuccess { step } => {
verification_handler.step_success(
tm_sender,
&verif_token,
timestamp,
EcssEnumU16::new(*step),
)?;
false
}
_ => false,
};
Ok(remove_entry)
}
fn handle_request_timeout(
&mut self,
active_request: &ActivePusActionRequestStd,
tm_sender: &impl EcssTmSender,
verification_handler: &impl VerificationReportingProvider,
time_stamp: &[u8],
) -> Result<(), Self::Error> {
generic_pus_request_timeout_handler(
tm_sender,
active_request,
verification_handler,
time_stamp,
"action",
)
}
}
#[derive(Default)]
pub struct ActionRequestConverter {}
impl PusTcToRequestConverter<ActivePusActionRequestStd, ActionRequest> for ActionRequestConverter {
type Error = GenericConversionError;
fn convert(
&mut self,
token: VerificationToken<TcStateAccepted>,
tc: &PusTcReader,
tm_sender: &(impl EcssTmSender + ?Sized),
verif_reporter: &impl VerificationReportingProvider,
time_stamp: &[u8],
) -> Result<(ActivePusActionRequestStd, ActionRequest), Self::Error> {
let subservice = tc.message_subtype_id();
let user_data = tc.user_data();
if user_data.len() < 8 {
verif_reporter
.start_failure(
tm_sender,
token,
FailParams::new_no_fail_data(time_stamp, &tmtc_err::NOT_ENOUGH_APP_DATA),
)
.expect("Sending start failure failed");
return Err(GenericConversionError::NotEnoughAppData {
expected: 8,
found: user_data.len(),
});
}
let target_id_and_apid = UniqueApidTargetId::from_pus_tc(tc).unwrap();
let action_id = u32::from_be_bytes(user_data[4..8].try_into().unwrap());
if subservice == 128 {
let req_variant = if user_data.len() == 8 {
ActionRequestVariant::NoData
} else {
ActionRequestVariant::VecData(user_data[8..].to_vec())
};
Ok((
ActivePusActionRequestStd::new(
action_id,
target_id_and_apid.into(),
token.into(),
Duration::from_secs(30),
),
ActionRequest::new(action_id, req_variant),
))
} else {
verif_reporter
.start_failure(
tm_sender,
token,
FailParams::new_no_fail_data(time_stamp, &tmtc_err::INVALID_PUS_SUBSERVICE),
)
.expect("Sending start failure failed");
Err(GenericConversionError::InvalidSubservice(subservice))
}
}
}
pub fn create_action_service(
tm_sender: TmTcSender,
tc_in_mem_converter: EcssTcCacher,
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
action_router: GenericRequestRouter,
reply_receiver: mpsc::Receiver<GenericMessage<ActionReplyPus>>,
) -> ActionServiceWrapper {
let action_request_handler = PusTargetedRequestService::new(
PusServiceHelper::new(
ids::generic_pus::PUS_ACTION.id(),
pus_action_rx,
tm_sender,
create_verification_reporter(PUS_ACTION.id(), PUS_ACTION.apid),
tc_in_mem_converter,
),
ActionRequestConverter::default(),
// TODO: Implementation which does not use run-time allocation? Maybe something like
// a bounded wrapper which pre-allocates using [HashMap::with_capacity]..
DefaultActiveActionRequestMap::default(),
ActionReplyHandler::default(),
action_router,
reply_receiver,
);
ActionServiceWrapper {
service: action_request_handler,
}
}
pub struct ActionServiceWrapper {
pub(crate) service: PusTargetedRequestService<
MpscTcReceiver,
VerificationReporter,
ActionRequestConverter,
ActionReplyHandler,
DefaultActiveActionRequestMap,
ActivePusActionRequestStd,
ActionRequest,
ActionReplyPus,
>,
}
impl TargetedPusService for ActionServiceWrapper {
const SERVICE_ID: u8 = PusServiceId::Action as u8;
const SERVICE_STR: &'static str = "action";
delegate::delegate! {
to self.service {
fn poll_and_handle_next_tc(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, PusPacketHandlingError>;
fn poll_and_handle_next_reply(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, EcssTmtcError>;
fn check_for_request_timeouts(&mut self);
}
}
}
#[cfg(test)]
mod tests {
use arbitrary_int::traits::Integer as _;
use satrs::pus::test_util::{
TEST_APID, TEST_COMPONENT_ID_0, TEST_COMPONENT_ID_1, TEST_UNIQUE_ID_0, TEST_UNIQUE_ID_1,
};
use satrs::pus::verification::test_util::TestVerificationReporter;
use satrs::pus::{verification, EcssTcVecCacher};
use satrs::request::MessageMetadata;
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use satrs::tmtc::PacketAsVec;
use satrs::ComponentId;
use satrs::{
res_code::ResultU16,
spacepackets::{
ecss::{
tc::{PusTcCreator, PusTcSecondaryHeader},
tm::PusTmReader,
WritablePusPacket,
},
SpHeader,
},
};
use crate::{
pus::tests::{PusConverterTestbench, ReplyHandlerTestbench, TargetedPusRequestTestbench},
requests::CompositeRequest,
};
use super::*;
impl
TargetedPusRequestTestbench<
ActionRequestConverter,
ActionReplyHandler,
DefaultActiveActionRequestMap,
ActivePusActionRequestStd,
ActionRequest,
ActionReplyPus,
>
{
pub fn new_for_action(owner_id: ComponentId, target_id: ComponentId) -> Self {
let _ = env_logger::builder().is_test(true).try_init();
let (tm_funnel_tx, tm_funnel_rx) = mpsc::sync_channel(5);
let (pus_action_tx, pus_action_rx) = mpsc::channel();
let (action_reply_tx, action_reply_rx) = mpsc::channel();
let (action_req_tx, action_req_rx) = mpsc::sync_channel(10);
let verif_reporter = TestVerificationReporter::new(owner_id);
let mut generic_req_router = GenericRequestRouter::default();
generic_req_router
.composite_router_map
.insert(target_id, action_req_tx);
Self {
service: PusTargetedRequestService::new(
PusServiceHelper::new(
owner_id,
pus_action_rx,
TmTcSender::Heap(tm_funnel_tx.clone()),
verif_reporter,
EcssTcCacher::Heap(EcssTcVecCacher::default()),
),
ActionRequestConverter::default(),
DefaultActiveActionRequestMap::default(),
ActionReplyHandler::default(),
generic_req_router,
action_reply_rx,
),
request_id: None,
pus_packet_tx: pus_action_tx,
tm_funnel_rx,
reply_tx: action_reply_tx,
request_rx: action_req_rx,
}
}
pub fn verify_packet_started(&self) {
self.service
.service_helper
.common
.verif_reporter
.check_next_is_started_success(
self.service.service_helper.id(),
self.request_id.expect("request ID not set").into(),
);
}
pub fn verify_packet_completed(&self) {
self.service
.service_helper
.common
.verif_reporter
.check_next_is_completion_success(
self.service.service_helper.id(),
self.request_id.expect("request ID not set").into(),
);
}
pub fn verify_tm_empty(&self) {
let packet = self.tm_funnel_rx.try_recv();
if let Err(mpsc::TryRecvError::Empty) = packet {
} else {
let tm = packet.unwrap();
let unexpected_tm = PusTmReader::new(&tm.packet, 7).unwrap();
panic!("unexpected TM packet {unexpected_tm:?}");
}
}
pub fn verify_next_tc_is_handled_properly(&mut self, time_stamp: &[u8]) {
let result = self.service.poll_and_handle_next_tc(time_stamp);
if let Err(e) = result {
panic!("unexpected error {:?}", e);
}
let result = result.unwrap();
match result {
HandlingStatus::HandledOne => (),
_ => panic!("unexpected result {result:?}"),
}
}
pub fn verify_all_tcs_handled(&mut self, time_stamp: &[u8]) {
let result = self.service.poll_and_handle_next_tc(time_stamp);
if let Err(e) = result {
panic!("unexpected error {:?}", e);
}
let result = result.unwrap();
match result {
HandlingStatus::Empty => (),
_ => panic!("unexpected result {result:?}"),
}
}
pub fn verify_next_reply_is_handled_properly(&mut self, time_stamp: &[u8]) {
let result = self.service.poll_and_handle_next_reply(time_stamp);
assert!(result.is_ok());
assert_eq!(result.unwrap(), HandlingStatus::HandledOne);
}
pub fn verify_all_replies_handled(&mut self, time_stamp: &[u8]) {
let result = self.service.poll_and_handle_next_reply(time_stamp);
assert!(result.is_ok());
assert_eq!(result.unwrap(), HandlingStatus::Empty);
}
pub fn add_tc(&mut self, tc: &PusTcCreator) {
self.request_id = Some(verification::RequestId::new(tc).into());
let token = self
.service
.service_helper
.verif_reporter_mut()
.start_verification(tc);
let accepted_token = self
.service
.service_helper
.verif_reporter()
.acceptance_success(self.service.service_helper.tm_sender(), token, &[0; 7])
.expect("TC acceptance failed");
self.service
.service_helper
.verif_reporter()
.check_next_was_added(accepted_token.request_id());
let id = self.service.service_helper.id();
self.service
.service_helper
.verif_reporter()
.check_next_is_acceptance_success(id, accepted_token.request_id());
self.pus_packet_tx
.send(EcssTcAndToken::new(
PacketAsVec::new(self.service.service_helper.id(), tc.to_vec().unwrap()),
accepted_token,
))
.unwrap();
}
}
#[test]
fn basic_request() {
let mut testbench = TargetedPusRequestTestbench::new_for_action(
TEST_COMPONENT_ID_0.id(),
TEST_COMPONENT_ID_1.id(),
);
// Create a basic action request and verify forwarding.
let sp_header = SpHeader::new_from_apid(TEST_APID);
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
let action_id = 5_u32;
let mut app_data: [u8; 8] = [0; 8];
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_1.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
let pus8_packet =
PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
testbench.add_tc(&pus8_packet);
let time_stamp: [u8; 7] = [0; 7];
testbench.verify_next_tc_is_handled_properly(&time_stamp);
testbench.verify_all_tcs_handled(&time_stamp);
testbench.verify_packet_started();
let possible_req = testbench.request_rx.try_recv();
assert!(possible_req.is_ok());
let req = possible_req.unwrap();
if let CompositeRequest::Action(action_req) = req.message {
assert_eq!(action_req.action_id, action_id);
assert_eq!(action_req.variant, ActionRequestVariant::NoData);
let action_reply = ActionReplyPus::new(action_id, ActionReplyVariant::Completed);
testbench
.reply_tx
.send(GenericMessage::new(req.requestor_info, action_reply))
.unwrap();
} else {
panic!("unexpected request type");
}
testbench.verify_next_reply_is_handled_properly(&time_stamp);
testbench.verify_all_replies_handled(&time_stamp);
testbench.verify_packet_completed();
testbench.verify_tm_empty();
}
#[test]
fn basic_request_routing_error() {
let mut testbench = TargetedPusRequestTestbench::new_for_action(
TEST_COMPONENT_ID_0.id(),
TEST_COMPONENT_ID_1.id(),
);
// Create a basic action request and verify forwarding.
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
let action_id = 5_u32;
let mut app_data: [u8; 8] = [0; 8];
// Invalid ID, routing should fail.
app_data[0..4].copy_from_slice(&0_u32.to_be_bytes());
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
let pus8_packet = PusTcCreator::new(
SpHeader::new_from_apid(TEST_APID),
sec_header,
&app_data,
CreatorConfig::default(),
);
testbench.add_tc(&pus8_packet);
let time_stamp: [u8; 7] = [0; 7];
let result = testbench.service.poll_and_handle_next_tc(&time_stamp);
assert!(result.is_err());
// Verify the correct result and completion failure.
}
#[test]
fn converter_action_req_no_data() {
let mut testbench = PusConverterTestbench::new(
TEST_COMPONENT_ID_0.raw(),
ActionRequestConverter::default(),
);
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
let action_id = 5_u32;
let mut app_data: [u8; 8] = [0; 8];
// Invalid ID, routing should fail.
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
let pus8_packet = PusTcCreator::new(
SpHeader::new_from_apid(TEST_APID),
sec_header,
&app_data,
CreatorConfig::default(),
);
let token = testbench.add_tc(&pus8_packet);
let result = testbench.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0);
assert!(result.is_ok());
let (active_req, request) = result.unwrap();
if let ActionRequestVariant::NoData = request.variant {
assert_eq!(request.action_id, action_id);
assert_eq!(active_req.action_id, action_id);
assert_eq!(
active_req.target_id(),
UniqueApidTargetId::new(TEST_APID, TEST_UNIQUE_ID_0).raw()
);
assert_eq!(
active_req.token().request_id(),
testbench.request_id().unwrap()
);
} else {
panic!("unexpected action request variant");
}
}
#[test]
fn converter_action_req_with_data() {
let mut testbench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ActionRequestConverter::default());
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
let action_id = 5_u32;
let mut app_data: [u8; 16] = [0; 16];
// Invalid ID, routing should fail.
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
for i in 0..8 {
app_data[i + 8] = i as u8;
}
let pus8_packet = PusTcCreator::new(
SpHeader::new_from_apid(TEST_APID),
sec_header,
&app_data,
CreatorConfig::default(),
);
let token = testbench.add_tc(&pus8_packet);
let result = testbench.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0);
assert!(result.is_ok());
let (active_req, request) = result.unwrap();
if let ActionRequestVariant::VecData(vec) = request.variant {
assert_eq!(request.action_id, action_id);
assert_eq!(active_req.action_id, action_id);
assert_eq!(vec, app_data[8..].to_vec());
} else {
panic!("unexpected action request variant");
}
}
#[test]
fn reply_handling_completion_success() {
let mut testbench =
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
let action_id = 5_u32;
let (req_id, active_req) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
let active_action_req =
ActivePusActionRequestStd::new_from_common_req(action_id, active_req);
let reply = ActionReplyPus::new(action_id, ActionReplyVariant::Completed);
let generic_reply = GenericMessage::new(MessageMetadata::new(req_id.into(), 0), reply);
let result = testbench.handle_reply(&generic_reply, &active_action_req, &[]);
assert!(result.is_ok());
assert!(result.unwrap());
testbench.verif_reporter.assert_full_completion_success(
TEST_COMPONENT_ID_0.id(),
req_id,
None,
);
}
#[test]
fn reply_handling_completion_failure() {
let mut testbench =
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
let action_id = 5_u32;
let (req_id, active_req) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
let active_action_req =
ActivePusActionRequestStd::new_from_common_req(action_id, active_req);
let error_code = ResultU16::new(2, 3);
let reply = ActionReplyPus::new(
action_id,
ActionReplyVariant::CompletionFailed {
error_code,
params: None,
},
);
let generic_reply = GenericMessage::new(MessageMetadata::new(req_id.into(), 0), reply);
let result = testbench.handle_reply(&generic_reply, &active_action_req, &[]);
assert!(result.is_ok());
assert!(result.unwrap());
testbench.verif_reporter.assert_completion_failure(
TEST_COMPONENT_ID_0.into(),
req_id,
None,
error_code.raw() as u64,
);
}
#[test]
fn reply_handling_step_success() {
let mut testbench =
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
let action_id = 5_u32;
let (req_id, active_req) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
let active_action_req =
ActivePusActionRequestStd::new_from_common_req(action_id, active_req);
let reply = ActionReplyPus::new(action_id, ActionReplyVariant::StepSuccess { step: 1 });
let generic_reply = GenericMessage::new(MessageMetadata::new(req_id.into(), 0), reply);
let result = testbench.handle_reply(&generic_reply, &active_action_req, &[]);
assert!(result.is_ok());
// Entry should not be removed, completion not done yet.
assert!(!result.unwrap());
testbench.verif_reporter.check_next_was_added(req_id);
testbench
.verif_reporter
.check_next_is_acceptance_success(TEST_COMPONENT_ID_0.raw(), req_id);
testbench
.verif_reporter
.check_next_is_started_success(TEST_COMPONENT_ID_0.raw(), req_id);
testbench
.verif_reporter
.check_next_is_step_success(TEST_COMPONENT_ID_0.raw(), req_id, 1);
}
#[test]
fn reply_handling_step_failure() {
let mut testbench =
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
let action_id = 5_u32;
let (req_id, active_req) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
let active_action_req =
ActivePusActionRequestStd::new_from_common_req(action_id, active_req);
let error_code = ResultU16::new(2, 3);
let reply = ActionReplyPus::new(
action_id,
ActionReplyVariant::StepFailed {
error_code,
step: 1,
params: None,
},
);
let generic_reply = GenericMessage::new(MessageMetadata::new(req_id.into(), 0), reply);
let result = testbench.handle_reply(&generic_reply, &active_action_req, &[]);
assert!(result.is_ok());
assert!(result.unwrap());
testbench.verif_reporter.check_next_was_added(req_id);
testbench
.verif_reporter
.check_next_is_acceptance_success(TEST_COMPONENT_ID_0.id(), req_id);
testbench
.verif_reporter
.check_next_is_started_success(TEST_COMPONENT_ID_0.id(), req_id);
testbench.verif_reporter.check_next_is_step_failure(
TEST_COMPONENT_ID_0.id(),
req_id,
error_code.raw().into(),
);
}
#[test]
fn reply_handling_unrequested_reply() {
let mut testbench =
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
let action_reply = ActionReplyPus::new(5_u32, ActionReplyVariant::Completed);
let unrequested_reply =
GenericMessage::new(MessageMetadata::new(10_u32, 15_u32), action_reply);
// Right now this function does not do a lot. We simply check that it does not panic or do
// weird stuff.
let result = testbench.handle_unrequested_reply(&unrequested_reply);
assert!(result.is_ok());
}
#[test]
fn reply_handling_reply_timeout() {
let mut testbench =
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
let action_id = 5_u32;
let (req_id, active_request) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
let result = testbench.handle_request_timeout(
&ActivePusActionRequestStd::new_from_common_req(action_id, active_request),
&[],
);
assert!(result.is_ok());
testbench.verif_reporter.assert_completion_failure(
TEST_COMPONENT_ID_0.raw(),
req_id,
None,
tmtc_err::REQUEST_TIMEOUT.raw() as u64,
);
}
}
@@ -0,0 +1,91 @@
use std::sync::mpsc;
use crate::pus::create_verification_reporter;
use crate::tmtc::sender::TmTcSender;
use satrs::pus::event_man::EventRequestWithToken;
use satrs::pus::event_srv::PusEventServiceHandler;
use satrs::pus::verification::VerificationReporter;
use satrs::pus::{
DirectPusPacketHandlerResult, EcssTcAndToken, EcssTcCacher, MpscTcReceiver,
PartialPusHandlingError, PusServiceHelper,
};
use satrs::spacepackets::ecss::PusServiceId;
use example_std::ids::generic_pus::PUS_EVENT_MANAGEMENT;
use super::{DirectPusService, HandlingStatus};
pub fn create_event_service(
tm_sender: TmTcSender,
tm_in_pool_converter: EcssTcCacher,
pus_event_rx: mpsc::Receiver<EcssTcAndToken>,
event_request_tx: mpsc::Sender<EventRequestWithToken>,
) -> EventServiceWrapper {
let pus_5_handler = PusEventServiceHandler::new(
PusServiceHelper::new(
PUS_EVENT_MANAGEMENT.id(),
pus_event_rx,
tm_sender,
create_verification_reporter(PUS_EVENT_MANAGEMENT.id(), PUS_EVENT_MANAGEMENT.apid),
tm_in_pool_converter,
),
event_request_tx,
);
EventServiceWrapper {
handler: pus_5_handler,
}
}
pub struct EventServiceWrapper {
pub handler:
PusEventServiceHandler<MpscTcReceiver, TmTcSender, EcssTcCacher, VerificationReporter>,
}
impl DirectPusService for EventServiceWrapper {
const SERVICE_ID: u8 = PusServiceId::Event as u8;
const SERVICE_STR: &'static str = "events";
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
let error_handler = |partial_error: &PartialPusHandlingError| {
log::warn!(
"PUS {}({}) partial error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
partial_error
);
};
let result = self
.handler
.poll_and_handle_next_tc(error_handler, time_stamp);
if let Err(e) = result {
log::warn!(
"PUS {}({}) error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
e
);
// To avoid permanent loops on continuous errors.
return HandlingStatus::Empty;
}
match result.unwrap() {
DirectPusPacketHandlerResult::Handled(handling_status) => return handling_status,
DirectPusPacketHandlerResult::CustomSubservice(subservice, _) => {
log::warn!(
"PUS {}({}) subservice {} not implemented",
Self::SERVICE_ID,
Self::SERVICE_STR,
subservice
);
}
DirectPusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
log::warn!(
"PUS {}({}) subservice {} not implemented",
Self::SERVICE_ID,
Self::SERVICE_STR,
subservice
);
}
}
HandlingStatus::HandledOne
}
}
+524
View File
@@ -0,0 +1,524 @@
use derive_new::new;
use satrs::hk::{CollectionIntervalFactor, HkRequest, HkRequestVariant, UniqueId};
use satrs::pus::verification::{
FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
VerificationReportingProvider, VerificationToken,
};
use satrs::pus::{
ActivePusRequestStd, ActiveRequest, DefaultActiveRequestMap, EcssTcAndToken, EcssTcCacher,
EcssTmSender, EcssTmtcError, GenericConversionError, MpscTcReceiver, PusPacketHandlingError,
PusReplyHandler, PusServiceHelper, PusTcToRequestConverter,
};
use satrs::request::{GenericMessage, UniqueApidTargetId};
use satrs::res_code::ResultU16;
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::{hk, PusPacket, PusServiceId};
use example_std::config::{hk_err, tmtc_err};
use example_std::ids::generic_pus::PUS_HK;
use std::sync::mpsc;
use std::time::Duration;
use crate::pus::{create_verification_reporter, generic_pus_request_timeout_handler};
use crate::requests::GenericRequestRouter;
use crate::tmtc::sender::TmTcSender;
use super::{HandlingStatus, PusTargetedRequestService, TargetedPusService};
#[derive(Clone, PartialEq, Debug, new)]
pub struct HkReply {
pub unique_id: UniqueId,
pub variant: HkReplyVariant,
}
#[derive(Clone, PartialEq, Debug)]
#[allow(dead_code)]
pub enum HkReplyVariant {
Ack,
Failed(ResultU16),
}
#[derive(Default)]
pub struct HkReplyHandler {}
impl PusReplyHandler<ActivePusRequestStd, HkReply> for HkReplyHandler {
type Error = EcssTmtcError;
fn handle_unrequested_reply(
&mut self,
reply: &GenericMessage<HkReply>,
_tm_sender: &impl EcssTmSender,
) -> Result<(), Self::Error> {
log::warn!("received unexpected reply for service 3: {reply:?}");
Ok(())
}
fn handle_reply(
&mut self,
reply: &GenericMessage<HkReply>,
active_request: &ActivePusRequestStd,
tm_sender: &impl EcssTmSender,
verification_handler: &impl VerificationReportingProvider,
time_stamp: &[u8],
) -> Result<bool, Self::Error> {
let started_token: VerificationToken<TcStateStarted> = active_request
.token()
.try_into()
.expect("invalid token state");
match reply.message.variant {
HkReplyVariant::Ack => {
verification_handler
.completion_success(tm_sender, started_token, time_stamp)
.expect("sending completion success verification failed");
}
HkReplyVariant::Failed(failure_code) => {
verification_handler
.completion_failure(
tm_sender,
started_token,
FailParams::new(time_stamp, &failure_code, &[]),
)
.expect("sending completion success verification failed");
}
};
Ok(true)
}
fn handle_request_timeout(
&mut self,
active_request: &ActivePusRequestStd,
tm_sender: &impl EcssTmSender,
verification_handler: &impl VerificationReportingProvider,
time_stamp: &[u8],
) -> Result<(), Self::Error> {
generic_pus_request_timeout_handler(
tm_sender,
active_request,
verification_handler,
time_stamp,
"HK",
)?;
Ok(())
}
}
pub struct HkRequestConverter {
timeout: Duration,
}
impl Default for HkRequestConverter {
fn default() -> Self {
Self {
timeout: Duration::from_secs(60),
}
}
}
impl PusTcToRequestConverter<ActivePusRequestStd, HkRequest> for HkRequestConverter {
type Error = GenericConversionError;
fn convert(
&mut self,
token: VerificationToken<TcStateAccepted>,
tc: &PusTcReader,
tm_sender: &(impl EcssTmSender + ?Sized),
verif_reporter: &impl VerificationReportingProvider,
time_stamp: &[u8],
) -> Result<(ActivePusRequestStd, HkRequest), Self::Error> {
let user_data = tc.user_data();
if user_data.is_empty() {
let user_data_len = user_data.len() as u32;
let user_data_len_raw = user_data_len.to_be_bytes();
verif_reporter
.start_failure(
tm_sender,
token,
FailParams::new(
time_stamp,
&tmtc_err::NOT_ENOUGH_APP_DATA,
&user_data_len_raw,
),
)
.expect("Sending start failure TM failed");
return Err(GenericConversionError::NotEnoughAppData {
expected: 4,
found: 0,
});
}
if user_data.len() < 8 {
let err = if user_data.len() < 4 {
&hk_err::TARGET_ID_MISSING
} else {
&hk_err::UNIQUE_ID_MISSING
};
let user_data_len = user_data.len() as u32;
let user_data_len_raw = user_data_len.to_be_bytes();
verif_reporter
.start_failure(
tm_sender,
token,
FailParams::new(time_stamp, err, &user_data_len_raw),
)
.expect("Sending start failure TM failed");
return Err(GenericConversionError::NotEnoughAppData {
expected: 8,
found: 4,
});
}
let subservice = tc.message_subtype_id();
let target_id_and_apid = UniqueApidTargetId::from_pus_tc(tc).expect("invalid tc format");
let unique_id = u32::from_be_bytes(tc.user_data()[4..8].try_into().unwrap());
let standard_subservice = hk::MessageSubtypeId::try_from(subservice);
if standard_subservice.is_err() {
verif_reporter
.start_failure(
tm_sender,
token,
FailParams::new(time_stamp, &tmtc_err::INVALID_PUS_SUBSERVICE, &[subservice]),
)
.expect("Sending start failure TM failed");
return Err(GenericConversionError::InvalidSubservice(subservice));
}
let request = match standard_subservice.unwrap() {
hk::MessageSubtypeId::TcEnableHkGeneration
| hk::MessageSubtypeId::TcEnableDiagGeneration => {
HkRequest::new(unique_id, HkRequestVariant::EnablePeriodic)
}
hk::MessageSubtypeId::TcDisableHkGeneration
| hk::MessageSubtypeId::TcDisableDiagGeneration => {
HkRequest::new(unique_id, HkRequestVariant::DisablePeriodic)
}
hk::MessageSubtypeId::TcReportHkReportStructures => todo!(),
hk::MessageSubtypeId::TmHkPacket => todo!(),
hk::MessageSubtypeId::TcGenerateOneShotHk
| hk::MessageSubtypeId::TcGenerateOneShotDiag => {
HkRequest::new(unique_id, HkRequestVariant::OneShot)
}
hk::MessageSubtypeId::TcModifyDiagCollectionInterval
| hk::MessageSubtypeId::TcModifyHkCollectionInterval => {
if user_data.len() < 12 {
verif_reporter
.start_failure(
tm_sender,
token,
FailParams::new_no_fail_data(
time_stamp,
&tmtc_err::NOT_ENOUGH_APP_DATA,
),
)
.expect("Sending start failure TM failed");
return Err(GenericConversionError::NotEnoughAppData {
expected: 12,
found: user_data.len(),
});
}
HkRequest::new(
unique_id,
HkRequestVariant::ModifyCollectionInterval(
CollectionIntervalFactor::from_be_bytes(
user_data[8..12].try_into().unwrap(),
),
),
)
}
_ => {
verif_reporter
.start_failure(
tm_sender,
token,
FailParams::new(
time_stamp,
&tmtc_err::PUS_SUBSERVICE_NOT_IMPLEMENTED,
&[subservice],
),
)
.expect("Sending start failure TM failed");
return Err(GenericConversionError::InvalidSubservice(subservice));
}
};
Ok((
ActivePusRequestStd::new(target_id_and_apid.into(), token, self.timeout),
request,
))
}
}
pub fn create_hk_service(
tm_sender: TmTcSender,
tc_in_mem_converter: EcssTcCacher,
pus_hk_rx: mpsc::Receiver<EcssTcAndToken>,
request_router: GenericRequestRouter,
reply_receiver: mpsc::Receiver<GenericMessage<HkReply>>,
) -> HkServiceWrapper {
let pus_3_handler = PusTargetedRequestService::new(
PusServiceHelper::new(
PUS_HK.id(),
pus_hk_rx,
tm_sender,
create_verification_reporter(PUS_HK.id(), PUS_HK.apid),
tc_in_mem_converter,
),
HkRequestConverter::default(),
DefaultActiveRequestMap::default(),
HkReplyHandler::default(),
request_router,
reply_receiver,
);
HkServiceWrapper {
service: pus_3_handler,
}
}
pub struct HkServiceWrapper {
pub(crate) service: PusTargetedRequestService<
MpscTcReceiver,
VerificationReporter,
HkRequestConverter,
HkReplyHandler,
DefaultActiveRequestMap<ActivePusRequestStd>,
ActivePusRequestStd,
HkRequest,
HkReply,
>,
}
impl TargetedPusService for HkServiceWrapper {
const SERVICE_ID: u8 = PusServiceId::Housekeeping as u8;
const SERVICE_STR: &'static str = "housekeeping";
delegate::delegate! {
to self.service {
fn poll_and_handle_next_tc(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, PusPacketHandlingError>;
fn poll_and_handle_next_reply(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, EcssTmtcError>;
fn check_for_request_timeouts(&mut self);
}
}
}
#[cfg(test)]
mod tests {
use arbitrary_int::traits::Integer as _;
use arbitrary_int::{u14, u21};
use satrs::pus::test_util::{
TEST_COMPONENT_ID_0, TEST_COMPONENT_ID_1, TEST_UNIQUE_ID_0, TEST_UNIQUE_ID_1,
};
use satrs::request::MessageMetadata;
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use satrs::{
hk::HkRequestVariant,
pus::test_util::TEST_APID,
request::GenericMessage,
spacepackets::{
ecss::{hk::MessageSubtypeId, tc::PusTcCreator},
SpHeader,
},
};
use example_std::config::tmtc_err;
use crate::pus::{
hk::HkReplyVariant,
tests::{PusConverterTestbench, ReplyHandlerTestbench},
};
use super::{HkReply, HkReplyHandler, HkRequestConverter};
#[test]
fn hk_converter_one_shot_req() {
let mut hk_bench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let target_id = TEST_UNIQUE_ID_0;
let unique_id = 5_u32;
let mut app_data: [u8; 8] = [0; 8];
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
let hk_req = PusTcCreator::new_simple(
sp_header,
MessageTypeId::new(3, MessageSubtypeId::TcGenerateOneShotHk as u8),
&app_data,
CreatorConfig::default(),
);
let accepted_token = hk_bench.add_tc(&hk_req);
let (_active_req, req) = hk_bench
.convert(accepted_token, &[], TEST_APID, TEST_UNIQUE_ID_0)
.expect("conversion failed");
assert_eq!(req.unique_id, unique_id);
if let HkRequestVariant::OneShot = req.variant {
} else {
panic!("unexpected HK request")
}
}
#[test]
fn hk_converter_enable_periodic_generation() {
let mut hk_bench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let target_id = TEST_UNIQUE_ID_0;
let unique_id = 5_u32;
let mut app_data: [u8; 8] = [0; 8];
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
let mut generic_check = |tc: &PusTcCreator| {
let accepted_token = hk_bench.add_tc(tc);
let (_active_req, req) = hk_bench
.convert(accepted_token, &[], TEST_APID, TEST_UNIQUE_ID_0)
.expect("conversion failed");
assert_eq!(req.unique_id, unique_id);
if let HkRequestVariant::EnablePeriodic = req.variant {
} else {
panic!("unexpected HK request")
}
};
let tc0 = PusTcCreator::new_simple(
sp_header,
MessageTypeId::new(3, MessageSubtypeId::TcEnableHkGeneration as u8),
&app_data,
CreatorConfig::default(),
);
generic_check(&tc0);
let tc1 = PusTcCreator::new_simple(
sp_header,
MessageTypeId::new(3, MessageSubtypeId::TcEnableDiagGeneration as u8),
&app_data,
CreatorConfig::default(),
);
generic_check(&tc1);
}
#[test]
fn hk_conversion_disable_periodic_generation() {
let mut hk_bench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let target_id = TEST_UNIQUE_ID_0;
let unique_id = 5_u32;
let mut app_data: [u8; 8] = [0; 8];
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
let mut generic_check = |tc: &PusTcCreator| {
let accepted_token = hk_bench.add_tc(tc);
let (_active_req, req) = hk_bench
.convert(accepted_token, &[], TEST_APID, TEST_UNIQUE_ID_0)
.expect("conversion failed");
assert_eq!(req.unique_id, unique_id);
if let HkRequestVariant::DisablePeriodic = req.variant {
} else {
panic!("unexpected HK request")
}
};
let tc0 = PusTcCreator::new_simple(
sp_header,
MessageTypeId::new(3, MessageSubtypeId::TcDisableHkGeneration as u8),
&app_data,
CreatorConfig::default(),
);
generic_check(&tc0);
let tc1 = PusTcCreator::new_simple(
sp_header,
MessageTypeId::new(3, MessageSubtypeId::TcDisableDiagGeneration as u8),
&app_data,
CreatorConfig::default(),
);
generic_check(&tc1);
}
#[test]
fn hk_conversion_modify_interval() {
let mut hk_bench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let target_id = TEST_UNIQUE_ID_0;
let unique_id = 5_u32;
let mut app_data: [u8; 12] = [0; 12];
let collection_interval_factor = 5_u32;
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
app_data[8..12].copy_from_slice(&collection_interval_factor.to_be_bytes());
let mut generic_check = |tc: &PusTcCreator| {
let accepted_token = hk_bench.add_tc(tc);
let (_active_req, req) = hk_bench
.convert(accepted_token, &[], TEST_APID, TEST_UNIQUE_ID_0)
.expect("conversion failed");
assert_eq!(req.unique_id, unique_id);
if let HkRequestVariant::ModifyCollectionInterval(interval_factor) = req.variant {
assert_eq!(interval_factor, collection_interval_factor);
} else {
panic!("unexpected HK request")
}
};
let tc0 = PusTcCreator::new_simple(
sp_header,
MessageTypeId::new(3, MessageSubtypeId::TcModifyHkCollectionInterval as u8),
&app_data,
CreatorConfig::default(),
);
generic_check(&tc0);
let tc1 = PusTcCreator::new_simple(
sp_header,
MessageTypeId::new(3, MessageSubtypeId::TcModifyDiagCollectionInterval as u8),
&app_data,
CreatorConfig::default(),
);
generic_check(&tc1);
}
#[test]
fn hk_reply_handler() {
let mut reply_testbench =
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), HkReplyHandler::default());
let sender_id = 2_u32;
let apid_target_id = u21::new(3);
let unique_id = 5_u32;
let (req_id, active_req) = reply_testbench.add_tc(TEST_APID, apid_target_id, &[]);
let reply = GenericMessage::new(
MessageMetadata::new(req_id.into(), sender_id),
HkReply::new(unique_id, HkReplyVariant::Ack),
);
let result = reply_testbench.handle_reply(&reply, &active_req, &[]);
assert!(result.is_ok());
assert!(result.unwrap());
reply_testbench
.verif_reporter
.assert_full_completion_success(TEST_COMPONENT_ID_0.raw(), req_id, None);
}
#[test]
fn reply_handling_unrequested_reply() {
let mut testbench =
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_1.id(), HkReplyHandler::default());
let action_reply = HkReply::new(5_u32, HkReplyVariant::Ack);
let unrequested_reply =
GenericMessage::new(MessageMetadata::new(10_u32, 15_u32), action_reply);
// Right now this function does not do a lot. We simply check that it does not panic or do
// weird stuff.
let result = testbench.handle_unrequested_reply(&unrequested_reply);
assert!(result.is_ok());
}
#[test]
fn reply_handling_reply_timeout() {
let mut testbench =
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_1.id(), HkReplyHandler::default());
let (req_id, active_request) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_1, &[]);
let result = testbench.handle_request_timeout(&active_request, &[]);
assert!(result.is_ok());
testbench.verif_reporter.assert_completion_failure(
TEST_COMPONENT_ID_1.raw(),
req_id,
None,
tmtc_err::REQUEST_TIMEOUT.raw() as u64,
);
}
}
+779
View File
@@ -0,0 +1,779 @@
use crate::requests::GenericRequestRouter;
use crate::tmtc::sender::TmTcSender;
use log::warn;
use satrs::pool::PoolAddr;
use satrs::pus::verification::{
self, FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
VerificationReporterConfig, VerificationReportingProvider, VerificationToken,
};
use satrs::pus::{
ActiveRequest, ActiveRequestStore, CacheAndReadRawEcssTc, EcssTcAndToken, EcssTcCacher,
EcssTcReceiver, EcssTmSender, EcssTmtcError, GenericConversionError, GenericRoutingError,
HandlingStatus, PusPacketHandlingError, PusReplyHandler, PusRequestRouter, PusServiceHelper,
PusTcToRequestConverter, TcInMemory,
};
use satrs::queue::{GenericReceiveError, GenericSendError};
use satrs::request::{Apid, GenericMessage, MessageMetadata};
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::{PusPacket, PusServiceId};
use satrs::tmtc::{PacketAsVec, PacketInPool};
use satrs::ComponentId;
use example_std::config::{tmtc_err, CustomPusServiceId};
use example_std::ids::generic_pus::PUS_ROUTING;
use example_std::TimestampHelper;
use std::fmt::Debug;
use std::sync::mpsc;
pub mod action;
pub mod event;
pub mod hk;
pub mod mode;
pub mod scheduler;
pub mod stack;
pub mod test;
pub fn create_verification_reporter(owner_id: ComponentId, apid: Apid) -> VerificationReporter {
let verif_cfg = VerificationReporterConfig::new(apid, 1, 2, 8);
// Every software component which needs to generate verification telemetry, gets a cloned
// verification reporter.
VerificationReporter::new(owner_id, &verif_cfg)
}
/*
/// Simple router structure which forwards PUS telecommands to dedicated handlers.
pub struct PusTcMpscRouter {
pub test_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
pub event_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
pub sched_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
pub hk_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
#[allow(dead_code)]
pub action_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
pub mode_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
}
pub struct PusTcDistributor {
#[allow(dead_code)]
pub id: ComponentId,
pub tm_sender: TmTcSender,
pub verif_reporter: VerificationReporter,
pub pus_router: PusTcMpscRouter,
stamp_helper: TimestampHelper,
}
impl PusTcDistributor {
pub fn new(tm_sender: TmTcSender, pus_router: PusTcMpscRouter) -> Self {
Self {
id: PUS_ROUTING.raw(),
tm_sender,
verif_reporter: create_verification_reporter(PUS_ROUTING.id(), PUS_ROUTING.apid),
pus_router,
stamp_helper: TimestampHelper::default(),
}
}
pub fn handle_tc_packet_vec(
&mut self,
packet_as_vec: PacketAsVec,
) -> Result<HandlingStatus, GenericSendError> {
self.handle_tc_generic(packet_as_vec.sender_id, None, &packet_as_vec.packet)
}
pub fn handle_tc_packet_in_store(
&mut self,
packet_in_pool: PacketInPool,
pus_tc_copy: &[u8],
) -> Result<HandlingStatus, GenericSendError> {
self.handle_tc_generic(
packet_in_pool.sender_id,
Some(packet_in_pool.store_addr),
pus_tc_copy,
)
}
pub fn handle_tc_generic(
&mut self,
sender_id: ComponentId,
addr_opt: Option<PoolAddr>,
raw_tc: &[u8],
) -> Result<HandlingStatus, GenericSendError> {
let pus_tc_result = PusTcReader::new(raw_tc);
if pus_tc_result.is_err() {
log::warn!(
"error creating PUS TC from raw data received from {}: {}",
sender_id,
pus_tc_result.unwrap_err()
);
log::warn!("raw data: {raw_tc:x?}");
// TODO: Shouldn't this be an error?
return Ok(HandlingStatus::HandledOne);
}
let pus_tc = pus_tc_result.unwrap();
let init_token = self.verif_reporter.start_verification(&pus_tc);
self.stamp_helper.update_from_now();
let accepted_token = self
.verif_reporter
.acceptance_success(&self.tm_sender, init_token, self.stamp_helper.stamp())
.expect("Acceptance success failure");
let service = PusServiceId::try_from(pus_tc.service_type_id());
let tc_in_memory: TcInMemory = if let Some(store_addr) = addr_opt {
PacketInPool::new(sender_id, store_addr).into()
} else {
PacketAsVec::new(sender_id, Vec::from(raw_tc)).into()
};
match service {
Ok(standard_service) => match standard_service {
PusServiceId::Test => self.pus_router.test_tc_sender.send(EcssTcAndToken {
tc_in_memory,
token: Some(accepted_token.into()),
})?,
PusServiceId::Housekeeping => {
self.pus_router.hk_tc_sender.send(EcssTcAndToken {
tc_in_memory,
token: Some(accepted_token.into()),
})?
}
PusServiceId::Event => self.pus_router.event_tc_sender.send(EcssTcAndToken {
tc_in_memory,
token: Some(accepted_token.into()),
})?,
PusServiceId::Scheduling => {
self.pus_router.sched_tc_sender.send(EcssTcAndToken {
tc_in_memory,
token: Some(accepted_token.into()),
})?
}
_ => {
let result = self.verif_reporter.start_failure(
&self.tm_sender,
accepted_token,
FailParams::new(
self.stamp_helper.stamp(),
&tmtc_err::PUS_SERVICE_NOT_IMPLEMENTED,
&[standard_service as u8],
),
);
if result.is_err() {
warn!("Sending verification failure failed");
}
}
},
Err(e) => {
if let Ok(custom_service) = CustomPusServiceId::try_from(e.number) {
match custom_service {
CustomPusServiceId::Mode => self
.pus_router
.mode_tc_sender
.send(EcssTcAndToken {
tc_in_memory,
token: Some(accepted_token.into()),
})
.map_err(|_| GenericSendError::RxDisconnected)?,
CustomPusServiceId::Health => {}
}
} else {
self.verif_reporter
.start_failure(
&self.tm_sender,
accepted_token,
FailParams::new(
self.stamp_helper.stamp(),
&tmtc_err::INVALID_PUS_SUBSERVICE,
&[e.number],
),
)
.expect("Start failure verification failed")
}
}
}
Ok(HandlingStatus::HandledOne)
}
}
*/
pub trait TargetedPusService {
const SERVICE_ID: u8;
const SERVICE_STR: &'static str;
fn poll_and_handle_next_tc_default_handler(&mut self, time_stamp: &[u8]) -> HandlingStatus {
let result = self.poll_and_handle_next_tc(time_stamp);
if let Err(e) = result {
log::error!(
"PUS service {}({})packet handling error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
e
);
// To avoid permanent loops on error cases.
return HandlingStatus::Empty;
}
result.unwrap()
}
fn poll_and_handle_next_reply_default_handler(&mut self, time_stamp: &[u8]) -> HandlingStatus {
// This only fails if all senders disconnected. Treat it like an empty queue.
self.poll_and_handle_next_reply(time_stamp)
.unwrap_or_else(|e| {
warn!(
"PUS servce {}({}): Handling reply failed with error {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
e
);
HandlingStatus::Empty
})
}
fn poll_and_handle_next_tc(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, PusPacketHandlingError>;
fn poll_and_handle_next_reply(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, EcssTmtcError>;
fn check_for_request_timeouts(&mut self);
}
/// Generic trait for services which handle packets directly. Kept minimal right now because
/// of the difficulty to allow flexible user code for these services..
pub trait DirectPusService {
const SERVICE_ID: u8;
const SERVICE_STR: &'static str;
fn poll_and_handle_next_tc(&mut self, timestamp: &[u8]) -> HandlingStatus;
}
/// This is a generic handler class for all PUS services where a PUS telecommand is converted
/// to a targeted request.
///
/// The generic steps for this process are the following
///
/// 1. Poll for TC packets
/// 2. Convert the raw packets to a [PusTcReader].
/// 3. Convert the PUS TC to a typed request using the [PusTcToRequestConverter].
/// 4. Route the requests using the [GenericRequestRouter].
/// 5. Add the request to the active request map using the [ActiveRequestMapProvider] abstraction.
/// 6. Check for replies which complete the forwarded request. The handler takes care of
/// the verification process.
/// 7. Check for timeouts of active requests. Generally, the timeout on the service level should
/// be highest expected timeout for the given target.
///
/// The handler exposes the following API:
///
/// 1. [Self::poll_and_handle_next_tc] which tries to poll and handle one TC packet, covering
/// steps 1-5.
/// 2. [Self::poll_and_check_next_reply] which tries to poll and handle one reply, covering step 6.
/// 3. [Self::check_for_request_timeouts] which checks for request timeouts, covering step 7.
pub struct PusTargetedRequestService<
TcReceiver: EcssTcReceiver,
VerificationReporter: VerificationReportingProvider,
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
ActiveRequestInfo: ActiveRequest,
RequestType,
ReplyType,
> {
pub service_helper:
PusServiceHelper<TcReceiver, TmTcSender, EcssTcCacher, VerificationReporter>,
pub request_router: GenericRequestRouter,
pub request_converter: RequestConverter,
pub active_request_map: ActiveRequestMapInstance,
pub reply_handler: ReplyHandler,
pub reply_receiver: mpsc::Receiver<GenericMessage<ReplyType>>,
phantom: std::marker::PhantomData<(RequestType, ActiveRequestInfo, ReplyType)>,
}
impl<
TcReceiver: EcssTcReceiver,
VerificationReporter: VerificationReportingProvider,
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
ActiveRequestInfo: ActiveRequest,
RequestType,
ReplyType,
>
PusTargetedRequestService<
TcReceiver,
VerificationReporter,
RequestConverter,
ReplyHandler,
ActiveRequestMapInstance,
ActiveRequestInfo,
RequestType,
ReplyType,
>
where
GenericRequestRouter: PusRequestRouter<RequestType, Error = GenericRoutingError>,
{
pub fn new(
service_helper: PusServiceHelper<
TcReceiver,
TmTcSender,
EcssTcCacher,
VerificationReporter,
>,
request_converter: RequestConverter,
active_request_map: ActiveRequestMapInstance,
reply_hook: ReplyHandler,
request_router: GenericRequestRouter,
reply_receiver: mpsc::Receiver<GenericMessage<ReplyType>>,
) -> Self {
Self {
service_helper,
request_converter,
active_request_map,
reply_handler: reply_hook,
request_router,
reply_receiver,
phantom: std::marker::PhantomData,
}
}
pub fn poll_and_handle_next_tc(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, PusPacketHandlingError> {
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
if possible_packet.is_none() {
return Ok(HandlingStatus::Empty);
}
let ecss_tc_and_token = possible_packet.unwrap();
self.service_helper
.tc_in_mem_converter_mut()
.cache(&ecss_tc_and_token.tc_in_memory)?;
let tc = self.service_helper.tc_in_mem_converter().convert()?;
let (mut request_info, request) = match self.request_converter.convert(
ecss_tc_and_token.token,
&tc,
self.service_helper.tm_sender(),
&self.service_helper.common.verif_reporter,
time_stamp,
) {
Ok((info, req)) => (info, req),
Err(e) => {
self.handle_conversion_to_request_error(&e, ecss_tc_and_token.token, time_stamp);
return Err(e.into());
}
};
let accepted_token: VerificationToken<TcStateAccepted> = request_info
.token()
.try_into()
.expect("token not in expected accepted state");
let verif_request_id = verification::RequestId::new(&tc).raw();
match self.request_router.route(
MessageMetadata::new(verif_request_id, self.service_helper.id()),
request_info.target_id(),
request,
) {
Ok(()) => {
let started_token = self
.service_helper
.verif_reporter()
.start_success(
&self.service_helper.common.tm_sender,
accepted_token,
time_stamp,
)
.expect("Start success failure");
request_info.set_token(started_token.into());
self.active_request_map
.insert(&verif_request_id, request_info);
}
Err(e) => {
self.request_router.handle_error_generic(
&request_info,
&tc,
e.clone(),
self.service_helper.tm_sender(),
self.service_helper.verif_reporter(),
time_stamp,
);
return Err(e.into());
}
}
Ok(HandlingStatus::HandledOne)
}
fn handle_conversion_to_request_error(
&mut self,
error: &GenericConversionError,
token: VerificationToken<TcStateAccepted>,
time_stamp: &[u8],
) {
match error {
GenericConversionError::WrongService(service) => {
let service_slice: [u8; 1] = [*service];
self.service_helper
.verif_reporter()
.completion_failure(
self.service_helper.tm_sender(),
token,
FailParams::new(time_stamp, &tmtc_err::INVALID_PUS_SERVICE, &service_slice),
)
.expect("Sending completion failure failed");
}
GenericConversionError::InvalidSubservice(subservice) => {
let subservice_slice: [u8; 1] = [*subservice];
self.service_helper
.verif_reporter()
.completion_failure(
self.service_helper.tm_sender(),
token,
FailParams::new(
time_stamp,
&tmtc_err::INVALID_PUS_SUBSERVICE,
&subservice_slice,
),
)
.expect("Sending completion failure failed");
}
GenericConversionError::NotEnoughAppData { expected, found } => {
let mut context_info = (*found as u32).to_be_bytes().to_vec();
context_info.extend_from_slice(&(*expected as u32).to_be_bytes());
self.service_helper
.verif_reporter()
.completion_failure(
self.service_helper.tm_sender(),
token,
FailParams::new(time_stamp, &tmtc_err::NOT_ENOUGH_APP_DATA, &context_info),
)
.expect("Sending completion failure failed");
}
// Do nothing.. this is service-level and can not be handled generically here.
GenericConversionError::InvalidAppData(_) => (),
}
}
pub fn poll_and_handle_next_reply(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, EcssTmtcError> {
match self.reply_receiver.try_recv() {
Ok(reply) => {
self.handle_reply(&reply, time_stamp)?;
Ok(HandlingStatus::HandledOne)
}
Err(e) => match e {
mpsc::TryRecvError::Empty => Ok(HandlingStatus::Empty),
mpsc::TryRecvError::Disconnected => Err(EcssTmtcError::Receive(
GenericReceiveError::TxDisconnected(None),
)),
},
}
}
pub fn handle_reply(
&mut self,
reply: &GenericMessage<ReplyType>,
time_stamp: &[u8],
) -> Result<(), EcssTmtcError> {
let active_req_opt = self.active_request_map.get(reply.request_id());
if active_req_opt.is_none() {
self.reply_handler
.handle_unrequested_reply(reply, &self.service_helper.common.tm_sender)?;
return Ok(());
}
let active_request = active_req_opt.unwrap();
let result = self.reply_handler.handle_reply(
reply,
active_request,
&self.service_helper.common.tm_sender,
&self.service_helper.common.verif_reporter,
time_stamp,
);
if result.is_err() || (result.is_ok() && *result.as_ref().unwrap()) {
self.active_request_map.remove(reply.request_id());
}
result.map(|_| ())
}
pub fn check_for_request_timeouts(&mut self) {
let mut requests_to_delete = Vec::new();
self.active_request_map
.for_each(|request_id, request_info| {
if request_info.has_timed_out() {
requests_to_delete.push(*request_id);
}
});
if !requests_to_delete.is_empty() {
for request_id in requests_to_delete {
self.active_request_map.remove(request_id);
}
}
}
}
/// Generic timeout handling: Handle the verification failure with a dedicated return code
/// and also log the error.
pub fn generic_pus_request_timeout_handler(
sender: &(impl EcssTmSender + ?Sized),
active_request: &(impl ActiveRequest + Debug),
verification_handler: &impl VerificationReportingProvider,
time_stamp: &[u8],
service_str: &'static str,
) -> Result<(), EcssTmtcError> {
log::warn!("timeout for active request {active_request:?} on {service_str} service");
let started_token: VerificationToken<TcStateStarted> = active_request
.token()
.try_into()
.expect("token not in expected started state");
verification_handler.completion_failure(
sender,
started_token,
FailParams::new(time_stamp, &tmtc_err::REQUEST_TIMEOUT, &[]),
)?;
Ok(())
}
#[cfg(test)]
pub(crate) mod tests {
use std::time::Duration;
use arbitrary_int::{u11, u21};
use satrs::pus::test_util::TEST_COMPONENT_ID_0;
use satrs::pus::{MpscTmAsVecSender, PusTmVariant};
use satrs::request::RequestId;
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use satrs::{
pus::{
verification::test_util::TestVerificationReporter, ActivePusRequestStd,
ActiveRequestStore, MpscTcReceiver,
},
request::UniqueApidTargetId,
spacepackets::{
ecss::{
tc::{PusTcCreator, PusTcSecondaryHeader},
WritablePusPacket,
},
SpHeader,
},
};
use crate::requests::CompositeRequest;
use super::*;
// Testbench dedicated to the testing of [PusReplyHandler]s
pub struct ReplyHandlerTestbench<
ReplyHandler: PusReplyHandler<ActiveRequestInfo, Reply, Error = EcssTmtcError>,
ActiveRequestInfo: ActiveRequest,
Reply,
> {
pub id: ComponentId,
pub verif_reporter: TestVerificationReporter,
pub reply_handler: ReplyHandler,
pub tm_receiver: mpsc::Receiver<PacketAsVec>,
pub default_timeout: Duration,
tm_sender: MpscTmAsVecSender,
phantom: std::marker::PhantomData<(ActiveRequestInfo, Reply)>,
}
impl<
ReplyHandler: PusReplyHandler<ActiveRequestInfo, Reply, Error = EcssTmtcError>,
ActiveRequestInfo: ActiveRequest,
Reply,
> ReplyHandlerTestbench<ReplyHandler, ActiveRequestInfo, Reply>
{
pub fn new(owner_id: ComponentId, reply_handler: ReplyHandler) -> Self {
let test_verif_reporter = TestVerificationReporter::new(owner_id);
let (tm_sender, tm_receiver) = mpsc::channel();
Self {
id: TEST_COMPONENT_ID_0.raw(),
verif_reporter: test_verif_reporter,
reply_handler,
default_timeout: Duration::from_secs(30),
tm_sender,
tm_receiver,
phantom: std::marker::PhantomData,
}
}
pub fn add_tc(
&mut self,
apid: u11,
apid_target: u21,
time_stamp: &[u8],
) -> (verification::RequestId, ActivePusRequestStd) {
let sp_header = SpHeader::new_from_apid(apid);
let sec_header_dummy = PusTcSecondaryHeader::new_simple(MessageTypeId::new(0, 0));
let init = self.verif_reporter.start_verification(&PusTcCreator::new(
sp_header,
sec_header_dummy,
&[],
CreatorConfig::default(),
));
let accepted = self
.verif_reporter
.acceptance_success(&self.tm_sender, init, time_stamp)
.expect("acceptance failed");
let started = self
.verif_reporter
.start_success(&self.tm_sender, accepted, time_stamp)
.expect("start failed");
(
started.request_id(),
ActivePusRequestStd::new(
UniqueApidTargetId::new(apid, apid_target).raw(),
started,
self.default_timeout,
),
)
}
pub fn handle_reply(
&mut self,
reply: &GenericMessage<Reply>,
active_request: &ActiveRequestInfo,
time_stamp: &[u8],
) -> Result<bool, ReplyHandler::Error> {
self.reply_handler.handle_reply(
reply,
active_request,
&self.tm_sender,
&self.verif_reporter,
time_stamp,
)
}
pub fn handle_unrequested_reply(
&mut self,
reply: &GenericMessage<Reply>,
) -> Result<(), ReplyHandler::Error> {
self.reply_handler
.handle_unrequested_reply(reply, &self.tm_sender)
}
pub fn handle_request_timeout(
&mut self,
active_request_info: &ActiveRequestInfo,
time_stamp: &[u8],
) -> Result<(), ReplyHandler::Error> {
self.reply_handler.handle_request_timeout(
active_request_info,
&self.tm_sender,
&self.verif_reporter,
time_stamp,
)
}
}
#[derive(Default)]
pub struct DummySender {}
/// Dummy sender component which does nothing on the [Self::send_tm] call.
///
/// Useful for unit tests.
impl EcssTmSender for DummySender {
fn send_tm(&self, _source_id: ComponentId, _tm: PusTmVariant) -> Result<(), EcssTmtcError> {
Ok(())
}
}
// Testbench dedicated to the testing of [PusTcToRequestConverter]s
pub struct PusConverterTestbench<
Converter: PusTcToRequestConverter<ActiveRequestInfo, Request, Error = GenericConversionError>,
ActiveRequestInfo: ActiveRequest,
Request,
> {
pub id: ComponentId,
pub verif_reporter: TestVerificationReporter,
pub converter: Converter,
dummy_sender: DummySender,
current_request_id: Option<verification::RequestId>,
current_packet: Option<Vec<u8>>,
phantom: std::marker::PhantomData<(ActiveRequestInfo, Request)>,
}
impl<
Converter: PusTcToRequestConverter<ActiveRequestInfo, Request, Error = GenericConversionError>,
ActiveRequestInfo: ActiveRequest,
Request,
> PusConverterTestbench<Converter, ActiveRequestInfo, Request>
{
pub fn new(owner_id: ComponentId, converter: Converter) -> Self {
let test_verif_reporter = TestVerificationReporter::new(owner_id);
Self {
id: owner_id,
verif_reporter: test_verif_reporter,
converter,
dummy_sender: DummySender::default(),
current_request_id: None,
current_packet: None,
phantom: std::marker::PhantomData,
}
}
pub fn add_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted> {
let token = self.verif_reporter.start_verification(tc);
self.current_request_id = Some(verification::RequestId::new(tc));
self.current_packet = Some(tc.to_vec().unwrap());
self.verif_reporter
.acceptance_success(&self.dummy_sender, token, &[])
.expect("acceptance failed")
}
pub fn request_id(&self) -> Option<verification::RequestId> {
self.current_request_id
}
pub fn convert(
&mut self,
token: VerificationToken<TcStateAccepted>,
time_stamp: &[u8],
expected_apid: u11,
expected_apid_target: u21,
) -> Result<(ActiveRequestInfo, Request), Converter::Error> {
if self.current_packet.is_none() {
return Err(GenericConversionError::InvalidAppData(
"call add_tc first".to_string(),
));
}
let current_packet = self.current_packet.take().unwrap();
let tc_reader = PusTcReader::new(&current_packet).unwrap();
let (active_info, request) = self.converter.convert(
token,
&tc_reader,
&self.dummy_sender,
&self.verif_reporter,
time_stamp,
)?;
assert_eq!(
active_info.token().request_id(),
self.request_id().expect("no request id is set")
);
assert_eq!(
active_info.target_id(),
UniqueApidTargetId::new(expected_apid, expected_apid_target).raw()
);
Ok((active_info, request))
}
}
pub struct TargetedPusRequestTestbench<
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
ActiveRequestInfo: ActiveRequest,
RequestType,
ReplyType,
> {
pub service: PusTargetedRequestService<
MpscTcReceiver,
TestVerificationReporter,
RequestConverter,
ReplyHandler,
ActiveRequestMapInstance,
ActiveRequestInfo,
RequestType,
ReplyType,
>,
pub request_id: Option<RequestId>,
pub tm_funnel_rx: mpsc::Receiver<PacketAsVec>,
pub pus_packet_tx: mpsc::Sender<EcssTcAndToken>,
pub reply_tx: mpsc::Sender<GenericMessage<ReplyType>>,
pub request_rx: mpsc::Receiver<GenericMessage<CompositeRequest>>,
}
}
+441
View File
@@ -0,0 +1,441 @@
use arbitrary_int::traits::Integer as _;
use arbitrary_int::u14;
use derive_new::new;
use satrs::mode_tree::{ModeNode, ModeParent};
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use example_std::ids;
use std::sync::mpsc;
use std::time::Duration;
use crate::requests::GenericRequestRouter;
use crate::tmtc::sender::TmTcSender;
use satrs::pus::verification::VerificationReporter;
use satrs::pus::{
DefaultActiveRequestMap, EcssTcAndToken, EcssTcCacher, MpscTcReceiver, PusPacketHandlingError,
PusServiceHelper,
};
use satrs::request::GenericMessage;
use satrs::{
mode::{ModeAndSubmode, ModeReply, ModeRequest},
pus::{
mode::Subservice,
verification::{
self, FailParams, TcStateAccepted, TcStateStarted, VerificationReportingProvider,
VerificationToken,
},
ActivePusRequestStd, ActiveRequest, EcssTmSender, EcssTmtcError, GenericConversionError,
PusReplyHandler, PusTcToRequestConverter, PusTmVariant,
},
request::UniqueApidTargetId,
spacepackets::{
ecss::{
tc::PusTcReader,
tm::{PusTmCreator, PusTmSecondaryHeader},
PusPacket,
},
SpHeader,
},
ComponentId,
};
use example_std::config::{mode_err, tmtc_err, CustomPusServiceId};
use super::{
create_verification_reporter, generic_pus_request_timeout_handler, HandlingStatus,
PusTargetedRequestService, TargetedPusService,
};
#[derive(new)]
pub struct ModeReplyHandler {
owner_id: ComponentId,
}
impl PusReplyHandler<ActivePusRequestStd, ModeReply> for ModeReplyHandler {
type Error = EcssTmtcError;
fn handle_unrequested_reply(
&mut self,
reply: &GenericMessage<ModeReply>,
_tm_sender: &impl EcssTmSender,
) -> Result<(), Self::Error> {
log::warn!("received unexpected reply for mode service 5: {reply:?}");
Ok(())
}
fn handle_reply(
&mut self,
reply: &GenericMessage<ModeReply>,
active_request: &ActivePusRequestStd,
tm_sender: &impl EcssTmSender,
verification_handler: &impl VerificationReportingProvider,
time_stamp: &[u8],
) -> Result<bool, Self::Error> {
let started_token: VerificationToken<TcStateStarted> = active_request
.token()
.try_into()
.expect("invalid token state");
match reply.message {
ModeReply::ModeReply(mode_reply) => {
let mut source_data: [u8; 12] = [0; 12];
mode_reply
.write_to_be_bytes(&mut source_data)
.expect("writing mode reply failed");
let req_id = verification::RequestId::from(reply.request_id());
let sp_header = SpHeader::new_for_unseg_tm(req_id.packet_id().apid(), u14::ZERO, 0);
let sec_header = PusTmSecondaryHeader::new(
MessageTypeId::new(200, Subservice::TmModeReply as u8),
0,
0,
time_stamp,
);
let pus_tm = PusTmCreator::new(
sp_header,
sec_header,
&source_data,
CreatorConfig::default(),
);
tm_sender.send_tm(self.owner_id, PusTmVariant::Direct(pus_tm))?;
verification_handler.completion_success(tm_sender, started_token, time_stamp)?;
}
ModeReply::CantReachMode(error_code) => {
verification_handler.completion_failure(
tm_sender,
started_token,
FailParams::new(time_stamp, &error_code, &[]),
)?;
}
ModeReply::WrongMode { expected, reached } => {
let mut error_info: [u8; 24] = [0; 24];
let mut written_len = expected
.write_to_be_bytes(&mut error_info[0..ModeAndSubmode::RAW_LEN])
.expect("writing expected mode failed");
written_len += reached
.write_to_be_bytes(&mut error_info[ModeAndSubmode::RAW_LEN..])
.expect("writing reached mode failed");
verification_handler.completion_failure(
tm_sender,
started_token,
FailParams::new(
time_stamp,
&mode_err::WRONG_MODE,
&error_info[..written_len],
),
)?;
}
ModeReply::ModeInfo(_mode_and_submode) => (),
};
Ok(true)
}
fn handle_request_timeout(
&mut self,
active_request: &ActivePusRequestStd,
tm_sender: &impl EcssTmSender,
verification_handler: &impl VerificationReportingProvider,
time_stamp: &[u8],
) -> Result<(), Self::Error> {
generic_pus_request_timeout_handler(
tm_sender,
active_request,
verification_handler,
time_stamp,
"HK",
)?;
Ok(())
}
}
#[derive(Default)]
pub struct ModeRequestConverter {}
impl PusTcToRequestConverter<ActivePusRequestStd, ModeRequest> for ModeRequestConverter {
type Error = GenericConversionError;
fn convert(
&mut self,
token: VerificationToken<TcStateAccepted>,
tc: &PusTcReader,
tm_sender: &(impl EcssTmSender + ?Sized),
verif_reporter: &impl VerificationReportingProvider,
time_stamp: &[u8],
) -> Result<(ActivePusRequestStd, ModeRequest), Self::Error> {
let subservice = tc.message_subtype_id();
let user_data = tc.user_data();
let not_enough_app_data = |expected: usize| {
verif_reporter
.start_failure(
tm_sender,
token,
FailParams::new_no_fail_data(time_stamp, &tmtc_err::NOT_ENOUGH_APP_DATA),
)
.expect("Sending start failure failed");
Err(GenericConversionError::NotEnoughAppData {
expected,
found: user_data.len(),
})
};
if user_data.len() < core::mem::size_of::<u32>() {
return not_enough_app_data(4);
}
let target_id_and_apid = UniqueApidTargetId::from_pus_tc(tc).unwrap();
let active_request =
ActivePusRequestStd::new(target_id_and_apid.into(), token, Duration::from_secs(30));
let subservice_typed = Subservice::try_from(subservice);
let invalid_subservice = || {
// Invalid subservice
verif_reporter
.start_failure(
tm_sender,
token,
FailParams::new_no_fail_data(time_stamp, &tmtc_err::INVALID_PUS_SUBSERVICE),
)
.expect("Sending start failure failed");
Err(GenericConversionError::InvalidSubservice(subservice))
};
if subservice_typed.is_err() {
return invalid_subservice();
}
let subservice_typed = subservice_typed.unwrap();
match subservice_typed {
Subservice::TcSetMode => {
if user_data.len() < core::mem::size_of::<u32>() + ModeAndSubmode::RAW_LEN {
return not_enough_app_data(4 + ModeAndSubmode::RAW_LEN);
}
let mode_and_submode = ModeAndSubmode::from_be_bytes(&tc.user_data()[4..])
.expect("mode and submode extraction failed");
Ok((
active_request,
ModeRequest::SetMode {
mode_and_submode,
forced: false,
},
))
}
Subservice::TcReadMode => Ok((active_request, ModeRequest::ReadMode)),
Subservice::TcAnnounceMode => Ok((active_request, ModeRequest::AnnounceMode)),
Subservice::TcAnnounceModeRecursive => {
Ok((active_request, ModeRequest::AnnounceModeRecursive))
}
_ => invalid_subservice(),
}
}
}
pub fn create_mode_service(
tm_sender: TmTcSender,
tc_in_mem_converter: EcssTcCacher,
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
mode_router: GenericRequestRouter,
reply_receiver: mpsc::Receiver<GenericMessage<ModeReply>>,
) -> ModeServiceWrapper {
let mode_request_handler = PusTargetedRequestService::new(
PusServiceHelper::new(
ids::generic_pus::PUS_MODE.id(),
pus_action_rx,
tm_sender,
create_verification_reporter(
ids::generic_pus::PUS_MODE.id(),
ids::generic_pus::PUS_MODE.apid,
),
tc_in_mem_converter,
),
ModeRequestConverter::default(),
DefaultActiveRequestMap::default(),
ModeReplyHandler::new(ids::generic_pus::PUS_MODE.id()),
mode_router,
reply_receiver,
);
ModeServiceWrapper {
service: mode_request_handler,
}
}
pub struct ModeServiceWrapper {
pub(crate) service: PusTargetedRequestService<
MpscTcReceiver,
VerificationReporter,
ModeRequestConverter,
ModeReplyHandler,
DefaultActiveRequestMap<ActivePusRequestStd>,
ActivePusRequestStd,
ModeRequest,
ModeReply,
>,
}
impl ModeNode for ModeServiceWrapper {
fn id(&self) -> ComponentId {
self.service.service_helper.id()
}
}
impl ModeParent for ModeServiceWrapper {
type Sender = mpsc::SyncSender<GenericMessage<ModeRequest>>;
fn add_mode_child(&mut self, id: ComponentId, request_sender: Self::Sender) {
self.service
.request_router
.mode_router_map
.insert(id, request_sender);
}
}
impl TargetedPusService for ModeServiceWrapper {
const SERVICE_ID: u8 = CustomPusServiceId::Mode as u8;
const SERVICE_STR: &'static str = "mode";
delegate::delegate! {
to self.service {
fn poll_and_handle_next_tc(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, PusPacketHandlingError>;
fn poll_and_handle_next_reply(
&mut self,
time_stamp: &[u8],
) -> Result<HandlingStatus, EcssTmtcError>;
fn check_for_request_timeouts(&mut self);
}
}
}
#[cfg(test)]
mod tests {
use arbitrary_int::traits::Integer;
use arbitrary_int::u14;
use satrs::pus::test_util::{TEST_APID, TEST_COMPONENT_ID_0, TEST_UNIQUE_ID_0};
use satrs::request::MessageMetadata;
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use satrs::{
mode::{ModeAndSubmode, ModeReply, ModeRequest},
pus::mode::Subservice,
request::GenericMessage,
spacepackets::{
ecss::tc::{PusTcCreator, PusTcSecondaryHeader},
SpHeader,
},
};
use example_std::config::tmtc_err;
use crate::pus::{
mode::ModeReplyHandler,
tests::{PusConverterTestbench, ReplyHandlerTestbench},
};
use super::ModeRequestConverter;
#[test]
fn mode_converter_read_mode_request() {
let mut testbench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let sec_header =
PusTcSecondaryHeader::new_simple(MessageTypeId::new(200, Subservice::TcReadMode as u8));
let mut app_data: [u8; 4] = [0; 4];
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
let token = testbench.add_tc(&tc);
let (_active_req, req) = testbench
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
.expect("conversion has failed");
assert_eq!(req, ModeRequest::ReadMode);
}
#[test]
fn mode_converter_set_mode_request() {
let mut testbench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let sec_header =
PusTcSecondaryHeader::new_simple(MessageTypeId::new(200, Subservice::TcSetMode as u8));
let mut app_data: [u8; 4 + ModeAndSubmode::RAW_LEN] = [0; 4 + ModeAndSubmode::RAW_LEN];
let mode_and_submode = ModeAndSubmode::new(2, 1);
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
mode_and_submode
.write_to_be_bytes(&mut app_data[4..])
.unwrap();
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
let token = testbench.add_tc(&tc);
let (_active_req, req) = testbench
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
.expect("conversion has failed");
assert_eq!(
req,
ModeRequest::SetMode {
mode_and_submode,
forced: false
}
);
}
#[test]
fn mode_converter_announce_mode() {
let mut testbench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(
200,
Subservice::TcAnnounceMode as u8,
));
let mut app_data: [u8; 4] = [0; 4];
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
let token = testbench.add_tc(&tc);
let (_active_req, req) = testbench
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
.expect("conversion has failed");
assert_eq!(req, ModeRequest::AnnounceMode);
}
#[test]
fn mode_converter_announce_mode_recursively() {
let mut testbench =
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(
200,
Subservice::TcAnnounceModeRecursive as u8,
));
let mut app_data: [u8; 4] = [0; 4];
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
let token = testbench.add_tc(&tc);
let (_active_req, req) = testbench
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
.expect("conversion has failed");
assert_eq!(req, ModeRequest::AnnounceModeRecursive);
}
#[test]
fn reply_handling_unrequested_reply() {
let mut testbench = ReplyHandlerTestbench::new(
TEST_COMPONENT_ID_0.id(),
ModeReplyHandler::new(TEST_COMPONENT_ID_0.id()),
);
let mode_reply = ModeReply::ModeReply(ModeAndSubmode::new(5, 1));
let unrequested_reply =
GenericMessage::new(MessageMetadata::new(10_u32, 15_u32), mode_reply);
// Right now this function does not do a lot. We simply check that it does not panic or do
// weird stuff.
let result = testbench.handle_unrequested_reply(&unrequested_reply);
assert!(result.is_ok());
}
#[test]
fn reply_handling_reply_timeout() {
let mut testbench = ReplyHandlerTestbench::new(
TEST_COMPONENT_ID_0.id(),
ModeReplyHandler::new(TEST_COMPONENT_ID_0.id()),
);
let (req_id, active_request) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
let result = testbench.handle_request_timeout(&active_request, &[]);
assert!(result.is_ok());
testbench.verif_reporter.assert_completion_failure(
TEST_COMPONENT_ID_0.raw(),
req_id,
None,
tmtc_err::REQUEST_TIMEOUT.raw() as u64,
);
}
}
@@ -0,0 +1,200 @@
use std::sync::mpsc;
use std::time::Duration;
use crate::pus::create_verification_reporter;
use crate::tmtc::sender::TmTcSender;
use log::info;
use satrs::pool::{PoolProvider, StaticMemoryPool};
use satrs::pus::scheduler::{PusSchedulerAlloc, TcInfo};
use satrs::pus::scheduler_srv::PusSchedServiceHandler;
use satrs::pus::verification::VerificationReporter;
use satrs::pus::{
DirectPusPacketHandlerResult, EcssTcAndToken, EcssTcCacher, MpscTcReceiver,
PartialPusHandlingError, PusServiceHelper,
};
use satrs::spacepackets::ecss::PusServiceId;
use satrs::tmtc::{PacketAsVec, PacketInPool, PacketSenderWithSharedPool};
use satrs::ComponentId;
use example_std::ids::sched::PUS_SCHED;
use super::{DirectPusService, HandlingStatus};
pub trait TcReleaseProvider {
fn release(&mut self, sender_id: ComponentId, enabled: bool, info: &TcInfo, tc: &[u8]) -> bool;
}
impl TcReleaseProvider for PacketSenderWithSharedPool {
fn release(
&mut self,
sender_id: ComponentId,
enabled: bool,
_info: &TcInfo,
tc: &[u8],
) -> bool {
if enabled {
let shared_pool = self.shared_pool.get_mut();
// Transfer TC from scheduler TC pool to shared TC pool.
let released_tc_addr = shared_pool
.0
.write()
.expect("locking pool failed")
.add(tc)
.expect("adding TC to shared pool failed");
self.sender
.send(PacketInPool::new(sender_id, released_tc_addr))
.expect("sending TC to TC source failed");
}
true
}
}
impl TcReleaseProvider for mpsc::SyncSender<PacketAsVec> {
fn release(
&mut self,
sender_id: ComponentId,
enabled: bool,
_info: &TcInfo,
tc: &[u8],
) -> bool {
if enabled {
// Send released TC to centralized TC source.
self.send(PacketAsVec::new(sender_id, tc.to_vec()))
.expect("sending TC to TC source failed");
}
true
}
}
#[allow(dead_code)]
pub enum TcReleaser {
Static(PacketSenderWithSharedPool),
Heap(mpsc::SyncSender<PacketAsVec>),
}
impl TcReleaseProvider for TcReleaser {
fn release(&mut self, sender_id: ComponentId, enabled: bool, info: &TcInfo, tc: &[u8]) -> bool {
match self {
TcReleaser::Static(sender) => sender.release(sender_id, enabled, info, tc),
TcReleaser::Heap(sender) => sender.release(sender_id, enabled, info, tc),
}
}
}
pub struct SchedulingServiceWrapper {
pub pus_11_handler: PusSchedServiceHandler<
MpscTcReceiver,
TmTcSender,
EcssTcCacher,
VerificationReporter,
PusSchedulerAlloc,
>,
pub sched_tc_pool: StaticMemoryPool,
pub releaser_buf: [u8; 4096],
pub tc_releaser: TcReleaser,
}
impl DirectPusService for SchedulingServiceWrapper {
const SERVICE_ID: u8 = PusServiceId::Verification as u8;
const SERVICE_STR: &'static str = "verification";
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
let error_handler = |partial_error: &PartialPusHandlingError| {
log::warn!(
"PUS {}({}) partial error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
partial_error
);
};
let result = self.pus_11_handler.poll_and_handle_next_tc(
error_handler,
time_stamp,
&mut self.sched_tc_pool,
);
if let Err(e) = result {
log::warn!(
"PUS {}({}) error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
e
);
// To avoid permanent loops on continuous errors.
return HandlingStatus::Empty;
}
match result.unwrap() {
DirectPusPacketHandlerResult::Handled(handling_status) => return handling_status,
DirectPusPacketHandlerResult::CustomSubservice(subservice, _) => {
log::warn!(
"PUS {}({}) subservice {} not implemented",
Self::SERVICE_ID,
Self::SERVICE_STR,
subservice
);
}
DirectPusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
log::warn!(
"PUS {}({}) subservice {} not implemented",
Self::SERVICE_ID,
Self::SERVICE_STR,
subservice
);
}
}
HandlingStatus::HandledOne
}
}
impl SchedulingServiceWrapper {
pub fn release_tcs(&mut self) {
let id = self.pus_11_handler.service_helper.id();
let releaser = |enabled: bool, info: &TcInfo, tc: &[u8]| -> bool {
self.tc_releaser.release(id, enabled, info, tc)
};
self.pus_11_handler
.scheduler_mut()
.update_time_from_now()
.unwrap();
let released_tcs = self
.pus_11_handler
.scheduler_mut()
.release_telecommands_with_buffer(
releaser,
&mut self.sched_tc_pool,
&mut self.releaser_buf,
)
.expect("releasing TCs failed");
if released_tcs > 0 {
info!("{released_tcs} TC(s) released from scheduler");
}
}
}
pub fn create_scheduler_service(
tm_sender: TmTcSender,
tc_in_mem_converter: EcssTcCacher,
tc_releaser: TcReleaser,
pus_sched_rx: mpsc::Receiver<EcssTcAndToken>,
sched_tc_pool: StaticMemoryPool,
) -> SchedulingServiceWrapper {
let scheduler = PusSchedulerAlloc::new_with_current_init_time(Duration::from_secs(5))
.expect("Creating PUS Scheduler failed");
let pus_11_handler = PusSchedServiceHandler::new(
PusServiceHelper::new(
PUS_SCHED.id(),
pus_sched_rx,
tm_sender,
create_verification_reporter(PUS_SCHED.id(), PUS_SCHED.apid),
tc_in_mem_converter,
),
scheduler,
);
SchedulingServiceWrapper {
pus_11_handler,
sched_tc_pool,
releaser_buf: [0; 4096],
tc_releaser,
}
}
@@ -0,0 +1,88 @@
use crate::pus::mode::ModeServiceWrapper;
use derive_new::new;
use satrs::spacepackets::time::{cds, TimeWriter};
use super::{
action::ActionServiceWrapper, event::EventServiceWrapper, hk::HkServiceWrapper,
scheduler::SchedulingServiceWrapper, test::TestCustomServiceWrapper, DirectPusService,
HandlingStatus, TargetedPusService,
};
#[derive(new)]
pub struct PusStack {
pub test_srv: TestCustomServiceWrapper,
pub hk_srv_wrapper: HkServiceWrapper,
pub event_srv: EventServiceWrapper,
pub action_srv_wrapper: ActionServiceWrapper,
pub schedule_srv: SchedulingServiceWrapper,
pub mode_srv: ModeServiceWrapper,
}
impl PusStack {
pub fn periodic_operation(&mut self) {
// Release all telecommands which reached their release time before calling the service
// handlers.
self.schedule_srv.release_tcs();
let timestamp = cds::CdsTime::now_with_u16_days()
.expect("time stamp generation error")
.to_vec()
.unwrap();
let mut loop_count = 0_u32;
// Hot loop which will run continuously until all request and reply handling is done.
loop {
let mut nothing_to_do = true;
Self::direct_service_checker(&mut self.test_srv, &timestamp, &mut nothing_to_do);
Self::direct_service_checker(&mut self.schedule_srv, &timestamp, &mut nothing_to_do);
Self::direct_service_checker(&mut self.event_srv, &timestamp, &mut nothing_to_do);
Self::targeted_service_checker(
&mut self.action_srv_wrapper,
&timestamp,
&mut nothing_to_do,
);
Self::targeted_service_checker(
&mut self.hk_srv_wrapper,
&timestamp,
&mut nothing_to_do,
);
Self::targeted_service_checker(&mut self.mode_srv, &timestamp, &mut nothing_to_do);
if nothing_to_do {
// Timeout checking is only done once.
self.action_srv_wrapper.check_for_request_timeouts();
self.hk_srv_wrapper.check_for_request_timeouts();
self.mode_srv.check_for_request_timeouts();
break;
}
// Safety mechanism to avoid infinite loops.
loop_count += 1;
if loop_count >= 500 {
log::warn!("reached PUS stack loop count 500, breaking");
break;
}
}
}
pub fn direct_service_checker<S: DirectPusService>(
service: &mut S,
timestamp: &[u8],
nothing_to_do: &mut bool,
) {
let handling_status = service.poll_and_handle_next_tc(timestamp);
if handling_status == HandlingStatus::HandledOne {
*nothing_to_do = false;
}
}
pub fn targeted_service_checker<S: TargetedPusService>(
service: &mut S,
timestamp: &[u8],
nothing_to_do: &mut bool,
) {
let request_handling = service.poll_and_handle_next_tc_default_handler(timestamp);
let reply_handling = service.poll_and_handle_next_reply_default_handler(timestamp);
if request_handling == HandlingStatus::HandledOne
|| reply_handling == HandlingStatus::HandledOne
{
*nothing_to_do = false;
}
}
}
+144
View File
@@ -0,0 +1,144 @@
use crate::pus::create_verification_reporter;
use crate::tmtc::sender::TmTcSender;
use log::info;
use satrs::event_man_legacy::{EventMessage, EventMessageU32};
use satrs::pus::test::PusService17TestHandler;
use satrs::pus::verification::{FailParams, VerificationReporter, VerificationReportingProvider};
use satrs::pus::PartialPusHandlingError;
use satrs::pus::{
CacheAndReadRawEcssTc, DirectPusPacketHandlerResult, EcssTcAndToken, EcssTcCacher,
MpscTcReceiver, PusServiceHelper,
};
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::{PusPacket, PusServiceId};
use example_std::config::{tmtc_err, TEST_EVENT};
use example_std::ids::generic_pus::PUS_TEST;
use std::sync::mpsc;
use super::{DirectPusService, HandlingStatus};
pub fn create_test_service(
tm_sender: TmTcSender,
tc_in_mem_converter: EcssTcCacher,
event_sender: mpsc::SyncSender<EventMessageU32>,
pus_test_rx: mpsc::Receiver<EcssTcAndToken>,
) -> TestCustomServiceWrapper {
let pus17_handler = PusService17TestHandler::new(PusServiceHelper::new(
PUS_TEST.id(),
pus_test_rx,
tm_sender,
create_verification_reporter(PUS_TEST.id(), PUS_TEST.apid),
tc_in_mem_converter,
));
TestCustomServiceWrapper {
handler: pus17_handler,
event_tx: event_sender,
}
}
pub struct TestCustomServiceWrapper {
pub handler:
PusService17TestHandler<MpscTcReceiver, TmTcSender, EcssTcCacher, VerificationReporter>,
pub event_tx: mpsc::SyncSender<EventMessageU32>,
}
impl DirectPusService for TestCustomServiceWrapper {
const SERVICE_ID: u8 = PusServiceId::Test as u8;
const SERVICE_STR: &'static str = "test";
fn poll_and_handle_next_tc(&mut self, timestamp: &[u8]) -> HandlingStatus {
let error_handler = |partial_error: &PartialPusHandlingError| {
log::warn!(
"PUS {}({}) partial error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
partial_error
);
};
let res = self
.handler
.poll_and_handle_next_tc(error_handler, timestamp);
if let Err(e) = res {
log::warn!(
"PUS {}({}) error: {:?}",
Self::SERVICE_ID,
Self::SERVICE_STR,
e
);
// To avoid permanent loops on continuous errors.
return HandlingStatus::Empty;
}
match res.unwrap() {
DirectPusPacketHandlerResult::Handled(handling_status) => {
if handling_status == HandlingStatus::HandledOne {
info!("Received PUS ping command TC[17,1]");
info!("Sent ping reply PUS TM[17,2]");
}
return handling_status;
}
DirectPusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
log::warn!(
"PUS {}({}) subservice {} not implemented",
Self::SERVICE_ID,
Self::SERVICE_STR,
subservice
);
}
DirectPusPacketHandlerResult::CustomSubservice(subservice, token) => {
let tc = PusTcReader::new(
self.handler
.service_helper
.tc_in_mem_converter
.tc_slice_raw(),
)
.unwrap();
if subservice == 128 {
info!("generating test event");
self.event_tx
.send(EventMessage::new(PUS_TEST.id(), TEST_EVENT.into()))
.expect("Sending test event failed");
match self.handler.service_helper.verif_reporter().start_success(
self.handler.service_helper.tm_sender(),
token,
timestamp,
) {
Ok(started_token) => {
if let Err(e) = self
.handler
.service_helper
.verif_reporter()
.completion_success(
self.handler.service_helper.tm_sender(),
started_token,
timestamp,
)
{
error_handler(&PartialPusHandlingError::Verification(e));
}
}
Err(e) => {
error_handler(&PartialPusHandlingError::Verification(e));
}
}
} else {
let fail_data = [tc.message_subtype_id()];
self.handler
.service_helper
.verif_reporter()
.start_failure(
self.handler.service_helper.tm_sender(),
token,
FailParams::new(
timestamp,
&tmtc_err::INVALID_PUS_SUBSERVICE,
&fail_data,
),
)
.expect("Sending start failure verification failed");
}
}
}
HandlingStatus::HandledOne
}
}
+156
View File
@@ -0,0 +1,156 @@
use std::collections::HashMap;
use std::sync::mpsc;
use log::warn;
use satrs::action::ActionRequest;
use satrs::hk::HkRequest;
use satrs::mode::ModeRequest;
use satrs::pus::verification::{
FailParams, TcStateAccepted, VerificationReportingProvider, VerificationToken,
};
use satrs::pus::{ActiveRequest, EcssTmSender, GenericRoutingError, PusRequestRouter};
use satrs::queue::GenericSendError;
use satrs::request::{GenericMessage, MessageMetadata, UniqueApidTargetId};
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::PusPacket;
use satrs::ComponentId;
use example_std::config::tmtc_err;
/*
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum CompositeRequest {
Hk(HkRequest),
Action(ActionRequest),
}
#[derive(Clone)]
pub struct GenericRequestRouter {
#[allow(dead_code)]
pub id: ComponentId,
// All messages which do not have a dedicated queue.
pub composite_router_map:
HashMap<ComponentId, mpsc::SyncSender<GenericMessage<CompositeRequest>>>,
pub mode_router_map: HashMap<ComponentId, mpsc::SyncSender<GenericMessage<ModeRequest>>>,
}
impl Default for GenericRequestRouter {
fn default() -> Self {
Self {
id: ids::generic_pus::PUS_ROUTING.raw(),
composite_router_map: Default::default(),
mode_router_map: Default::default(),
}
}
}
impl GenericRequestRouter {
pub(crate) fn handle_error_generic(
&self,
active_request: &impl ActiveRequest,
tc: &PusTcReader,
error: GenericRoutingError,
tm_sender: &(impl EcssTmSender + ?Sized),
verif_reporter: &impl VerificationReportingProvider,
time_stamp: &[u8],
) {
warn!(
"Routing request for service {} failed: {error:?}",
tc.service_type_id()
);
let accepted_token: VerificationToken<TcStateAccepted> = active_request
.token()
.try_into()
.expect("token is not in accepted state");
match error {
GenericRoutingError::UnknownTargetId(id) => {
let apid_target_id = UniqueApidTargetId::from(id);
warn!("Target APID for request: {}", apid_target_id.apid);
warn!("Target Unique ID for request: {}", apid_target_id.unique_id);
let mut fail_data: [u8; core::mem::size_of::<ComponentId>()] =
[0; core::mem::size_of::<ComponentId>()];
fail_data.copy_from_slice(&id.to_be_bytes());
verif_reporter
.completion_failure(
tm_sender,
accepted_token,
FailParams::new(time_stamp, &tmtc_err::UNKNOWN_TARGET_ID, &fail_data),
)
.expect("Sending start failure failed");
}
GenericRoutingError::Send(_) => {
let mut fail_data: [u8; 8] = [0; 8];
fail_data.copy_from_slice(&active_request.target_id().to_be_bytes());
verif_reporter
.completion_failure(
tm_sender,
accepted_token,
FailParams::new(time_stamp, &tmtc_err::ROUTING_ERROR, &fail_data),
)
.expect("Sending start failure failed");
}
}
}
}
impl PusRequestRouter<HkRequest> for GenericRequestRouter {
type Error = GenericRoutingError;
fn route(
&self,
requestor_info: MessageMetadata,
target_id: ComponentId,
hk_request: HkRequest,
) -> Result<(), Self::Error> {
if let Some(sender) = self.composite_router_map.get(&target_id) {
sender
.send(GenericMessage::new(
requestor_info,
CompositeRequest::Hk(hk_request),
))
.map_err(|_| GenericRoutingError::Send(GenericSendError::RxDisconnected))?;
return Ok(());
}
Err(GenericRoutingError::UnknownTargetId(target_id))
}
}
impl PusRequestRouter<ActionRequest> for GenericRequestRouter {
type Error = GenericRoutingError;
fn route(
&self,
requestor_info: MessageMetadata,
target_id: ComponentId,
action_request: ActionRequest,
) -> Result<(), Self::Error> {
if let Some(sender) = self.composite_router_map.get(&target_id) {
sender
.send(GenericMessage::new(
requestor_info,
CompositeRequest::Action(action_request),
))
.map_err(|_| GenericRoutingError::Send(GenericSendError::RxDisconnected))?;
return Ok(());
}
Err(GenericRoutingError::UnknownTargetId(target_id))
}
}
impl PusRequestRouter<ModeRequest> for GenericRequestRouter {
type Error = GenericRoutingError;
fn route(
&self,
requestor_info: MessageMetadata,
target_id: ComponentId,
request: ModeRequest,
) -> Result<(), Self::Error> {
if let Some(sender) = self.mode_router_map.get(&target_id) {
sender
.send(GenericMessage::new(requestor_info, request))
.map_err(|_| GenericRoutingError::Send(GenericSendError::RxDisconnected))?;
return Ok(());
}
Err(GenericRoutingError::UnknownTargetId(target_id))
}
}
*/
+158
View File
@@ -0,0 +1,158 @@
extern crate alloc;
use std::{
sync::mpsc,
time::{Duration, Instant},
};
use satrs::spacepackets::{CcsdsPacketIdAndPsc, time::cds::CdsTime};
pub use types::ComponentId;
use types::ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned};
pub mod config;
/// Simple type modelling packet stored in the heap. This structure is intended to
/// be used when sending a packet via a message queue, so it also contains the sender ID.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct PacketAsVec {
pub sender_id: ComponentId,
pub packet: Vec<u8>,
}
impl PacketAsVec {
pub fn new(sender_id: ComponentId, packet: Vec<u8>) -> Self {
Self { sender_id, packet }
}
}
pub struct TimestampHelper {
stamper: CdsTime,
time_stamp: [u8; 7],
}
impl TimestampHelper {
pub fn stamp(&self) -> &[u8] {
&self.time_stamp
}
pub fn update_from_now(&mut self) {
self.stamper
.update_from_now()
.expect("Updating timestamp failed");
self.stamper
.write_to_bytes(&mut self.time_stamp)
.expect("Writing timestamp failed");
}
}
impl Default for TimestampHelper {
fn default() -> Self {
Self {
stamper: CdsTime::now_with_u16_days().expect("creating time stamper failed"),
time_stamp: Default::default(),
}
}
}
/// Helper structure for periodic HK generation of a single set.
#[derive(Debug)]
pub struct HkHelperSingleSet {
pub enabled: bool,
pub frequency: Duration,
pub last_generated: Option<Instant>,
}
impl HkHelperSingleSet {
#[inline]
pub const fn new(enabled: bool, init_frequency: Duration) -> Self {
Self {
enabled,
frequency: init_frequency,
last_generated: None,
}
}
#[inline]
pub const fn enabled(&self) -> bool {
self.enabled
}
/// Check whether a new HK packet needs to be generated.
pub fn needs_generation(&mut self) -> bool {
if !self.enabled {
return false;
}
if self.last_generated.is_none() {
self.last_generated = Some(Instant::now());
return true;
}
let last_generated = self.last_generated.unwrap();
if Instant::now() - last_generated >= self.frequency {
self.last_generated = Some(Instant::now());
return true;
}
false
}
}
#[derive(Debug)]
pub struct TmtcQueues {
pub tc_rx: mpsc::Receiver<CcsdsTcPacketOwned>,
pub tm_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
}
#[derive(Debug)]
pub struct ModeHelper<Mode, TransitionState> {
pub current: Mode,
pub target: Option<Mode>,
pub tc_commander: Option<CcsdsPacketIdAndPsc>,
pub transition_start: Option<Instant>,
pub timeout: Duration,
pub transition_state: TransitionState,
}
impl<Mode: Copy + Clone, TransitionState: Default> ModeHelper<Mode, TransitionState> {
pub fn new(init_mode: Mode, timeout: Duration) -> Self {
Self {
current: init_mode,
target: Default::default(),
tc_commander: Default::default(),
transition_start: None,
timeout,
transition_state: Default::default(),
}
}
pub fn start(&mut self, target: Mode) {
self.target = Some(target);
self.transition_start = Some(Instant::now());
self.transition_state = TransitionState::default();
}
#[inline]
pub fn transition_active(&self) -> bool {
self.target.is_some()
}
pub fn timed_out(&self) -> bool {
if self.target.is_none() {
return false;
}
if let Some(transition_start) = self.transition_start {
return Instant::now() - transition_start >= self.timeout;
}
false
}
pub fn finish(&mut self, success: bool) -> Option<CcsdsPacketIdAndPsc> {
self.target?;
if success {
self.current = self.target.take().unwrap();
} else {
self.target = None;
}
self.transition_state = Default::default();
self.transition_start = None;
self.tc_commander.take()
}
}
+26
View File
@@ -0,0 +1,26 @@
use std::str::FromStr as _;
pub fn setup_logger() -> Result<(), fern::InitError> {
// Read the RUST_LOG environment variable and parse it.
// If the variable is not set or is invalid, default to Debug.
let mut log_level = log::LevelFilter::Info;
if let Ok(log_level_str) = std::env::var("RUST_LOG") {
log_level = log::LevelFilter::from_str(&log_level_str).unwrap_or(log::LevelFilter::Info);
}
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
std::thread::current().name().expect("unnamed_thread"),
record.level(),
message
))
})
.level(log_level)
.chain(std::io::stdout())
.chain(fern::log_file("output.log")?)
.apply()?;
Ok(())
}
+475
View File
@@ -0,0 +1,475 @@
use std::{
net::{IpAddr, SocketAddr},
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
mpsc,
},
thread,
time::Duration,
};
use eps::{
PowerSwitchHelper,
pcdu::{PcduHandler, SerialInterfaceDummy, SerialInterfaceToSim, SerialSimInterfaceWrapper},
};
use example_std::{
TmtcQueues,
config::{
OBSW_SERVER_ADDR, PACKET_ID_VALIDATOR, SERVER_PORT,
tasks::{FREQ_MS_AOCS, FREQ_MS_CONTROLLER, FREQ_MS_UDP_TMTC, SIM_CLIENT_IDLE_DELAY_MS},
},
};
use interface::{
sim_client_udp::create_sim_client,
tcp::{SyncTcpTmSource, TcpTask},
udp::UdpTmtcServer,
};
use log::info;
use logger::setup_logger;
use satrs::{
HandlingStatus,
hal::std::{tcp_server::ServerConfig, udp_server::UdpTcServer},
spacepackets::time::cds::CdsTime,
};
use tmtc::sender::TmTcSender;
use tmtc::{tc_source::TcSourceTask, tm_sink::TmSink};
use types::{ComponentId, DeviceMode};
use crate::{
acs::{ctrl, mgm, mgm_assembly, mgt, subsystem},
controller::Controller,
eps::pcdu::SwitchSet,
event_manager::EventManager,
interface::udp::UdpTmHandlerWithChannel,
tmtc::tc_source::CcsdsDistributor,
};
mod acs;
mod ccsds;
mod controller;
mod device_fdir;
mod device_mode;
mod eps;
mod event_manager;
mod interface;
mod logger;
mod tmtc;
fn main() {
static KILL_SIGNAL: AtomicBool = AtomicBool::new(false);
setup_logger().expect("setting up logging with fern failed");
println!("Runng OBSW example");
ctrlc::set_handler(move || {
log::info!("Received Ctrl-C, shutting down");
KILL_SIGNAL.store(true, Ordering::Relaxed);
})
.expect("Error setting Ctrl-C handler");
let (tc_source_tx, tc_source_rx) = mpsc::sync_channel(50);
let (tm_sink_tx, tm_sink_rx) = mpsc::sync_channel(50);
let (tm_server_tx, tm_server_rx) = mpsc::sync_channel(50);
let (sim_request_tx, sim_request_rx) = mpsc::channel();
let (mgm_0_sim_reply_tx, mgm_0_sim_reply_rx) = mpsc::channel();
let (mgm_1_sim_reply_tx, mgm_1_sim_reply_rx) = mpsc::channel();
let (mgt_sim_reply_tx, mgt_sim_reply_rx) = mpsc::channel();
let (pcdu_sim_reply_tx, pcdu_sim_reply_rx) = mpsc::channel();
let mut opt_sim_client = create_sim_client(sim_request_rx);
let (mgm_0_handler_tc_tx, mgm_0_handler_tc_rx) = mpsc::sync_channel(10);
let (mgm_1_handler_tc_tx, mgm_1_handler_tc_rx) = mpsc::sync_channel(10);
let (mgt_handler_tc_tx, mgt_handler_tc_rx) = mpsc::sync_channel(10);
let (mgm_assembly_tc_tx, mgm_assembly_tc_rx) = mpsc::sync_channel(10);
let (acs_subsystem_tc_tx, acs_subsystem_tc_rx) = mpsc::sync_channel(10);
let (pcdu_handler_tc_tx, pcdu_handler_tc_rx) = mpsc::sync_channel(30);
let (controller_tc_tx, controller_tc_rx) = mpsc::sync_channel(10);
let (event_manager_tc_tx, event_manager_tc_rx) = mpsc::sync_channel(10);
let (mgt_request_tx, mgt_request_rx) = mpsc::sync_channel(5);
let (mgt_report_tx, mgt_report_rx) = mpsc::sync_channel(5);
let (acs_ctrl_request_tx, acs_ctrl_request_rx) = mpsc::sync_channel(5);
let (acs_ctrl_response_tx, acs_ctrl_response_rx) = mpsc::sync_channel(5);
// These message handles need to go into the MGM assembly and ACS subsystem.
let (mgm_assembly_request_tx, mgm_assembly_request_rx) = mpsc::sync_channel(5);
let (mgm_assembly_report_tx, mgm_assembly_report_rx) = mpsc::sync_channel(5);
// These message handles need to go into the MGM assembly and MGM devices.
let (mgm_0_mode_request_tx, mgm_0_mode_request_rx) = mpsc::sync_channel(5);
let (mgm_1_mode_request_tx, mgm_1_mode_request_rx) = mpsc::sync_channel(5);
let (mgm_0_mode_report_tx, mgm_0_mode_report_rx) = mpsc::sync_channel(5);
let (mgm_1_mode_report_tx, mgm_1_mode_report_rx) = mpsc::sync_channel(5);
let (pcdu_handler_mode_tx, _pcdu_handler_mode_rx) = mpsc::sync_channel(5);
let (event_ctrl_tx, event_ctrl_rx) = mpsc::sync_channel(10);
let (mgm_event_tx, mgm_event_rx) = mpsc::sync_channel(10);
let (mgm_assembly_event_tx, mgm_assembly_event_rx) = mpsc::sync_channel(10);
let (mgt_event_tx, mgt_event_rx) = mpsc::sync_channel(10);
let (pcdu_event_tx, pcdu_event_rx) = mpsc::sync_channel(10);
let (tc_source_event_tx, tc_source_event_rx) = mpsc::sync_channel(10);
let mut event_manager = EventManager::new(
event_manager_tc_rx,
event_ctrl_rx,
mgm_event_rx,
mgm_assembly_event_rx,
mgt_event_rx,
pcdu_event_rx,
tc_source_event_rx,
tm_sink_tx.clone(),
);
let mut controller = Controller::new(controller_tc_rx, tm_sink_tx.clone(), event_ctrl_tx);
let ccsds_distributor = CcsdsDistributor::default();
let mut tc_source = TcSourceTask::new(tc_source_rx, ccsds_distributor, tc_source_event_tx);
tc_source.add_target(ComponentId::EpsPcdu, pcdu_handler_tc_tx);
tc_source.add_target(ComponentId::Controller, controller_tc_tx);
tc_source.add_target(ComponentId::AcsMgm0, mgm_0_handler_tc_tx);
tc_source.add_target(ComponentId::AcsMgm1, mgm_1_handler_tc_tx);
tc_source.add_target(ComponentId::AcsMgmAssembly, mgm_assembly_tc_tx);
tc_source.add_target(ComponentId::AcsMgt, mgt_handler_tc_tx);
tc_source.add_target(ComponentId::AcsSubsystem, acs_subsystem_tc_tx);
tc_source.add_target(ComponentId::EventManager, event_manager_tc_tx);
let tc_sender = TmTcSender::Normal(tc_source_tx.clone());
let udp_tm_handler = UdpTmHandlerWithChannel {
tm_rx: tm_server_rx,
};
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
let udp_tc_server = UdpTcServer::new(
ComponentId::UdpServer as u32,
sock_addr,
2048,
tc_sender.clone(),
)
.expect("creating UDP TMTC server failed");
let mut udp_tmtc_server = UdpTmtcServer {
udp_tc_server,
tm_handler: udp_tm_handler.into(),
};
let tcp_server_cfg = ServerConfig::new(
ComponentId::TcpServer as u32,
sock_addr,
Duration::from_millis(400),
4096,
8192,
);
let sync_tm_tcp_source = SyncTcpTmSource::new(200);
let mut tcp_server = TcpTask::new(
tcp_server_cfg,
sync_tm_tcp_source.clone(),
tc_sender,
PACKET_ID_VALIDATOR.clone(),
)
.expect("tcp server creation failed");
let mut tm_sink = TmSink::new(sync_tm_tcp_source, tm_sink_rx, tm_server_tx);
let shared_switch_set = Arc::new(Mutex::new(SwitchSet::new_with_init_switches_unknown()));
let (switch_request_tx, switch_request_rx) = mpsc::sync_channel(20);
let switch_helper = PowerSwitchHelper::new(switch_request_tx, shared_switch_set.clone());
// Global FDIR health table, shared by all software objects.
let health_table = satrs::health::HealthTableMapSync::default();
let shared_mgm_0_set = Arc::default();
let shared_mgm_1_set = Arc::default();
let (mgm_0_spi_interface, mgm_1_spi_interface) = if let Some(sim_client) =
opt_sim_client.as_mut()
{
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgm0Lis3Mdl, mgm_0_sim_reply_tx);
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgm1Lis3Mdl, mgm_1_sim_reply_tx);
(
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
id: mgm::MgmId::_0,
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgm_0_sim_reply_rx,
}),
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
id: mgm::MgmId::_1,
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgm_1_sim_reply_rx,
}),
)
} else {
(
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
)
};
let mut mgm_0_handler = mgm::MgmHandlerLis3Mdl::new(
mgm::MgmId::_0,
TmtcQueues {
tc_rx: mgm_0_handler_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
switch_helper.clone(),
mgm_0_spi_interface,
shared_mgm_0_set,
mgm::ModeLeafHelper {
request_rx: mgm_0_mode_request_rx,
report_tx: mgm_0_mode_report_tx,
},
Duration::from_millis(1000),
health_table.clone(),
mgm_event_tx.clone(),
);
let mut mgm_1_handler = mgm::MgmHandlerLis3Mdl::new(
mgm::MgmId::_1,
TmtcQueues {
tc_rx: mgm_1_handler_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
switch_helper.clone(),
mgm_1_spi_interface,
shared_mgm_1_set,
mgm::ModeLeafHelper {
request_rx: mgm_1_mode_request_rx,
report_tx: mgm_1_mode_report_tx,
},
Duration::from_millis(1000),
health_table.clone(),
mgm_event_tx,
);
let mut mgm_assembly = mgm_assembly::Assembly::new(
mgm_assembly::ParentQueueHelper {
request_rx: mgm_assembly_request_rx,
report_tx: mgm_assembly_report_tx,
},
mgm_assembly::ChildrenQueueHelper {
request_tx_queues: [mgm_0_mode_request_tx, mgm_1_mode_request_tx],
report_rx_queues: [mgm_0_mode_report_rx, mgm_1_mode_report_rx],
},
TmtcQueues {
tc_rx: mgm_assembly_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
Duration::from_millis(2000),
mgm_assembly_event_tx,
);
let mut acs_controller = ctrl::Controller::new(ctrl::ModeLeafHelper {
request_rx: acs_ctrl_request_rx,
report_tx: acs_ctrl_response_tx,
});
let mgt_com = if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgt, mgt_sim_reply_tx);
mgt::MgtCommunication::Sim(mgt::SimInterface {
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgt_sim_reply_rx,
})
} else {
mgt::MgtCommunication::Dummy(mgt::DummyInterface::default())
};
let mut mgt_handler = mgt::MgtHandler::new(
TmtcQueues {
tc_rx: mgt_handler_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
switch_helper.clone(),
mgt_com,
mgt::ModeLeafHelper {
request_rx: mgt_request_rx,
report_tx: mgt_report_tx,
},
Duration::from_millis(1000),
health_table.clone(),
mgt_event_tx,
);
let mut acs_subsystem = subsystem::Subsystem::new(
subsystem::ModeRequestSenders {
mode_request_ctrl: acs_ctrl_request_tx,
mode_request_mgm_assy: mgm_assembly_request_tx,
mode_request_mgt: mgt_request_tx,
},
subsystem::ModeReportReceivers {
mode_response_ctrl: acs_ctrl_response_rx,
mode_response_mgm_assy: mgm_assembly_report_rx,
mode_response_mgt: mgt_report_rx,
},
TmtcQueues {
tc_rx: acs_subsystem_tc_rx,
tm_tx: tm_sink_tx.clone(),
},
);
let pcdu_serial_interface = if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client.add_reply_recipient(minisim_types::ComponentId::Pcdu, pcdu_sim_reply_tx);
SerialSimInterfaceWrapper::Sim(SerialInterfaceToSim::new(
sim_request_tx.clone(),
pcdu_sim_reply_rx,
))
} else {
SerialSimInterfaceWrapper::Dummy(SerialInterfaceDummy::default())
};
let mut pcdu_handler = PcduHandler::new(
pcdu_handler_tc_rx,
tm_sink_tx.clone(),
switch_request_rx,
pcdu_serial_interface,
shared_switch_set,
DeviceMode::Normal,
pcdu_event_tx,
);
// The PCDU is a critical component which should be in normal mode immediately.
pcdu_handler_mode_tx
.send(types::pcdu::request::Request::Mode(DeviceMode::Normal))
.expect("sending initial mode request failed");
info!("Starting TMTC and UDP task");
let jh_udp_tmtc = thread::Builder::new()
.name("TMTC & UDP".to_string())
.spawn(move || {
info!("Running UDP server on port {SERVER_PORT}");
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
udp_tmtc_server.periodic_operation();
tc_source.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_UDP_TMTC));
}
})
.unwrap();
info!("Starting TCP task");
let jh_tcp = thread::Builder::new()
.name("TCP".to_string())
.spawn(move || {
info!("Running TCP server on port {SERVER_PORT}");
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
tcp_server.periodic_operation();
}
})
.unwrap();
info!("Starting TM funnel task");
let jh_tm_funnel = thread::Builder::new()
.name("TM SINK".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
tm_sink.operation();
}
})
.unwrap();
let mut opt_jh_sim_client = None;
if let Some(mut sim_client) = opt_sim_client {
info!("Starting UDP sim client task");
opt_jh_sim_client = Some(
thread::Builder::new()
.name("SIM ADAPTER".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
if sim_client.operation() == HandlingStatus::Empty {
std::thread::sleep(Duration::from_millis(SIM_CLIENT_IDLE_DELAY_MS));
}
}
})
.unwrap(),
);
}
info!("Starting AOCS thread");
let jh_aocs = thread::Builder::new()
.name("AOCS".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
mgm_0_handler.periodic_operation();
mgm_1_handler.periodic_operation();
mgm_assembly.periodic_operation();
acs_controller.periodic_operation();
mgt_handler.periodic_operation();
acs_subsystem.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_AOCS));
}
})
.unwrap();
info!("Starting EPS thread");
let jh_eps = thread::Builder::new()
.name("EPS".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
// TODO: We should introduce something like a fixed timeslot helper to allow a more
// declarative API. It would also be very useful for the AOCS task.
//
// TODO: The fixed timeslot handler exists.. use it.
// TODO: Why not just use sync code in the PCDU handler, and fully delay there?
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::RegularOp);
thread::sleep(Duration::from_millis(50));
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::PollAndRecvReplies);
thread::sleep(Duration::from_millis(50));
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::PollAndRecvReplies);
thread::sleep(Duration::from_millis(300));
}
})
.unwrap();
info!("Starting controller thread");
let jh_controller_thread = thread::Builder::new()
.name("CTRL".to_string())
.spawn(move || {
loop {
if KILL_SIGNAL.load(Ordering::Relaxed) {
break;
}
controller.periodic_operation();
event_manager.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_CONTROLLER));
}
})
.unwrap();
jh_udp_tmtc
.join()
.expect("Joining UDP TMTC server thread failed");
jh_tcp
.join()
.expect("Joining TCP TMTC server thread failed");
jh_tm_funnel
.join()
.expect("Joining TM Funnel thread failed");
if let Some(jh_sim_client) = opt_jh_sim_client {
jh_sim_client
.join()
.expect("Joining SIM client thread failed");
}
jh_aocs.join().expect("Joining AOCS thread failed");
jh_eps.join().expect("Joining EPS thread failed");
jh_controller_thread
.join()
.expect("Joining PUS handler thread failed");
}
pub fn update_time(time_provider: &mut CdsTime, timestamp: &mut [u8]) {
time_provider
.update_from_now()
.expect("Could not get current time");
time_provider
.write_to_bytes(timestamp)
.expect("Writing timestamp failed");
}
+3
View File
@@ -0,0 +1,3 @@
pub mod sender;
pub mod tc_source;
pub mod tm_sink;
+55
View File
@@ -0,0 +1,55 @@
use std::{cell::RefCell, collections::VecDeque, sync::mpsc};
use satrs::{
ComponentId,
queue::GenericSendError,
tmtc::{PacketAsVec, PacketHandler},
};
#[derive(Default, Debug, Clone)]
pub struct MockSender(pub RefCell<VecDeque<PacketAsVec>>);
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub enum TmTcSender {
Normal(mpsc::SyncSender<PacketAsVec>),
Mock(MockSender),
}
impl TmTcSender {
#[allow(dead_code)]
pub fn get_mock_sender(&mut self) -> Option<&mut MockSender> {
match self {
TmTcSender::Mock(sender) => Some(sender),
_ => None,
}
}
}
impl PacketHandler for TmTcSender {
type Error = GenericSendError;
fn handle_packet(&self, sender_id: ComponentId, packet: &[u8]) -> Result<(), Self::Error> {
match self {
TmTcSender::Normal(sync_sender) => {
if let Err(e) = sync_sender.send(PacketAsVec::new(sender_id, packet.to_vec())) {
log::error!("Error sending packet via Heap TM/TC sender: {:?}", e);
}
}
TmTcSender::Mock(sender) => {
sender.handle_packet(sender_id, packet).unwrap();
}
}
Ok(())
}
}
impl PacketHandler for MockSender {
type Error = GenericSendError;
fn handle_packet(&self, sender_id: ComponentId, tc_raw: &[u8]) -> Result<(), Self::Error> {
let mut mut_queue = self.0.borrow_mut();
mut_queue.push_back(PacketAsVec::new(sender_id, tc_raw.to_vec()));
Ok(())
}
}
+240
View File
@@ -0,0 +1,240 @@
use satrs::{
ComponentId as RawComponentId, HandlingStatus,
spacepackets::{CcsdsPacketReader, ChecksumType},
tmtc::PacketAsVec,
};
use std::{
collections::HashMap,
sync::mpsc::{self, TryRecvError},
};
use types::{ComponentId, TcHeader, ccsds::CcsdsTcPacketOwned, tmtc};
pub type CcsdsDistributor = HashMap<ComponentId, std::sync::mpsc::SyncSender<CcsdsTcPacketOwned>>;
// TC source components where the heap is the backing memory of the received telecommands.
pub struct TcSourceTask {
pub tc_receiver: mpsc::Receiver<PacketAsVec>,
ccsds_distributor: CcsdsDistributor,
event_tx: mpsc::SyncSender<(ComponentId, tmtc::Event)>,
}
impl TcSourceTask {
pub fn new(
tc_receiver: mpsc::Receiver<PacketAsVec>,
ccsds_distributor: CcsdsDistributor,
event_tx: mpsc::SyncSender<(ComponentId, tmtc::Event)>,
) -> Self {
Self {
tc_receiver,
ccsds_distributor,
event_tx,
}
}
pub fn add_target(
&mut self,
target_id: ComponentId,
sender: mpsc::SyncSender<CcsdsTcPacketOwned>,
) {
self.ccsds_distributor.insert(target_id, sender);
}
pub fn periodic_operation(&mut self) {
loop {
if self.poll_tc() == HandlingStatus::Empty {
break;
}
}
}
pub fn poll_tc(&mut self) -> HandlingStatus {
match self.tc_receiver.try_recv() {
Ok(packet) => {
log::debug!("received raw packet: {:?}", packet);
let ccsds_tc_reader_result =
CcsdsPacketReader::new(&packet.packet, Some(ChecksumType::WithCrc16));
if ccsds_tc_reader_result.is_err() {
log::warn!(
"received invalid CCSDS TC packet: {:?}",
ccsds_tc_reader_result.err()
);
self.send_event(packet.sender_id, tmtc::Event::InvalidTcPacket);
return HandlingStatus::HandledOne;
}
let ccsds_tc_reader = ccsds_tc_reader_result.unwrap();
let tc_header_result =
postcard::take_from_bytes::<TcHeader>(ccsds_tc_reader.user_data());
if tc_header_result.is_err() {
log::warn!(
"received CCSDS TC packet with invalid TC header: {:?}",
tc_header_result.err()
);
self.send_event(packet.sender_id, tmtc::Event::InvalidTcHeader);
return HandlingStatus::HandledOne;
}
let (tc_header, payload) = tc_header_result.unwrap();
if let Some(sender) = self.ccsds_distributor.get(&tc_header.target_id) {
log::debug!("sending TC packet to target ID: {:?}", tc_header.target_id);
sender
.send(CcsdsTcPacketOwned {
sp_header: *ccsds_tc_reader.sp_header(),
tc_header,
payload: payload.to_vec(),
})
.ok();
} else {
log::warn!("no TC handler for target ID {:?}", tc_header.target_id);
self.send_event(
packet.sender_id,
tmtc::Event::UnknownTargetId(tc_header.target_id),
);
}
HandlingStatus::HandledOne
}
Err(e) => match e {
TryRecvError::Empty => HandlingStatus::Empty,
TryRecvError::Disconnected => {
log::warn!("tmtc thread: sender disconnected");
HandlingStatus::Empty
}
},
}
}
/// `sender_id` is the raw ID tagged on the received packet, which is not necessarily a
/// known [ComponentId] (e.g. a spoofed or garbled packet). Falls back to [ComponentId::Ground]
/// as the event sender in that case.
fn send_event(&self, sender_id: RawComponentId, event: tmtc::Event) {
let sender_id = ComponentId::try_from(sender_id).unwrap_or_else(|_| {
log::warn!("TC source event for unknown raw sender ID {}", sender_id);
ComponentId::Ground
});
if let Err(e) = self.event_tx.send((sender_id, event)) {
log::warn!("failed to send TC source event: {}", e);
}
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc::TryRecvError;
use arbitrary_int::u11;
use satrs::spacepackets::{
CcsdsPacketCreatorOwned, ChecksumType, PacketType, SpacePacketHeader,
};
use types::{Apid, MessageType, ccsds::CcsdsTcPacketOwned};
use super::*;
struct Testbench {
tc_tx: mpsc::SyncSender<PacketAsVec>,
target_rx: mpsc::Receiver<CcsdsTcPacketOwned>,
event_rx: mpsc::Receiver<(ComponentId, tmtc::Event)>,
tc_source: TcSourceTask,
}
impl Testbench {
fn new() -> Self {
let (tc_tx, tc_source_rx) = mpsc::sync_channel(5);
let (target_tx, target_rx) = mpsc::sync_channel(5);
let (event_tx, event_rx) = mpsc::sync_channel(5);
let mut tc_source =
TcSourceTask::new(tc_source_rx, CcsdsDistributor::default(), event_tx);
tc_source.add_target(ComponentId::EpsPcdu, target_tx);
Self {
tc_tx,
target_rx,
event_rx,
tc_source,
}
}
}
fn valid_tc_raw(target_id: ComponentId) -> Vec<u8> {
CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, MessageType::Ping),
(),
)
.to_vec()
}
/// A structurally valid CCSDS packet (correct length and CRC), but with arbitrary user data
/// instead of a postcard-encoded [TcHeader].
fn raw_ccsds_tc_with_user_data(user_data: &[u8]) -> Vec<u8> {
CcsdsPacketCreatorOwned::new(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
PacketType::Tc,
user_data,
Some(ChecksumType::WithCrc16),
)
.unwrap()
.to_vec()
}
#[test]
fn test_valid_tc_is_routed_without_event() {
let mut tb = Testbench::new();
tb.tc_tx
.send(PacketAsVec::new(
ComponentId::UdpServer as u32,
valid_tc_raw(ComponentId::EpsPcdu),
))
.unwrap();
tb.tc_source.periodic_operation();
tb.target_rx.try_recv().expect("TC was not routed");
assert!(matches!(tb.event_rx.try_recv(), Err(TryRecvError::Empty)));
}
#[test]
fn test_invalid_ccsds_packet_sends_event() {
let mut tb = Testbench::new();
tb.tc_tx
.send(PacketAsVec::new(
ComponentId::UdpServer as u32,
vec![1, 2, 3],
))
.unwrap();
tb.tc_source.periodic_operation();
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
assert_eq!(sender_id, ComponentId::UdpServer);
assert!(matches!(event, tmtc::Event::InvalidTcPacket));
}
#[test]
fn test_invalid_tc_header_sends_event() {
let mut tb = Testbench::new();
tb.tc_tx
.send(PacketAsVec::new(
ComponentId::UdpServer as u32,
// A single byte is enough to decode the `ComponentId` discriminant, but not
// enough for the trailing `MessageType`, so `TcHeader` deserialization fails
// while the CCSDS packet itself stays valid.
raw_ccsds_tc_with_user_data(&[0]),
))
.unwrap();
tb.tc_source.periodic_operation();
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
assert_eq!(sender_id, ComponentId::UdpServer);
assert!(matches!(event, tmtc::Event::InvalidTcHeader));
}
#[test]
fn test_unknown_target_id_sends_event() {
let mut tb = Testbench::new();
tb.tc_tx
.send(PacketAsVec::new(
ComponentId::UdpServer as u32,
valid_tc_raw(ComponentId::Ground),
))
.unwrap();
tb.tc_source.periodic_operation();
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
assert_eq!(sender_id, ComponentId::UdpServer);
assert!(matches!(
event,
tmtc::Event::UnknownTargetId(ComponentId::Ground)
));
}
}
+58
View File
@@ -0,0 +1,58 @@
use std::{
collections::HashMap,
sync::mpsc::{self},
};
use arbitrary_int::{u11, u14};
use satrs::spacepackets::seq_count::{SequenceCounter, SequenceCounterCcsdsSimple};
use types::ccsds::CcsdsTmPacketOwned;
use crate::interface::tcp::SyncTcpTmSource;
#[derive(Default)]
pub struct CcsdsSeqCounterMap {
apid_seq_counter_map: HashMap<u11, SequenceCounterCcsdsSimple>,
}
impl CcsdsSeqCounterMap {
pub fn get_and_increment(&mut self, apid: u11) -> u14 {
u14::new(
self.apid_seq_counter_map
.entry(apid)
.or_default()
.get_and_increment(),
)
}
}
pub struct TmSink {
seq_counter_map: CcsdsSeqCounterMap,
sync_tm_tcp_source: SyncTcpTmSource,
tm_funnel_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
tm_server_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
}
impl TmSink {
pub fn new(
sync_tm_tcp_source: SyncTcpTmSource,
tm_funnel_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
tm_server_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
) -> Self {
Self {
seq_counter_map: Default::default(),
sync_tm_tcp_source,
tm_funnel_rx,
tm_server_tx,
}
}
pub fn operation(&mut self) {
if let Ok(mut tm) = self.tm_funnel_rx.try_recv() {
tm.sp_header
.set_seq_count(self.seq_counter_map.get_and_increment(tm.sp_header.apid()));
self.sync_tm_tcp_source.add_tm(&tm.to_vec());
self.tm_server_tx
.send(tm)
.expect("Sending TM to server failed");
}
}
}