From ae8e39f626fe372cda1647cba4c99dc1caae7dc4 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 9 Mar 2024 15:11:11 +0100 Subject: [PATCH 1/3] First version of asynchronix based mini simulator - Basic simulator with 3 devices - Can be driven via a UDP interface - Design allows to drive the simulation via different interface in the future by using Request/Reply messaging. --- .gitignore | 3 +- Cargo.toml | 1 + coverage.py | 10 +- satrs-minisim/Cargo.toml | 24 ++ satrs-minisim/src/acs.rs | 349 ++++++++++++++++++++++++++ satrs-minisim/src/controller.rs | 197 +++++++++++++++ satrs-minisim/src/eps.rs | 187 ++++++++++++++ satrs-minisim/src/lib.rs | 246 ++++++++++++++++++ satrs-minisim/src/main.rs | 103 ++++++++ satrs-minisim/src/test_helpers.rs | 56 +++++ satrs-minisim/src/time.rs | 5 + satrs-minisim/src/udp.rs | 403 ++++++++++++++++++++++++++++++ satrs/src/power.rs | 36 +++ 13 files changed, 1616 insertions(+), 4 deletions(-) create mode 100644 satrs-minisim/Cargo.toml create mode 100644 satrs-minisim/src/acs.rs create mode 100644 satrs-minisim/src/controller.rs create mode 100644 satrs-minisim/src/eps.rs create mode 100644 satrs-minisim/src/lib.rs create mode 100644 satrs-minisim/src/main.rs create mode 100644 satrs-minisim/src/test_helpers.rs create mode 100644 satrs-minisim/src/time.rs create mode 100644 satrs-minisim/src/udp.rs diff --git a/.gitignore b/.gitignore index 70a1972..2e17483 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -/target +target/ + /Cargo.lock /.idea/* diff --git a/Cargo.toml b/Cargo.toml index 6cd72ab..10ec0a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "satrs", "satrs-mib", "satrs-example", + "satrs-minisim", "satrs-shared", ] diff --git a/coverage.py b/coverage.py index 126a101..3b1c3c4 100755 --- a/coverage.py +++ b/coverage.py @@ -18,15 +18,19 @@ def generate_cov_report(open_report: bool, format: str, package: str): out_path = "./target/debug/coverage" if format == "lcov": out_path = "./target/debug/lcov.info" - os.system( + grcov_cmd = ( f"grcov . -s . --binary-path ./target/debug/ -t {format} --branch --ignore-not-existing " f"-o {out_path}" ) + print(f"Running: {grcov_cmd}") + os.system(grcov_cmd) if format == "lcov": - os.system( + lcov_cmd = ( "genhtml -o ./target/debug/coverage/ --show-details --highlight --ignore-errors source " "--legend ./target/debug/lcov.info" ) + print(f"Running: {lcov_cmd}") + os.system(lcov_cmd) if open_report: coverage_report_path = os.path.abspath("./target/debug/coverage/index.html") webbrowser.open_new_tab(coverage_report_path) @@ -43,7 +47,7 @@ def main(): parser.add_argument( "-p", "--package", - choices=["satrs"], + choices=["satrs", "satrs-minisim"], default="satrs", help="Choose project to generate coverage for", ) diff --git a/satrs-minisim/Cargo.toml b/satrs-minisim/Cargo.toml new file mode 100644 index 0000000..3d6d904 --- /dev/null +++ b/satrs-minisim/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "satrs-minisim" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +log = "0.4" +thiserror = "1" + +[dependencies.asynchronix] +version = "0.2.1" +# path = "../../asynchronix/asynchronix" +# git = "https://github.com/us-irs/asynchronix.git" +# branch = "clock-not-sendable" + +[dependencies.satrs] +path = "../satrs" + +[dev-dependencies] +delegate = "0.12" diff --git a/satrs-minisim/src/acs.rs b/satrs-minisim/src/acs.rs new file mode 100644 index 0000000..cd0e4f8 --- /dev/null +++ b/satrs-minisim/src/acs.rs @@ -0,0 +1,349 @@ +use std::{f32::consts::PI, sync::mpsc, time::Duration}; + +use asynchronix::{ + model::{Model, Output}, + time::Scheduler, +}; +use satrs::power::SwitchStateBinary; +use satrs_minisim::{ + acs::{MgmReply, MgmSensorValues, MgtDipole, MgtHkSet, MgtReply, MGT_GEN_MAGNETIC_FIELD}, + SimReply, SimTarget, +}; + +use crate::time::current_millis; + +// Earth magnetic field varies between -30 uT and 30 uT +const AMPLITUDE_MGM: f32 = 0.03; +// Lets start with a simple frequency here. +const FREQUENCY_MGM: f32 = 1.0; +const PHASE_X: f32 = 0.0; +// Different phases to have different values on the other axes. +const PHASE_Y: f32 = 0.1; +const PHASE_Z: f32 = 0.2; + +/// Simple model for a magnetometer where the measure magnetic fields are modeled with sine waves. +/// +/// Please note that that a more realistic MGM model wouold include the following components +/// which are not included here to simplify the model: +/// +/// 1. It would probably generate signed [i16] values which need to be converted to SI units +/// because it is a digital sensor +/// 2. It would sample the magnetic field at a high fixed rate. This might not be possible for +/// a general purpose OS, but self self-sampling at a relatively high rate (20-40 ms) might +/// stil lbe possible. +pub struct MagnetometerModel { + pub switch_state: SwitchStateBinary, + pub periodicity: Duration, + pub external_mag_field: Option, + pub reply_sender: mpsc::Sender, +} + +impl MagnetometerModel { + pub fn new(periodicity: Duration, reply_sender: mpsc::Sender) -> Self { + Self { + switch_state: SwitchStateBinary::Off, + periodicity, + external_mag_field: None, + reply_sender, + } + } + + pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) { + self.switch_state = switch_state; + } + + pub async fn send_sensor_values(&mut self, _: (), scheduler: &Scheduler) { + self.reply_sender + .send(SimReply::new( + SimTarget::Mgm, + MgmReply { + switch_state: self.switch_state, + sensor_values: self + .calculate_current_mgm_tuple(current_millis(scheduler.time())), + }, + )) + .expect("sending MGM sensor values failed"); + } + + // Devices like magnetorquers generate a strong magnetic field which overrides the default + // model for the measured magnetic field. + pub async fn apply_external_magnetic_field(&mut self, field: MgmSensorValues) { + self.external_mag_field = Some(field); + } + + fn calculate_current_mgm_tuple(&self, time_ms: u64) -> MgmSensorValues { + if SwitchStateBinary::On == self.switch_state { + if let Some(ext_field) = self.external_mag_field { + return ext_field; + } + let base_sin_val = 2.0 * PI * FREQUENCY_MGM * (time_ms as f32 / 1000.0); + return MgmSensorValues { + x: AMPLITUDE_MGM * (base_sin_val + PHASE_X).sin(), + y: AMPLITUDE_MGM * (base_sin_val + PHASE_Y).sin(), + z: AMPLITUDE_MGM * (base_sin_val + PHASE_Z).sin(), + }; + } + MgmSensorValues { + x: 0.0, + y: 0.0, + z: 0.0, + } + } +} + +impl Model for MagnetometerModel {} + +pub struct MagnetorquerModel { + switch_state: SwitchStateBinary, + torquing: bool, + torque_dipole: MgtDipole, + pub gen_magnetic_field: Output, + reply_sender: mpsc::Sender, +} + +impl MagnetorquerModel { + pub fn new(reply_sender: mpsc::Sender) -> Self { + Self { + switch_state: SwitchStateBinary::Off, + torquing: false, + torque_dipole: MgtDipole::default(), + gen_magnetic_field: Output::new(), + reply_sender, + } + } + + pub async fn apply_torque( + &mut self, + duration_and_dipole: (Duration, MgtDipole), + scheduler: &Scheduler, + ) { + self.torque_dipole = duration_and_dipole.1; + self.torquing = true; + if scheduler + .schedule_event(duration_and_dipole.0, Self::clear_torque, ()) + .is_err() + { + log::warn!("torque clearing can only be set for a future time."); + } + self.generate_magnetic_field(()).await; + } + + pub async fn clear_torque(&mut self, _: ()) { + self.torque_dipole = MgtDipole::default(); + self.torquing = false; + self.generate_magnetic_field(()).await; + } + + pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) { + self.switch_state = switch_state; + self.generate_magnetic_field(()).await; + } + + pub async fn request_housekeeping_data(&mut self, _: (), scheduler: &Scheduler) { + if self.switch_state != SwitchStateBinary::On { + return; + } + scheduler + .schedule_event(Duration::from_millis(15), Self::send_housekeeping_data, ()) + .expect("requesting housekeeping data failed") + } + + pub fn send_housekeeping_data(&mut self) { + let mgt_reply = MgtReply::Hk(MgtHkSet { + dipole: self.torque_dipole, + torquing: self.torquing, + }); + self.reply_sender + .send(SimReply::new(SimTarget::Mgt, mgt_reply)) + .unwrap(); + } + + fn calc_magnetic_field(&self, _: MgtDipole) -> MgmSensorValues { + // Simplified model: Just returns some fixed magnetic field for now. + // Later, we could make this more fancy by incorporating the commanded dipole. + MGT_GEN_MAGNETIC_FIELD + } + + /// A torquing magnetorquer generates a magnetic field. This function can be used to apply + /// the magnetic field. + async fn generate_magnetic_field(&mut self, _: ()) { + if self.switch_state != SwitchStateBinary::On || !self.torquing { + return; + } + self.gen_magnetic_field + .send(self.calc_magnetic_field(self.torque_dipole)) + .await; + } +} + +impl Model for MagnetorquerModel {} + +#[cfg(test)] +pub mod tests { + use std::time::Duration; + + use satrs::power::SwitchStateBinary; + use satrs_minisim::{ + acs::{MgmReply, MgmRequest, MgtDipole, MgtHkSet, MgtReply, MgtRequest}, + eps::PcduSwitch, + SimRequest, SimTarget, + }; + + use crate::{eps::tests::switch_device_on, test_helpers::SimTestbench}; + + #[test] + fn test_basic_mgm_request() { + let mut sim_testbench = SimTestbench::new(); + let mgm_request = MgmRequest::RequestSensorData; + let request = SimRequest::new(SimTarget::Mgm, mgm_request); + sim_testbench + .send_request(request) + .expect("sending MGM request failed"); + sim_testbench.handle_sim_requests(); + sim_testbench.step(); + let sim_reply = sim_testbench.try_receive_next_reply(); + assert!(sim_reply.is_some()); + let sim_reply = sim_reply.unwrap(); + assert_eq!(sim_reply.target(), SimTarget::Mgm); + let reply: MgmReply = serde_json::from_str(sim_reply.reply()) + .expect("failed to deserialize MGM sensor values"); + assert_eq!(reply.switch_state, SwitchStateBinary::Off); + assert_eq!(reply.sensor_values.x, 0.0); + assert_eq!(reply.sensor_values.y, 0.0); + assert_eq!(reply.sensor_values.z, 0.0); + } + + #[test] + fn test_basic_mgm_request_switched_on() { + let mut sim_testbench = SimTestbench::new(); + switch_device_on(&mut sim_testbench, PcduSwitch::Mgm); + + let mgm_request = MgmRequest::RequestSensorData; + let mut request = SimRequest::new(SimTarget::Mgm, mgm_request); + sim_testbench + .send_request(request) + .expect("sending MGM request failed"); + sim_testbench.handle_sim_requests(); + sim_testbench.step(); + let mut sim_reply_res = sim_testbench.try_receive_next_reply(); + assert!(sim_reply_res.is_some()); + let mut sim_reply = sim_reply_res.unwrap(); + assert_eq!(sim_reply.target(), SimTarget::Mgm); + let first_reply: MgmReply = serde_json::from_str(sim_reply.reply()) + .expect("failed to deserialize MGM sensor values"); + let mgm_request = MgmRequest::RequestSensorData; + sim_testbench.step_by(Duration::from_millis(50)); + + request = SimRequest::new(SimTarget::Mgm, mgm_request); + sim_testbench + .send_request(request) + .expect("sending MGM request failed"); + sim_testbench.handle_sim_requests(); + sim_testbench.step(); + sim_reply_res = sim_testbench.try_receive_next_reply(); + assert!(sim_reply_res.is_some()); + sim_reply = sim_reply_res.unwrap(); + + let second_reply: MgmReply = serde_json::from_str(sim_reply.reply()) + .expect("failed to deserialize MGM sensor values"); + // Check that the values are changing. + assert!(first_reply != second_reply); + } + + #[test] + fn test_basic_mgt_request_is_off() { + let mut sim_testbench = SimTestbench::new(); + let mgt_request = MgtRequest::RequestHk; + let request = SimRequest::new(SimTarget::Mgt, mgt_request); + sim_testbench + .send_request(request) + .expect("sending MGM request failed"); + sim_testbench.handle_sim_requests(); + sim_testbench.step(); + let sim_reply_res = sim_testbench.try_receive_next_reply(); + assert!(sim_reply_res.is_none()); + } + + #[test] + fn test_basic_mgt_request_is_on() { + let mut sim_testbench = SimTestbench::new(); + switch_device_on(&mut sim_testbench, PcduSwitch::Mgt); + let mgt_request = MgtRequest::RequestHk; + let request = SimRequest::new(SimTarget::Mgt, mgt_request); + sim_testbench + .send_request(request) + .expect("sending MGM request failed"); + sim_testbench.handle_sim_requests(); + sim_testbench.step(); + let sim_reply_res = sim_testbench.try_receive_next_reply(); + assert!(sim_reply_res.is_some()); + let sim_reply = sim_reply_res.unwrap(); + let mgt_reply: MgtReply = serde_json::from_str(sim_reply.reply()) + .expect("failed to deserialize MGM sensor values"); + match mgt_reply { + MgtReply::Hk(hk) => { + assert_eq!(hk.dipole, MgtDipole::default()); + assert!(!hk.torquing); + } + _ => panic!("unexpected reply"), + } + } + + fn check_mgt_hk(sim_testbench: &mut SimTestbench, expected_hk_set: MgtHkSet) { + let mgt_request = MgtRequest::RequestHk; + let request = SimRequest::new(SimTarget::Mgt, mgt_request); + sim_testbench + .send_request(request) + .expect("sending MGM request failed"); + sim_testbench.handle_sim_requests(); + sim_testbench.step(); + let sim_reply_res = sim_testbench.try_receive_next_reply(); + assert!(sim_reply_res.is_some()); + let sim_reply = sim_reply_res.unwrap(); + let mgt_reply: MgtReply = serde_json::from_str(sim_reply.reply()) + .expect("failed to deserialize MGM sensor values"); + match mgt_reply { + MgtReply::Hk(hk) => { + assert_eq!(hk, expected_hk_set); + } + _ => panic!("unexpected reply"), + } + } + + #[test] + fn test_basic_mgt_request_is_on_and_torquing() { + let mut sim_testbench = SimTestbench::new(); + switch_device_on(&mut sim_testbench, PcduSwitch::Mgt); + let commanded_dipole = MgtDipole { + x: -200, + y: 200, + z: 1000, + }; + let mgt_request = MgtRequest::ApplyTorque { + duration: Duration::from_millis(100), + dipole: commanded_dipole, + }; + let request = SimRequest::new(SimTarget::Mgt, mgt_request); + sim_testbench + .send_request(request) + .expect("sending MGM request failed"); + sim_testbench.handle_sim_requests(); + sim_testbench.step_by(Duration::from_millis(5)); + + check_mgt_hk( + &mut sim_testbench, + MgtHkSet { + dipole: commanded_dipole, + torquing: true, + }, + ); + sim_testbench.step_by(Duration::from_millis(100)); + check_mgt_hk( + &mut sim_testbench, + MgtHkSet { + dipole: MgtDipole::default(), + torquing: false, + }, + ); + } +} diff --git a/satrs-minisim/src/controller.rs b/satrs-minisim/src/controller.rs new file mode 100644 index 0000000..6560b92 --- /dev/null +++ b/satrs-minisim/src/controller.rs @@ -0,0 +1,197 @@ +use std::{sync::mpsc, time::Duration}; + +use asynchronix::{ + simulation::{Address, Simulation}, + time::{Clock, MonotonicTime, SystemClock}, +}; +use satrs_minisim::{ + acs::{MgmRequest, MgtRequest}, + eps::PcduRequest, + RequestError, SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimTarget, +}; + +use crate::{ + acs::{MagnetometerModel, MagnetorquerModel}, + eps::PcduModel, +}; + +// The simulation controller processes requests and drives the simulation. +pub struct SimController { + pub sys_clock: SystemClock, + pub request_receiver: mpsc::Receiver, + pub reply_sender: mpsc::Sender, + pub simulation: Simulation, + pub mgm_addr: Address, + pub pcdu_addr: Address, + pub mgt_addr: Address, +} + +impl SimController { + pub fn new( + sys_clock: SystemClock, + request_receiver: mpsc::Receiver, + reply_sender: mpsc::Sender, + simulation: Simulation, + mgm_addr: Address, + pcdu_addr: Address, + mgt_addr: Address, + ) -> Self { + Self { + sys_clock, + request_receiver, + reply_sender, + simulation, + mgm_addr, + pcdu_addr, + mgt_addr, + } + } + + pub fn run(&mut self, start_time: MonotonicTime, udp_polling_interval_ms: u64) { + let mut t = start_time + Duration::from_millis(udp_polling_interval_ms); + self.sys_clock.synchronize(t); + loop { + // Check for UDP requests every millisecond. Shift the simulator ahead here to prevent + // replies lying in the past. + t += Duration::from_millis(udp_polling_interval_ms); + self.simulation + .step_until(t) + .expect("simulation step failed"); + self.handle_sim_requests(); + + self.sys_clock.synchronize(t); + } + } + + pub fn handle_sim_requests(&mut self) { + loop { + match self.request_receiver.try_recv() { + Ok(request) => { + if let Err(e) = match request.target() { + SimTarget::SimCtrl => self.handle_ctrl_request(&request), + SimTarget::Mgm => self.handle_mgm_request(&request), + SimTarget::Mgt => self.handle_mgt_request(&request), + SimTarget::Pcdu => self.handle_pcdu_request(&request), + } { + self.handle_invalid_request_with_valid_target(e, &request) + } + } + Err(e) => match e { + mpsc::TryRecvError::Empty => break, + mpsc::TryRecvError::Disconnected => { + panic!("all request sender disconnected") + } + }, + } + } + } + + fn handle_ctrl_request(&mut self, request: &SimRequest) -> serde_json::Result<()> { + let sim_ctrl_request: SimCtrlRequest = serde_json::from_str(request.request())?; + match sim_ctrl_request { + SimCtrlRequest::Ping => { + self.reply_sender + .send(SimReply::new(SimTarget::SimCtrl, SimCtrlReply::Pong)) + .expect("sending reply from sim controller failed"); + } + } + Ok(()) + } + fn handle_mgm_request(&mut self, request: &SimRequest) -> serde_json::Result<()> { + let mgm_request: MgmRequest = serde_json::from_str(request.request())?; + match mgm_request { + MgmRequest::RequestSensorData => { + self.simulation.send_event( + MagnetometerModel::send_sensor_values, + (), + &self.mgm_addr, + ); + } + } + Ok(()) + } + + fn handle_pcdu_request(&mut self, request: &SimRequest) -> serde_json::Result<()> { + let pcdu_request: PcduRequest = serde_json::from_str(request.request())?; + match pcdu_request { + PcduRequest::RequestSwitchInfo => { + self.simulation + .send_event(PcduModel::request_switch_info, (), &self.pcdu_addr); + } + PcduRequest::SwitchDevice { switch, state } => { + self.simulation.send_event( + PcduModel::switch_device, + (switch, state), + &self.pcdu_addr, + ); + } + } + Ok(()) + } + + fn handle_mgt_request(&mut self, request: &SimRequest) -> serde_json::Result<()> { + let mgt_request: MgtRequest = serde_json::from_str(request.request())?; + match mgt_request { + MgtRequest::ApplyTorque { duration, dipole } => self.simulation.send_event( + MagnetorquerModel::apply_torque, + (duration, dipole), + &self.mgt_addr, + ), + MgtRequest::RequestHk => self.simulation.send_event( + MagnetorquerModel::request_housekeeping_data, + (), + &self.mgt_addr, + ), + } + Ok(()) + } + + fn handle_invalid_request_with_valid_target( + &self, + error: serde_json::Error, + request: &SimRequest, + ) { + log::warn!( + "received invalid {:?} request: {:?}", + request.target(), + error + ); + self.reply_sender + .send(SimReply::new( + SimTarget::SimCtrl, + SimCtrlReply::from(RequestError::TargetRequestMissmatch(request.clone())), + )) + .expect("sending reply from sim controller failed"); + } +} + +#[cfg(test)] +mod tests { + use crate::test_helpers::SimTestbench; + + use super::*; + + #[test] + fn test_basic_ping() { + let mut sim_testbench = SimTestbench::new(); + let sim_ctrl_request = SimCtrlRequest::Ping; + let request = SimRequest::new(SimTarget::SimCtrl, sim_ctrl_request); + sim_testbench + .send_request(request) + .expect("sending sim ctrl request failed"); + sim_testbench.handle_sim_requests(); + sim_testbench.step(); + let sim_reply = sim_testbench.try_receive_next_reply(); + assert!(sim_reply.is_some()); + let sim_reply = sim_reply.unwrap(); + assert_eq!(sim_reply.target(), SimTarget::SimCtrl); + let reply: SimCtrlReply = serde_json::from_str(sim_reply.reply()) + .expect("failed to deserialize MGM sensor values"); + assert_eq!(reply, SimCtrlReply::Pong); + } + + #[test] + fn test_invalid_request() { + // TODO: Implement this test. Check for the expected reply. + } +} diff --git a/satrs-minisim/src/eps.rs b/satrs-minisim/src/eps.rs new file mode 100644 index 0000000..6a14ae4 --- /dev/null +++ b/satrs-minisim/src/eps.rs @@ -0,0 +1,187 @@ +use std::{collections::HashMap, sync::mpsc, time::Duration}; + +use asynchronix::{ + model::{Model, Output}, + time::Scheduler, +}; +use satrs::power::SwitchStateBinary; +use satrs_minisim::{ + eps::{PcduReply, PcduSwitch, SwitchMap}, + SimReply, SimTarget, +}; + +pub const SWITCH_INFO_DELAY_MS: u64 = 10; + +pub struct PcduModel { + pub switcher_map: SwitchMap, + pub mgm_switch: Output, + pub mgt_switch: Output, + pub reply_sender: mpsc::Sender, +} + +impl PcduModel { + pub fn new(reply_sender: mpsc::Sender) -> Self { + let mut switcher_map = HashMap::new(); + switcher_map.insert(PcduSwitch::Mgm, SwitchStateBinary::Off); + switcher_map.insert(PcduSwitch::Mgt, SwitchStateBinary::Off); + + Self { + switcher_map, + mgm_switch: Output::new(), + mgt_switch: Output::new(), + reply_sender, + } + } + + pub async fn request_switch_info(&mut self, _: (), scheduler: &Scheduler) { + scheduler + .schedule_event( + Duration::from_millis(SWITCH_INFO_DELAY_MS), + Self::send_switch_info, + (), + ) + .expect("requesting switch info failed"); + } + + pub fn send_switch_info(&mut self) { + let switch_info = PcduReply::SwitchInfo(self.switcher_map.clone()); + let reply = SimReply::new(SimTarget::Pcdu, switch_info); + self.reply_sender.send(reply).unwrap(); + } + + pub async fn switch_device( + &mut self, + switch_and_target_state: (PcduSwitch, SwitchStateBinary), + ) { + let val = self + .switcher_map + .get_mut(&switch_and_target_state.0) + .unwrap_or_else(|| panic!("switch {:?} not found", switch_and_target_state.0)); + *val = switch_and_target_state.1; + match switch_and_target_state.0 { + PcduSwitch::Mgm => { + self.mgm_switch.send(switch_and_target_state.1).await; + } + PcduSwitch::Mgt => { + self.mgt_switch.send(switch_and_target_state.1).await; + } + } + } +} + +impl Model for PcduModel {} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use std::time::Duration; + + use satrs_minisim::{eps::PcduRequest, SimRequest, SimTarget}; + + use crate::test_helpers::SimTestbench; + + fn switch_device( + sim_testbench: &mut SimTestbench, + switch: PcduSwitch, + target: SwitchStateBinary, + ) { + let pcdu_request = PcduRequest::SwitchDevice { + switch, + state: target, + }; + let request = SimRequest::new(SimTarget::Pcdu, pcdu_request); + sim_testbench + .send_request(request) + .expect("sending MGM switch request failed"); + sim_testbench.handle_sim_requests(); + sim_testbench.step(); + } + + #[allow(dead_code)] + pub(crate) fn switch_device_off(sim_testbench: &mut SimTestbench, switch: PcduSwitch) { + switch_device(sim_testbench, switch, SwitchStateBinary::Off); + } + pub(crate) fn switch_device_on(sim_testbench: &mut SimTestbench, switch: PcduSwitch) { + switch_device(sim_testbench, switch, SwitchStateBinary::On); + } + + pub(crate) fn get_all_off_switch_map() -> SwitchMap { + let mut switcher_map = SwitchMap::new(); + switcher_map.insert(super::PcduSwitch::Mgm, super::SwitchStateBinary::Off); + switcher_map.insert(super::PcduSwitch::Mgt, super::SwitchStateBinary::Off); + switcher_map + } + + fn check_switch_state(sim_testbench: &mut SimTestbench, expected_switch_map: &SwitchMap) { + let pcdu_request = PcduRequest::RequestSwitchInfo; + let request = SimRequest::new(SimTarget::Pcdu, pcdu_request); + sim_testbench + .send_request(request) + .expect("sending MGM request failed"); + sim_testbench.handle_sim_requests(); + sim_testbench.step(); + let sim_reply = sim_testbench.try_receive_next_reply(); + assert!(sim_reply.is_some()); + let sim_reply = sim_reply.unwrap(); + assert_eq!(sim_reply.target(), SimTarget::Pcdu); + let pcdu_reply: PcduReply = serde_json::from_str(&sim_reply.reply()) + .expect("failed to deserialize PCDU switch info"); + match pcdu_reply { + PcduReply::SwitchInfo(switch_map) => { + assert_eq!(switch_map, *expected_switch_map); + } + } + } + + fn test_pcdu_switching_single_switch(switch: PcduSwitch, target: SwitchStateBinary) { + let mut sim_testbench = SimTestbench::new(); + switch_device(&mut sim_testbench, switch, target); + let mut switcher_map = get_all_off_switch_map(); + *switcher_map.get_mut(&switch).unwrap() = target; + check_switch_state(&mut sim_testbench, &switcher_map); + } + + #[test] + fn test_pcdu_switcher_request() { + let mut sim_testbench = SimTestbench::new(); + let pcdu_request = PcduRequest::RequestSwitchInfo; + let request = SimRequest::new(SimTarget::Pcdu, pcdu_request); + sim_testbench + .send_request(request) + .expect("sending MGM request failed"); + sim_testbench.handle_sim_requests(); + sim_testbench.step_by(Duration::from_millis(1)); + + let sim_reply = sim_testbench.try_receive_next_reply(); + assert!(sim_reply.is_none()); + // Reply takes 20ms + sim_testbench.step_by(Duration::from_millis(25)); + let sim_reply = sim_testbench.try_receive_next_reply(); + assert!(sim_reply.is_some()); + let sim_reply = sim_reply.unwrap(); + assert_eq!(sim_reply.target(), SimTarget::Pcdu); + let pcdu_reply: PcduReply = serde_json::from_str(&sim_reply.reply()) + .expect("failed to deserialize PCDU switch info"); + match pcdu_reply { + PcduReply::SwitchInfo(switch_map) => { + assert_eq!(switch_map, get_all_off_switch_map()); + } + } + } + + #[test] + fn test_pcdu_switching_mgm_on() { + test_pcdu_switching_single_switch(PcduSwitch::Mgm, SwitchStateBinary::On); + } + + #[test] + fn test_pcdu_switching_mgt_on() { + test_pcdu_switching_single_switch(PcduSwitch::Mgt, SwitchStateBinary::On); + } + + #[test] + fn test_pcdu_switching_mgt_off() { + test_pcdu_switching_single_switch(PcduSwitch::Mgt, SwitchStateBinary::On); + test_pcdu_switching_single_switch(PcduSwitch::Mgt, SwitchStateBinary::Off); + } +} diff --git a/satrs-minisim/src/lib.rs b/satrs-minisim/src/lib.rs new file mode 100644 index 0000000..342aad3 --- /dev/null +++ b/satrs-minisim/src/lib.rs @@ -0,0 +1,246 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SimTarget { + SimCtrl, + Mgm, + Mgt, + Pcdu, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SimRequest { + target: SimTarget, + request: String, +} + +impl SimRequest { + pub fn new(device: SimTarget, request: T) -> Self { + Self { + target: device, + request: serde_json::to_string(&request).unwrap(), + } + } + + pub fn target(&self) -> SimTarget { + self.target + } + + pub fn request(&self) -> &String { + &self.request + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SimReply { + target: SimTarget, + reply: String, +} + +impl SimReply { + pub fn new(device: SimTarget, reply: T) -> Self { + Self { + target: device, + reply: serde_json::to_string(&reply).unwrap(), + } + } + + pub fn target(&self) -> SimTarget { + self.target + } + + pub fn reply(&self) -> &String { + &self.reply + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SimCtrlRequest { + Ping, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum RequestError { + TargetRequestMissmatch(SimRequest), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SimCtrlReply { + Pong, + InvalidRequest(RequestError), +} + +impl From for SimCtrlReply { + fn from(error: RequestError) -> Self { + SimCtrlReply::InvalidRequest(error) + } +} + +pub mod eps { + use super::*; + use std::collections::HashMap; + + use satrs::power::SwitchStateBinary; + + pub type SwitchMap = HashMap; + + #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] + pub enum PcduSwitch { + Mgm = 0, + Mgt = 1, + } + + #[derive(Debug, Copy, Clone, Serialize, Deserialize)] + pub enum PcduRequest { + SwitchDevice { + switch: PcduSwitch, + state: SwitchStateBinary, + }, + RequestSwitchInfo, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub enum PcduReply { + SwitchInfo(SwitchMap), + } +} + +pub mod acs { + use std::time::Duration; + + use satrs::power::SwitchStateBinary; + + use super::*; + + #[derive(Debug, Copy, Clone, Serialize, Deserialize)] + pub enum MgmRequest { + RequestSensorData, + } + + // Normally, small magnetometers generate their output as a signed 16 bit raw format or something + // similar which needs to be converted to a signed float value with physical units. We will + // simplify this now and generate the signed float values directly. + #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] + pub struct MgmSensorValues { + pub x: f32, + pub y: f32, + pub z: f32, + } + + #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] + pub struct MgmReply { + pub switch_state: SwitchStateBinary, + pub sensor_values: MgmSensorValues, + } + + pub const MGT_GEN_MAGNETIC_FIELD: MgmSensorValues = MgmSensorValues { + x: 0.03, + y: -0.03, + z: 0.03, + }; + + // Simple model using i16 values. + #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct MgtDipole { + pub x: i16, + pub y: i16, + pub z: i16, + } + + #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] + pub enum MgtRequestType { + ApplyTorque, + } + + #[derive(Debug, Copy, Clone, Serialize, Deserialize)] + pub enum MgtRequest { + ApplyTorque { + duration: Duration, + dipole: MgtDipole, + }, + RequestHk, + } + + #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct MgtHkSet { + pub dipole: MgtDipole, + pub torquing: bool, + } + + #[derive(Debug, Copy, Clone, Serialize, Deserialize)] + pub enum MgtReply { + Ack(MgtRequestType), + Nak(MgtRequestType), + Hk(MgtHkSet), + } +} + +pub mod udp { + use std::{ + net::{SocketAddr, UdpSocket}, + time::Duration, + }; + + use thiserror::Error; + + use crate::{SimReply, SimRequest}; + + #[derive(Error, Debug)] + pub enum ReceptionError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Serde JSON error: {0}")] + SerdeJson(#[from] serde_json::Error), + } + + pub struct SimUdpClient { + socket: UdpSocket, + pub reply_buf: [u8; 4096], + } + + impl SimUdpClient { + pub fn new( + server_addr: &SocketAddr, + non_blocking: bool, + read_timeot_ms: Option, + ) -> std::io::Result { + let socket = UdpSocket::bind("127.0.0.1:0")?; + socket.set_nonblocking(non_blocking)?; + socket + .connect(server_addr) + .expect("could not connect to server addr"); + if let Some(read_timeout) = read_timeot_ms { + // Set a read timeout so the test does not hang on failures. + socket.set_read_timeout(Some(Duration::from_millis(read_timeout)))?; + } + Ok(Self { + socket, + reply_buf: [0; 4096], + }) + } + + pub fn set_nonblocking(&self, non_blocking: bool) -> std::io::Result<()> { + self.socket.set_nonblocking(non_blocking) + } + + pub fn set_read_timeout(&self, read_timeout_ms: u64) -> std::io::Result<()> { + self.socket + .set_read_timeout(Some(Duration::from_millis(read_timeout_ms))) + } + + pub fn send_request(&self, sim_request: &SimRequest) -> std::io::Result { + self.socket.send( + &serde_json::to_vec(sim_request).expect("conversion of request to vector failed"), + ) + } + + pub fn recv_raw(&mut self) -> std::io::Result { + self.socket.recv(&mut self.reply_buf) + } + + pub fn recv_sim_reply(&mut self) -> Result { + let read_len = self.recv_raw()?; + Ok(serde_json::from_slice(&self.reply_buf[0..read_len])?) + } + } +} diff --git a/satrs-minisim/src/main.rs b/satrs-minisim/src/main.rs new file mode 100644 index 0000000..bfa4a26 --- /dev/null +++ b/satrs-minisim/src/main.rs @@ -0,0 +1,103 @@ +use acs::{MagnetometerModel, MagnetorquerModel}; +use asynchronix::simulation::{Mailbox, SimInit}; +use asynchronix::time::{MonotonicTime, SystemClock}; +use controller::SimController; +use eps::PcduModel; +use satrs_minisim::{SimReply, SimRequest}; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, SystemTime}; +use udp::SimUdpServer; + +mod acs; +mod controller; +mod eps; +#[cfg(test)] +mod test_helpers; +mod time; +mod udp; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ThreadingModel { + Default = 0, + Single = 1, +} + +fn create_sim_controller( + threading_model: ThreadingModel, + start_time: MonotonicTime, + reply_sender: mpsc::Sender, + request_receiver: mpsc::Receiver, +) -> SimController { + // Instantiate models and their mailboxes. + let mgm_model = MagnetometerModel::new(Duration::from_millis(50), reply_sender.clone()); + + let mgm_mailbox = Mailbox::new(); + let mgm_addr = mgm_mailbox.address(); + let pcdu_mailbox = Mailbox::new(); + let pcdu_addr = pcdu_mailbox.address(); + let mgt_mailbox = Mailbox::new(); + let mgt_addr = mgt_mailbox.address(); + + let mut pcdu_model = PcduModel::new(reply_sender.clone()); + pcdu_model + .mgm_switch + .connect(MagnetometerModel::switch_device, &mgm_addr); + + let mut mgt_model = MagnetorquerModel::new(reply_sender.clone()); + // Input connections. + pcdu_model + .mgt_switch + .connect(MagnetorquerModel::switch_device, &mgt_addr); + // Output connections. + mgt_model + .gen_magnetic_field + .connect(MagnetometerModel::apply_external_magnetic_field, &mgm_addr); + + // Instantiate the simulator + let sys_clock = SystemClock::from_system_time(start_time, SystemTime::now()); + let sim_init = if threading_model == ThreadingModel::Single { + SimInit::with_num_threads(1) + } else { + SimInit::new() + }; + let simulation = sim_init + .add_model(mgm_model, mgm_mailbox) + .add_model(pcdu_model, pcdu_mailbox) + .add_model(mgt_model, mgt_mailbox) + .init(start_time); + SimController::new( + sys_clock, + request_receiver, + reply_sender, + simulation, + mgm_addr, + pcdu_addr, + mgt_addr, + ) +} + +fn main() { + let (request_sender, request_receiver) = mpsc::channel(); + let (reply_sender, reply_receiver) = mpsc::channel(); + let t0 = MonotonicTime::EPOCH; + let mut sim_ctrl = + create_sim_controller(ThreadingModel::Default, t0, reply_sender, request_receiver); + + // This thread schedules the simulator. + let sim_thread = thread::spawn(move || { + sim_ctrl.run(t0, 1); + }); + + let mut udp_server = SimUdpServer::new(0, request_sender, reply_receiver, 200, None) + .expect("could not create UDP request server"); + // This thread manages the simulator UDP server. + let udp_tc_thread = thread::spawn(move || { + udp_server.run(); + }); + + sim_thread.join().expect("joining simulation thread failed"); + udp_tc_thread + .join() + .expect("joining UDP server thread failed"); +} diff --git a/satrs-minisim/src/test_helpers.rs b/satrs-minisim/src/test_helpers.rs new file mode 100644 index 0000000..a8bf0a1 --- /dev/null +++ b/satrs-minisim/src/test_helpers.rs @@ -0,0 +1,56 @@ +use delegate::delegate; +use std::{sync::mpsc, time::Duration}; + +use asynchronix::time::MonotonicTime; +use satrs_minisim::{SimReply, SimRequest}; + +use crate::{controller::SimController, create_sim_controller, ThreadingModel}; + +pub struct SimTestbench { + pub sim_controller: SimController, + pub reply_receiver: mpsc::Receiver, + pub request_sender: mpsc::Sender, +} + +impl SimTestbench { + pub fn new() -> Self { + let (request_sender, request_receiver) = mpsc::channel(); + let (reply_sender, reply_receiver) = mpsc::channel(); + let t0 = MonotonicTime::EPOCH; + let sim_ctrl = + create_sim_controller(ThreadingModel::Single, t0, reply_sender, request_receiver); + + Self { + sim_controller: sim_ctrl, + reply_receiver, + request_sender, + } + } + + delegate! { + to self.sim_controller { + pub fn handle_sim_requests(&mut self); + } + to self.sim_controller.simulation { + pub fn step(&mut self); + pub fn step_by(&mut self, duration: Duration); + } + } + + pub fn send_request(&self, request: SimRequest) -> Result<(), mpsc::SendError> { + self.request_sender.send(request) + } + + pub fn try_receive_next_reply(&self) -> Option { + match self.reply_receiver.try_recv() { + Ok(reply) => Some(reply), + Err(e) => { + if e == mpsc::TryRecvError::Empty { + None + } else { + panic!("reply_receiver disconnected"); + } + } + } + } +} diff --git a/satrs-minisim/src/time.rs b/satrs-minisim/src/time.rs new file mode 100644 index 0000000..63ae327 --- /dev/null +++ b/satrs-minisim/src/time.rs @@ -0,0 +1,5 @@ +use asynchronix::time::MonotonicTime; + +pub fn current_millis(time: MonotonicTime) -> u64 { + (time.as_secs() as u64 * 1000) + (time.subsec_nanos() as u64 / 1_000_000) +} diff --git a/satrs-minisim/src/udp.rs b/satrs-minisim/src/udp.rs new file mode 100644 index 0000000..7958540 --- /dev/null +++ b/satrs-minisim/src/udp.rs @@ -0,0 +1,403 @@ +use std::{ + collections::VecDeque, + io::ErrorKind, + net::{SocketAddr, UdpSocket}, + sync::{atomic::AtomicBool, mpsc, Arc}, + time::Duration, +}; + +use satrs_minisim::{SimReply, SimRequest}; + +// A UDP server which handles all TC received by a client application. +pub struct SimUdpServer { + socket: UdpSocket, + request_sender: mpsc::Sender, + // shared_last_sender: SharedSocketAddr, + reply_receiver: mpsc::Receiver, + reply_queue: VecDeque, + max_num_replies: usize, + // Stop signal to stop the server. Required for unittests and useful to allow clean shutdown + // of the application. + stop_signal: Option>, + idle_sleep_period_ms: u64, + req_buf: [u8; 4096], + sender_addr: Option, +} + +impl SimUdpServer { + pub fn new( + local_port: u16, + request_sender: mpsc::Sender, + reply_receiver: mpsc::Receiver, + max_num_replies: usize, + stop_signal: Option>, + ) -> std::io::Result { + let socket = UdpSocket::bind(SocketAddr::from(([0, 0, 0, 0], local_port)))?; + socket.set_nonblocking(true)?; + Ok(Self { + socket, + request_sender, + reply_receiver, + reply_queue: VecDeque::new(), + max_num_replies, + stop_signal, + idle_sleep_period_ms: 3, + req_buf: [0; 4096], + sender_addr: None, + }) + } + + #[allow(dead_code)] + pub fn server_addr(&self) -> std::io::Result { + self.socket.local_addr() + } + + pub fn run(&mut self) { + loop { + if let Some(stop_signal) = &self.stop_signal { + if stop_signal.load(std::sync::atomic::Ordering::Relaxed) { + break; + } + } + let processed_requests = self.process_requests(); + let processed_replies = self.process_replies(); + let sent_replies = self.send_replies(); + // Sleep for a bit if there is nothing to do to prevent burning CPU cycles. Delay + // should be kept short to ensure responsiveness of the system. + if !processed_requests && !processed_replies && !sent_replies { + std::thread::sleep(Duration::from_millis(self.idle_sleep_period_ms)); + } + } + } + + fn process_requests(&mut self) -> bool { + let mut processed_requests = false; + loop { + // Blocks for a certain amount of time until data is received to allow doing periodic + // work like checking the stop signal. + let (bytes_read, src) = match self.socket.recv_from(&mut self.req_buf) { + Ok((bytes_read, src)) => (bytes_read, src), + Err(e) if e.kind() == ErrorKind::WouldBlock => { + // Continue to perform regular checks like the stop signal. + break; + } + Err(e) => { + // Handle unexpected errors (e.g., socket closed) here. + log::error!("unexpected request server error: {e}"); + break; + } + }; + + self.sender_addr = Some(src); + + // Convert the buffer into a string slice and print the message. + let req_string = std::str::from_utf8(&self.req_buf[..bytes_read]) + .expect("Could not write buffer as string"); + log::info!("Received request from {}: {}", src, req_string); + let sim_req: serde_json::Result = serde_json::from_str(req_string); + if sim_req.is_err() { + log::warn!( + "received UDP request with invalid format: {}", + sim_req.unwrap_err() + ); + return processed_requests; + } + self.request_sender.send(sim_req.unwrap()).unwrap(); + processed_requests = true; + } + processed_requests + } + + fn process_replies(&mut self) -> bool { + let mut processed_replies = false; + loop { + match self.reply_receiver.try_recv() { + Ok(reply) => { + if self.reply_queue.len() >= self.max_num_replies { + self.reply_queue.pop_front(); + } + self.reply_queue.push_back(reply); + processed_replies = true; + } + Err(e) => match e { + mpsc::TryRecvError::Empty => return processed_replies, + mpsc::TryRecvError::Disconnected => { + log::error!("all UDP reply senders disconnected") + } + }, + } + } + } + + fn send_replies(&mut self) -> bool { + if self.sender_addr.is_none() { + return false; + } + let mut sent_replies = false; + while !self.reply_queue.is_empty() { + let next_reply_to_send = self.reply_queue.pop_front().unwrap(); + self.socket + .send_to( + serde_json::to_string(&next_reply_to_send) + .unwrap() + .as_bytes(), + self.sender_addr.unwrap(), + ) + .expect("sending reply failed"); + sent_replies = true; + } + sent_replies + } +} + +#[cfg(test)] +mod tests { + use std::{ + io::ErrorKind, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc, Arc, + }, + time::Duration, + }; + + use satrs_minisim::{ + eps::{PcduReply, PcduRequest}, + udp::{ReceptionError, SimUdpClient}, + SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimTarget, + }; + + use crate::eps::tests::get_all_off_switch_map; + use delegate::delegate; + + use super::SimUdpServer; + + // Wait time to ensure even possibly laggy systems like CI servers can run the tests. + const SERVER_WAIT_TIME_MS: u64 = 50; + + struct UdpTestbench { + client: SimUdpClient, + stop_signal: Arc, + request_receiver: mpsc::Receiver, + reply_sender: mpsc::Sender, + } + + impl UdpTestbench { + pub fn new( + client_non_blocking: bool, + client_read_timeout_ms: Option, + max_num_replies: usize, + ) -> std::io::Result<(Self, SimUdpServer)> { + let (request_sender, request_receiver) = mpsc::channel(); + let (reply_sender, reply_receiver) = mpsc::channel(); + let stop_signal = Arc::new(AtomicBool::new(false)); + let server = SimUdpServer::new( + 0, + request_sender, + reply_receiver, + max_num_replies, + Some(stop_signal.clone()), + )?; + let server_addr = server.server_addr()?; + Ok(( + Self { + client: SimUdpClient::new( + &server_addr, + client_non_blocking, + client_read_timeout_ms, + )?, + stop_signal, + request_receiver, + reply_sender, + }, + server, + )) + } + + pub fn try_recv_request(&self) -> Result { + self.request_receiver.try_recv() + } + + pub fn stop(&self) { + self.stop_signal.store(true, Ordering::Relaxed); + } + + pub fn send_reply(&self, sim_reply: &SimReply) { + self.reply_sender + .send(sim_reply.clone()) + .expect("sending sim reply failed"); + } + + delegate! { + to self.client { + pub fn send_request(&self, sim_request: &SimRequest) -> std::io::Result; + pub fn recv_sim_reply(&mut self) -> Result; + } + } + + pub fn check_no_sim_reply_available(&mut self) { + if let Err(ReceptionError::Io(ref io_error)) = self.recv_sim_reply() { + if io_error.kind() == ErrorKind::WouldBlock { + // Continue to perform regular checks like the stop signal. + return; + } else { + // Handle unexpected errors (e.g., socket closed) here. + panic!("unexpected request server error: {io_error}"); + } + } + panic!("unexpected reply available"); + } + + pub fn check_next_sim_reply(&mut self, expected_reply: &SimReply) { + match self.recv_sim_reply() { + Ok(received_sim_reply) => assert_eq!(expected_reply, &received_sim_reply), + Err(e) => match e { + ReceptionError::Io(ref io_error) => { + if io_error.kind() == ErrorKind::WouldBlock { + // Continue to perform regular checks like the stop signal. + panic!("no simulation reply received"); + } else { + // Handle unexpected errors (e.g., socket closed) here. + panic!("unexpected request server error: {e}"); + } + } + ReceptionError::SerdeJson(json_error) => { + panic!("unexpected JSON error: {json_error}"); + } + }, + } + } + } + #[test] + fn test_basic_udp_request_reception() { + let (udp_testbench, mut udp_server) = + UdpTestbench::new(true, Some(SERVER_WAIT_TIME_MS), 10) + .expect("could not create testbench"); + let server_thread = std::thread::spawn(move || udp_server.run()); + let sim_request = SimRequest::new(SimTarget::Pcdu, PcduRequest::RequestSwitchInfo); + udp_testbench + .send_request(&sim_request) + .expect("sending request failed"); + std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS)); + // Check that the sim request has arrives and was forwarded. + let received_sim_request = udp_testbench + .try_recv_request() + .expect("did not receive request"); + assert_eq!(sim_request, received_sim_request); + // Stop the server. + udp_testbench.stop(); + server_thread.join().unwrap(); + } + + #[test] + fn test_udp_reply_server() { + let (mut udp_testbench, mut udp_server) = + UdpTestbench::new(false, Some(SERVER_WAIT_TIME_MS), 10) + .expect("could not create testbench"); + let server_thread = std::thread::spawn(move || udp_server.run()); + udp_testbench + .send_request(&SimRequest::new(SimTarget::SimCtrl, SimCtrlRequest::Ping)) + .expect("sending request failed"); + + let sim_reply = SimReply::new( + SimTarget::Pcdu, + PcduReply::SwitchInfo(get_all_off_switch_map()), + ); + udp_testbench.send_reply(&sim_reply); + + udp_testbench.check_next_sim_reply(&sim_reply); + + // Stop the server. + udp_testbench.stop(); + server_thread.join().unwrap(); + } + + #[test] + fn test_udp_req_server_and_reply_sender() { + let (mut udp_testbench, mut udp_server) = + UdpTestbench::new(false, Some(SERVER_WAIT_TIME_MS), 10) + .expect("could not create testbench"); + + let server_thread = std::thread::spawn(move || udp_server.run()); + + // Send a ping so that the server knows the address of the client. + // Do not check that the request arrives on the receiver side, is done by other test. + udp_testbench + .send_request(&SimRequest::new(SimTarget::SimCtrl, SimCtrlRequest::Ping)) + .expect("sending request failed"); + + // Send a reply to the server, ensure it gets forwarded to the client. + let sim_reply = SimReply::new( + SimTarget::Pcdu, + PcduReply::SwitchInfo(get_all_off_switch_map()), + ); + udp_testbench.send_reply(&sim_reply); + std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS)); + + // Now we check that the reply server can send back replies to the client. + udp_testbench.check_next_sim_reply(&sim_reply); + + udp_testbench.stop(); + server_thread.join().unwrap(); + } + + #[test] + fn test_udp_replies_client_unconnected() { + let (mut udp_testbench, mut udp_server) = + UdpTestbench::new(true, None, 10).expect("could not create testbench"); + + let server_thread = std::thread::spawn(move || udp_server.run()); + + // Send a reply to the server. The client is not connected, so it won't get forwarded. + let sim_reply = SimReply::new( + SimTarget::Pcdu, + PcduReply::SwitchInfo(get_all_off_switch_map()), + ); + udp_testbench.send_reply(&sim_reply); + std::thread::sleep(Duration::from_millis(10)); + + udp_testbench.check_no_sim_reply_available(); + + // Connect by sending a ping. + udp_testbench + .send_request(&SimRequest::new(SimTarget::SimCtrl, SimCtrlRequest::Ping)) + .expect("sending request failed"); + std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS)); + + udp_testbench.check_next_sim_reply(&sim_reply); + + // Now we check that the reply server can sent back replies to the client. + udp_testbench.stop(); + server_thread.join().unwrap(); + } + + #[test] + fn test_udp_reply_server_old_replies_overwritten() { + let (mut udp_testbench, mut udp_server) = + UdpTestbench::new(true, None, 3).expect("could not create testbench"); + + let server_thread = std::thread::spawn(move || udp_server.run()); + + // The server only caches up to 3 replies. + let sim_reply = SimReply::new(SimTarget::SimCtrl, SimCtrlReply::Pong); + for _ in 0..4 { + udp_testbench.send_reply(&sim_reply); + } + std::thread::sleep(Duration::from_millis(20)); + + udp_testbench.check_no_sim_reply_available(); + + // Connect by sending a ping. + udp_testbench + .send_request(&SimRequest::new(SimTarget::SimCtrl, SimCtrlRequest::Ping)) + .expect("sending request failed"); + std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS)); + + for _ in 0..3 { + udp_testbench.check_next_sim_reply(&sim_reply); + } + udp_testbench.check_no_sim_reply_available(); + udp_testbench.stop(); + server_thread.join().unwrap(); + } +} diff --git a/satrs/src/power.rs b/satrs/src/power.rs index 1675c01..1e1fda1 100644 --- a/satrs/src/power.rs +++ b/satrs/src/power.rs @@ -24,6 +24,42 @@ pub enum SwitchState { Faulty = 3, } +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum SwitchStateBinary { + Off = 0, + On = 1, +} + +impl TryFrom for SwitchStateBinary { + type Error = (); + fn try_from(value: SwitchState) -> Result { + match value { + SwitchState::Off => Ok(SwitchStateBinary::Off), + SwitchState::On => Ok(SwitchStateBinary::On), + _ => Err(()), + } + } +} + +impl> From for SwitchStateBinary { + fn from(value: T) -> Self { + if value.into() == 0 { + return SwitchStateBinary::Off; + } + SwitchStateBinary::On + } +} + +impl From for SwitchState { + fn from(value: SwitchStateBinary) -> Self { + match value { + SwitchStateBinary::Off => SwitchState::Off, + SwitchStateBinary::On => SwitchState::On, + } + } +} + pub type SwitchId = u16; /// Generic trait for a device capable of turning on and off switches. From d3fb5045458b5c18ad8aad919e2fe7222e944db4 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 9 Mar 2024 15:14:15 +0100 Subject: [PATCH 2/3] clean up manifest file --- satrs-minisim/Cargo.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/satrs-minisim/Cargo.toml b/satrs-minisim/Cargo.toml index 3d6d904..cb8e29c 100644 --- a/satrs-minisim/Cargo.toml +++ b/satrs-minisim/Cargo.toml @@ -13,9 +13,6 @@ thiserror = "1" [dependencies.asynchronix] version = "0.2.1" -# path = "../../asynchronix/asynchronix" -# git = "https://github.com/us-irs/asynchronix.git" -# branch = "clock-not-sendable" [dependencies.satrs] path = "../satrs" From 7387be3bc3bbcd16e32df36fcf9860af146cfae2 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 11 Mar 2024 10:26:48 +0100 Subject: [PATCH 3/3] new request/reponse API --- satrs-minisim/src/acs.rs | 59 ++++----- satrs-minisim/src/controller.rs | 38 +++--- satrs-minisim/src/eps.rs | 24 ++-- satrs-minisim/src/lib.rs | 205 ++++++++++++++++++++++++++------ satrs-minisim/src/udp.rs | 37 ++---- 5 files changed, 233 insertions(+), 130 deletions(-) diff --git a/satrs-minisim/src/acs.rs b/satrs-minisim/src/acs.rs index cd0e4f8..141840d 100644 --- a/satrs-minisim/src/acs.rs +++ b/satrs-minisim/src/acs.rs @@ -7,7 +7,7 @@ use asynchronix::{ use satrs::power::SwitchStateBinary; use satrs_minisim::{ acs::{MgmReply, MgmSensorValues, MgtDipole, MgtHkSet, MgtReply, MGT_GEN_MAGNETIC_FIELD}, - SimReply, SimTarget, + SimReply, }; use crate::time::current_millis; @@ -54,14 +54,10 @@ impl MagnetometerModel { pub async fn send_sensor_values(&mut self, _: (), scheduler: &Scheduler) { self.reply_sender - .send(SimReply::new( - SimTarget::Mgm, - MgmReply { - switch_state: self.switch_state, - sensor_values: self - .calculate_current_mgm_tuple(current_millis(scheduler.time())), - }, - )) + .send(SimReply::new(MgmReply { + switch_state: self.switch_state, + sensor_values: self.calculate_current_mgm_tuple(current_millis(scheduler.time())), + })) .expect("sending MGM sensor values failed"); } @@ -149,12 +145,11 @@ impl MagnetorquerModel { } pub fn send_housekeeping_data(&mut self) { - let mgt_reply = MgtReply::Hk(MgtHkSet { - dipole: self.torque_dipole, - torquing: self.torquing, - }); self.reply_sender - .send(SimReply::new(SimTarget::Mgt, mgt_reply)) + .send(SimReply::new(MgtReply::Hk(MgtHkSet { + dipole: self.torque_dipole, + torquing: self.torquing, + }))) .unwrap(); } @@ -186,7 +181,7 @@ pub mod tests { use satrs_minisim::{ acs::{MgmReply, MgmRequest, MgtDipole, MgtHkSet, MgtReply, MgtRequest}, eps::PcduSwitch, - SimRequest, SimTarget, + SerializableSimMsgPayload, SimMessageProvider, SimRequest, SimTarget, }; use crate::{eps::tests::switch_device_on, test_helpers::SimTestbench}; @@ -194,8 +189,7 @@ pub mod tests { #[test] fn test_basic_mgm_request() { let mut sim_testbench = SimTestbench::new(); - let mgm_request = MgmRequest::RequestSensorData; - let request = SimRequest::new(SimTarget::Mgm, mgm_request); + let request = SimRequest::new(MgmRequest::RequestSensorData); sim_testbench .send_request(request) .expect("sending MGM request failed"); @@ -205,7 +199,7 @@ pub mod tests { assert!(sim_reply.is_some()); let sim_reply = sim_reply.unwrap(); assert_eq!(sim_reply.target(), SimTarget::Mgm); - let reply: MgmReply = serde_json::from_str(sim_reply.reply()) + let reply = MgmReply::from_sim_message(&sim_reply) .expect("failed to deserialize MGM sensor values"); assert_eq!(reply.switch_state, SwitchStateBinary::Off); assert_eq!(reply.sensor_values.x, 0.0); @@ -218,8 +212,7 @@ pub mod tests { let mut sim_testbench = SimTestbench::new(); switch_device_on(&mut sim_testbench, PcduSwitch::Mgm); - let mgm_request = MgmRequest::RequestSensorData; - let mut request = SimRequest::new(SimTarget::Mgm, mgm_request); + let mut request = SimRequest::new(MgmRequest::RequestSensorData); sim_testbench .send_request(request) .expect("sending MGM request failed"); @@ -229,12 +222,11 @@ pub mod tests { assert!(sim_reply_res.is_some()); let mut sim_reply = sim_reply_res.unwrap(); assert_eq!(sim_reply.target(), SimTarget::Mgm); - let first_reply: MgmReply = serde_json::from_str(sim_reply.reply()) + let first_reply = MgmReply::from_sim_message(&sim_reply) .expect("failed to deserialize MGM sensor values"); - let mgm_request = MgmRequest::RequestSensorData; sim_testbench.step_by(Duration::from_millis(50)); - request = SimRequest::new(SimTarget::Mgm, mgm_request); + request = SimRequest::new(MgmRequest::RequestSensorData); sim_testbench .send_request(request) .expect("sending MGM request failed"); @@ -244,7 +236,7 @@ pub mod tests { assert!(sim_reply_res.is_some()); sim_reply = sim_reply_res.unwrap(); - let second_reply: MgmReply = serde_json::from_str(sim_reply.reply()) + let second_reply = MgmReply::from_sim_message(&sim_reply) .expect("failed to deserialize MGM sensor values"); // Check that the values are changing. assert!(first_reply != second_reply); @@ -253,8 +245,7 @@ pub mod tests { #[test] fn test_basic_mgt_request_is_off() { let mut sim_testbench = SimTestbench::new(); - let mgt_request = MgtRequest::RequestHk; - let request = SimRequest::new(SimTarget::Mgt, mgt_request); + let request = SimRequest::new(MgtRequest::RequestHk); sim_testbench .send_request(request) .expect("sending MGM request failed"); @@ -268,8 +259,8 @@ pub mod tests { fn test_basic_mgt_request_is_on() { let mut sim_testbench = SimTestbench::new(); switch_device_on(&mut sim_testbench, PcduSwitch::Mgt); - let mgt_request = MgtRequest::RequestHk; - let request = SimRequest::new(SimTarget::Mgt, mgt_request); + let request = SimRequest::new(MgtRequest::RequestHk); + sim_testbench .send_request(request) .expect("sending MGM request failed"); @@ -278,7 +269,7 @@ pub mod tests { let sim_reply_res = sim_testbench.try_receive_next_reply(); assert!(sim_reply_res.is_some()); let sim_reply = sim_reply_res.unwrap(); - let mgt_reply: MgtReply = serde_json::from_str(sim_reply.reply()) + let mgt_reply = MgtReply::from_sim_message(&sim_reply) .expect("failed to deserialize MGM sensor values"); match mgt_reply { MgtReply::Hk(hk) => { @@ -290,8 +281,7 @@ pub mod tests { } fn check_mgt_hk(sim_testbench: &mut SimTestbench, expected_hk_set: MgtHkSet) { - let mgt_request = MgtRequest::RequestHk; - let request = SimRequest::new(SimTarget::Mgt, mgt_request); + let request = SimRequest::new(MgtRequest::RequestHk); sim_testbench .send_request(request) .expect("sending MGM request failed"); @@ -300,7 +290,7 @@ pub mod tests { let sim_reply_res = sim_testbench.try_receive_next_reply(); assert!(sim_reply_res.is_some()); let sim_reply = sim_reply_res.unwrap(); - let mgt_reply: MgtReply = serde_json::from_str(sim_reply.reply()) + let mgt_reply = MgtReply::from_sim_message(&sim_reply) .expect("failed to deserialize MGM sensor values"); match mgt_reply { MgtReply::Hk(hk) => { @@ -319,11 +309,10 @@ pub mod tests { y: 200, z: 1000, }; - let mgt_request = MgtRequest::ApplyTorque { + let request = SimRequest::new(MgtRequest::ApplyTorque { duration: Duration::from_millis(100), dipole: commanded_dipole, - }; - let request = SimRequest::new(SimTarget::Mgt, mgt_request); + }); sim_testbench .send_request(request) .expect("sending MGM request failed"); diff --git a/satrs-minisim/src/controller.rs b/satrs-minisim/src/controller.rs index 6560b92..d725e45 100644 --- a/satrs-minisim/src/controller.rs +++ b/satrs-minisim/src/controller.rs @@ -7,7 +7,8 @@ use asynchronix::{ use satrs_minisim::{ acs::{MgmRequest, MgtRequest}, eps::PcduRequest, - RequestError, SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimTarget, + SerializableSimMsgPayload, SimCtrlReply, SimCtrlRequest, SimMessageProvider, SimReply, + SimRequest, SimRequestError, SimTarget, }; use crate::{ @@ -86,19 +87,19 @@ impl SimController { } } - fn handle_ctrl_request(&mut self, request: &SimRequest) -> serde_json::Result<()> { - let sim_ctrl_request: SimCtrlRequest = serde_json::from_str(request.request())?; + fn handle_ctrl_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> { + let sim_ctrl_request = SimCtrlRequest::from_sim_message(request)?; match sim_ctrl_request { SimCtrlRequest::Ping => { self.reply_sender - .send(SimReply::new(SimTarget::SimCtrl, SimCtrlReply::Pong)) + .send(SimReply::new(SimCtrlReply::Pong)) .expect("sending reply from sim controller failed"); } } Ok(()) } - fn handle_mgm_request(&mut self, request: &SimRequest) -> serde_json::Result<()> { - let mgm_request: MgmRequest = serde_json::from_str(request.request())?; + fn handle_mgm_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> { + let mgm_request = MgmRequest::from_sim_message(request)?; match mgm_request { MgmRequest::RequestSensorData => { self.simulation.send_event( @@ -111,8 +112,8 @@ impl SimController { Ok(()) } - fn handle_pcdu_request(&mut self, request: &SimRequest) -> serde_json::Result<()> { - let pcdu_request: PcduRequest = serde_json::from_str(request.request())?; + fn handle_pcdu_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> { + let pcdu_request = PcduRequest::from_sim_message(request)?; match pcdu_request { PcduRequest::RequestSwitchInfo => { self.simulation @@ -129,8 +130,8 @@ impl SimController { Ok(()) } - fn handle_mgt_request(&mut self, request: &SimRequest) -> serde_json::Result<()> { - let mgt_request: MgtRequest = serde_json::from_str(request.request())?; + fn handle_mgt_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> { + let mgt_request = MgtRequest::from_sim_message(request)?; match mgt_request { MgtRequest::ApplyTorque { duration, dipole } => self.simulation.send_event( MagnetorquerModel::apply_torque, @@ -148,7 +149,7 @@ impl SimController { fn handle_invalid_request_with_valid_target( &self, - error: serde_json::Error, + error: SimRequestError, request: &SimRequest, ) { log::warn!( @@ -157,10 +158,7 @@ impl SimController { error ); self.reply_sender - .send(SimReply::new( - SimTarget::SimCtrl, - SimCtrlReply::from(RequestError::TargetRequestMissmatch(request.clone())), - )) + .send(SimReply::new(SimCtrlReply::from(error))) .expect("sending reply from sim controller failed"); } } @@ -174,8 +172,7 @@ mod tests { #[test] fn test_basic_ping() { let mut sim_testbench = SimTestbench::new(); - let sim_ctrl_request = SimCtrlRequest::Ping; - let request = SimRequest::new(SimTarget::SimCtrl, sim_ctrl_request); + let request = SimRequest::new(SimCtrlRequest::Ping); sim_testbench .send_request(request) .expect("sending sim ctrl request failed"); @@ -185,13 +182,8 @@ mod tests { assert!(sim_reply.is_some()); let sim_reply = sim_reply.unwrap(); assert_eq!(sim_reply.target(), SimTarget::SimCtrl); - let reply: SimCtrlReply = serde_json::from_str(sim_reply.reply()) + let reply = SimCtrlReply::from_sim_message(&sim_reply) .expect("failed to deserialize MGM sensor values"); assert_eq!(reply, SimCtrlReply::Pong); } - - #[test] - fn test_invalid_request() { - // TODO: Implement this test. Check for the expected reply. - } } diff --git a/satrs-minisim/src/eps.rs b/satrs-minisim/src/eps.rs index 6a14ae4..2873348 100644 --- a/satrs-minisim/src/eps.rs +++ b/satrs-minisim/src/eps.rs @@ -7,7 +7,7 @@ use asynchronix::{ use satrs::power::SwitchStateBinary; use satrs_minisim::{ eps::{PcduReply, PcduSwitch, SwitchMap}, - SimReply, SimTarget, + SimReply, }; pub const SWITCH_INFO_DELAY_MS: u64 = 10; @@ -44,8 +44,7 @@ impl PcduModel { } pub fn send_switch_info(&mut self) { - let switch_info = PcduReply::SwitchInfo(self.switcher_map.clone()); - let reply = SimReply::new(SimTarget::Pcdu, switch_info); + let reply = SimReply::new(PcduReply::SwitchInfo(self.switcher_map.clone())); self.reply_sender.send(reply).unwrap(); } @@ -76,7 +75,9 @@ pub(crate) mod tests { use super::*; use std::time::Duration; - use satrs_minisim::{eps::PcduRequest, SimRequest, SimTarget}; + use satrs_minisim::{ + eps::PcduRequest, SerializableSimMsgPayload, SimMessageProvider, SimRequest, SimTarget, + }; use crate::test_helpers::SimTestbench; @@ -85,11 +86,10 @@ pub(crate) mod tests { switch: PcduSwitch, target: SwitchStateBinary, ) { - let pcdu_request = PcduRequest::SwitchDevice { + let request = SimRequest::new(PcduRequest::SwitchDevice { switch, state: target, - }; - let request = SimRequest::new(SimTarget::Pcdu, pcdu_request); + }); sim_testbench .send_request(request) .expect("sending MGM switch request failed"); @@ -113,8 +113,7 @@ pub(crate) mod tests { } fn check_switch_state(sim_testbench: &mut SimTestbench, expected_switch_map: &SwitchMap) { - let pcdu_request = PcduRequest::RequestSwitchInfo; - let request = SimRequest::new(SimTarget::Pcdu, pcdu_request); + let request = SimRequest::new(PcduRequest::RequestSwitchInfo); sim_testbench .send_request(request) .expect("sending MGM request failed"); @@ -124,7 +123,7 @@ pub(crate) mod tests { assert!(sim_reply.is_some()); let sim_reply = sim_reply.unwrap(); assert_eq!(sim_reply.target(), SimTarget::Pcdu); - let pcdu_reply: PcduReply = serde_json::from_str(&sim_reply.reply()) + let pcdu_reply = PcduReply::from_sim_message(&sim_reply) .expect("failed to deserialize PCDU switch info"); match pcdu_reply { PcduReply::SwitchInfo(switch_map) => { @@ -144,8 +143,7 @@ pub(crate) mod tests { #[test] fn test_pcdu_switcher_request() { let mut sim_testbench = SimTestbench::new(); - let pcdu_request = PcduRequest::RequestSwitchInfo; - let request = SimRequest::new(SimTarget::Pcdu, pcdu_request); + let request = SimRequest::new(PcduRequest::RequestSwitchInfo); sim_testbench .send_request(request) .expect("sending MGM request failed"); @@ -160,7 +158,7 @@ pub(crate) mod tests { assert!(sim_reply.is_some()); let sim_reply = sim_reply.unwrap(); assert_eq!(sim_reply.target(), SimTarget::Pcdu); - let pcdu_reply: PcduReply = serde_json::from_str(&sim_reply.reply()) + let pcdu_reply = PcduReply::from_sim_message(&sim_reply) .expect("failed to deserialize PCDU switch info"); match pcdu_reply { PcduReply::SwitchInfo(switch_map) => { diff --git a/satrs-minisim/src/lib.rs b/satrs-minisim/src/lib.rs index 342aad3..215e5d4 100644 --- a/satrs-minisim/src/lib.rs +++ b/satrs-minisim/src/lib.rs @@ -1,4 +1,4 @@ -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum SimTarget { @@ -9,48 +9,100 @@ pub enum SimTarget { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SimRequest { - target: SimTarget, - request: String, +pub struct SimMessage { + pub target: SimTarget, + pub payload: String, } -impl SimRequest { - pub fn new(device: SimTarget, request: T) -> Self { - Self { - target: device, - request: serde_json::to_string(&request).unwrap(), - } - } - - pub fn target(&self) -> SimTarget { - self.target - } - - pub fn request(&self) -> &String { - &self.request - } +/// A generic simulation request type. Right now, the payload data is expected to be +/// JSON, which might be changed in the future. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SimRequest { + inner: SimMessage, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SimMessageType { + Request, + Reply, +} + +/// Generic trait implemented by simulation request or reply payloads. It ties the request or +/// reply to a specific target and provides an API which does boilerplate tasks like checking the +/// validity of the target. +pub trait SerializableSimMsgPayload: + Serialize + DeserializeOwned + Sized +{ + const TARGET: SimTarget; + + fn from_sim_message(sim_message: &P) -> Result> { + if sim_message.target() == Self::TARGET { + return Ok(serde_json::from_str(sim_message.payload())?); + } + Err(SimMessageError::TargetRequestMissmatch(sim_message.clone())) + } +} + +pub trait SimMessageProvider: Serialize + DeserializeOwned + Clone + Sized { + fn msg_type(&self) -> SimMessageType; + fn target(&self) -> SimTarget; + fn payload(&self) -> &String; + fn from_raw_data(data: &[u8]) -> serde_json::Result { + serde_json::from_slice(data) + } +} + +impl SimRequest { + pub fn new>(serializable_request: T) -> Self { + Self { + inner: SimMessage { + target: T::TARGET, + payload: serde_json::to_string(&serializable_request).unwrap(), + }, + } + } +} + +impl SimMessageProvider for SimRequest { + fn target(&self) -> SimTarget { + self.inner.target + } + fn payload(&self) -> &String { + &self.inner.payload + } + + fn msg_type(&self) -> SimMessageType { + SimMessageType::Request + } +} + +/// A generic simulation reply type. Right now, the payload data is expected to be +/// JSON, which might be changed inthe future. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SimReply { - target: SimTarget, - reply: String, + inner: SimMessage, } impl SimReply { - pub fn new(device: SimTarget, reply: T) -> Self { + pub fn new>(serializable_reply: T) -> Self { Self { - target: device, - reply: serde_json::to_string(&reply).unwrap(), + inner: SimMessage { + target: T::TARGET, + payload: serde_json::to_string(&serializable_reply).unwrap(), + }, } } +} - pub fn target(&self) -> SimTarget { - self.target +impl SimMessageProvider for SimReply { + fn target(&self) -> SimTarget { + self.inner.target } - - pub fn reply(&self) -> &String { - &self.reply + fn payload(&self) -> &String { + &self.inner.payload + } + fn msg_type(&self) -> SimMessageType { + SimMessageType::Reply } } @@ -59,19 +111,37 @@ pub enum SimCtrlRequest { Ping, } +impl SerializableSimMsgPayload for SimCtrlRequest { + const TARGET: SimTarget = SimTarget::SimCtrl; +} + +pub type SimReplyError = SimMessageError; +pub type SimRequestError = SimMessageError; + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum RequestError { - TargetRequestMissmatch(SimRequest), +pub enum SimMessageError

{ + SerdeJson(String), + TargetRequestMissmatch(P), +} + +impl

From for SimMessageError

{ + fn from(error: serde_json::Error) -> SimMessageError

{ + SimMessageError::SerdeJson(error.to_string()) + } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum SimCtrlReply { Pong, - InvalidRequest(RequestError), + InvalidRequest(SimRequestError), } -impl From for SimCtrlReply { - fn from(error: RequestError) -> Self { +impl SerializableSimMsgPayload for SimCtrlReply { + const TARGET: SimTarget = SimTarget::SimCtrl; +} + +impl From for SimCtrlReply { + fn from(error: SimRequestError) -> Self { SimCtrlReply::InvalidRequest(error) } } @@ -99,10 +169,18 @@ pub mod eps { RequestSwitchInfo, } + impl SerializableSimMsgPayload for PcduRequest { + const TARGET: SimTarget = SimTarget::Pcdu; + } + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PcduReply { SwitchInfo(SwitchMap), } + + impl SerializableSimMsgPayload for PcduReply { + const TARGET: SimTarget = SimTarget::Pcdu; + } } pub mod acs { @@ -117,6 +195,10 @@ pub mod acs { RequestSensorData, } + impl SerializableSimMsgPayload for MgmRequest { + const TARGET: SimTarget = SimTarget::Mgm; + } + // Normally, small magnetometers generate their output as a signed 16 bit raw format or something // similar which needs to be converted to a signed float value with physical units. We will // simplify this now and generate the signed float values directly. @@ -133,6 +215,10 @@ pub mod acs { pub sensor_values: MgmSensorValues, } + impl SerializableSimMsgPayload for MgmReply { + const TARGET: SimTarget = SimTarget::Mgm; + } + pub const MGT_GEN_MAGNETIC_FIELD: MgmSensorValues = MgmSensorValues { x: 0.03, y: -0.03, @@ -161,6 +247,10 @@ pub mod acs { RequestHk, } + impl SerializableSimMsgPayload for MgtRequest { + const TARGET: SimTarget = SimTarget::Mgt; + } + #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct MgtHkSet { pub dipole: MgtDipole, @@ -173,6 +263,10 @@ pub mod acs { Nak(MgtRequestType), Hk(MgtHkSet), } + + impl SerializableSimMsgPayload for MgtReply { + const TARGET: SimTarget = SimTarget::Mgm; + } } pub mod udp { @@ -244,3 +338,46 @@ pub mod udp { } } } + +#[cfg(test)] +pub mod tests { + use super::*; + + #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub enum DummyRequest { + Ping, + } + + impl SerializableSimMsgPayload for DummyRequest { + const TARGET: SimTarget = SimTarget::SimCtrl; + } + + #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub enum DummyReply { + Pong, + } + + impl SerializableSimMsgPayload for DummyReply { + const TARGET: SimTarget = SimTarget::SimCtrl; + } + + #[test] + fn test_basic_request() { + let sim_request = SimRequest::new(DummyRequest::Ping); + assert_eq!(sim_request.target(), SimTarget::SimCtrl); + assert_eq!(sim_request.msg_type(), SimMessageType::Request); + let dummy_request = + DummyRequest::from_sim_message(&sim_request).expect("deserialization failed"); + assert_eq!(dummy_request, DummyRequest::Ping); + } + + #[test] + fn test_basic_reply() { + let sim_reply = SimReply::new(DummyReply::Pong); + assert_eq!(sim_reply.target(), SimTarget::SimCtrl); + assert_eq!(sim_reply.msg_type(), SimMessageType::Reply); + let dummy_request = + DummyReply::from_sim_message(&sim_reply).expect("deserialization failed"); + assert_eq!(dummy_request, DummyReply::Pong); + } +} diff --git a/satrs-minisim/src/udp.rs b/satrs-minisim/src/udp.rs index 7958540..36800b7 100644 --- a/satrs-minisim/src/udp.rs +++ b/satrs-minisim/src/udp.rs @@ -6,7 +6,7 @@ use std::{ time::Duration, }; -use satrs_minisim::{SimReply, SimRequest}; +use satrs_minisim::{SimMessageProvider, SimReply, SimRequest}; // A UDP server which handles all TC received by a client application. pub struct SimUdpServer { @@ -90,11 +90,7 @@ impl SimUdpServer { self.sender_addr = Some(src); - // Convert the buffer into a string slice and print the message. - let req_string = std::str::from_utf8(&self.req_buf[..bytes_read]) - .expect("Could not write buffer as string"); - log::info!("Received request from {}: {}", src, req_string); - let sim_req: serde_json::Result = serde_json::from_str(req_string); + let sim_req = SimRequest::from_raw_data(&self.req_buf[..bytes_read]); if sim_req.is_err() { log::warn!( "received UDP request with invalid format: {}", @@ -164,7 +160,7 @@ mod tests { use satrs_minisim::{ eps::{PcduReply, PcduRequest}, udp::{ReceptionError, SimUdpClient}, - SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimTarget, + SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, }; use crate::eps::tests::get_all_off_switch_map; @@ -274,7 +270,7 @@ mod tests { UdpTestbench::new(true, Some(SERVER_WAIT_TIME_MS), 10) .expect("could not create testbench"); let server_thread = std::thread::spawn(move || udp_server.run()); - let sim_request = SimRequest::new(SimTarget::Pcdu, PcduRequest::RequestSwitchInfo); + let sim_request = SimRequest::new(PcduRequest::RequestSwitchInfo); udp_testbench .send_request(&sim_request) .expect("sending request failed"); @@ -296,13 +292,10 @@ mod tests { .expect("could not create testbench"); let server_thread = std::thread::spawn(move || udp_server.run()); udp_testbench - .send_request(&SimRequest::new(SimTarget::SimCtrl, SimCtrlRequest::Ping)) + .send_request(&SimRequest::new(SimCtrlRequest::Ping)) .expect("sending request failed"); - let sim_reply = SimReply::new( - SimTarget::Pcdu, - PcduReply::SwitchInfo(get_all_off_switch_map()), - ); + let sim_reply = SimReply::new(PcduReply::SwitchInfo(get_all_off_switch_map())); udp_testbench.send_reply(&sim_reply); udp_testbench.check_next_sim_reply(&sim_reply); @@ -323,14 +316,11 @@ mod tests { // Send a ping so that the server knows the address of the client. // Do not check that the request arrives on the receiver side, is done by other test. udp_testbench - .send_request(&SimRequest::new(SimTarget::SimCtrl, SimCtrlRequest::Ping)) + .send_request(&SimRequest::new(SimCtrlRequest::Ping)) .expect("sending request failed"); // Send a reply to the server, ensure it gets forwarded to the client. - let sim_reply = SimReply::new( - SimTarget::Pcdu, - PcduReply::SwitchInfo(get_all_off_switch_map()), - ); + let sim_reply = SimReply::new(PcduReply::SwitchInfo(get_all_off_switch_map())); udp_testbench.send_reply(&sim_reply); std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS)); @@ -349,10 +339,7 @@ mod tests { let server_thread = std::thread::spawn(move || udp_server.run()); // Send a reply to the server. The client is not connected, so it won't get forwarded. - let sim_reply = SimReply::new( - SimTarget::Pcdu, - PcduReply::SwitchInfo(get_all_off_switch_map()), - ); + let sim_reply = SimReply::new(PcduReply::SwitchInfo(get_all_off_switch_map())); udp_testbench.send_reply(&sim_reply); std::thread::sleep(Duration::from_millis(10)); @@ -360,7 +347,7 @@ mod tests { // Connect by sending a ping. udp_testbench - .send_request(&SimRequest::new(SimTarget::SimCtrl, SimCtrlRequest::Ping)) + .send_request(&SimRequest::new(SimCtrlRequest::Ping)) .expect("sending request failed"); std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS)); @@ -379,7 +366,7 @@ mod tests { let server_thread = std::thread::spawn(move || udp_server.run()); // The server only caches up to 3 replies. - let sim_reply = SimReply::new(SimTarget::SimCtrl, SimCtrlReply::Pong); + let sim_reply = SimReply::new(SimCtrlReply::Pong); for _ in 0..4 { udp_testbench.send_reply(&sim_reply); } @@ -389,7 +376,7 @@ mod tests { // Connect by sending a ping. udp_testbench - .send_request(&SimRequest::new(SimTarget::SimCtrl, SimCtrlRequest::Ping)) + .send_request(&SimRequest::new(SimCtrlRequest::Ping)) .expect("sending request failed"); std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));