Merge pull request 'simplify minisim' (#285) from simplify-minisim into main

Reviewed-on: #285
This commit was merged in pull request #285.
This commit is contained in:
2026-09-24 15:27:57 +02:00
13 changed files with 606 additions and 1038 deletions
+17 -25
View File
@@ -3,9 +3,8 @@ use arbitrary_int::u11;
use clap::Parser as _;
use satrs_example::config::{OBSW_SERVER_ADDR, SERVER_PORT};
use satrs_minisim::{
SerializableSimMsgPayload, SimComponent, SimCtrlReply, SimCtrlRequest, SimMessageProvider,
SimReply, SimRequest, acs, acs::MgmRequestLis3Mdl, acs::MgmRequestLis3MdlMgm0,
acs::MgmRequestLis3MdlMgm1, acs::SpiFault, udp::SIM_CTRL_PORT,
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime, acs::mgm,
udp::SIM_CTRL_PORT,
};
use spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
use std::{
@@ -98,12 +97,12 @@ enum FaultMode {
AllOnes,
}
impl From<FaultMode> for acs::SpiFaultMode {
impl From<FaultMode> for mgm::SpiFaultMode {
fn from(mode: FaultMode) -> Self {
match mode {
FaultMode::None => acs::SpiFaultMode::None,
FaultMode::AllZeros => acs::SpiFaultMode::AllZeros,
FaultMode::AllOnes => acs::SpiFaultMode::AllOnes,
FaultMode::None => mgm::SpiFaultMode::None,
FaultMode::AllZeros => mgm::SpiFaultMode::AllZeros,
FaultMode::AllOnes => mgm::SpiFaultMode::AllOnes,
}
}
}
@@ -205,7 +204,7 @@ fn handle_mgm_command(
if let Some(mode) = args.fault {
inject_mgm_failure(
target_id,
SpiFault {
mgm::SpiFault {
mode: mode.into(),
cleared_by_power_cycle: args.fault_kind == FaultKind::Transient,
},
@@ -500,26 +499,20 @@ fn main() -> anyhow::Result<()> {
/// Confirms the simulator is actually reachable first (same ping/pong check the OBSW's own
/// internal sim client does, see `SimClientUdp::attempt_connection`), since a fire-and-forget
/// UDP send would otherwise silently do nothing if minisim is not running.
fn inject_mgm_failure(target_id: types::ComponentId, fault: SpiFault) -> anyhow::Result<()> {
fn inject_mgm_failure(target_id: types::ComponentId, fault: mgm::SpiFault) -> anyhow::Result<()> {
let sim_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), SIM_CTRL_PORT);
let sim_socket = UdpSocket::bind("127.0.0.1:0")?;
sim_socket.set_read_timeout(Some(Duration::from_millis(200)))?;
let mut reply_buf = [0u8; 4096];
let ping = SimRequest::new_with_epoch_time(SimCtrlRequest::Ping);
let ping = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
sim_socket.send_to(&serde_json::to_vec(&ping)?, sim_addr)?;
match sim_socket.recv(&mut reply_buf) {
Ok(len) => {
let reply: SimReply = serde_json::from_slice(&reply_buf[..len])?;
if reply.component() != SimComponent::SimCtrl {
if reply != SimReply::SimCtrl(SimCtrlReply::Pong) {
bail!("unexpected reply while checking minisim connectivity: {reply:?}");
}
match SimCtrlReply::from_sim_message(&reply).expect("invalid SIM reply") {
SimCtrlReply::Pong => {}
SimCtrlReply::InvalidRequest(e) => {
bail!("minisim rejected connectivity ping: {e:?}")
}
}
}
Err(e)
if matches!(
@@ -532,16 +525,15 @@ fn inject_mgm_failure(target_id: types::ComponentId, fault: SpiFault) -> anyhow:
Err(e) => return Err(e.into()),
}
let fault_request = MgmRequestLis3Mdl::SetSpiFault(fault);
let request = match target_id {
types::ComponentId::AcsMgm0 => {
SimRequest::new_with_epoch_time(MgmRequestLis3MdlMgm0(fault_request))
}
types::ComponentId::AcsMgm1 => {
SimRequest::new_with_epoch_time(MgmRequestLis3MdlMgm1(fault_request))
}
let id = match target_id {
types::ComponentId::AcsMgm0 => mgm::Id::Mgm0,
types::ComponentId::AcsMgm1 => mgm::Id::Mgm1,
_ => bail!("SPI fault injection is not supported for {target_id:?}"),
};
let request = SimRequestWithTime::new_with_epoch_time(SimRequest::Mgm {
id,
request: mgm::Request::SetSpiFault(fault),
});
sim_socket.send_to(&serde_json::to_vec(&request)?, sim_addr)?;
log::info!("injected SPI fault {fault:?} into minisim {target_id:?}");
Ok(())
+75 -169
View File
@@ -1,13 +1,7 @@
use std::{f32::consts::PI, sync::mpsc, time::Duration};
use std::{f32::consts::PI, sync::mpsc};
use nexosim::model::{Context, Model};
use satrs_minisim::{
acs::{
mgm::{MgmId, MgmReply, MgmReplyWrapper},
MgmSensorValuesMicroTesla, SpiFault,
},
SimReply,
};
use satrs_minisim::{acs::mgm, SimReply};
use types::pcdu::SwitchStateBinary;
use crate::time::current_millis;
@@ -25,24 +19,21 @@ const PHASE_Z: f32 = 0.2;
/// An ideal sensor 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 still be possible and is probably sufficient for many OBSW needs.
pub struct MagnetometerModel {
pub id: MgmId,
pub struct MgmModel {
pub id: mgm::Id,
pub switch_state: SwitchStateBinary,
#[allow(dead_code)]
pub periodicity: Duration,
pub external_mag_field: Option<MgmSensorValuesMicroTesla>,
pub spi_fault: SpiFault,
pub external_mag_field: Option<mgm::SensorValuesMicroTesla>,
pub spi_fault: mgm::SpiFault,
pub reply_sender: mpsc::Sender<SimReply>,
}
impl MagnetometerModel {
pub fn new(mgm_id: MgmId, periodicity: Duration, reply_sender: mpsc::Sender<SimReply>) -> Self {
impl MgmModel {
pub fn new(mgm_id: mgm::Id, reply_sender: mpsc::Sender<SimReply>) -> Self {
Self {
id: mgm_id,
switch_state: SwitchStateBinary::Off,
periodicity,
external_mag_field: None,
spi_fault: SpiFault::default(),
spi_fault: mgm::SpiFault::default(),
reply_sender,
}
}
@@ -50,48 +41,48 @@ impl MagnetometerModel {
pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) {
self.switch_state = switch_state;
if switch_state == SwitchStateBinary::Off && self.spi_fault.cleared_by_power_cycle {
self.spi_fault = SpiFault::default();
self.spi_fault = mgm::SpiFault::default();
}
}
/// Force (or clear) a stuck-bus SPI fault, for FDIR testing purposes.
pub async fn set_spi_fault(&mut self, fault: SpiFault) {
pub async fn set_spi_fault(&mut self, fault: mgm::SpiFault) {
self.spi_fault = fault;
}
pub async fn send_sensor_values(&mut self, _: (), scheduler: &mut Context<Self>) {
let reply = MgmReplyWrapper {
mgm_id: self.id,
reply: MgmReply::new(
let reply = SimReply::Mgm {
id: self.id,
reply: mgm::Reply::new(
self.switch_state,
self.calculate_current_mgm_tuple(current_millis(scheduler.time())),
self.spi_fault.mode,
),
};
self.reply_sender
.send(reply.to_sim_reply())
.send(reply)
.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: MgmSensorValuesMicroTesla) {
pub async fn apply_external_magnetic_field(&mut self, field: mgm::SensorValuesMicroTesla) {
self.external_mag_field = Some(field);
}
fn calculate_current_mgm_tuple(&self, time_ms: u64) -> MgmSensorValuesMicroTesla {
fn calculate_current_mgm_tuple(&self, time_ms: u64) -> mgm::SensorValuesMicroTesla {
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 MgmSensorValuesMicroTesla {
return mgm::SensorValuesMicroTesla {
x: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_X).sin(),
y: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_Y).sin(),
z: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_Z).sin(),
};
}
MgmSensorValuesMicroTesla {
mgm::SensorValuesMicroTesla {
x: 0.0,
y: 0.0,
z: 0.0,
@@ -99,20 +90,13 @@ impl MagnetometerModel {
}
}
impl Model for MagnetometerModel {}
impl Model for MgmModel {}
#[cfg(test)]
mod tests {
use std::time::Duration;
use satrs_minisim::{
acs::{
mgm::{self, MgmId, MgmReply, MgmReplyWrapper},
MgmRequestLis3Mdl, MgmRequestLis3MdlMgm0, MgmRequestLis3MdlMgm1, SpiFault,
SpiFaultMode,
},
SimComponent, SimMessageProvider, SimRequest,
};
use satrs_minisim::{acs::mgm, SimReply, SimRequest};
use types::pcdu::{SwitchId, SwitchStateBinary};
use crate::{
@@ -120,72 +104,55 @@ mod tests {
test_helpers::SimTestbench,
};
#[test]
fn test_basic_mgm_request() {
let mut sim_testbench = SimTestbench::new();
let request = SimRequest::new_with_epoch_time(MgmRequestLis3MdlMgm0(
MgmRequestLis3Mdl::RequestSensorData,
));
sim_testbench
.send_request(request)
.expect("sending MGM request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step().unwrap();
let sim_reply = sim_testbench.try_receive_next_reply();
assert!(sim_reply.is_some());
let sim_reply = sim_reply.unwrap();
assert_eq!(sim_reply.component(), SimComponent::Mgm0Lis3Mdl);
let wrapper = MgmReplyWrapper::from_sim_reply(&sim_reply)
.expect("failed to deserialize MGM sensor values");
assert_eq!(wrapper.mgm_id, MgmId::Mgm0);
assert_eq!(wrapper.reply.switch_state, SwitchStateBinary::Off);
assert_eq!(wrapper.reply.sensor_values.x, 0.0);
assert_eq!(wrapper.reply.sensor_values.y, 0.0);
assert_eq!(wrapper.reply.sensor_values.z, 0.0);
fn request_sensor_data(sim_testbench: &mut SimTestbench, id: mgm::Id) -> mgm::Reply {
let sim_reply = sim_testbench
.request_reply(SimRequest::Mgm {
id,
request: mgm::Request::RequestSensorData,
})
.expect("no MGM reply received");
let SimReply::Mgm {
id: reply_id,
reply,
} = sim_reply
else {
panic!("unexpected reply {sim_reply:?}");
};
assert_eq!(reply_id, id);
reply
}
fn inject_spi_fault(sim_testbench: &mut SimTestbench, cleared_by_power_cycle: bool) {
let fault_request = SimRequest::new_with_epoch_time(MgmRequestLis3MdlMgm0(
MgmRequestLis3Mdl::SetSpiFault(SpiFault {
mode: SpiFaultMode::AllOnes,
sim_testbench.send_and_step(SimRequest::Mgm {
id: mgm::Id::Mgm0,
request: mgm::Request::SetSpiFault(mgm::SpiFault {
mode: mgm::SpiFaultMode::AllOnes,
cleared_by_power_cycle,
}),
));
sim_testbench
.send_request(fault_request)
.expect("sending MGM fault injection request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step().unwrap();
});
}
fn request_mgm_reply(sim_testbench: &mut SimTestbench) -> MgmReply {
let data_request = SimRequest::new_with_epoch_time(MgmRequestLis3MdlMgm0(
MgmRequestLis3Mdl::RequestSensorData,
));
sim_testbench
.send_request(data_request)
.expect("sending MGM request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step().unwrap();
let sim_reply = sim_testbench
.try_receive_next_reply()
.expect("no MGM reply received");
MgmReplyWrapper::from_sim_reply(&sim_reply)
.expect("failed to deserialize MGM sensor values")
.reply
}
fn is_stuck_bus_reply(reply: &MgmReply) -> bool {
fn is_stuck_bus_reply(reply: &mgm::Reply) -> bool {
reply.raw.x == -1 && reply.raw.y == -1 && reply.raw.z == -1
}
#[test]
fn test_basic_mgm_request() {
let mut sim_testbench = SimTestbench::new();
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
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_mgm_spi_fault_injection_all_ones() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
inject_spi_fault(&mut sim_testbench, false);
let reply = request_mgm_reply(&mut sim_testbench);
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
// Even though the device is switched on, the injected fault forces a stuck-bus reply.
assert_eq!(reply.switch_state, SwitchStateBinary::On);
assert!(is_stuck_bus_reply(&reply));
@@ -196,12 +163,14 @@ mod tests {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
inject_spi_fault(&mut sim_testbench, true);
assert!(is_stuck_bus_reply(&request_mgm_reply(&mut sim_testbench)));
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert!(is_stuck_bus_reply(&reply));
switch_device_off(&mut sim_testbench, SwitchId::Mgm0);
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
sim_testbench.step_until(Duration::from_millis(50)).unwrap();
assert!(!is_stuck_bus_reply(&request_mgm_reply(&mut sim_testbench)));
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert!(!is_stuck_bus_reply(&reply));
}
#[test]
@@ -212,7 +181,7 @@ mod tests {
switch_device_off(&mut sim_testbench, SwitchId::Mgm0);
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
let reply = request_mgm_reply(&mut sim_testbench);
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert_eq!(reply.switch_state, SwitchStateBinary::On);
assert!(is_stuck_bus_reply(&reply));
}
@@ -222,56 +191,21 @@ mod tests {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
let mut request = SimRequest::new_with_epoch_time(MgmRequestLis3MdlMgm0(
MgmRequestLis3Mdl::RequestSensorData,
));
sim_testbench
.send_request(request)
.expect("sending MGM request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step().unwrap();
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.component(), SimComponent::Mgm0Lis3Mdl);
let first_reply = MgmReplyWrapper::from_sim_reply(&sim_reply)
.expect("failed to deserialize MGM sensor values")
.reply;
let first_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
sim_testbench.step_until(Duration::from_millis(50)).unwrap();
let second_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
request = SimRequest::new_with_epoch_time(MgmRequestLis3MdlMgm0(
MgmRequestLis3Mdl::RequestSensorData,
));
sim_testbench
.send_request(request)
.expect("sending MGM request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step().unwrap();
sim_reply_res = sim_testbench.try_receive_next_reply();
assert!(sim_reply_res.is_some());
sim_reply = sim_reply_res.unwrap();
let second_reply = MgmReplyWrapper::from_sim_reply(&sim_reply)
.expect("failed to deserialize MGM sensor values")
.reply;
let x_conv_back = second_reply.raw.x as f32
* mgm::FIELD_LSB_PER_GAUSS_4_SENS
* mgm::GAUSS_TO_MICROTESLA_FACTOR as f32;
let y_conv_back = second_reply.raw.y as f32
* mgm::FIELD_LSB_PER_GAUSS_4_SENS
* mgm::GAUSS_TO_MICROTESLA_FACTOR as f32;
let z_conv_back = second_reply.raw.z as f32
* mgm::FIELD_LSB_PER_GAUSS_4_SENS
* mgm::GAUSS_TO_MICROTESLA_FACTOR as f32;
let diff_x = (second_reply.sensor_values.x - x_conv_back).abs();
assert!(diff_x < 0.01, "diff x too large: {}", diff_x);
let diff_y = (second_reply.sensor_values.y - y_conv_back).abs();
assert!(diff_y < 0.01, "diff y too large: {}", diff_y);
let diff_z = (second_reply.sensor_values.z - z_conv_back).abs();
assert!(diff_z < 0.01, "diff z too large: {}", diff_z);
// assert_eq!(second_reply.raw_reply, SwitchStateBinary::On);
let to_microtesla = |raw: i16| {
raw as f32 * mgm::FIELD_LSB_PER_GAUSS_4_SENS * mgm::GAUSS_TO_MICROTESLA_FACTOR as f32
};
let values = second_reply.sensor_values;
let raw = second_reply.raw;
for (value, raw) in [(values.x, raw.x), (values.y, raw.y), (values.z, raw.z)] {
let diff = (value - to_microtesla(raw)).abs();
assert!(diff < 0.01, "raw value conversion diff too large: {diff}");
}
// Check that the values are changing.
assert!(first_reply != second_reply);
assert_ne!(first_reply, second_reply);
}
#[test]
@@ -279,37 +213,9 @@ mod tests {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgm1);
for request in [
SimRequest::new_with_epoch_time(MgmRequestLis3MdlMgm0(
MgmRequestLis3Mdl::RequestSensorData,
)),
SimRequest::new_with_epoch_time(MgmRequestLis3MdlMgm1(
MgmRequestLis3Mdl::RequestSensorData,
)),
] {
sim_testbench
.send_request(request)
.expect("sending MGM request failed");
}
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step().unwrap();
let sim_reply = sim_testbench
.try_receive_next_reply()
.expect("no MGM0 reply received");
assert_eq!(sim_reply.component(), SimComponent::Mgm0Lis3Mdl);
let mgm_0_reply = MgmReplyWrapper::from_sim_reply(&sim_reply)
.expect("failed to deserialize MGM0 sensor values");
assert_eq!(mgm_0_reply.mgm_id, MgmId::Mgm0);
assert_eq!(mgm_0_reply.reply.switch_state, SwitchStateBinary::Off);
let sim_reply = sim_testbench
.try_receive_next_reply()
.expect("no MGM1 reply received");
assert_eq!(sim_reply.component(), SimComponent::Mgm1Lis3Mdl);
let mgm_1_reply = MgmReplyWrapper::from_sim_reply(&sim_reply)
.expect("failed to deserialize MGM1 sensor values");
assert_eq!(mgm_1_reply.mgm_id, MgmId::Mgm1);
assert_eq!(mgm_1_reply.reply.switch_state, SwitchStateBinary::On);
let mgm_0_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
assert_eq!(mgm_0_reply.switch_state, SwitchStateBinary::Off);
let mgm_1_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm1);
assert_eq!(mgm_1_reply.switch_state, SwitchStateBinary::On);
}
}
+42 -75
View File
@@ -3,26 +3,27 @@ use nexosim::{
ports::Output,
};
use satrs_minisim::{
acs::{MgmSensorValuesMicroTesla, MgtDipole, MgtHkSet, MgtReply, MGT_GEN_MAGNETIC_FIELD},
acs::{mgm, mgt},
SimReply,
};
use std::{sync::mpsc, time::Duration};
use types::pcdu::SwitchStateBinary;
pub struct MagnetorquerModel {
/// Simple magnetorquer simulation model.
pub struct MgtModel {
switch_state: SwitchStateBinary,
torquing: bool,
torque_dipole: MgtDipole,
pub gen_magnetic_field: Output<MgmSensorValuesMicroTesla>,
torque_dipole: mgt::Dipole,
pub gen_magnetic_field: Output<mgm::SensorValuesMicroTesla>,
reply_sender: mpsc::Sender<SimReply>,
}
impl MagnetorquerModel {
impl MgtModel {
pub fn new(reply_sender: mpsc::Sender<SimReply>) -> Self {
Self {
switch_state: SwitchStateBinary::Off,
torquing: false,
torque_dipole: MgtDipole::default(),
torque_dipole: mgt::Dipole::default(),
gen_magnetic_field: Output::new(),
reply_sender,
}
@@ -30,7 +31,7 @@ impl MagnetorquerModel {
pub async fn apply_torque(
&mut self,
duration_and_dipole: (Duration, MgtDipole),
duration_and_dipole: (Duration, mgt::Dipole),
cx: &mut Context<Self>,
) {
self.torque_dipole = duration_and_dipole.1;
@@ -45,7 +46,7 @@ impl MagnetorquerModel {
}
pub async fn clear_torque(&mut self, _: ()) {
self.torque_dipole = MgtDipole::default();
self.torque_dipole = mgt::Dipole::default();
self.torquing = false;
self.generate_magnetic_field(()).await;
}
@@ -65,17 +66,17 @@ impl MagnetorquerModel {
pub fn send_housekeeping_data(&mut self) {
self.reply_sender
.send(SimReply::new(&MgtReply::Hk(MgtHkSet {
.send(SimReply::from(mgt::Reply::Hk(mgt::HkSet {
dipole: self.torque_dipole,
torquing: self.torquing,
})))
.unwrap();
}
fn calc_magnetic_field(&self, _: MgtDipole) -> MgmSensorValuesMicroTesla {
fn calc_magnetic_field(&self, _: mgt::Dipole) -> mgm::SensorValuesMicroTesla {
// 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
mgm::MGT_GEN_MAGNETIC_FIELD
}
/// A torquing magnetorquer generates a magnetic field. This function can be used to apply
@@ -90,113 +91,79 @@ impl MagnetorquerModel {
}
}
impl Model for MagnetorquerModel {}
impl Model for MgtModel {}
#[cfg(test)]
mod tests {
use std::time::Duration;
use satrs_minisim::{
acs::{MgtDipole, MgtHkSet, MgtReply, MgtRequest},
SerializableSimMsgPayload, SimRequest,
};
use satrs_minisim::{acs::mgt, SimReply, SimRequestWithTime};
use types::pcdu::SwitchId;
use crate::{eps::tests::switch_device_on, test_helpers::SimTestbench};
fn request_hk(sim_testbench: &mut SimTestbench) -> Option<mgt::HkSet> {
let sim_reply = sim_testbench.request_reply(mgt::Request::RequestHk)?;
let SimReply::Mgt(mgt::Reply::Hk(hk)) = sim_reply else {
panic!("unexpected reply {sim_reply:?}");
};
Some(hk)
}
#[test]
fn test_basic_mgt_request_is_off() {
let mut sim_testbench = SimTestbench::new();
let request = SimRequest::new_with_epoch_time(MgtRequest::RequestHk);
sim_testbench
.send_request(request)
.expect("sending MGM request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step().unwrap();
let sim_reply_res = sim_testbench.try_receive_next_reply();
assert!(sim_reply_res.is_none());
assert!(request_hk(&mut sim_testbench).is_none());
}
#[test]
fn test_basic_mgt_request_is_on() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
let request = SimRequest::new_with_epoch_time(MgtRequest::RequestHk);
sim_testbench
.send_request(request)
.expect("sending MGM request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step().unwrap();
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_with_epoch_time(MgtRequest::RequestHk);
sim_testbench
.send_request(request)
.expect("sending MGM request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step().unwrap();
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"),
}
assert_eq!(
request_hk(&mut sim_testbench),
Some(mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
})
);
}
#[test]
fn test_basic_mgt_request_is_on_and_torquing() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
let commanded_dipole = MgtDipole {
let commanded_dipole = mgt::Dipole {
x: -200,
y: 200,
z: 1000,
};
let request = SimRequest::new_with_epoch_time(MgtRequest::ApplyTorque {
let request = SimRequestWithTime::new_with_epoch_time(mgt::Request::ApplyTorque {
duration: Duration::from_millis(100),
dipole: commanded_dipole,
});
sim_testbench
.send_request(request)
.expect("sending MGM request failed");
.expect("sending MGT request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step_until(Duration::from_millis(5)).unwrap();
check_mgt_hk(
&mut sim_testbench,
MgtHkSet {
assert_eq!(
request_hk(&mut sim_testbench),
Some(mgt::HkSet {
dipole: commanded_dipole,
torquing: true,
},
})
);
sim_testbench
.step_until(Duration::from_millis(100))
.unwrap();
check_mgt_hk(
&mut sim_testbench,
MgtHkSet {
dipole: MgtDipole::default(),
assert_eq!(
request_hk(&mut sim_testbench),
Some(mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
},
})
);
}
}
+101 -130
View File
@@ -1,18 +1,20 @@
use std::{sync::mpsc, time::Duration};
use std::{
sync::mpsc,
time::{Duration, SystemTime},
};
use nexosim::{
simulation::{Address, Scheduler, Simulation},
simulation::{Address, Mailbox, SimInit, Simulation},
time::{Clock, MonotonicTime, SystemClock},
};
use satrs_minisim::{
acs::{MgmRequestLis3Mdl, MgmRequestLis3MdlMgm0, MgmRequestLis3MdlMgm1, MgtRequest},
acs::{mgm, mgt},
eps::PcduRequest,
SerializableSimMsgPayload, SimComponent, SimCtrlReply, SimCtrlRequest, SimMessageProvider,
SimReply, SimRequest, SimRequestError,
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
};
use crate::{
acs::{mgm::MagnetometerModel, mgt::MagnetorquerModel},
acs::{mgm::MgmModel, mgt::MgtModel},
eps::PcduModel,
};
@@ -23,55 +25,85 @@ const MGM_REQ_WIRETAPPING: bool = false;
const PCDU_REQ_WIRETAPPING: bool = false;
const MGT_REQ_WIRETAPPING: bool = false;
pub struct ModelAddrWrapper {
mgm_0_addr: Address<MagnetometerModel>,
mgm_1_addr: Address<MagnetometerModel>,
pcdu_addr: Address<PcduModel>,
mgt_addr: Address<MagnetorquerModel>,
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ThreadingModel {
Default = 0,
Single = 1,
}
struct ModelAddresses {
mgm_0: Address<MgmModel>,
mgm_1: Address<MgmModel>,
pcdu: Address<PcduModel>,
mgt: Address<MgtModel>,
}
// The simulation controller processes requests and drives the simulation.
#[allow(dead_code)]
pub struct SimController {
pub sys_clock: SystemClock,
pub request_receiver: mpsc::Receiver<SimRequest>,
pub reply_sender: mpsc::Sender<SimReply>,
sys_clock: SystemClock,
request_receiver: mpsc::Receiver<SimRequestWithTime>,
reply_sender: mpsc::Sender<SimReply>,
pub simulation: Simulation,
pub scheduler: Scheduler,
pub addr_wrapper: ModelAddrWrapper,
addrs: ModelAddresses,
}
impl ModelAddrWrapper {
pub fn new(
mgm_0_addr: Address<MagnetometerModel>,
mgm_1_addr: Address<MagnetometerModel>,
pcdu_addr: Address<PcduModel>,
mgt_addr: Address<MagnetorquerModel>,
) -> Self {
Self {
mgm_0_addr,
mgm_1_addr,
pcdu_addr,
mgt_addr,
}
}
}
impl SimController {
pub fn new(
sys_clock: SystemClock,
request_receiver: mpsc::Receiver<SimRequest>,
threading_model: ThreadingModel,
start_time: MonotonicTime,
reply_sender: mpsc::Sender<SimReply>,
simulation: Simulation,
scheduler: Scheduler,
addr_wrapper: ModelAddrWrapper,
request_receiver: mpsc::Receiver<SimRequestWithTime>,
) -> Self {
let mgm_0_model = MgmModel::new(mgm::Id::Mgm0, reply_sender.clone());
let mgm_1_model = MgmModel::new(mgm::Id::Mgm1, reply_sender.clone());
let mut pcdu_model = PcduModel::new(reply_sender.clone());
let mut mgt_model = MgtModel::new(reply_sender.clone());
let mgm_0_mailbox = Mailbox::new();
let mgm_1_mailbox = Mailbox::new();
let pcdu_mailbox = Mailbox::new();
let mgt_mailbox = Mailbox::new();
let addrs = ModelAddresses {
mgm_0: mgm_0_mailbox.address(),
mgm_1: mgm_1_mailbox.address(),
pcdu: pcdu_mailbox.address(),
mgt: mgt_mailbox.address(),
};
pcdu_model
.mgm_0_switch
.connect(MgmModel::switch_device, &addrs.mgm_0);
pcdu_model
.mgm_1_switch
.connect(MgmModel::switch_device, &addrs.mgm_1);
pcdu_model
.mgt_switch
.connect(MgtModel::switch_device, &addrs.mgt);
mgt_model
.gen_magnetic_field
.connect(MgmModel::apply_external_magnetic_field, &addrs.mgm_0);
mgt_model
.gen_magnetic_field
.connect(MgmModel::apply_external_magnetic_field, &addrs.mgm_1);
let sim_init = if threading_model == ThreadingModel::Single {
SimInit::with_num_threads(1)
} else {
SimInit::new()
};
let (simulation, _scheduler) = sim_init
.add_model(mgm_0_model, mgm_0_mailbox, "MGM 0 model")
.add_model(mgm_1_model, mgm_1_mailbox, "MGM 1 model")
.add_model(pcdu_model, pcdu_mailbox, "PCDU model")
.add_model(mgt_model, mgt_mailbox, "MGT model")
.init(start_time)
.unwrap();
Self {
sys_clock,
sys_clock: SystemClock::from_system_time(start_time, SystemTime::now()),
request_receiver,
reply_sender,
simulation,
scheduler,
addr_wrapper,
addrs,
}
}
@@ -97,14 +129,11 @@ impl SimController {
if request.timestamp < old_timestamp && WARNING_FOR_STALE_DATA {
log::warn!("stale data with timestamp {:?} received", request.timestamp);
}
if let Err(e) = match request.component() {
SimComponent::SimCtrl => self.handle_ctrl_request(&request),
SimComponent::Mgm0Lis3Mdl => self.handle_mgm_request(0, &request),
SimComponent::Mgm1Lis3Mdl => self.handle_mgm_request(1, &request),
SimComponent::Mgt => self.handle_mgt_request(&request),
SimComponent::Pcdu => self.handle_pcdu_request(&request),
} {
self.handle_invalid_request_with_valid_target(e, &request)
match request.request {
SimRequest::SimCtrl(request) => self.handle_ctrl_request(request),
SimRequest::Mgm { id, request } => self.handle_mgm_request(id, request),
SimRequest::Mgt(request) => self.handle_mgt_request(request),
SimRequest::Pcdu(request) => self.handle_pcdu_request(request),
}
}
Err(e) => match e {
@@ -117,8 +146,7 @@ impl SimController {
}
}
fn handle_ctrl_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> {
let sim_ctrl_request = SimCtrlRequest::from_sim_message(request)?;
fn handle_ctrl_request(&mut self, sim_ctrl_request: SimCtrlRequest) {
if SIM_CTRL_REQ_WIRETAPPING {
log::info!("received sim ctrl request: {sim_ctrl_request:?}");
}
@@ -126,115 +154,67 @@ impl SimController {
SimCtrlRequest::Ping => {
log::info!("received ping request, a client is connecting");
self.reply_sender
.send(SimReply::new(&SimCtrlReply::Pong))
.send(SimReply::from(SimCtrlReply::Pong))
.expect("sending reply from sim controller failed");
}
}
Ok(())
}
fn handle_mgm_request(
&mut self,
mgm_idx: usize,
request: &SimRequest,
) -> Result<(), SimRequestError> {
let (mgm_request, addr) = match mgm_idx {
0 => (
MgmRequestLis3MdlMgm0::from_sim_message(request)?.0,
&self.addr_wrapper.mgm_0_addr,
),
1 => (
MgmRequestLis3MdlMgm1::from_sim_message(request)?.0,
&self.addr_wrapper.mgm_1_addr,
),
_ => panic!("invalid mgm index"),
fn handle_mgm_request(&mut self, mgm_id: mgm::Id, mgm_request: mgm::Request) {
let addr = match mgm_id {
mgm::Id::Mgm0 => &self.addrs.mgm_0,
mgm::Id::Mgm1 => &self.addrs.mgm_1,
};
if MGM_REQ_WIRETAPPING {
log::info!("received MGM{mgm_idx} request: {mgm_request:?}");
log::info!("received {mgm_id:?} request: {mgm_request:?}");
}
match mgm_request {
MgmRequestLis3Mdl::RequestSensorData => {
mgm::Request::RequestSensorData => {
self.simulation
.process_event(MagnetometerModel::send_sensor_values, (), addr)
.process_event(MgmModel::send_sensor_values, (), addr)
.expect("event execution error for mgm");
}
MgmRequestLis3Mdl::SetSpiFault(fault_mode) => {
log::info!("MGM{mgm_idx}: setting SPI fault mode to {fault_mode:?}");
mgm::Request::SetSpiFault(fault_mode) => {
log::info!("{mgm_id:?}: setting SPI fault mode to {fault_mode:?}");
self.simulation
.process_event(MagnetometerModel::set_spi_fault, fault_mode, addr)
.process_event(MgmModel::set_spi_fault, fault_mode, addr)
.expect("event execution error for mgm");
}
}
Ok(())
}
fn handle_pcdu_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> {
let pcdu_request = PcduRequest::from_sim_message(request)?;
fn handle_pcdu_request(&mut self, pcdu_request: PcduRequest) {
if PCDU_REQ_WIRETAPPING {
log::info!("received PCDU request: {pcdu_request:?}");
}
match pcdu_request {
PcduRequest::RequestSwitchInfo => {
self.simulation
.process_event(
PcduModel::request_switch_info,
(),
&self.addr_wrapper.pcdu_addr,
)
.process_event(PcduModel::request_switch_info, (), &self.addrs.pcdu)
.unwrap();
}
PcduRequest::SwitchDevice { switch, state } => {
self.simulation
.process_event(
PcduModel::switch_device,
(switch, state),
&self.addr_wrapper.pcdu_addr,
)
.process_event(PcduModel::switch_device, (switch, state), &self.addrs.pcdu)
.unwrap();
}
}
Ok(())
}
fn handle_mgt_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> {
let mgt_request = MgtRequest::from_sim_message(request)?;
fn handle_mgt_request(&mut self, mgt_request: mgt::Request) {
if MGT_REQ_WIRETAPPING {
log::info!("received MGT request: {mgt_request:?}");
}
match mgt_request {
MgtRequest::ApplyTorque { duration, dipole } => self
mgt::Request::ApplyTorque { duration, dipole } => self
.simulation
.process_event(
MagnetorquerModel::apply_torque,
(duration, dipole),
&self.addr_wrapper.mgt_addr,
)
.process_event(MgtModel::apply_torque, (duration, dipole), &self.addrs.mgt)
.unwrap(),
MgtRequest::RequestHk => self
mgt::Request::RequestHk => self
.simulation
.process_event(
MagnetorquerModel::request_housekeeping_data,
(),
&self.addr_wrapper.mgt_addr,
)
.process_event(MgtModel::request_housekeeping_data, (), &self.addrs.mgt)
.unwrap(),
};
Ok(())
}
fn handle_invalid_request_with_valid_target(
&self,
error: SimRequestError,
request: &SimRequest,
) {
log::warn!(
"received invalid {:?} request: {:?}",
request.component(),
error
);
self.reply_sender
.send(SimReply::new(&SimCtrlReply::from(error)))
.expect("sending reply from sim controller failed");
}
}
@@ -247,18 +227,9 @@ mod tests {
#[test]
fn test_basic_ping() {
let mut sim_testbench = SimTestbench::new();
let request = SimRequest::new_with_epoch_time(SimCtrlRequest::Ping);
sim_testbench
.send_request(request)
.expect("sending sim ctrl request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step().unwrap();
let sim_reply = sim_testbench.try_receive_next_reply();
assert!(sim_reply.is_some());
let sim_reply = sim_reply.unwrap();
assert_eq!(sim_reply.component(), SimComponent::SimCtrl);
let reply = SimCtrlReply::from_sim_message(&sim_reply)
.expect("failed to deserialize MGM sensor values");
assert_eq!(reply, SimCtrlReply::Pong);
assert_eq!(
sim_testbench.request_reply(SimCtrlRequest::Ping),
Some(SimReply::SimCtrl(SimCtrlReply::Pong))
);
}
}
+22 -44
View File
@@ -38,7 +38,7 @@ impl PcduModel {
}
pub fn send_switch_info(&mut self) {
let reply = SimReply::new(&PcduReply::SwitchInfo(self.switcher_map.0.clone()));
let reply = SimReply::from(PcduReply::SwitchInfo(self.switcher_map.0.clone()));
self.reply_sender.send(reply).unwrap();
}
@@ -75,9 +75,7 @@ pub(crate) mod tests {
use super::*;
use std::time::Duration;
use satrs_minisim::{
eps::PcduRequest, SerializableSimMsgPayload, SimComponent, SimMessageProvider, SimRequest,
};
use satrs_minisim::{eps::PcduRequest, SimRequestWithTime};
use types::pcdu::SwitchMapBinary;
use crate::test_helpers::SimTestbench;
@@ -87,18 +85,12 @@ pub(crate) mod tests {
switch: SwitchId,
target: SwitchStateBinary,
) {
let request = SimRequest::new_with_epoch_time(PcduRequest::SwitchDevice {
sim_testbench.send_and_step(PcduRequest::SwitchDevice {
switch,
state: target,
});
sim_testbench
.send_request(request)
.expect("sending MGM switch request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step().unwrap();
}
#[allow(dead_code)]
pub(crate) fn switch_device_off(sim_testbench: &mut SimTestbench, switch: SwitchId) {
switch_device(sim_testbench, switch, SwitchStateBinary::Off);
}
@@ -110,24 +102,18 @@ pub(crate) mod tests {
SwitchMapBinaryWrapper::default().0
}
fn unwrap_switch_map(sim_reply: SimReply) -> SwitchMapBinary {
let SimReply::Pcdu(PcduReply::SwitchInfo(switch_map)) = sim_reply else {
panic!("unexpected reply {sim_reply:?}");
};
switch_map
}
fn check_switch_state(sim_testbench: &mut SimTestbench, expected_switch_map: &SwitchMapBinary) {
let request = SimRequest::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
sim_testbench
.send_request(request)
.expect("sending MGM request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step().unwrap();
let sim_reply = sim_testbench.try_receive_next_reply();
assert!(sim_reply.is_some());
let sim_reply = sim_reply.unwrap();
assert_eq!(sim_reply.component(), SimComponent::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);
}
}
let sim_reply = sim_testbench
.request_reply(PcduRequest::RequestSwitchInfo)
.expect("no PCDU reply received");
assert_eq!(unwrap_switch_map(sim_reply), *expected_switch_map);
}
fn test_pcdu_switching_single_switch(switch: SwitchId, target: SwitchStateBinary) {
@@ -141,28 +127,20 @@ pub(crate) mod tests {
#[test]
fn test_pcdu_switcher_request() {
let mut sim_testbench = SimTestbench::new();
let request = SimRequest::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
let request = SimRequestWithTime::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
sim_testbench
.send_request(request)
.expect("sending MGM request failed");
.expect("sending PCDU request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step_until(Duration::from_millis(1)).unwrap();
assert!(sim_testbench.try_receive_next_reply().is_none());
let sim_reply = sim_testbench.try_receive_next_reply();
assert!(sim_reply.is_none());
// Reply takes 20ms
// The reply is delayed by SWITCH_INFO_DELAY_MS.
sim_testbench.step_until(Duration::from_millis(25)).unwrap();
let sim_reply = sim_testbench.try_receive_next_reply();
assert!(sim_reply.is_some());
let sim_reply = sim_reply.unwrap();
assert_eq!(sim_reply.component(), SimComponent::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());
}
}
let sim_reply = sim_testbench
.try_receive_next_reply()
.expect("no PCDU reply received");
assert_eq!(unwrap_switch_map(sim_reply), get_all_off_switch_map());
}
#[test]
+189 -369
View File
@@ -1,8 +1,14 @@
use nexosim::time::MonotonicTime;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde::{Deserialize, Serialize};
use crate::{
acs::{mgm, mgt},
eps::{PcduReply, PcduRequest},
};
/// Used by clients to route replies to the component handling them.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum SimComponent {
pub enum ComponentId {
SimCtrl,
Mgm0Lis3Mdl,
Mgm1Lis3Mdl,
@@ -10,122 +16,85 @@ pub enum SimComponent {
Pcdu,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimMessage {
pub target: SimComponent,
pub payload: String,
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SimRequest {
SimCtrl(SimCtrlRequest),
Mgm { id: mgm::Id, request: mgm::Request },
Mgt(mgt::Request),
Pcdu(PcduRequest),
}
/// 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,
impl From<SimCtrlRequest> for SimRequest {
fn from(request: SimCtrlRequest) -> Self {
Self::SimCtrl(request)
}
}
impl From<mgt::Request> for SimRequest {
fn from(request: mgt::Request) -> Self {
Self::Mgt(request)
}
}
impl From<PcduRequest> for SimRequest {
fn from(request: PcduRequest) -> Self {
Self::Pcdu(request)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimRequestWithTime {
pub request: SimRequest,
pub timestamp: MonotonicTime,
}
#[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: SimComponent;
fn from_sim_message(sim_message: &P) -> Result<Self, SimMessageError<P>> {
if sim_message.component() == 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 component(&self) -> SimComponent;
fn payload(&self) -> &String;
fn from_raw_data(data: &[u8]) -> serde_json::Result<Self> {
serde_json::from_slice(data)
}
}
impl SimRequest {
pub fn new_with_epoch_time<T: SerializableSimMsgPayload<SimRequest>>(
serializable_request: T,
) -> Self {
Self::new(serializable_request, MonotonicTime::EPOCH)
}
pub fn new<T: SerializableSimMsgPayload<SimRequest>>(
serializable_request: T,
timestamp: MonotonicTime,
) -> Self {
impl SimRequestWithTime {
pub fn new(request: impl Into<SimRequest>, timestamp: MonotonicTime) -> Self {
Self {
inner: SimMessage {
target: T::TARGET,
payload: serde_json::to_string(&serializable_request).unwrap(),
},
request: request.into(),
timestamp,
}
}
}
impl SimMessageProvider for SimRequest {
fn component(&self) -> SimComponent {
self.inner.target
}
fn payload(&self) -> &String {
&self.inner.payload
}
fn msg_type(&self) -> SimMessageType {
SimMessageType::Request
pub fn new_with_epoch_time(request: impl Into<SimRequest>) -> Self {
Self::new(request, MonotonicTime::EPOCH)
}
}
/// A generic simulation reply 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 SimReply {
inner: SimMessage,
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SimReply {
SimCtrl(SimCtrlReply),
Mgm { id: mgm::Id, reply: mgm::Reply },
Mgt(mgt::Reply),
Pcdu(PcduReply),
}
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(),
},
}
}
/// For payloads where the target is only known at runtime.
pub fn new_with_target<T: Serialize>(target: SimComponent, reply: &T) -> Self {
Self {
inner: SimMessage {
target,
payload: serde_json::to_string(reply).unwrap(),
},
pub fn component(&self) -> ComponentId {
match self {
SimReply::SimCtrl(_) => ComponentId::SimCtrl,
SimReply::Mgm { id, .. } => id.sim_component(),
SimReply::Mgt(_) => ComponentId::Mgt,
SimReply::Pcdu(_) => ComponentId::Pcdu,
}
}
}
impl SimMessageProvider for SimReply {
fn component(&self) -> SimComponent {
self.inner.target
impl From<SimCtrlReply> for SimReply {
fn from(reply: SimCtrlReply) -> Self {
Self::SimCtrl(reply)
}
fn payload(&self) -> &String {
&self.inner.payload
}
impl From<mgt::Reply> for SimReply {
fn from(reply: mgt::Reply) -> Self {
Self::Mgt(reply)
}
fn msg_type(&self) -> SimMessageType {
SimMessageType::Reply
}
impl From<PcduReply> for SimReply {
fn from(reply: PcduReply) -> Self {
Self::Pcdu(reply)
}
}
@@ -134,52 +103,15 @@ pub enum SimCtrlRequest {
Ping,
}
impl SerializableSimMsgPayload<SimRequest> for SimCtrlRequest {
const TARGET: SimComponent = SimComponent::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: SimComponent = SimComponent::SimCtrl;
}
impl From<SimRequestError> for SimCtrlReply {
fn from(error: SimRequestError) -> Self {
SimCtrlReply::InvalidRequest(error)
}
}
pub mod eps {
use super::*;
use types::pcdu::{SwitchId, SwitchMapBinary, SwitchStateBinary};
#[derive(Debug, Copy, Clone)]
#[repr(u8)]
pub enum PcduRequestId {
SwitchDevice = 0,
RequestSwitchInfo = 1,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PcduRequest {
SwitchDevice {
@@ -189,102 +121,61 @@ pub mod eps {
RequestSwitchInfo,
}
impl SerializableSimMsgPayload<SimRequest> for PcduRequest {
const TARGET: SimComponent = SimComponent::Pcdu;
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PcduReply {
// Ack,
SwitchInfo(SwitchMapBinary),
}
impl SerializableSimMsgPayload<SimReply> for PcduReply {
const TARGET: SimComponent = SimComponent::Pcdu;
}
}
pub mod acs {
use std::time::Duration;
use types::pcdu::SwitchStateBinary;
use super::*;
/// Fault mode injected on the simulated SPI bus, independent of the switch state.
///
/// Models the classic symptom of a stuck SPI bus: an undriven MISO line commonly reads
/// back as all-1s, a shorted/grounded one as all-0s.
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpiFaultMode {
#[default]
None,
AllZeros,
AllOnes,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpiFault {
pub mode: SpiFaultMode,
/// The fault is cleared when the device is switched off, so a power cycle recovers
/// from it.
pub cleared_by_power_cycle: bool,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum MgmRequestLis3Mdl {
RequestSensorData,
/// Force the raw register reply into a stuck-bus pattern, regardless of switch state.
/// Used to test FDIR handling of SPI bus faults.
SetSpiFault(SpiFault),
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct MgmRequestLis3MdlMgm0(pub MgmRequestLis3Mdl);
impl SerializableSimMsgPayload<SimRequest> for MgmRequestLis3MdlMgm0 {
const TARGET: SimComponent = SimComponent::Mgm0Lis3Mdl;
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct MgmRequestLis3MdlMgm1(pub MgmRequestLis3Mdl);
impl SerializableSimMsgPayload<SimRequest> for MgmRequestLis3MdlMgm1 {
const TARGET: SimComponent = SimComponent::Mgm1Lis3Mdl;
}
// 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. The unit is micro tesla.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct MgmSensorValuesMicroTesla {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct MgmReplyCommon {}
pub const MGT_GEN_MAGNETIC_FIELD: MgmSensorValuesMicroTesla = MgmSensorValuesMicroTesla {
x: 30.0,
y: -30.0,
z: 30.0,
};
pub const ALL_ONES_SENSOR_VAL: i16 = 0xffff_u16 as i16;
pub const ALL_ZEROS_SENSOR_VAL: i16 = 0;
/// MGM module strongly based on the LIS3MDL device.
pub mod mgm {
use serde::{Deserialize, Serialize};
use types::pcdu::SwitchStateBinary;
use super::*;
use crate::ComponentId;
/// Fault mode injected on the simulated SPI bus, independent of the switch state.
///
/// Models the classic symptom of a stuck SPI bus: an undriven MISO line commonly reads
/// back as all-1s, a shorted/grounded one as all-0s.
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpiFaultMode {
#[default]
None,
AllZeros,
AllOnes,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpiFault {
pub mode: SpiFaultMode,
/// The fault is cleared when the device is switched off, so a power cycle recovers
/// from it.
pub cleared_by_power_cycle: bool,
}
// 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. The unit is micro tesla.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct SensorValuesMicroTesla {
pub x: f32,
pub y: f32,
pub z: f32,
}
pub const MGT_GEN_MAGNETIC_FIELD: SensorValuesMicroTesla = SensorValuesMicroTesla {
x: 30.0,
y: -30.0,
z: 30.0,
};
pub const ALL_ONES_SENSOR_VAL: i16 = 0xffff_u16 as i16;
pub const ALL_ZEROS_SENSOR_VAL: i16 = 0;
// Field data register scaling
pub const GAUSS_TO_MICROTESLA_FACTOR: u32 = 100;
pub const FIELD_LSB_PER_GAUSS_4_SENS: f32 = 1.0 / 6842.0;
pub const FIELD_LSB_PER_GAUSS_8_SENS: f32 = 1.0 / 3421.0;
pub const FIELD_LSB_PER_GAUSS_12_SENS: f32 = 1.0 / 2281.0;
pub const FIELD_LSB_PER_GAUSS_16_SENS: f32 = 1.0 / 1711.0;
#[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct RawValues {
@@ -292,165 +183,115 @@ pub mod acs {
pub y: i16,
pub z: i16,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Request {
RequestSensorData,
/// Force the raw register reply into a stuck-bus pattern, regardless of switch state.
/// Used to test FDIR handling of SPI bus faults.
SetSpiFault(SpiFault),
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct MgmReply {
pub struct Reply {
pub switch_state: SwitchStateBinary,
pub sensor_values: MgmSensorValuesMicroTesla,
pub sensor_values: SensorValuesMicroTesla,
// Raw sensor values which are transmitted by the LIS3 device in little-endian
// order.
pub raw: RawValues,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum MgmId {
pub enum Id {
Mgm0,
Mgm1,
}
impl MgmId {
pub const fn sim_component(&self) -> SimComponent {
impl Id {
pub const fn sim_component(&self) -> ComponentId {
match self {
MgmId::Mgm0 => SimComponent::Mgm0Lis3Mdl,
MgmId::Mgm1 => SimComponent::Mgm1Lis3Mdl,
Id::Mgm0 => ComponentId::Mgm0Lis3Mdl,
Id::Mgm1 => ComponentId::Mgm1Lis3Mdl,
}
}
}
/// Does not implement [SerializableSimMsgPayload] because the target depends on the
/// MGM ID, which is only known at runtime.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct MgmReplyWrapper {
pub mgm_id: MgmId,
pub reply: MgmReply,
}
impl MgmReplyWrapper {
pub fn to_sim_reply(&self) -> SimReply {
SimReply::new_with_target(self.mgm_id.sim_component(), self)
}
pub fn from_sim_reply(sim_reply: &SimReply) -> Result<Self, SimReplyError> {
let wrapper: Self = serde_json::from_str(sim_reply.payload())?;
if wrapper.mgm_id.sim_component() != sim_reply.component() {
return Err(SimMessageError::TargetRequestMissmatch(sim_reply.clone()));
impl RawValues {
pub const fn splat(value: i16) -> Self {
Self {
x: value,
y: value,
z: value,
}
}
pub fn from_microtesla(values: SensorValuesMicroTesla) -> Self {
let to_raw = |microtesla: f32| {
(microtesla / (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
.round() as i16
};
Self {
x: to_raw(values.x),
y: to_raw(values.y),
z: to_raw(values.z),
}
Ok(wrapper)
}
}
impl MgmReply {
impl Reply {
pub fn new(
switch_state: SwitchStateBinary,
sensor_values: MgmSensorValuesMicroTesla,
sensor_values: SensorValuesMicroTesla,
fault_mode: SpiFaultMode,
) -> Self {
match fault_mode {
SpiFaultMode::AllZeros => {
return Self {
switch_state,
sensor_values,
raw: RawValues {
x: ALL_ZEROS_SENSOR_VAL,
y: ALL_ZEROS_SENSOR_VAL,
z: ALL_ZEROS_SENSOR_VAL,
},
};
// An injected fault always wins. A switched off device reads back like an
// undriven bus.
let raw = match (fault_mode, switch_state) {
(SpiFaultMode::AllZeros, _) => RawValues::splat(ALL_ZEROS_SENSOR_VAL),
(SpiFaultMode::AllOnes, _) | (SpiFaultMode::None, SwitchStateBinary::Off) => {
RawValues::splat(ALL_ONES_SENSOR_VAL)
}
SpiFaultMode::AllOnes => {
return Self {
switch_state,
sensor_values,
raw: RawValues {
x: ALL_ONES_SENSOR_VAL,
y: ALL_ONES_SENSOR_VAL,
z: ALL_ONES_SENSOR_VAL,
},
};
}
SpiFaultMode::None => (),
}
match switch_state {
SwitchStateBinary::Off => Self {
switch_state,
sensor_values,
raw: RawValues {
x: ALL_ONES_SENSOR_VAL,
y: ALL_ONES_SENSOR_VAL,
z: ALL_ONES_SENSOR_VAL,
},
},
SwitchStateBinary::On => {
let mut raw_reply: [u8; 7] = [0; 7];
let raw_x: i16 = (sensor_values.x
/ (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
.round() as i16;
let raw_y: i16 = (sensor_values.y
/ (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
.round() as i16;
let raw_z: i16 = (sensor_values.z
/ (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
.round() as i16;
// The first byte is a dummy byte.
raw_reply[1..3].copy_from_slice(&raw_x.to_be_bytes());
raw_reply[3..5].copy_from_slice(&raw_y.to_be_bytes());
raw_reply[5..7].copy_from_slice(&raw_z.to_be_bytes());
Self {
switch_state,
sensor_values,
raw: RawValues {
x: raw_x,
y: raw_y,
z: raw_z,
},
}
(SpiFaultMode::None, SwitchStateBinary::On) => {
RawValues::from_microtesla(sensor_values)
}
};
Self {
switch_state,
sensor_values,
raw,
}
}
}
}
// 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,
}
pub mod mgt {
use std::time::Duration;
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum MgtRequestType {
ApplyTorque,
}
use serde::{Deserialize, Serialize};
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum MgtRequest {
ApplyTorque {
duration: Duration,
dipole: MgtDipole,
},
RequestHk,
}
// Simple model using i16 values.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Dipole {
pub x: i16,
pub y: i16,
pub z: i16,
}
impl SerializableSimMsgPayload<SimRequest> for MgtRequest {
const TARGET: SimComponent = SimComponent::Mgt;
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Request {
ApplyTorque { duration: Duration, dipole: Dipole },
RequestHk,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MgtHkSet {
pub dipole: MgtDipole,
pub torquing: bool,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HkSet {
pub dipole: Dipole,
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: SimComponent = SimComponent::Mgm0Lis3Mdl;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Reply {
Hk(HkSet),
}
}
}
@@ -462,41 +303,20 @@ pub mod udp {
pub mod tests {
use super::*;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DummyRequest {
Ping,
}
impl SerializableSimMsgPayload<SimRequest> for DummyRequest {
const TARGET: SimComponent = SimComponent::SimCtrl;
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DummyReply {
Pong,
}
impl SerializableSimMsgPayload<SimReply> for DummyReply {
const TARGET: SimComponent = SimComponent::SimCtrl;
#[test]
fn test_request_serde_roundtrip() {
let sim_request = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
let json = serde_json::to_string(&sim_request).unwrap();
let deserialized: SimRequestWithTime = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, sim_request);
}
#[test]
fn test_basic_request() {
let sim_request = SimRequest::new_with_epoch_time(DummyRequest::Ping);
assert_eq!(sim_request.component(), SimComponent::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.component(), SimComponent::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);
fn test_reply_serde_roundtrip() {
let sim_reply = SimReply::from(SimCtrlReply::Pong);
assert_eq!(sim_reply.component(), ComponentId::SimCtrl);
let json = serde_json::to_string(&sim_reply).unwrap();
let deserialized: SimReply = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, sim_reply);
}
}
+3 -84
View File
@@ -1,14 +1,8 @@
use acs::{mgm::MagnetometerModel, mgt::MagnetorquerModel};
use controller::{ModelAddrWrapper, SimController};
use eps::PcduModel;
use nexosim::simulation::{Mailbox, SimInit};
use nexosim::time::{MonotonicTime, SystemClock};
use satrs_minisim::acs::mgm::MgmId;
use controller::{SimController, ThreadingModel};
use nexosim::time::MonotonicTime;
use satrs_minisim::udp::SIM_CTRL_PORT;
use satrs_minisim::{SimReply, SimRequest};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, SystemTime};
use udp::SimUdpServer;
mod acs;
@@ -19,87 +13,12 @@ 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_0_model =
MagnetometerModel::new(MgmId::Mgm0, Duration::from_millis(50), reply_sender.clone());
let mgm_1_model =
MagnetometerModel::new(MgmId::Mgm1, Duration::from_millis(50), reply_sender.clone());
let mgm_0_mailbox = Mailbox::new();
let mgm_0_addr = mgm_0_mailbox.address();
let mgm_1_mailbox = Mailbox::new();
let mgm_1_addr = mgm_1_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_0_switch
.connect(MagnetometerModel::switch_device, &mgm_0_addr);
pcdu_model
.mgm_1_switch
.connect(MagnetometerModel::switch_device, &mgm_1_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_0_addr,
);
mgt_model.gen_magnetic_field.connect(
MagnetometerModel::apply_external_magnetic_field,
&mgm_1_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 addrs = ModelAddrWrapper::new(mgm_0_addr, mgm_1_addr, pcdu_addr, mgt_addr);
let (simulation, scheduler) = sim_init
.add_model(mgm_0_model, mgm_0_mailbox, "MGM 0 model")
.add_model(mgm_1_model, mgm_1_mailbox, "MGM 1 model")
.add_model(pcdu_model, pcdu_mailbox, "PCDU model")
.add_model(mgt_model, mgt_mailbox, "MGT model")
.init(start_time)
.unwrap();
SimController::new(
sys_clock,
request_receiver,
reply_sender,
simulation,
scheduler,
addrs,
)
}
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);
SimController::new(ThreadingModel::Default, t0, reply_sender, request_receiver);
// Configure logger at runtime
fern::Dispatch::new()
// Perform allocation-free log formatting
+21 -5
View File
@@ -5,14 +5,14 @@ use nexosim::{
simulation::ExecutionError,
time::{Deadline, MonotonicTime},
};
use satrs_minisim::{SimReply, SimRequest};
use satrs_minisim::{SimReply, SimRequest, SimRequestWithTime};
use crate::{controller::SimController, create_sim_controller, ThreadingModel};
use crate::controller::{SimController, ThreadingModel};
pub struct SimTestbench {
pub sim_controller: SimController,
pub reply_receiver: mpsc::Receiver<SimReply>,
pub request_sender: mpsc::Sender<SimRequest>,
pub request_sender: mpsc::Sender<SimRequestWithTime>,
}
impl SimTestbench {
@@ -21,7 +21,7 @@ impl SimTestbench {
let (reply_sender, reply_receiver) = mpsc::channel();
let t0 = MonotonicTime::EPOCH;
let sim_ctrl =
create_sim_controller(ThreadingModel::Single, t0, reply_sender, request_receiver);
SimController::new(ThreadingModel::Single, t0, reply_sender, request_receiver);
Self {
sim_controller: sim_ctrl,
@@ -43,10 +43,26 @@ impl SimTestbench {
}
}
pub fn send_request(&self, request: SimRequest) -> Result<(), mpsc::SendError<SimRequest>> {
pub fn send_request(
&self,
request: SimRequestWithTime,
) -> Result<(), mpsc::SendError<SimRequestWithTime>> {
self.request_sender.send(request)
}
/// Sends the request and steps the simulation to the next scheduled event.
pub fn send_and_step(&mut self, request: impl Into<SimRequest>) {
self.send_request(SimRequestWithTime::new_with_epoch_time(request))
.expect("sending request failed");
self.handle_sim_requests_time_agnostic();
self.step().unwrap();
}
pub fn request_reply(&mut self, request: impl Into<SimRequest>) -> Option<SimReply> {
self.send_and_step(request);
self.try_receive_next_reply()
}
pub fn try_receive_next_reply(&self) -> Option<SimReply> {
match self.reply_receiver.try_recv() {
Ok(reply) => Some(reply),
+27 -20
View File
@@ -6,13 +6,12 @@ use std::{
time::Duration,
};
use satrs_minisim::{SimMessageProvider, SimReply, SimRequest};
use satrs_minisim::{SimReply, SimRequestWithTime};
// 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,
request_sender: mpsc::Sender<SimRequestWithTime>,
reply_receiver: mpsc::Receiver<SimReply>,
reply_queue: VecDeque<SimReply>,
max_num_replies: usize,
@@ -27,7 +26,7 @@ pub struct SimUdpServer {
impl SimUdpServer {
pub fn new(
local_port: u16,
request_sender: mpsc::Sender<SimRequest>,
request_sender: mpsc::Sender<SimRequestWithTime>,
reply_receiver: mpsc::Receiver<SimReply>,
max_num_replies: usize,
stop_signal: Option<Arc<AtomicBool>>,
@@ -47,7 +46,7 @@ impl SimUdpServer {
})
}
#[allow(dead_code)]
#[cfg(test)]
pub fn server_addr(&self) -> std::io::Result<SocketAddr> {
self.socket.local_addr()
}
@@ -90,7 +89,7 @@ impl SimUdpServer {
self.sender_addr = Some(src);
let sim_req = SimRequest::from_raw_data(&self.req_buf[..bytes_read]);
let sim_req = serde_json::from_slice::<SimRequestWithTime>(&self.req_buf[..bytes_read]);
if let Err(e) = sim_req {
log::warn!("received UDP request with invalid format: {}", e);
return processed_requests;
@@ -157,7 +156,7 @@ mod tests {
use satrs_minisim::{
eps::{PcduReply, PcduRequest},
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest,
SimCtrlReply, SimCtrlRequest, SimReply, SimRequestWithTime,
};
use crate::eps::tests::get_all_off_switch_map;
@@ -202,7 +201,7 @@ mod tests {
})
}
pub fn send_request(&self, sim_request: &SimRequest) -> std::io::Result<usize> {
pub fn send_request(&self, sim_request: &SimRequestWithTime) -> std::io::Result<usize> {
self.socket.send(
&serde_json::to_vec(sim_request).expect("conversion of request to vector failed"),
)
@@ -220,7 +219,7 @@ mod tests {
struct UdpTestbench {
client: SimUdpTestClient,
stop_signal: Arc<AtomicBool>,
request_receiver: mpsc::Receiver<SimRequest>,
request_receiver: mpsc::Receiver<SimRequestWithTime>,
reply_sender: mpsc::Sender<SimReply>,
}
@@ -256,7 +255,7 @@ mod tests {
))
}
pub fn try_recv_request(&self) -> Result<SimRequest, mpsc::TryRecvError> {
pub fn try_recv_request(&self) -> Result<SimRequestWithTime, mpsc::TryRecvError> {
self.request_receiver.try_recv()
}
@@ -272,7 +271,7 @@ mod tests {
delegate! {
to self.client {
pub fn send_request(&self, sim_request: &SimRequest) -> std::io::Result<usize>;
pub fn send_request(&self, sim_request: &SimRequestWithTime) -> std::io::Result<usize>;
pub fn recv_sim_reply(&mut self) -> Result<SimReply, ReceptionError>;
}
}
@@ -316,7 +315,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_with_epoch_time(PcduRequest::RequestSwitchInfo);
let sim_request = SimRequestWithTime::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
udp_testbench
.send_request(&sim_request)
.expect("sending request failed");
@@ -338,10 +337,12 @@ mod tests {
.expect("could not create testbench");
let server_thread = std::thread::spawn(move || udp_server.run());
udp_testbench
.send_request(&SimRequest::new_with_epoch_time(SimCtrlRequest::Ping))
.send_request(&SimRequestWithTime::new_with_epoch_time(
SimCtrlRequest::Ping,
))
.expect("sending request failed");
let sim_reply = SimReply::new(&PcduReply::SwitchInfo(get_all_off_switch_map()));
let sim_reply = SimReply::from(PcduReply::SwitchInfo(get_all_off_switch_map()));
udp_testbench.send_reply(&sim_reply);
udp_testbench.check_next_sim_reply(&sim_reply);
@@ -362,11 +363,13 @@ 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_with_epoch_time(SimCtrlRequest::Ping))
.send_request(&SimRequestWithTime::new_with_epoch_time(
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()));
let sim_reply = SimReply::from(PcduReply::SwitchInfo(get_all_off_switch_map()));
udp_testbench.send_reply(&sim_reply);
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
@@ -385,7 +388,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(&PcduReply::SwitchInfo(get_all_off_switch_map()));
let sim_reply = SimReply::from(PcduReply::SwitchInfo(get_all_off_switch_map()));
udp_testbench.send_reply(&sim_reply);
std::thread::sleep(Duration::from_millis(10));
@@ -393,7 +396,9 @@ mod tests {
// Connect by sending a ping.
udp_testbench
.send_request(&SimRequest::new_with_epoch_time(SimCtrlRequest::Ping))
.send_request(&SimRequestWithTime::new_with_epoch_time(
SimCtrlRequest::Ping,
))
.expect("sending request failed");
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
@@ -412,7 +417,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(&SimCtrlReply::Pong);
let sim_reply = SimReply::from(SimCtrlReply::Pong);
for _ in 0..4 {
udp_testbench.send_reply(&sim_reply);
}
@@ -422,7 +427,9 @@ mod tests {
// Connect by sending a ping.
udp_testbench
.send_request(&SimRequest::new_with_epoch_time(SimCtrlRequest::Ping))
.send_request(&SimRequestWithTime::new_with_epoch_time(
SimCtrlRequest::Ping,
))
.expect("sending request failed");
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
+30 -25
View File
@@ -2,11 +2,9 @@ use satrs::fdir::{FaultCounterStd, FaultResponse, RecoveryEvent, RecoveryFdir};
use satrs::health::HealthTableMapSync;
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use satrs_example::{HkHelperSingleSet, TimestampHelper, TmtcQueues};
use satrs_minisim::acs::mgm::{
FIELD_LSB_PER_GAUSS_4_SENS, GAUSS_TO_MICROTESLA_FACTOR, MgmReplyWrapper, RawValues,
};
use satrs_minisim::acs::{MgmRequestLis3Mdl, MgmRequestLis3MdlMgm0, MgmRequestLis3MdlMgm1};
use satrs_minisim::{SimReply, SimRequest};
use satrs_minisim::acs::mgm as sim_mgm;
use satrs_minisim::acs::mgm::{FIELD_LSB_PER_GAUSS_4_SENS, GAUSS_TO_MICROTESLA_FACTOR};
use satrs_minisim::{SimReply, SimRequest, SimRequestWithTime};
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
@@ -76,7 +74,7 @@ impl MgmId {
#[derive(Default)]
pub struct SpiDummyInterface {
pub dummy_values: RawValues,
pub dummy_values: sim_mgm::RawValues,
}
impl SpiDummyInterface {
@@ -90,7 +88,7 @@ impl SpiDummyInterface {
#[derive(Default)]
pub struct TestSpiInterface {
pub call_count: u32,
pub next_mgm_data: RawValues,
pub next_mgm_data: sim_mgm::RawValues,
}
impl TestSpiInterface {
@@ -104,26 +102,33 @@ impl TestSpiInterface {
pub struct SpiSimInterface {
pub id: MgmId,
pub sim_request_tx: mpsc::Sender<SimRequest>,
pub sim_request_tx: mpsc::Sender<SimRequestWithTime>,
pub sim_reply_rx: mpsc::Receiver<SimReply>,
}
impl SpiSimInterface {
// Right now, we only support requesting sensor data and not configuration of the sensor.
fn transfer(&mut self, _tx: &[u8], rx: &mut [u8]) {
let mgm_sensor_request = MgmRequestLis3Mdl::RequestSensorData;
let sim_request = match self.id {
MgmId::_0 => SimRequest::new_with_epoch_time(MgmRequestLis3MdlMgm0(mgm_sensor_request)),
MgmId::_1 => SimRequest::new_with_epoch_time(MgmRequestLis3MdlMgm1(mgm_sensor_request)),
let sim_id = match self.id {
MgmId::_0 => sim_mgm::Id::Mgm0,
MgmId::_1 => sim_mgm::Id::Mgm1,
};
let sim_request = SimRequestWithTime::new_with_epoch_time(SimRequest::Mgm {
id: sim_id,
request: sim_mgm::Request::RequestSensorData,
});
if let Err(e) = self.sim_request_tx.send(sim_request) {
log::error!("failed to send MGM LIS3 request: {e}");
}
match self.sim_reply_rx.recv_timeout(Duration::from_millis(50)) {
Ok(sim_reply) => {
let sim_reply_lis3 = MgmReplyWrapper::from_sim_reply(&sim_reply)
.expect("failed to parse LIS3 reply")
.reply;
let sim_reply_lis3 = match sim_reply {
SimReply::Mgm { id, reply } if id == sim_id => reply,
_ => {
log::warn!("unexpected MGM LIS3 SIM reply: {sim_reply:?}");
return;
}
};
rx[X_LOWBYTE_IDX..X_LOWBYTE_IDX + 2]
.copy_from_slice(&sim_reply_lis3.raw.x.to_le_bytes());
rx[Y_LOWBYTE_IDX..Y_LOWBYTE_IDX + 2]
@@ -647,7 +652,7 @@ mod tests {
use arbitrary_int::u11;
use satrs::health::{HealthState, HealthTableProvider};
use satrs::spacepackets::SpacePacketHeader;
use satrs_minisim::acs::mgm::RawValues;
use satrs_minisim::acs::mgm as sim_mgm;
use types::{
Apid, ComponentId, TcHeader,
acs::mgm::request::HkRequest,
@@ -766,7 +771,7 @@ mod tests {
}
pub fn inject_stuck_bus(&mut self) {
self.test_spi_interface().next_mgm_data = RawValues {
self.test_spi_interface().next_mgm_data = sim_mgm::RawValues {
x: -1,
y: -1,
z: -1,
@@ -907,7 +912,7 @@ mod tests {
#[test]
fn test_normal_handler_mgm_set_conversion() {
let mut testbench = MgmTestbench::new();
let raw_values = RawValues {
let raw_values = sim_mgm::RawValues {
x: 1000,
y: -1000,
z: 1000,
@@ -1060,7 +1065,7 @@ mod tests {
fn test_spi_fault_below_threshold_stays_healthy() {
let mut testbench = MgmTestbench::new();
testbench.switch_to_normal();
testbench.test_spi_interface().next_mgm_data = RawValues {
testbench.test_spi_interface().next_mgm_data = sim_mgm::RawValues {
x: -1,
y: -1,
z: -1,
@@ -1101,7 +1106,7 @@ mod tests {
testbench.drain_switch_requests();
testbench.mode_report_rx.try_iter().for_each(drop);
testbench.exceed_spi_fault_threshold();
testbench.test_spi_interface().next_mgm_data = RawValues::default();
testbench.test_spi_interface().next_mgm_data = sim_mgm::RawValues::default();
let call_count = testbench.test_spi_interface().call_count;
testbench.complete_power_cycle();
@@ -1274,7 +1279,7 @@ mod tests {
testbench.drain_events();
testbench.mode_report_rx.try_iter().for_each(drop);
testbench.exceed_spi_fault_threshold();
testbench.test_spi_interface().next_mgm_data = RawValues::default();
testbench.test_spi_interface().next_mgm_data = sim_mgm::RawValues::default();
// The switch never turns off. Every failed power cycle costs a recovery attempt.
for _ in 0..RECOVERY_THRESHOLD {
@@ -1312,7 +1317,7 @@ mod tests {
let mut testbench = MgmTestbench::new();
testbench.switch_to_normal();
testbench.exceed_spi_fault_threshold();
testbench.test_spi_interface().next_mgm_data = RawValues::default();
testbench.test_spi_interface().next_mgm_data = sim_mgm::RawValues::default();
testbench
.tc_tx
.send(create_request_tc(
@@ -1377,7 +1382,7 @@ mod tests {
testbench
.health_table
.set_health(ComponentId::AcsMgm0.into(), HealthState::ExternalControl);
testbench.test_spi_interface().next_mgm_data = RawValues {
testbench.test_spi_interface().next_mgm_data = sim_mgm::RawValues {
x: -1,
y: -1,
z: -1,
@@ -1396,7 +1401,7 @@ mod tests {
fn test_recovering_from_spi_fault_clears_invalid_data_flag() {
let mut testbench = MgmTestbench::new();
testbench.switch_to_normal();
testbench.test_spi_interface().next_mgm_data = RawValues {
testbench.test_spi_interface().next_mgm_data = sim_mgm::RawValues {
x: -1,
y: -1,
z: -1,
@@ -1405,7 +1410,7 @@ mod tests {
assert!(!testbench.handler.shared_mgm_set.lock().unwrap().valid);
// Bus recovers before the threshold is exceeded.
testbench.test_spi_interface().next_mgm_data = RawValues::default();
testbench.test_spi_interface().next_mgm_data = sim_mgm::RawValues::default();
testbench.handler.periodic_operation();
assert_eq!(
testbench.health_table.health(ComponentId::AcsMgm0.into()),
+12 -7
View File
@@ -9,7 +9,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use satrs_example::TimestampHelper;
use satrs_minisim::{
SerializableSimMsgPayload, SimReply, SimRequest,
SimReply, SimRequestWithTime,
eps::{PcduReply, PcduRequest},
};
use serde::{Deserialize, Serialize};
@@ -120,7 +120,7 @@ pub trait SerialInterface {
#[derive(new)]
pub struct SerialInterfaceToSim {
pub sim_request_tx: mpsc::Sender<SimRequest>,
pub sim_request_tx: mpsc::Sender<SimRequestWithTime>,
pub sim_reply_rx: mpsc::Receiver<SimReply>,
}
@@ -136,7 +136,7 @@ impl SerialInterface for SerialInterfaceToSim {
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
let request: PcduRequest = serde_json::from_slice(data).expect("expected a PCDU request");
self.sim_request_tx
.send(SimRequest::new_with_epoch_time(request))
.send(SimRequestWithTime::new_with_epoch_time(request))
.expect("failed to send request to simulation");
Ok(())
}
@@ -190,7 +190,7 @@ impl SerialInterface for SerialInterfaceDummy {
}
PcduRequest::RequestSwitchInfo => {
let mut reply_deque_mut = self.reply_deque.borrow_mut();
reply_deque_mut.push_back(SimReply::new(&PcduReply::SwitchInfo(
reply_deque_mut.push_back(SimReply::from(PcduReply::SwitchInfo(
switch_map_mut.clone(),
)));
}
@@ -510,7 +510,10 @@ impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
pub fn poll_and_handle_replies(&mut self) {
if let Err(e) = self.com_interface.try_recv_replies(|reply| {
let sim_reply: SimReply = serde_json::from_slice(reply).expect("invalid reply format");
let pcdu_reply = PcduReply::from_sim_message(&sim_reply).expect("invalid reply format");
let SimReply::Pcdu(pcdu_reply) = sim_reply else {
log::warn!("unexpected PCDU SIM reply: {sim_reply:?}");
return;
};
match pcdu_reply {
PcduReply::SwitchInfo(switch_info) => {
let switch_map_wrapper =
@@ -674,8 +677,10 @@ mod tests {
assert_eq!(reply_received_mut.len(), expected_queue_len);
let reply_received = reply_received_mut.pop_front().unwrap();
let sim_reply: SimReply = serde_json::from_str(&reply_received).unwrap();
let pcdu_reply = PcduReply::from_sim_message(&sim_reply).unwrap();
assert_eq!(pcdu_reply, PcduReply::SwitchInfo(expected_map));
assert_eq!(
sim_reply,
SimReply::Pcdu(PcduReply::SwitchInfo(expected_map))
);
}
}
+43 -60
View File
@@ -6,15 +6,14 @@ use std::{
};
use satrs::HandlingStatus;
use satrs_minisim::{
SerializableSimMsgPayload, SimComponent, SimMessageProvider, SimReply, SimRequest,
udp::SIM_CTRL_PORT,
};
use satrs_minisim::{ComponentId, SimReply, SimRequestWithTime, udp::SIM_CTRL_PORT};
use satrs_minisim::{SimCtrlReply, SimCtrlRequest};
struct SimReplyMap(pub HashMap<SimComponent, mpsc::Sender<SimReply>>);
struct SimReplyMap(pub HashMap<ComponentId, mpsc::Sender<SimReply>>);
pub fn create_sim_client(sim_request_rx: mpsc::Receiver<SimRequest>) -> Option<SimClientUdp> {
pub fn create_sim_client(
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
) -> Option<SimClientUdp> {
match SimClientUdp::new(
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, SIM_CTRL_PORT)),
sim_request_rx,
@@ -45,7 +44,7 @@ pub enum SimClientCreationError {
pub struct SimClientUdp {
udp_client: UdpSocket,
simulator_addr: SocketAddr,
sim_request_rx: mpsc::Receiver<SimRequest>,
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
reply_map: SimReplyMap,
reply_buf: [u8; 4096],
}
@@ -53,7 +52,7 @@ pub struct SimClientUdp {
impl SimClientUdp {
pub fn new(
simulator_addr: SocketAddr,
sim_request_rx: mpsc::Receiver<SimRequest>,
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
) -> Result<Self, SimClientCreationError> {
let mut reply_buf: [u8; 4096] = [0; 4096];
let mut udp_client = UdpSocket::bind("127.0.0.1:0")?;
@@ -74,22 +73,15 @@ impl SimClientUdp {
simulator_addr: SocketAddr,
reply_buf: &mut [u8],
) -> Result<(), SimClientCreationError> {
let sim_req = SimRequest::new_with_epoch_time(SimCtrlRequest::Ping);
let sim_req = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
let sim_req_json = serde_json::to_string(&sim_req).expect("failed to serialize SimRequest");
udp_client.send_to(sim_req_json.as_bytes(), simulator_addr)?;
match udp_client.recv(reply_buf) {
Ok(reply_len) => {
let sim_reply: SimReply = serde_json::from_slice(&reply_buf[0..reply_len])?;
if sim_reply.component() != SimComponent::SimCtrl {
return Err(SimClientCreationError::ReplyIsNotPong(sim_reply));
}
let sim_ctrl_reply =
SimCtrlReply::from_sim_message(&sim_reply).expect("invalid SIM reply");
match sim_ctrl_reply {
SimCtrlReply::InvalidRequest(_) => {
panic!("received invalid request reply from UDP sim server")
}
SimCtrlReply::Pong => Ok(()),
match sim_reply {
SimReply::SimCtrl(SimCtrlReply::Pong) => Ok(()),
_ => Err(SimClientCreationError::ReplyIsNotPong(sim_reply)),
}
}
Err(e) => {
@@ -174,7 +166,7 @@ impl SimClientUdp {
pub fn add_reply_recipient(
&mut self,
component: SimComponent,
component: ComponentId,
reply_sender: mpsc::Sender<SimReply>,
) {
self.reply_map.0.insert(component, reply_sender);
@@ -195,8 +187,7 @@ pub mod tests {
};
use satrs_minisim::{
SerializableSimMsgPayload, SimComponent, SimCtrlReply, SimCtrlRequest, SimMessageProvider,
SimReply, SimRequest,
ComponentId, SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
eps::{PcduReply, PcduRequest},
};
@@ -204,7 +195,7 @@ pub mod tests {
struct UdpSimTestServer {
udp_server: UdpSocket,
request_tx: mpsc::Sender<SimRequest>,
request_tx: mpsc::Sender<SimRequestWithTime>,
reply_rx: mpsc::Receiver<SimReply>,
last_sender: Option<SocketAddr>,
stop_signal: Arc<AtomicBool>,
@@ -213,7 +204,7 @@ pub mod tests {
impl UdpSimTestServer {
pub fn new(
request_tx: mpsc::Sender<SimRequest>,
request_tx: mpsc::Sender<SimRequestWithTime>,
reply_rx: mpsc::Receiver<SimReply>,
stop_signal: Arc<AtomicBool>,
) -> Self {
@@ -262,28 +253,19 @@ pub mod tests {
loop {
match self.udp_server.recv_from(&mut self.recv_buf) {
Ok((read_bytes, from)) => {
let sim_request: SimRequest =
let sim_request: SimRequestWithTime =
serde_json::from_slice(&self.recv_buf[0..read_bytes])
.expect("failed to deserialize SimRequest");
if sim_request.component() == SimComponent::SimCtrl {
// For a ping, we perform the reply handling here directly
let sim_ctrl_request =
SimCtrlRequest::from_sim_message(&sim_request)
.expect("failed to convert SimRequest to SimCtrlRequest");
match sim_ctrl_request {
SimCtrlRequest::Ping => {
no_data_received = false;
self.last_sender = Some(from);
let sim_reply = SimReply::new(&SimCtrlReply::Pong);
let sim_reply_json = serde_json::to_string(&sim_reply)
.expect("failed to serialize SimReply");
self.udp_server
.send_to(sim_reply_json.as_bytes(), from)
.expect(
"failed to send reply to client from UDP server",
);
}
};
// For a ping, we perform the reply handling here directly
if sim_request.request == SimRequest::SimCtrl(SimCtrlRequest::Ping) {
no_data_received = false;
self.last_sender = Some(from);
let sim_reply = SimReply::from(SimCtrlReply::Pong);
let sim_reply_json = serde_json::to_string(&sim_reply)
.expect("failed to serialize SimReply");
self.udp_server
.send_to(sim_reply_json.as_bytes(), from)
.expect("failed to send reply to client from UDP server");
}
// Forward each SIM request for testing purposes.
self.request_tx
@@ -332,9 +314,10 @@ pub mod tests {
let sim_request = server_sim_request_rx
.recv_timeout(Duration::from_millis(50))
.expect("no SIM request received");
let ping_request = SimCtrlRequest::from_sim_message(&sim_request)
.expect("failed to create SimCtrlRequest");
assert_eq!(ping_request, SimCtrlRequest::Ping);
assert_eq!(
sim_request.request,
SimRequest::SimCtrl(SimCtrlRequest::Ping)
);
// Stop the server.
stop_signal.store(true, Ordering::Relaxed);
jh0.join().unwrap();
@@ -360,18 +343,19 @@ pub mod tests {
// Creating the client also performs the connection test.
let mut client = SimClientUdp::new(server_addr, client_sim_req_rx).unwrap();
client.add_reply_recipient(SimComponent::Pcdu, client_pcdu_reply_tx);
client.add_reply_recipient(ComponentId::Pcdu, client_pcdu_reply_tx);
let sim_request = server_sim_request_rx
.recv_timeout(Duration::from_millis(50))
.expect("no SIM request received");
let ping_request = SimCtrlRequest::from_sim_message(&sim_request)
.expect("failed to create SimCtrlRequest");
assert_eq!(ping_request, SimCtrlRequest::Ping);
assert_eq!(
sim_request.request,
SimRequest::SimCtrl(SimCtrlRequest::Ping)
);
let pcdu_req = PcduRequest::RequestSwitchInfo;
client_sim_req_tx
.send(SimRequest::new_with_epoch_time(pcdu_req))
.send(SimRequestWithTime::new_with_epoch_time(pcdu_req))
.expect("send failed");
client.operation();
@@ -379,14 +363,15 @@ pub mod tests {
let sim_request = server_sim_request_rx
.recv_timeout(Duration::from_millis(50))
.expect("no SIM request received");
let req_recvd_on_server =
PcduRequest::from_sim_message(&sim_request).expect("failed to create SimCtrlRequest");
matches!(req_recvd_on_server, PcduRequest::RequestSwitchInfo);
assert_eq!(
sim_request.request,
SimRequest::Pcdu(PcduRequest::RequestSwitchInfo)
);
// We inject the reply ourselves.
let pcdu_reply = PcduReply::SwitchInfo(HashMap::new());
server_sim_reply_tx
.send(SimReply::new(&pcdu_reply))
.send(SimReply::from(pcdu_reply.clone()))
.expect("sending PCDU reply failed");
// Now we verify that the reply is sent by the UDP server back to the client, and then
@@ -397,10 +382,8 @@ pub mod tests {
match client_pcdu_reply_rx.try_recv() {
Ok(sim_reply) => {
assert_eq!(sim_reply.component(), SimComponent::Pcdu);
let pcdu_reply_from_client = PcduReply::from_sim_message(&sim_reply)
.expect("failed to create PcduReply");
assert_eq!(pcdu_reply_from_client, pcdu_reply);
assert_eq!(sim_reply.component(), ComponentId::Pcdu);
assert_eq!(sim_reply, SimReply::Pcdu(pcdu_reply.clone()));
pcdu_reply_received = true;
break;
}
+24 -25
View File
@@ -173,30 +173,29 @@ fn main() {
let shared_mgm_0_set = Arc::default();
let shared_mgm_1_set = Arc::default();
let (mgm_0_spi_interface, mgm_1_spi_interface) =
if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client
.add_reply_recipient(satrs_minisim::SimComponent::Mgm0Lis3Mdl, mgm_0_sim_reply_tx);
sim_client
.add_reply_recipient(satrs_minisim::SimComponent::Mgm1Lis3Mdl, mgm_1_sim_reply_tx);
(
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
id: mgm::MgmId::_0,
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgm_0_sim_reply_rx,
}),
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
id: mgm::MgmId::_1,
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgm_1_sim_reply_rx,
}),
)
} else {
(
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
)
};
let (mgm_0_spi_interface, mgm_1_spi_interface) = if let Some(sim_client) =
opt_sim_client.as_mut()
{
sim_client.add_reply_recipient(satrs_minisim::ComponentId::Mgm0Lis3Mdl, mgm_0_sim_reply_tx);
sim_client.add_reply_recipient(satrs_minisim::ComponentId::Mgm1Lis3Mdl, mgm_1_sim_reply_tx);
(
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
id: mgm::MgmId::_0,
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgm_0_sim_reply_rx,
}),
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
id: mgm::MgmId::_1,
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgm_1_sim_reply_rx,
}),
)
} else {
(
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
)
};
let mut mgm_0_handler = mgm::MgmHandlerLis3Mdl::new(
mgm::MgmId::_0,
TmtcQueues {
@@ -276,7 +275,7 @@ fn main() {
);
let pcdu_serial_interface = if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client.add_reply_recipient(satrs_minisim::SimComponent::Pcdu, pcdu_sim_reply_tx);
sim_client.add_reply_recipient(satrs_minisim::ComponentId::Pcdu, pcdu_sim_reply_tx);
SerialSimInterfaceWrapper::Sim(SerialInterfaceToSim::new(
sim_request_tx.clone(),
pcdu_sim_reply_rx,