Merge pull request 'update nexosim to v1' (#289) from update-nexosim into main

Reviewed-on: #289
This commit was merged in pull request #289.
This commit is contained in:
2026-09-24 16:56:42 +02:00
6 changed files with 162 additions and 93 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ strum = { version = "0.28", features = ["derive"] }
num_enum = "0.7"
humantime = "2"
tai-time = { version = "0.3", features = ["serde"] }
nexosim = { version = "0.3.1" }
nexosim = "1"
satrs = { path = "../../satrs" }
types = { path = "../types" }
+18 -16
View File
@@ -1,7 +1,11 @@
use std::{f32::consts::PI, sync::mpsc};
use std::f32::consts::PI;
use nexosim::model::{Context, Model};
use nexosim::{
model::{Context, Model},
ports::Output,
};
use satrs_minisim::{acs::mgm, SimReply};
use serde::{Deserialize, Serialize};
use types::pcdu::SwitchStateBinary;
use crate::time::current_millis;
@@ -19,22 +23,24 @@ 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.
#[derive(Serialize, Deserialize)]
pub struct MgmModel {
pub id: mgm::Id,
pub switch_state: SwitchStateBinary,
pub external_mag_field: Option<mgm::SensorValuesMicroTesla>,
pub spi_fault: mgm::SpiFault,
pub reply_sender: mpsc::Sender<SimReply>,
id: mgm::Id,
switch_state: SwitchStateBinary,
external_mag_field: Option<mgm::SensorValuesMicroTesla>,
spi_fault: mgm::SpiFault,
pub reply: Output<SimReply>,
}
#[Model]
impl MgmModel {
pub fn new(mgm_id: mgm::Id, reply_sender: mpsc::Sender<SimReply>) -> Self {
pub fn new(mgm_id: mgm::Id) -> Self {
Self {
id: mgm_id,
switch_state: SwitchStateBinary::Off,
external_mag_field: None,
spi_fault: mgm::SpiFault::default(),
reply_sender,
reply: Output::new(),
}
}
@@ -50,18 +56,16 @@ impl MgmModel {
self.spi_fault = fault;
}
pub async fn send_sensor_values(&mut self, _: (), scheduler: &mut Context<Self>) {
pub async fn send_sensor_values(&mut self, _: (), cx: &Context<Self>) {
let reply = SimReply::Mgm {
id: self.id,
reply: mgm::Reply::new(
self.switch_state,
self.calculate_current_mgm_tuple(current_millis(scheduler.time())),
self.calculate_current_mgm_tuple(current_millis(cx.time())),
self.spi_fault.mode,
),
};
self.reply_sender
.send(reply)
.expect("sending MGM sensor values failed");
self.reply.send(reply).await;
}
// Devices like magnetorquers generate a strong magnetic field which overrides the default
@@ -94,8 +98,6 @@ impl MgmModel {
}
}
impl Model for MgmModel {}
#[cfg(test)]
mod tests {
use std::time::Duration;
+24 -17
View File
@@ -1,45 +1,48 @@
use nexosim::{
model::{Context, Model},
model::{schedulable, Context, Model},
ports::Output,
};
use satrs_minisim::{
acs::{mgm, mgt},
SimReply,
};
use std::{sync::mpsc, time::Duration};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use types::pcdu::SwitchStateBinary;
/// Simple magnetorquer simulation model.
#[derive(Serialize, Deserialize)]
pub struct MgtModel {
switch_state: SwitchStateBinary,
torquing: bool,
torque_dipole: mgt::Dipole,
pub gen_magnetic_field: Output<mgm::SensorValuesMicroTesla>,
pub clear_magnetic_field: Output<()>,
reply_sender: mpsc::Sender<SimReply>,
pub reply: Output<SimReply>,
}
#[Model]
impl MgtModel {
pub fn new(reply_sender: mpsc::Sender<SimReply>) -> Self {
pub fn new() -> Self {
Self {
switch_state: SwitchStateBinary::Off,
torquing: false,
torque_dipole: mgt::Dipole::default(),
gen_magnetic_field: Output::new(),
clear_magnetic_field: Output::new(),
reply_sender,
reply: Output::new(),
}
}
pub async fn apply_torque(
&mut self,
duration_and_dipole: (Duration, mgt::Dipole),
cx: &mut Context<Self>,
cx: &Context<Self>,
) {
self.torque_dipole = duration_and_dipole.1;
self.torquing = true;
if cx
.schedule_event(duration_and_dipole.0, Self::clear_torque, ())
.schedule_event(duration_and_dipole.0, schedulable!(Self::clear_torque), ())
.is_err()
{
log::warn!("torque clearing can only be set for a future time.");
@@ -47,7 +50,8 @@ impl MgtModel {
self.generate_magnetic_field(()).await;
}
pub async fn clear_torque(&mut self, _: ()) {
#[nexosim(schedulable)]
async fn clear_torque(&mut self) {
self.torque_dipole = mgt::Dipole::default();
self.torquing = false;
self.clear_magnetic_field.send(()).await;
@@ -57,25 +61,30 @@ impl MgtModel {
self.switch_state = switch_state;
match switch_state {
SwitchStateBinary::On => self.generate_magnetic_field(()).await,
SwitchStateBinary::Off => self.clear_torque(()).await,
SwitchStateBinary::Off => self.clear_torque().await,
}
}
pub async fn request_housekeeping_data(&mut self, _: (), cx: &mut Context<Self>) {
pub async fn request_housekeeping_data(&mut self, _: (), cx: &Context<Self>) {
if self.switch_state != SwitchStateBinary::On {
return;
}
cx.schedule_event(Duration::from_millis(15), Self::send_housekeeping_data, ())
.expect("requesting housekeeping data failed")
cx.schedule_event(
Duration::from_millis(15),
schedulable!(Self::send_housekeeping_data),
(),
)
.expect("requesting housekeeping data failed")
}
pub fn send_housekeeping_data(&mut self) {
self.reply_sender
#[nexosim(schedulable)]
async fn send_housekeeping_data(&mut self) {
self.reply
.send(SimReply::from(mgt::Reply::Hk(mgt::HkSet {
dipole: self.torque_dipole,
torquing: self.torquing,
})))
.unwrap();
.await;
}
fn calc_magnetic_field(&self, _: mgt::Dipole) -> mgm::SensorValuesMicroTesla {
@@ -96,8 +105,6 @@ impl MgtModel {
}
}
impl Model for MgtModel {}
#[cfg(test)]
mod tests {
use std::time::Duration;
+102 -41
View File
@@ -4,14 +4,16 @@ use std::{
};
use nexosim::{
simulation::{Address, Mailbox, SimInit, Simulation},
time::{Clock, MonotonicTime, SystemClock},
ports::{event_queue, EventQueueReader, EventSinkReader, EventSource, SinkState},
simulation::{EventId, ExecutionError, Mailbox, SimInit, Simulation},
time::{Clock, Deadline, MonotonicTime, SystemClock},
};
use satrs_minisim::{
acs::{mgm, mgt},
eps::PcduRequest,
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
};
use types::pcdu::{SwitchId, SwitchStateBinary};
use crate::{
acs::{mgm::MgmModel, mgt::MgtModel},
@@ -31,11 +33,32 @@ pub enum ThreadingModel {
Single = 1,
}
struct ModelAddresses {
mgm_0: Address<MgmModel>,
mgm_1: Address<MgmModel>,
pcdu: Address<PcduModel>,
mgt: Address<MgtModel>,
struct MgmInputs {
send_sensor_values: EventId<()>,
set_spi_fault: EventId<mgm::SpiFault>,
}
impl MgmInputs {
fn register(sim_init: &mut SimInit, mailbox: &Mailbox<MgmModel>) -> Self {
Self {
send_sensor_values: EventSource::new()
.connect(MgmModel::send_sensor_values, mailbox)
.register(sim_init),
set_spi_fault: EventSource::new()
.connect(MgmModel::set_spi_fault, mailbox)
.register(sim_init),
}
}
}
/// Model inputs which are driven by simulation requests.
struct ModelInputs {
mgm_0: MgmInputs,
mgm_1: MgmInputs,
pcdu_request_switch_info: EventId<()>,
pcdu_switch_device: EventId<(SwitchId, SwitchStateBinary)>,
mgt_apply_torque: EventId<(Duration, mgt::Dipole)>,
mgt_request_hk: EventId<()>,
}
// The simulation controller processes requests and drives the simulation.
@@ -43,8 +66,9 @@ pub struct SimController {
sys_clock: SystemClock,
request_receiver: mpsc::Receiver<SimRequestWithTime>,
reply_sender: mpsc::Sender<SimReply>,
pub simulation: Simulation,
addrs: ModelAddresses,
simulation: Simulation,
inputs: ModelInputs,
model_replies: EventQueueReader<SimReply>,
}
impl SimController {
@@ -54,50 +78,66 @@ impl SimController {
reply_sender: mpsc::Sender<SimReply>,
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 mut mgm_0_model = MgmModel::new(mgm::Id::Mgm0);
let mut mgm_1_model = MgmModel::new(mgm::Id::Mgm1);
let mut pcdu_model = PcduModel::new();
let mut mgt_model = MgtModel::new();
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);
.connect(MgmModel::switch_device, &mgm_0_mailbox);
pcdu_model
.mgm_1_switch
.connect(MgmModel::switch_device, &addrs.mgm_1);
.connect(MgmModel::switch_device, &mgm_1_mailbox);
pcdu_model
.mgt_switch
.connect(MgtModel::switch_device, &addrs.mgt);
.connect(MgtModel::switch_device, &mgt_mailbox);
mgt_model
.gen_magnetic_field
.connect(MgmModel::apply_external_magnetic_field, &addrs.mgm_0);
.connect(MgmModel::apply_external_magnetic_field, &mgm_0_mailbox);
mgt_model
.gen_magnetic_field
.connect(MgmModel::apply_external_magnetic_field, &addrs.mgm_1);
.connect(MgmModel::apply_external_magnetic_field, &mgm_1_mailbox);
mgt_model
.clear_magnetic_field
.connect(MgmModel::clear_external_magnetic_field, &addrs.mgm_0);
.connect(MgmModel::clear_external_magnetic_field, &mgm_0_mailbox);
mgt_model
.clear_magnetic_field
.connect(MgmModel::clear_external_magnetic_field, &addrs.mgm_1);
.connect(MgmModel::clear_external_magnetic_field, &mgm_1_mailbox);
let sim_init = if threading_model == ThreadingModel::Single {
let (reply_sink, model_replies) = event_queue(SinkState::Enabled);
mgm_0_model.reply.connect_sink(reply_sink.clone());
mgm_1_model.reply.connect_sink(reply_sink.clone());
pcdu_model.reply.connect_sink(reply_sink.clone());
mgt_model.reply.connect_sink(reply_sink);
let mut sim_init = if threading_model == ThreadingModel::Single {
SimInit::with_num_threads(1)
} else {
SimInit::new()
};
let (simulation, _scheduler) = sim_init
let inputs = ModelInputs {
mgm_0: MgmInputs::register(&mut sim_init, &mgm_0_mailbox),
mgm_1: MgmInputs::register(&mut sim_init, &mgm_1_mailbox),
pcdu_request_switch_info: EventSource::new()
.connect(PcduModel::request_switch_info, &pcdu_mailbox)
.register(&mut sim_init),
pcdu_switch_device: EventSource::new()
.connect(PcduModel::switch_device, &pcdu_mailbox)
.register(&mut sim_init),
mgt_apply_torque: EventSource::new()
.connect(MgtModel::apply_torque, &mgt_mailbox)
.register(&mut sim_init),
mgt_request_hk: EventSource::new()
.connect(MgtModel::request_housekeeping_data, &mgt_mailbox)
.register(&mut sim_init),
};
let simulation = 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")
@@ -109,7 +149,29 @@ impl SimController {
request_receiver,
reply_sender,
simulation,
addrs,
inputs,
model_replies,
}
}
#[cfg(test)]
pub fn step(&mut self) -> Result<(), ExecutionError> {
self.simulation.step()?;
self.forward_model_replies();
Ok(())
}
pub fn step_until(&mut self, deadline: impl Deadline) -> Result<(), ExecutionError> {
self.simulation.step_until(deadline)?;
self.forward_model_replies();
Ok(())
}
fn forward_model_replies(&mut self) {
while let Some(reply) = self.model_replies.try_read() {
self.reply_sender
.send(reply)
.expect("sending model reply failed");
}
}
@@ -122,9 +184,7 @@ impl SimController {
t += Duration::from_millis(udp_polling_interval_ms);
let _synch_status = self.sys_clock.synchronize(t);
self.handle_sim_requests(t_old);
self.simulation
.step_until(t)
.expect("simulation step failed");
self.step_until(t).expect("simulation step failed");
}
}
@@ -150,6 +210,7 @@ impl SimController {
},
}
}
self.forward_model_replies();
}
fn handle_ctrl_request(&mut self, sim_ctrl_request: SimCtrlRequest) {
@@ -167,9 +228,9 @@ impl SimController {
}
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,
let inputs = match mgm_id {
mgm::Id::Mgm0 => &self.inputs.mgm_0,
mgm::Id::Mgm1 => &self.inputs.mgm_1,
};
if MGM_REQ_WIRETAPPING {
log::info!("received {mgm_id:?} request: {mgm_request:?}");
@@ -177,13 +238,13 @@ impl SimController {
match mgm_request {
mgm::Request::RequestSensorData => {
self.simulation
.process_event(MgmModel::send_sensor_values, (), addr)
.process_event(&inputs.send_sensor_values, ())
.expect("event execution error for mgm");
}
mgm::Request::SetSpiFault(fault_mode) => {
log::info!("{mgm_id:?}: setting SPI fault mode to {fault_mode:?}");
self.simulation
.process_event(MgmModel::set_spi_fault, fault_mode, addr)
.process_event(&inputs.set_spi_fault, fault_mode)
.expect("event execution error for mgm");
}
}
@@ -196,12 +257,12 @@ impl SimController {
match pcdu_request {
PcduRequest::RequestSwitchInfo => {
self.simulation
.process_event(PcduModel::request_switch_info, (), &self.addrs.pcdu)
.process_event(&self.inputs.pcdu_request_switch_info, ())
.unwrap();
}
PcduRequest::SwitchDevice { switch, state } => {
self.simulation
.process_event(PcduModel::switch_device, (switch, state), &self.addrs.pcdu)
.process_event(&self.inputs.pcdu_switch_device, (switch, state))
.unwrap();
}
}
@@ -214,11 +275,11 @@ impl SimController {
match mgt_request {
mgt::Request::ApplyTorque { duration, dipole } => self
.simulation
.process_event(MgtModel::apply_torque, (duration, dipole), &self.addrs.mgt)
.process_event(&self.inputs.mgt_apply_torque, (duration, dipole))
.unwrap(),
mgt::Request::RequestHk => self
.simulation
.process_event(MgtModel::request_housekeeping_data, (), &self.addrs.mgt)
.process_event(&self.inputs.mgt_request_hk, ())
.unwrap(),
};
}
+17 -16
View File
@@ -1,45 +1,49 @@
use std::{sync::mpsc, time::Duration};
use std::time::Duration;
use nexosim::{
model::{Context, Model},
model::{schedulable, Context, Model},
ports::Output,
};
use satrs_minisim::{eps::PcduReply, SimReply};
use types::pcdu::{SwitchId, SwitchMapBinaryWrapper, SwitchStateBinary};
use serde::{Deserialize, Serialize};
use types::pcdu::{SwitchId, SwitchMapBinary, SwitchMapBinaryWrapper, SwitchStateBinary};
pub const SWITCH_INFO_DELAY_MS: u64 = 10;
#[derive(Serialize, Deserialize)]
pub struct PcduModel {
pub switcher_map: SwitchMapBinaryWrapper,
switcher_map: SwitchMapBinary,
pub mgm_0_switch: Output<SwitchStateBinary>,
pub mgm_1_switch: Output<SwitchStateBinary>,
pub mgt_switch: Output<SwitchStateBinary>,
pub reply_sender: mpsc::Sender<SimReply>,
pub reply: Output<SimReply>,
}
#[Model]
impl PcduModel {
pub fn new(reply_sender: mpsc::Sender<SimReply>) -> Self {
pub fn new() -> Self {
Self {
switcher_map: Default::default(),
switcher_map: SwitchMapBinaryWrapper::default().0,
mgm_0_switch: Output::new(),
mgm_1_switch: Output::new(),
mgt_switch: Output::new(),
reply_sender,
reply: Output::new(),
}
}
pub async fn request_switch_info(&mut self, _: (), cx: &mut Context<Self>) {
pub async fn request_switch_info(&mut self, _: (), cx: &Context<Self>) {
cx.schedule_event(
Duration::from_millis(SWITCH_INFO_DELAY_MS),
Self::send_switch_info,
schedulable!(Self::send_switch_info),
(),
)
.expect("requesting switch info failed");
}
pub fn send_switch_info(&mut self) {
let reply = SimReply::from(PcduReply::SwitchInfo(self.switcher_map.0.clone()));
self.reply_sender.send(reply).unwrap();
#[nexosim(schedulable)]
async fn send_switch_info(&mut self) {
let reply = SimReply::from(PcduReply::SwitchInfo(self.switcher_map.clone()));
self.reply.send(reply).await;
}
pub async fn switch_device(&mut self, switch_and_target_state: (SwitchId, SwitchStateBinary)) {
@@ -50,7 +54,6 @@ impl PcduModel {
);
let val = self
.switcher_map
.0
.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;
@@ -68,8 +71,6 @@ impl PcduModel {
}
}
impl Model for PcduModel {}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
@@ -36,8 +36,6 @@ impl SimTestbench {
delegate! {
to self.sim_controller {
pub fn handle_sim_requests(&mut self, old_timestamp: MonotonicTime);
}
to self.sim_controller.simulation {
pub fn step(&mut self) -> Result<(), ExecutionError>;
pub fn step_until(&mut self, duration: impl Deadline) -> Result<(), ExecutionError>;
}