First version of asynchronix based mini simulator #139
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
/target
|
target/
|
||||||
|
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
|
|
||||||
/.idea/*
|
/.idea/*
|
||||||
|
@ -4,6 +4,7 @@ members = [
|
|||||||
"satrs",
|
"satrs",
|
||||||
"satrs-mib",
|
"satrs-mib",
|
||||||
"satrs-example",
|
"satrs-example",
|
||||||
|
"satrs-minisim",
|
||||||
"satrs-shared",
|
"satrs-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
10
coverage.py
10
coverage.py
@ -18,15 +18,19 @@ def generate_cov_report(open_report: bool, format: str, package: str):
|
|||||||
out_path = "./target/debug/coverage"
|
out_path = "./target/debug/coverage"
|
||||||
if format == "lcov":
|
if format == "lcov":
|
||||||
out_path = "./target/debug/lcov.info"
|
out_path = "./target/debug/lcov.info"
|
||||||
os.system(
|
grcov_cmd = (
|
||||||
f"grcov . -s . --binary-path ./target/debug/ -t {format} --branch --ignore-not-existing "
|
f"grcov . -s . --binary-path ./target/debug/ -t {format} --branch --ignore-not-existing "
|
||||||
f"-o {out_path}"
|
f"-o {out_path}"
|
||||||
)
|
)
|
||||||
|
print(f"Running: {grcov_cmd}")
|
||||||
|
os.system(grcov_cmd)
|
||||||
if format == "lcov":
|
if format == "lcov":
|
||||||
os.system(
|
lcov_cmd = (
|
||||||
"genhtml -o ./target/debug/coverage/ --show-details --highlight --ignore-errors source "
|
"genhtml -o ./target/debug/coverage/ --show-details --highlight --ignore-errors source "
|
||||||
"--legend ./target/debug/lcov.info"
|
"--legend ./target/debug/lcov.info"
|
||||||
)
|
)
|
||||||
|
print(f"Running: {lcov_cmd}")
|
||||||
|
os.system(lcov_cmd)
|
||||||
if open_report:
|
if open_report:
|
||||||
coverage_report_path = os.path.abspath("./target/debug/coverage/index.html")
|
coverage_report_path = os.path.abspath("./target/debug/coverage/index.html")
|
||||||
webbrowser.open_new_tab(coverage_report_path)
|
webbrowser.open_new_tab(coverage_report_path)
|
||||||
@ -43,7 +47,7 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-p",
|
"-p",
|
||||||
"--package",
|
"--package",
|
||||||
choices=["satrs"],
|
choices=["satrs", "satrs-minisim"],
|
||||||
default="satrs",
|
default="satrs",
|
||||||
help="Choose project to generate coverage for",
|
help="Choose project to generate coverage for",
|
||||||
)
|
)
|
||||||
|
21
satrs-minisim/Cargo.toml
Normal file
21
satrs-minisim/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[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"
|
||||||
|
|
||||||
|
[dependencies.satrs]
|
||||||
|
path = "../satrs"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
delegate = "0.12"
|
338
satrs-minisim/src/acs.rs
Normal file
338
satrs-minisim/src/acs.rs
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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<MgmSensorValues>,
|
||||||
|
pub reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MagnetometerModel {
|
||||||
|
pub fn new(periodicity: Duration, reply_sender: mpsc::Sender<SimReply>) -> 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>) {
|
||||||
|
self.reply_sender
|
||||||
|
.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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<MgmSensorValues>,
|
||||||
|
reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MagnetorquerModel {
|
||||||
|
pub fn new(reply_sender: mpsc::Sender<SimReply>) -> 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>,
|
||||||
|
) {
|
||||||
|
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<Self>) {
|
||||||
|
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) {
|
||||||
|
self.reply_sender
|
||||||
|
.send(SimReply::new(MgtReply::Hk(MgtHkSet {
|
||||||
|
dipole: self.torque_dipole,
|
||||||
|
torquing: self.torquing,
|
||||||
|
})))
|
||||||
|
.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,
|
||||||
|
SerializableSimMsgPayload, SimMessageProvider, 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 request = SimRequest::new(MgmRequest::RequestSensorData);
|
||||||
|
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::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);
|
||||||
|
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 mut request = SimRequest::new(MgmRequest::RequestSensorData);
|
||||||
|
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::from_sim_message(&sim_reply)
|
||||||
|
.expect("failed to deserialize MGM sensor values");
|
||||||
|
sim_testbench.step_by(Duration::from_millis(50));
|
||||||
|
|
||||||
|
request = SimRequest::new(MgmRequest::RequestSensorData);
|
||||||
|
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::from_sim_message(&sim_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 request = SimRequest::new(MgtRequest::RequestHk);
|
||||||
|
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 request = SimRequest::new(MgtRequest::RequestHk);
|
||||||
|
|
||||||
|
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::from_sim_message(&sim_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 request = SimRequest::new(MgtRequest::RequestHk);
|
||||||
|
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::from_sim_message(&sim_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 request = SimRequest::new(MgtRequest::ApplyTorque {
|
||||||
|
duration: Duration::from_millis(100),
|
||||||
|
dipole: commanded_dipole,
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
189
satrs-minisim/src/controller.rs
Normal file
189
satrs-minisim/src/controller.rs
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
use std::{sync::mpsc, time::Duration};
|
||||||
|
|
||||||
|
use asynchronix::{
|
||||||
|
simulation::{Address, Simulation},
|
||||||
|
time::{Clock, MonotonicTime, SystemClock},
|
||||||
|
};
|
||||||
|
use satrs_minisim::{
|
||||||
|
acs::{MgmRequest, MgtRequest},
|
||||||
|
eps::PcduRequest,
|
||||||
|
SerializableSimMsgPayload, SimCtrlReply, SimCtrlRequest, SimMessageProvider, SimReply,
|
||||||
|
SimRequest, SimRequestError, 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<SimRequest>,
|
||||||
|
pub reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
pub simulation: Simulation,
|
||||||
|
pub mgm_addr: Address<MagnetometerModel>,
|
||||||
|
pub pcdu_addr: Address<PcduModel>,
|
||||||
|
pub mgt_addr: Address<MagnetorquerModel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimController {
|
||||||
|
pub fn new(
|
||||||
|
sys_clock: SystemClock,
|
||||||
|
request_receiver: mpsc::Receiver<SimRequest>,
|
||||||
|
reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
simulation: Simulation,
|
||||||
|
mgm_addr: Address<MagnetometerModel>,
|
||||||
|
pcdu_addr: Address<PcduModel>,
|
||||||
|
mgt_addr: Address<MagnetorquerModel>,
|
||||||
|
) -> 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) -> Result<(), SimRequestError> {
|
||||||
|
let sim_ctrl_request = SimCtrlRequest::from_sim_message(request)?;
|
||||||
|
match sim_ctrl_request {
|
||||||
|
SimCtrlRequest::Ping => {
|
||||||
|
self.reply_sender
|
||||||
|
.send(SimReply::new(SimCtrlReply::Pong))
|
||||||
|
.expect("sending reply from sim controller failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
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(
|
||||||
|
MagnetometerModel::send_sensor_values,
|
||||||
|
(),
|
||||||
|
&self.mgm_addr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
.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) -> Result<(), SimRequestError> {
|
||||||
|
let mgt_request = MgtRequest::from_sim_message(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: SimRequestError,
|
||||||
|
request: &SimRequest,
|
||||||
|
) {
|
||||||
|
log::warn!(
|
||||||
|
"received invalid {:?} request: {:?}",
|
||||||
|
request.target(),
|
||||||
|
error
|
||||||
|
);
|
||||||
|
self.reply_sender
|
||||||
|
.send(SimReply::new(SimCtrlReply::from(error)))
|
||||||
|
.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 request = SimRequest::new(SimCtrlRequest::Ping);
|
||||||
|
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::from_sim_message(&sim_reply)
|
||||||
|
.expect("failed to deserialize MGM sensor values");
|
||||||
|
assert_eq!(reply, SimCtrlReply::Pong);
|
||||||
|
}
|
||||||
|
}
|
185
satrs-minisim/src/eps.rs
Normal file
185
satrs-minisim/src/eps.rs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const SWITCH_INFO_DELAY_MS: u64 = 10;
|
||||||
|
|
||||||
|
pub struct PcduModel {
|
||||||
|
pub switcher_map: SwitchMap,
|
||||||
|
pub mgm_switch: Output<SwitchStateBinary>,
|
||||||
|
pub mgt_switch: Output<SwitchStateBinary>,
|
||||||
|
pub reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PcduModel {
|
||||||
|
pub fn new(reply_sender: mpsc::Sender<SimReply>) -> 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<Self>) {
|
||||||
|
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 reply = SimReply::new(PcduReply::SwitchInfo(self.switcher_map.clone()));
|
||||||
|
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, SerializableSimMsgPayload, SimMessageProvider, SimRequest, SimTarget,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::test_helpers::SimTestbench;
|
||||||
|
|
||||||
|
fn switch_device(
|
||||||
|
sim_testbench: &mut SimTestbench,
|
||||||
|
switch: PcduSwitch,
|
||||||
|
target: SwitchStateBinary,
|
||||||
|
) {
|
||||||
|
let request = SimRequest::new(PcduRequest::SwitchDevice {
|
||||||
|
switch,
|
||||||
|
state: target,
|
||||||
|
});
|
||||||
|
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 request = SimRequest::new(PcduRequest::RequestSwitchInfo);
|
||||||
|
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::from_sim_message(&sim_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 request = SimRequest::new(PcduRequest::RequestSwitchInfo);
|
||||||
|
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::from_sim_message(&sim_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);
|
||||||
|
}
|
||||||
|
}
|
383
satrs-minisim/src/lib.rs
Normal file
383
satrs-minisim/src/lib.rs
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
use serde::{de::DeserializeOwned, 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 SimMessage {
|
||||||
|
pub target: SimTarget,
|
||||||
|
pub payload: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<P: SimMessageProvider>:
|
||||||
|
Serialize + DeserializeOwned + Sized
|
||||||
|
{
|
||||||
|
const TARGET: SimTarget;
|
||||||
|
|
||||||
|
fn from_sim_message(sim_message: &P) -> Result<Self, SimMessageError<P>> {
|
||||||
|
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<Self> {
|
||||||
|
serde_json::from_slice(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimRequest {
|
||||||
|
pub fn new<T: SerializableSimMsgPayload<SimRequest>>(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 {
|
||||||
|
inner: SimMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimReply {
|
||||||
|
pub fn new<T: SerializableSimMsgPayload<SimReply>>(serializable_reply: T) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: SimMessage {
|
||||||
|
target: T::TARGET,
|
||||||
|
payload: serde_json::to_string(&serializable_reply).unwrap(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimMessageProvider for SimReply {
|
||||||
|
fn target(&self) -> SimTarget {
|
||||||
|
self.inner.target
|
||||||
|
}
|
||||||
|
fn payload(&self) -> &String {
|
||||||
|
&self.inner.payload
|
||||||
|
}
|
||||||
|
fn msg_type(&self) -> SimMessageType {
|
||||||
|
SimMessageType::Reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum SimCtrlRequest {
|
||||||
|
Ping,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimRequest> for SimCtrlRequest {
|
||||||
|
const TARGET: SimTarget = SimTarget::SimCtrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SimReplyError = SimMessageError<SimReply>;
|
||||||
|
pub type SimRequestError = SimMessageError<SimRequest>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum SimMessageError<P> {
|
||||||
|
SerdeJson(String),
|
||||||
|
TargetRequestMissmatch(P),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> From<serde_json::Error> for SimMessageError<P> {
|
||||||
|
fn from(error: serde_json::Error) -> SimMessageError<P> {
|
||||||
|
SimMessageError::SerdeJson(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum SimCtrlReply {
|
||||||
|
Pong,
|
||||||
|
InvalidRequest(SimRequestError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimReply> for SimCtrlReply {
|
||||||
|
const TARGET: SimTarget = SimTarget::SimCtrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SimRequestError> for SimCtrlReply {
|
||||||
|
fn from(error: SimRequestError) -> Self {
|
||||||
|
SimCtrlReply::InvalidRequest(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod eps {
|
||||||
|
use super::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use satrs::power::SwitchStateBinary;
|
||||||
|
|
||||||
|
pub type SwitchMap = HashMap<PcduSwitch, SwitchStateBinary>;
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimRequest> for PcduRequest {
|
||||||
|
const TARGET: SimTarget = SimTarget::Pcdu;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum PcduReply {
|
||||||
|
SwitchInfo(SwitchMap),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimReply> for PcduReply {
|
||||||
|
const TARGET: SimTarget = SimTarget::Pcdu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod acs {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use satrs::power::SwitchStateBinary;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum MgmRequest {
|
||||||
|
RequestSensorData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimRequest> 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.
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimReply> for MgmReply {
|
||||||
|
const TARGET: SimTarget = SimTarget::Mgm;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimRequest> for MgtRequest {
|
||||||
|
const TARGET: SimTarget = SimTarget::Mgt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimReply> for MgtReply {
|
||||||
|
const TARGET: SimTarget = SimTarget::Mgm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<u64>,
|
||||||
|
) -> std::io::Result<Self> {
|
||||||
|
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<usize> {
|
||||||
|
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<usize> {
|
||||||
|
self.socket.recv(&mut self.reply_buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recv_sim_reply(&mut self) -> Result<SimReply, ReceptionError> {
|
||||||
|
let read_len = self.recv_raw()?;
|
||||||
|
Ok(serde_json::from_slice(&self.reply_buf[0..read_len])?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum DummyRequest {
|
||||||
|
Ping,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimRequest> for DummyRequest {
|
||||||
|
const TARGET: SimTarget = SimTarget::SimCtrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum DummyReply {
|
||||||
|
Pong,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableSimMsgPayload<SimReply> 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);
|
||||||
|
}
|
||||||
|
}
|
103
satrs-minisim/src/main.rs
Normal file
103
satrs-minisim/src/main.rs
Normal file
@ -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<SimReply>,
|
||||||
|
request_receiver: mpsc::Receiver<SimRequest>,
|
||||||
|
) -> 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");
|
||||||
|
}
|
56
satrs-minisim/src/test_helpers.rs
Normal file
56
satrs-minisim/src/test_helpers.rs
Normal file
@ -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<SimReply>,
|
||||||
|
pub request_sender: mpsc::Sender<SimRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<SimRequest>> {
|
||||||
|
self.request_sender.send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_receive_next_reply(&self) -> Option<SimReply> {
|
||||||
|
match self.reply_receiver.try_recv() {
|
||||||
|
Ok(reply) => Some(reply),
|
||||||
|
Err(e) => {
|
||||||
|
if e == mpsc::TryRecvError::Empty {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
panic!("reply_receiver disconnected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
satrs-minisim/src/time.rs
Normal file
5
satrs-minisim/src/time.rs
Normal file
@ -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)
|
||||||
|
}
|
390
satrs-minisim/src/udp.rs
Normal file
390
satrs-minisim/src/udp.rs
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
io::ErrorKind,
|
||||||
|
net::{SocketAddr, UdpSocket},
|
||||||
|
sync::{atomic::AtomicBool, mpsc, Arc},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use satrs_minisim::{SimMessageProvider, SimReply, SimRequest};
|
||||||
|
|
||||||
|
// A UDP server which handles all TC received by a client application.
|
||||||
|
pub struct SimUdpServer {
|
||||||
|
socket: UdpSocket,
|
||||||
|
request_sender: mpsc::Sender<SimRequest>,
|
||||||
|
// shared_last_sender: SharedSocketAddr,
|
||||||
|
reply_receiver: mpsc::Receiver<SimReply>,
|
||||||
|
reply_queue: VecDeque<SimReply>,
|
||||||
|
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<Arc<AtomicBool>>,
|
||||||
|
idle_sleep_period_ms: u64,
|
||||||
|
req_buf: [u8; 4096],
|
||||||
|
sender_addr: Option<SocketAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimUdpServer {
|
||||||
|
pub fn new(
|
||||||
|
local_port: u16,
|
||||||
|
request_sender: mpsc::Sender<SimRequest>,
|
||||||
|
reply_receiver: mpsc::Receiver<SimReply>,
|
||||||
|
max_num_replies: usize,
|
||||||
|
stop_signal: Option<Arc<AtomicBool>>,
|
||||||
|
) -> std::io::Result<Self> {
|
||||||
|
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<SocketAddr> {
|
||||||
|
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);
|
||||||
|
|
||||||
|
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: {}",
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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<AtomicBool>,
|
||||||
|
request_receiver: mpsc::Receiver<SimRequest>,
|
||||||
|
reply_sender: mpsc::Sender<SimReply>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UdpTestbench {
|
||||||
|
pub fn new(
|
||||||
|
client_non_blocking: bool,
|
||||||
|
client_read_timeout_ms: Option<u64>,
|
||||||
|
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<SimRequest, mpsc::TryRecvError> {
|
||||||
|
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<usize>;
|
||||||
|
pub fn recv_sim_reply(&mut self) -> Result<SimReply, ReceptionError>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(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(SimCtrlRequest::Ping))
|
||||||
|
.expect("sending request failed");
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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(SimCtrlRequest::Ping))
|
||||||
|
.expect("sending request failed");
|
||||||
|
|
||||||
|
// Send a reply to the server, ensure it gets forwarded to the client.
|
||||||
|
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));
|
||||||
|
|
||||||
|
// 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(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(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(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(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();
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,42 @@ pub enum SwitchState {
|
|||||||
Faulty = 3,
|
Faulty = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
pub enum SwitchStateBinary {
|
||||||
|
Off = 0,
|
||||||
|
On = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<SwitchState> for SwitchStateBinary {
|
||||||
|
type Error = ();
|
||||||
|
fn try_from(value: SwitchState) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
SwitchState::Off => Ok(SwitchStateBinary::Off),
|
||||||
|
SwitchState::On => Ok(SwitchStateBinary::On),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Into<u64>> From<T> for SwitchStateBinary {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
if value.into() == 0 {
|
||||||
|
return SwitchStateBinary::Off;
|
||||||
|
}
|
||||||
|
SwitchStateBinary::On
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SwitchStateBinary> for SwitchState {
|
||||||
|
fn from(value: SwitchStateBinary) -> Self {
|
||||||
|
match value {
|
||||||
|
SwitchStateBinary::Off => SwitchState::Off,
|
||||||
|
SwitchStateBinary::On => SwitchState::On,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type SwitchId = u16;
|
pub type SwitchId = u16;
|
||||||
|
|
||||||
/// Generic trait for a device capable of turning on and off switches.
|
/// Generic trait for a device capable of turning on and off switches.
|
||||||
|
Loading…
Reference in New Issue
Block a user