MGT handler
This commit is contained in:
@@ -15,7 +15,7 @@ use std::{
|
||||
},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use types::{Apid, MessageType, TcHeader, acs::mgm::request::HkRequest};
|
||||
use types::{Apid, Message as _, MessageType, TcHeader, acs::mgm::request::HkRequest};
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
pub struct Cli {
|
||||
@@ -33,6 +33,7 @@ enum Commands {
|
||||
Mgm0(MgmArgs),
|
||||
Mgm1(MgmArgs),
|
||||
MgmAssy(MgmAssemblyArgs),
|
||||
Mgt(MgtArgs),
|
||||
AcsSubsystem(SubsystemArgs),
|
||||
EventManager(EventManagerArgs),
|
||||
}
|
||||
@@ -67,6 +68,7 @@ enum EventSenderSelect {
|
||||
Mgm0,
|
||||
Mgm1,
|
||||
MgmAssy,
|
||||
Mgt,
|
||||
Pcdu,
|
||||
UdpServer,
|
||||
TcpServer,
|
||||
@@ -79,6 +81,7 @@ impl From<EventSenderSelect> for types::ComponentId {
|
||||
EventSenderSelect::Controller => types::ComponentId::Controller,
|
||||
EventSenderSelect::Mgm0 => types::ComponentId::AcsMgm0,
|
||||
EventSenderSelect::Mgm1 => types::ComponentId::AcsMgm1,
|
||||
EventSenderSelect::Mgt => types::ComponentId::AcsMgt,
|
||||
EventSenderSelect::MgmAssy => types::ComponentId::AcsMgmAssembly,
|
||||
EventSenderSelect::Pcdu => types::ComponentId::EpsPcdu,
|
||||
EventSenderSelect::UdpServer => types::ComponentId::UdpServer,
|
||||
@@ -169,6 +172,25 @@ struct MgmArgs {
|
||||
health: Option<HealthStateSelect>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||
struct MgtArgs {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
/// Housekeeping request for the status data set.
|
||||
#[arg(long, value_enum)]
|
||||
hk: Option<HkSelect>,
|
||||
/// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`.
|
||||
#[arg(long)]
|
||||
hk_interval_ms: Option<u64>,
|
||||
#[arg(short, long)]
|
||||
mode: Option<DeviceModeSelect>,
|
||||
/// Apply a dipole, given as `x,y,z`. Only accepted in normal mode.
|
||||
#[arg(long, value_name = "X,Y,Z", value_parser = parse_dipole, allow_hyphen_values = true)]
|
||||
torque: Option<types::acs::mgt::Dipole>,
|
||||
#[arg(long, default_value_t = 1000)]
|
||||
torque_duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||
struct MgmAssemblyArgs {
|
||||
#[arg(short, long)]
|
||||
@@ -204,6 +226,78 @@ pub enum SubsystemModeSelect {
|
||||
Safe,
|
||||
}
|
||||
|
||||
fn hk_request_type(
|
||||
hk: HkSelect,
|
||||
hk_interval_ms: Option<u64>,
|
||||
) -> anyhow::Result<types::HkRequestType> {
|
||||
let opt_interval = hk_interval_ms.map(Duration::from_millis);
|
||||
Ok(match hk {
|
||||
HkSelect::OneShot => types::HkRequestType::OneShot,
|
||||
HkSelect::EnablePeriodic => types::HkRequestType::EnablePeriodic(opt_interval),
|
||||
HkSelect::DisablePeriodic => types::HkRequestType::DisablePeriodic,
|
||||
HkSelect::ModifyInterval => types::HkRequestType::ModifyInterval(
|
||||
opt_interval.context("--hk-interval-ms is required for modify-interval")?,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_dipole(value: &str) -> Result<types::acs::mgt::Dipole, String> {
|
||||
let axes: Vec<i16> = value
|
||||
.split(',')
|
||||
.map(|axis| axis.trim().parse::<i16>().map_err(|e| e.to_string()))
|
||||
.collect::<Result<_, _>>()?;
|
||||
let [x, y, z] = axes[..] else {
|
||||
return Err(format!("expected 3 values, got {}", axes.len()));
|
||||
};
|
||||
Ok(types::acs::mgt::Dipole { x, y, z })
|
||||
}
|
||||
|
||||
fn send_mgt_request(
|
||||
client: &UdpSocket,
|
||||
addr: SocketAddr,
|
||||
request: types::acs::mgt::request::Request,
|
||||
) {
|
||||
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(types::ComponentId::AcsMgt, request.message_type()),
|
||||
request,
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
log::info!(
|
||||
"sending MGT request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
client.send_to(&packet.to_vec(), addr).unwrap();
|
||||
}
|
||||
|
||||
fn handle_mgt_command(client: &UdpSocket, addr: SocketAddr, args: MgtArgs) -> anyhow::Result<()> {
|
||||
use types::acs::mgt::request::{ModeRequest, Request};
|
||||
|
||||
if args.ping {
|
||||
send_mgt_request(client, addr, Request::Ping);
|
||||
}
|
||||
if let Some(hk) = args.hk {
|
||||
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
|
||||
send_mgt_request(client, addr, Request::Hk(req_type));
|
||||
}
|
||||
if let Some(mode) = args.mode {
|
||||
let mode = match mode {
|
||||
DeviceModeSelect::Off => types::DeviceMode::Off,
|
||||
DeviceModeSelect::Normal => types::DeviceMode::Normal,
|
||||
};
|
||||
send_mgt_request(client, addr, Request::Mode(ModeRequest::SetMode(mode)));
|
||||
}
|
||||
if let Some(dipole) = args.torque {
|
||||
let request = Request::ApplyTorque {
|
||||
dipole,
|
||||
duration: Duration::from_millis(args.torque_duration_ms),
|
||||
};
|
||||
send_mgt_request(client, addr, request);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_mgm_command(
|
||||
client: &UdpSocket,
|
||||
addr: SocketAddr,
|
||||
@@ -235,15 +329,7 @@ fn handle_mgm_command(
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
if let Some(hk) = args.hk {
|
||||
let opt_interval = args.hk_interval_ms.map(Duration::from_millis);
|
||||
let req_type = match hk {
|
||||
HkSelect::OneShot => types::HkRequestType::OneShot,
|
||||
HkSelect::EnablePeriodic => types::HkRequestType::EnablePeriodic(opt_interval),
|
||||
HkSelect::DisablePeriodic => types::HkRequestType::DisablePeriodic,
|
||||
HkSelect::ModifyInterval => types::HkRequestType::ModifyInterval(
|
||||
opt_interval.context("--hk-interval-ms is required for modify-interval")?,
|
||||
),
|
||||
};
|
||||
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Hk),
|
||||
@@ -399,6 +485,7 @@ fn main() -> anyhow::Result<()> {
|
||||
Commands::Mgm1(args) => {
|
||||
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm1, args)?
|
||||
}
|
||||
Commands::Mgt(args) => handle_mgt_command(&client, addr, args)?,
|
||||
Commands::MgmAssy(mgm_assembly_args) => {
|
||||
let target_id = types::ComponentId::AcsMgmAssembly;
|
||||
if mgm_assembly_args.ping {
|
||||
@@ -576,6 +663,7 @@ fn handle_event(sender_id: types::ComponentId, data: &[u8]) {
|
||||
types::ComponentId::AcsMgmAssembly => {
|
||||
log_event::<types::acs::mgm_assembly::Event>(sender_id, data)
|
||||
}
|
||||
types::ComponentId::AcsMgt => log_event::<types::acs::mgt::Event>(sender_id, data),
|
||||
types::ComponentId::EpsPcdu => log_event::<types::pcdu::Event>(sender_id, data),
|
||||
// TC source events are sent with the ID of the packet source.
|
||||
types::ComponentId::UdpServer
|
||||
@@ -660,10 +748,42 @@ fn handle_raw_tm_packet(data: &[u8]) -> anyhow::Result<()> {
|
||||
);
|
||||
}
|
||||
types::ComponentId::AcsController => todo!(),
|
||||
types::ComponentId::AcsMgt => todo!(),
|
||||
types::ComponentId::AcsMgt => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::acs::mgt::response::Response>(remainder);
|
||||
log::info!("Received response from MGT: {:?}", response.unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => todo!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_dipole() {
|
||||
assert_eq!(
|
||||
parse_dipole("-200, 200,1000"),
|
||||
Ok(types::acs::mgt::Dipole {
|
||||
x: -200,
|
||||
y: 200,
|
||||
z: 1000
|
||||
})
|
||||
);
|
||||
assert!(parse_dipole("1,2").is_err());
|
||||
assert!(parse_dipole("1,2,3,4").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_torque_argument() {
|
||||
let cli = Cli::try_parse_from(["client", "mgt", "--torque", "-200,200,1000"]).unwrap();
|
||||
let Some(Commands::Mgt(args)) = cli.commands else {
|
||||
panic!("expected mgt subcommand");
|
||||
};
|
||||
assert_eq!(args.torque.map(|dipole| dipole.x), Some(-200));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ sat-rs minisim
|
||||
======
|
||||
|
||||
This crate contains a mini-simulator based on the open-source discrete-event simulation framework
|
||||
[asynchronix](https://github.com/asynchronics/asynchronix).
|
||||
[nexosim](https://github.com/asynchronics/nexosim).
|
||||
|
||||
Right now, this crate is primarily used together with the
|
||||
[`satrs-example` application](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
|
||||
@@ -29,4 +29,4 @@ still have similar device handlers.
|
||||
|
||||
The following graph shows the high-level architecture of the mini-simulator.
|
||||
|
||||
<img src="../images/minisim-arch/minisim-arch.png" alt="Mini simulator architecture" width="500" class="center"/>
|
||||
<img src="../../images/minisim-arch/minisim-arch.png" alt="Mini simulator architecture" width="500" class="center"/>
|
||||
|
||||
@@ -70,6 +70,10 @@ impl MgmModel {
|
||||
self.external_mag_field = Some(field);
|
||||
}
|
||||
|
||||
pub async fn clear_external_magnetic_field(&mut self, _: ()) {
|
||||
self.external_mag_field = None;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -15,6 +15,7 @@ pub struct MgtModel {
|
||||
torquing: bool,
|
||||
torque_dipole: mgt::Dipole,
|
||||
pub gen_magnetic_field: Output<mgm::SensorValuesMicroTesla>,
|
||||
pub clear_magnetic_field: Output<()>,
|
||||
reply_sender: mpsc::Sender<SimReply>,
|
||||
}
|
||||
|
||||
@@ -25,6 +26,7 @@ impl MgtModel {
|
||||
torquing: false,
|
||||
torque_dipole: mgt::Dipole::default(),
|
||||
gen_magnetic_field: Output::new(),
|
||||
clear_magnetic_field: Output::new(),
|
||||
reply_sender,
|
||||
}
|
||||
}
|
||||
@@ -48,12 +50,15 @@ impl MgtModel {
|
||||
pub async fn clear_torque(&mut self, _: ()) {
|
||||
self.torque_dipole = mgt::Dipole::default();
|
||||
self.torquing = false;
|
||||
self.generate_magnetic_field(()).await;
|
||||
self.clear_magnetic_field.send(()).await;
|
||||
}
|
||||
|
||||
pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) {
|
||||
self.switch_state = switch_state;
|
||||
self.generate_magnetic_field(()).await;
|
||||
match switch_state {
|
||||
SwitchStateBinary::On => self.generate_magnetic_field(()).await,
|
||||
SwitchStateBinary::Off => self.clear_torque(()).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_housekeeping_data(&mut self, _: (), cx: &mut Context<Self>) {
|
||||
@@ -97,8 +102,12 @@ impl Model for MgtModel {}
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use satrs_minisim::{acs::mgt, SimReply, SimRequestWithTime};
|
||||
use types::pcdu::SwitchId;
|
||||
use satrs_minisim::{
|
||||
acs::{mgm, mgt},
|
||||
eps::PcduRequest,
|
||||
SimReply, SimRequest, SimRequestWithTime,
|
||||
};
|
||||
use types::pcdu::{SwitchId, SwitchStateBinary};
|
||||
|
||||
use crate::{eps::tests::switch_device_on, test_helpers::SimTestbench};
|
||||
|
||||
@@ -166,4 +175,73 @@ mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// Processes the request without stepping, so scheduled events like the torque clearing do
|
||||
/// not fire.
|
||||
fn process_without_step(sim_testbench: &mut SimTestbench, request: impl Into<SimRequest>) {
|
||||
sim_testbench
|
||||
.send_request(SimRequestWithTime::new_with_epoch_time(request))
|
||||
.expect("sending request failed");
|
||||
sim_testbench.handle_sim_requests_time_agnostic();
|
||||
}
|
||||
|
||||
fn read_mgm_0_field(sim_testbench: &mut SimTestbench) -> mgm::SensorValuesMicroTesla {
|
||||
process_without_step(
|
||||
sim_testbench,
|
||||
SimRequest::Mgm {
|
||||
id: mgm::Id::Mgm0,
|
||||
request: mgm::Request::RequestSensorData,
|
||||
},
|
||||
);
|
||||
let sim_reply = sim_testbench
|
||||
.try_receive_next_reply()
|
||||
.expect("no MGM reply received");
|
||||
let SimReply::Mgm { reply, .. } = sim_reply else {
|
||||
panic!("unexpected reply {sim_reply:?}");
|
||||
};
|
||||
reply.sensor_values
|
||||
}
|
||||
|
||||
fn start_torquing(sim_testbench: &mut SimTestbench, duration: Duration) {
|
||||
switch_device_on(sim_testbench, SwitchId::Mgm0);
|
||||
switch_device_on(sim_testbench, SwitchId::Mgt);
|
||||
process_without_step(
|
||||
sim_testbench,
|
||||
mgt::Request::ApplyTorque {
|
||||
duration,
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
},
|
||||
);
|
||||
assert_eq!(read_mgm_0_field(sim_testbench), mgm::MGT_GEN_MAGNETIC_FIELD);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mgm_field_cleared_after_torquing() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
start_torquing(&mut sim_testbench, Duration::from_millis(100));
|
||||
sim_testbench
|
||||
.step_until(Duration::from_millis(100))
|
||||
.unwrap();
|
||||
assert_ne!(
|
||||
read_mgm_0_field(&mut sim_testbench),
|
||||
mgm::MGT_GEN_MAGNETIC_FIELD
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mgm_field_cleared_by_switching_mgt_off() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
start_torquing(&mut sim_testbench, Duration::from_millis(100));
|
||||
process_without_step(
|
||||
&mut sim_testbench,
|
||||
PcduRequest::SwitchDevice {
|
||||
switch: SwitchId::Mgt,
|
||||
state: SwitchStateBinary::Off,
|
||||
},
|
||||
);
|
||||
assert_ne!(
|
||||
read_mgm_0_field(&mut sim_testbench),
|
||||
mgm::MGT_GEN_MAGNETIC_FIELD
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,12 @@ impl SimController {
|
||||
mgt_model
|
||||
.gen_magnetic_field
|
||||
.connect(MgmModel::apply_external_magnetic_field, &addrs.mgm_1);
|
||||
mgt_model
|
||||
.clear_magnetic_field
|
||||
.connect(MgmModel::clear_external_magnetic_field, &addrs.mgm_0);
|
||||
mgt_model
|
||||
.clear_magnetic_field
|
||||
.connect(MgmModel::clear_external_magnetic_field, &addrs.mgm_1);
|
||||
|
||||
let sim_init = if threading_model == ThreadingModel::Single {
|
||||
SimInit::with_num_threads(1)
|
||||
|
||||
+536
-59
@@ -1,119 +1,596 @@
|
||||
use std::sync::mpsc::{Receiver, SyncSender, TryRecvError};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use types::acs::mgt::{Mode, request::ModeRequest, response::ModeReport};
|
||||
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||
use satrs_example::{HkHelperSingleSet, TmtcQueues};
|
||||
use satrs_minisim::acs::mgt as sim_mgt;
|
||||
use satrs_minisim::{SimReply, SimRequestWithTime};
|
||||
use types::acs::mgt::{
|
||||
self, HkSet,
|
||||
request::{ModeRequest, Request},
|
||||
response::{ModeResponse, Response},
|
||||
};
|
||||
use types::pcdu::SwitchId;
|
||||
use types::{ComponentId, DeviceMode, HkRequestType};
|
||||
|
||||
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||
use crate::device_mode::{ModeTransitionEvent, SwitchAndModeHelper};
|
||||
use crate::eps::PowerSwitchHelper;
|
||||
|
||||
/// Interface for ideal device which never fails.
|
||||
#[derive(Default)]
|
||||
pub struct DummyInterface {
|
||||
dipole: sim_mgt::Dipole,
|
||||
torque_end: Option<Instant>,
|
||||
hk_requested: bool,
|
||||
}
|
||||
|
||||
impl DummyInterface {
|
||||
fn send(&mut self, request: sim_mgt::Request) {
|
||||
match request {
|
||||
sim_mgt::Request::ApplyTorque { duration, dipole } => {
|
||||
self.dipole = dipole;
|
||||
self.torque_end = Some(Instant::now() + duration);
|
||||
}
|
||||
sim_mgt::Request::RequestHk => self.hk_requested = true,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_recv_hk(&mut self) -> Option<sim_mgt::HkSet> {
|
||||
if !std::mem::take(&mut self.hk_requested) {
|
||||
return None;
|
||||
}
|
||||
let torquing = self.torque_end.is_some_and(|end| Instant::now() < end);
|
||||
Some(sim_mgt::HkSet {
|
||||
dipole: if torquing {
|
||||
self.dipole
|
||||
} else {
|
||||
sim_mgt::Dipole::default()
|
||||
},
|
||||
torquing,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Records all requests and returns injected HK replies.
|
||||
#[derive(Default)]
|
||||
pub struct TestInterface {
|
||||
pub sent_requests: Vec<sim_mgt::Request>,
|
||||
pub hk_replies: VecDeque<sim_mgt::HkSet>,
|
||||
}
|
||||
|
||||
pub struct SimInterface {
|
||||
pub sim_request_tx: mpsc::Sender<SimRequestWithTime>,
|
||||
pub sim_reply_rx: mpsc::Receiver<SimReply>,
|
||||
}
|
||||
|
||||
impl SimInterface {
|
||||
fn send(&mut self, request: sim_mgt::Request) {
|
||||
if let Err(e) = self
|
||||
.sim_request_tx
|
||||
.send(SimRequestWithTime::new_with_epoch_time(request))
|
||||
{
|
||||
log::error!("failed to send MGT SIM request: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
fn try_recv_hk(&mut self) -> Option<sim_mgt::HkSet> {
|
||||
let sim_reply = self.sim_reply_rx.try_recv().ok()?;
|
||||
match sim_reply {
|
||||
SimReply::Mgt(sim_mgt::Reply::Hk(hk)) => Some(hk),
|
||||
_ => {
|
||||
log::warn!("unexpected MGT SIM reply: {sim_reply:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum MgtCommunication {
|
||||
Dummy(DummyInterface),
|
||||
Sim(SimInterface),
|
||||
#[allow(dead_code)]
|
||||
Test(TestInterface),
|
||||
}
|
||||
|
||||
impl MgtCommunication {
|
||||
fn send(&mut self, request: sim_mgt::Request) {
|
||||
match self {
|
||||
MgtCommunication::Dummy(dummy) => dummy.send(request),
|
||||
MgtCommunication::Sim(sim) => sim.send(request),
|
||||
MgtCommunication::Test(test) => test.sent_requests.push(request),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_recv_hk(&mut self) -> Option<sim_mgt::HkSet> {
|
||||
match self {
|
||||
MgtCommunication::Dummy(dummy) => dummy.try_recv_hk(),
|
||||
MgtCommunication::Sim(sim) => sim.try_recv_hk(),
|
||||
MgtCommunication::Test(test) => test.hk_replies.pop_front(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper component for communication with a parent component, which is usually an assembly
|
||||
/// or a subsystem.
|
||||
pub struct ModeLeafHelper {
|
||||
pub request_rx: Receiver<ModeRequest>,
|
||||
pub report_tx: SyncSender<ModeReport>,
|
||||
pub request_rx: mpsc::Receiver<ModeRequest>,
|
||||
pub report_tx: mpsc::SyncSender<ModeResponse>,
|
||||
}
|
||||
|
||||
/// Dummy magnetorquer (MGT) device handler. It has no actual actuation logic and reaches any
|
||||
/// commanded mode immediately, which is sufficient to exercise the mode commanding of its
|
||||
/// parent subsystem.
|
||||
pub struct Mgt {
|
||||
mode: Mode,
|
||||
/// Magnetorquer (MGT) device handler.
|
||||
///
|
||||
/// The device is powered through the PCDU and only accepts torque commands in normal mode.
|
||||
/// In normal mode, the device HK is polled every cycle. The replies arrive asynchronously and
|
||||
/// are cached as the HK set of the handler.
|
||||
pub struct MgtHandler {
|
||||
tmtc_queues: TmtcQueues,
|
||||
pub com: MgtCommunication,
|
||||
hk_set: HkSet,
|
||||
hk_helper: HkHelperSingleSet,
|
||||
switch_and_mode_helper: SwitchAndModeHelper<DeviceMode>,
|
||||
mode_leaf_helper: ModeLeafHelper,
|
||||
event_tx: mpsc::SyncSender<mgt::Event>,
|
||||
}
|
||||
|
||||
impl Mgt {
|
||||
pub fn new(mode_leaf_helper: ModeLeafHelper) -> Self {
|
||||
impl MgtHandler {
|
||||
pub fn new(
|
||||
tmtc_queues: TmtcQueues,
|
||||
switch_helper: PowerSwitchHelper,
|
||||
com: MgtCommunication,
|
||||
mode_leaf_helper: ModeLeafHelper,
|
||||
mode_timeout: Duration,
|
||||
event_tx: mpsc::SyncSender<mgt::Event>,
|
||||
) -> Self {
|
||||
Self {
|
||||
mode: Mode::Off,
|
||||
tmtc_queues,
|
||||
com,
|
||||
hk_set: HkSet::default(),
|
||||
hk_helper: HkHelperSingleSet::new(false, Duration::from_millis(200)),
|
||||
switch_and_mode_helper: SwitchAndModeHelper::new(
|
||||
DeviceMode::Off,
|
||||
mode_timeout,
|
||||
switch_helper,
|
||||
SwitchId::Mgt,
|
||||
),
|
||||
mode_leaf_helper,
|
||||
event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[inline]
|
||||
pub fn mode(&self) -> Mode {
|
||||
self.mode
|
||||
pub fn mode(&self) -> DeviceMode {
|
||||
self.switch_and_mode_helper.mode()
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.handle_telecommands();
|
||||
self.handle_mode_leaf_handling();
|
||||
|
||||
if let Some(event) = self.switch_and_mode_helper.handle_mode_transition() {
|
||||
match event {
|
||||
ModeTransitionEvent::Reached(tc_commander) => {
|
||||
self.handle_mode_reached(tc_commander)
|
||||
}
|
||||
ModeTransitionEvent::Failed(tc_commander) => {
|
||||
self.handle_mode_transition_failure(tc_commander)
|
||||
}
|
||||
// No power cycles are started without FDIR.
|
||||
ModeTransitionEvent::PowerCycleDone
|
||||
| ModeTransitionEvent::PowerCycleFailed { .. } => (),
|
||||
}
|
||||
}
|
||||
|
||||
if self.ready_for_commanding() {
|
||||
self.com.send(sim_mgt::Request::RequestHk);
|
||||
}
|
||||
while let Some(hk) = self.com.try_recv_hk() {
|
||||
self.hk_set = HkSet {
|
||||
valid: true,
|
||||
dipole: types::acs::mgt::Dipole {
|
||||
x: hk.dipole.x,
|
||||
y: hk.dipole.y,
|
||||
z: hk.dipole.z,
|
||||
},
|
||||
torquing: hk.torquing,
|
||||
};
|
||||
}
|
||||
|
||||
if self.hk_helper.needs_generation() {
|
||||
self.send_telemetry(None, Response::Hk(self.hk_set));
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mode_leaf_handling(&mut self) {
|
||||
loop {
|
||||
match self.mode_leaf_helper.request_rx.try_recv() {
|
||||
Ok(request) => match request {
|
||||
ModeRequest::SetMode(mode) => {
|
||||
log::info!("MGT: transitioning to mode {:?}", mode);
|
||||
self.mode = mode;
|
||||
self.report_mode();
|
||||
}
|
||||
ModeRequest::ReadMode => self.report_mode(),
|
||||
},
|
||||
Err(e) => match e {
|
||||
TryRecvError::Empty => break,
|
||||
TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
|
||||
},
|
||||
fn ready_for_commanding(&self) -> bool {
|
||||
self.mode() == DeviceMode::Normal && self.switch_and_mode_helper.target().is_none()
|
||||
}
|
||||
|
||||
fn handle_telecommands(&mut self) {
|
||||
while let Ok(packet) = self.tmtc_queues.tc_rx.try_recv() {
|
||||
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
let request = match postcard::from_bytes::<Request>(&packet.payload) {
|
||||
Ok(request) => request,
|
||||
Err(e) => {
|
||||
log::warn!("MGT: failed to deserialize request: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
log::info!(
|
||||
"MGT: received request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
tc_id.raw()
|
||||
);
|
||||
match request {
|
||||
Request::Ping => self.send_telemetry(Some(tc_id), Response::Ok),
|
||||
Request::Hk(hk_request) => self.handle_hk_request(tc_id, hk_request),
|
||||
Request::Mode(ModeRequest::SetMode(mode)) => {
|
||||
self.start_transition(mode, Some(tc_id))
|
||||
}
|
||||
Request::Mode(ModeRequest::ReadMode) => self
|
||||
.send_telemetry(Some(tc_id), Response::Mode(ModeResponse::Mode(self.mode()))),
|
||||
Request::ApplyTorque { dipole, duration } => {
|
||||
self.handle_torque_command(tc_id, dipole, duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn report_mode(&self) {
|
||||
fn handle_mode_leaf_handling(&mut self) {
|
||||
while let Ok(request) = self.mode_leaf_helper.request_rx.try_recv() {
|
||||
match request {
|
||||
ModeRequest::SetMode(mode) => self.start_transition(mode, None),
|
||||
ModeRequest::ReadMode => self.report_mode_to_parent(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_hk_request(&mut self, tc_id: CcsdsPacketIdAndPsc, hk_request: HkRequestType) {
|
||||
match hk_request {
|
||||
HkRequestType::OneShot => self.send_telemetry(Some(tc_id), Response::Hk(self.hk_set)),
|
||||
HkRequestType::EnablePeriodic(opt_interval) => {
|
||||
self.hk_helper.enabled = true;
|
||||
if let Some(interval) = opt_interval {
|
||||
self.hk_helper.frequency = interval;
|
||||
}
|
||||
}
|
||||
HkRequestType::DisablePeriodic => self.hk_helper.enabled = false,
|
||||
HkRequestType::ModifyInterval(interval) => self.hk_helper.frequency = interval,
|
||||
_ => log::warn!("MGT: unhandled HK request"),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_torque_command(
|
||||
&mut self,
|
||||
tc_id: CcsdsPacketIdAndPsc,
|
||||
dipole: types::acs::mgt::Dipole,
|
||||
duration: Duration,
|
||||
) {
|
||||
if !self.ready_for_commanding() {
|
||||
log::warn!("MGT: rejecting torque command, device not in normal mode");
|
||||
self.send_telemetry(Some(tc_id), Response::NotInNormalMode);
|
||||
return;
|
||||
}
|
||||
self.com.send(sim_mgt::Request::ApplyTorque {
|
||||
duration,
|
||||
dipole: sim_mgt::Dipole {
|
||||
x: dipole.x,
|
||||
y: dipole.y,
|
||||
z: dipole.z,
|
||||
},
|
||||
});
|
||||
self.send_telemetry(Some(tc_id), Response::Ok);
|
||||
}
|
||||
|
||||
fn start_transition(
|
||||
&mut self,
|
||||
target_mode: DeviceMode,
|
||||
tc_commander: Option<CcsdsPacketIdAndPsc>,
|
||||
) {
|
||||
log::info!("MGT: transitioning to mode {:?}", target_mode);
|
||||
if target_mode == DeviceMode::Off {
|
||||
self.hk_set = HkSet::default();
|
||||
}
|
||||
self.switch_and_mode_helper
|
||||
.start_transition(target_mode, tc_commander);
|
||||
}
|
||||
|
||||
fn handle_mode_reached(&mut self, tc_commander: Option<CcsdsPacketIdAndPsc>) {
|
||||
log::info!("MGT: mode {:?} reached", self.mode());
|
||||
self.send_event(mgt::Event::ModeChanged(self.mode()));
|
||||
if tc_commander.is_some() {
|
||||
self.send_telemetry(tc_commander, Response::Ok);
|
||||
}
|
||||
self.report_mode_to_parent();
|
||||
}
|
||||
|
||||
fn handle_mode_transition_failure(&mut self, tc_commander: Option<CcsdsPacketIdAndPsc>) {
|
||||
if tc_commander.is_some() {
|
||||
self.send_telemetry(tc_commander, Response::Mode(ModeResponse::SetModeTimeout));
|
||||
}
|
||||
self.mode_leaf_helper
|
||||
.report_tx
|
||||
.send(ModeReport::Mode(self.mode))
|
||||
.expect("failed to send mode report to parent");
|
||||
.send(ModeResponse::SetModeTimeout)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn report_mode_to_parent(&self) {
|
||||
self.mode_leaf_helper
|
||||
.report_tx
|
||||
.send(ModeResponse::Mode(self.mode()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn send_event(&self, event: mgt::Event) {
|
||||
if let Err(e) = self.event_tx.send(event) {
|
||||
log::warn!("MGT: failed to send event {:?}: {}", event, e);
|
||||
}
|
||||
}
|
||||
|
||||
fn send_telemetry(&self, tc_id: Option<CcsdsPacketIdAndPsc>, response: Response) {
|
||||
match pack_ccsds_tm_packet_for_now(ComponentId::AcsMgt, tc_id, &response) {
|
||||
Ok(packet) => {
|
||||
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
|
||||
log::warn!("MGT: failed to send TM packet: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => log::warn!("MGT: failed to pack TM packet: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::mpsc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use arbitrary_int::u11;
|
||||
use satrs::spacepackets::SpacePacketHeader;
|
||||
use types::{
|
||||
Apid, Message as _, TcHeader,
|
||||
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||
pcdu::{SwitchRequest, SwitchState, SwitchStateBinary},
|
||||
};
|
||||
|
||||
use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct MgtTestbench {
|
||||
request_tx: SyncSender<ModeRequest>,
|
||||
report_rx: Receiver<ModeReport>,
|
||||
mgt: Mgt,
|
||||
parent_request_tx: mpsc::SyncSender<ModeRequest>,
|
||||
parent_report_rx: mpsc::Receiver<ModeResponse>,
|
||||
shared_switch_set: SharedSwitchSet,
|
||||
switch_rx: mpsc::Receiver<SwitchRequest>,
|
||||
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
|
||||
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
event_rx: mpsc::Receiver<mgt::Event>,
|
||||
handler: MgtHandler,
|
||||
}
|
||||
|
||||
impl MgtTestbench {
|
||||
fn new() -> Self {
|
||||
let (request_tx, request_rx) = mpsc::sync_channel(5);
|
||||
let (report_tx, report_rx) = mpsc::sync_channel(5);
|
||||
Self {
|
||||
request_tx,
|
||||
report_rx,
|
||||
mgt: Mgt::new(ModeLeafHelper {
|
||||
let (parent_request_tx, request_rx) = mpsc::sync_channel(5);
|
||||
let (report_tx, parent_report_rx) = mpsc::sync_channel(5);
|
||||
let (tc_tx, tc_rx) = mpsc::sync_channel(10);
|
||||
let (tm_tx, tm_rx) = mpsc::sync_channel(10);
|
||||
let (switch_tx, switch_rx) = mpsc::sync_channel(10);
|
||||
let (event_tx, event_rx) = mpsc::sync_channel(10);
|
||||
let mut switch_map = SwitchMap::new();
|
||||
switch_map.insert(SwitchId::Mgt, SwitchState::Off);
|
||||
let shared_switch_set = SharedSwitchSet::new(Mutex::new(SwitchSet::new(switch_map)));
|
||||
let handler = MgtHandler::new(
|
||||
TmtcQueues { tc_rx, tm_tx },
|
||||
PowerSwitchHelper::new(switch_tx, shared_switch_set.clone()),
|
||||
MgtCommunication::Test(TestInterface::default()),
|
||||
ModeLeafHelper {
|
||||
request_rx,
|
||||
report_tx,
|
||||
}),
|
||||
},
|
||||
Duration::from_millis(100),
|
||||
event_tx,
|
||||
);
|
||||
Self {
|
||||
parent_request_tx,
|
||||
parent_report_rx,
|
||||
shared_switch_set,
|
||||
switch_rx,
|
||||
tc_tx,
|
||||
tm_rx,
|
||||
event_rx,
|
||||
handler,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_tc(&self, request: Request) {
|
||||
self.tc_tx
|
||||
.send(CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(ComponentId::AcsMgt, request.message_type()),
|
||||
request,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn next_response(&self) -> Response {
|
||||
let tm = self.tm_rx.try_recv().expect("no TM generated");
|
||||
assert_eq!(tm.tm_header.sender_id, ComponentId::AcsMgt);
|
||||
postcard::from_bytes(&tm.payload).expect("invalid MGT response")
|
||||
}
|
||||
|
||||
/// Completes the power switch handshake for a commanded switch-on.
|
||||
fn switch_to_normal(&mut self) {
|
||||
self.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Normal)));
|
||||
self.handler.periodic_operation();
|
||||
self.shared_switch_set
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_switch_state(SwitchId::Mgt, SwitchState::On);
|
||||
self.handler.periodic_operation();
|
||||
assert_eq!(self.handler.mode(), DeviceMode::Normal);
|
||||
assert_eq!(self.next_response(), Response::Ok);
|
||||
}
|
||||
|
||||
fn test_interface(&mut self) -> &mut TestInterface {
|
||||
match &mut self.handler.com {
|
||||
MgtCommunication::Test(test) => test,
|
||||
_ => panic!("unexpected MGT interface"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initial_mode() {
|
||||
let testbench = MgtTestbench::new();
|
||||
assert_eq!(testbench.mgt.mode(), Mode::Off);
|
||||
fn test_initial_state_no_polling() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||
assert!(testbench.test_interface().sent_requests.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_mode() {
|
||||
fn test_switch_to_normal() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Normal)));
|
||||
testbench.handler.periodic_operation();
|
||||
let switch_request = testbench.switch_rx.try_recv().expect("no switch request");
|
||||
assert_eq!(switch_request.switch_id, SwitchId::Mgt);
|
||||
assert_eq!(switch_request.target_state, SwitchStateBinary::On);
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||
|
||||
testbench
|
||||
.shared_switch_set
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_switch_state(SwitchId::Mgt, SwitchState::On);
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Normal);
|
||||
assert_eq!(testbench.next_response(), Response::Ok);
|
||||
assert!(matches!(
|
||||
testbench.event_rx.try_recv(),
|
||||
Ok(mgt::Event::ModeChanged(DeviceMode::Normal))
|
||||
));
|
||||
assert_eq!(
|
||||
testbench.parent_report_rx.try_recv(),
|
||||
Ok(ModeResponse::Mode(DeviceMode::Normal))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mode_command_from_parent() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench
|
||||
.request_tx
|
||||
.send(ModeRequest::SetMode(Mode::Normal))
|
||||
.parent_request_tx
|
||||
.send(ModeRequest::SetMode(DeviceMode::Normal))
|
||||
.unwrap();
|
||||
testbench.mgt.periodic_operation();
|
||||
assert_eq!(testbench.mgt.mode(), Mode::Normal);
|
||||
let report = testbench.report_rx.try_recv().expect("no mode report sent");
|
||||
assert_eq!(report, ModeReport::Mode(Mode::Normal));
|
||||
testbench.handler.periodic_operation();
|
||||
testbench
|
||||
.shared_switch_set
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_switch_state(SwitchId::Mgt, SwitchState::On);
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(
|
||||
testbench.parent_report_rx.try_recv(),
|
||||
Ok(ModeResponse::Mode(DeviceMode::Normal))
|
||||
);
|
||||
// Commanded by the parent, so no TC response is expected.
|
||||
assert!(testbench.tm_rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_mode() {
|
||||
fn test_torque_command_rejected_when_off() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.request_tx.send(ModeRequest::ReadMode).unwrap();
|
||||
testbench.mgt.periodic_operation();
|
||||
let report = testbench.report_rx.try_recv().expect("no mode report sent");
|
||||
assert_eq!(report, ModeReport::Mode(Mode::Off));
|
||||
testbench.send_tc(Request::ApplyTorque {
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
duration: Duration::from_millis(100),
|
||||
});
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.next_response(), Response::NotInNormalMode);
|
||||
assert!(testbench.test_interface().sent_requests.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_torque_command_forwarded_in_normal_mode() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
testbench.test_interface().sent_requests.clear();
|
||||
|
||||
testbench.send_tc(Request::ApplyTorque {
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
duration: Duration::from_millis(100),
|
||||
});
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.next_response(), Response::Ok);
|
||||
assert_eq!(
|
||||
testbench.test_interface().sent_requests.first(),
|
||||
Some(&sim_mgt::Request::ApplyTorque {
|
||||
duration: Duration::from_millis(100),
|
||||
dipole: sim_mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hk_polling_updates_hk_set() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
assert!(
|
||||
testbench
|
||||
.test_interface()
|
||||
.sent_requests
|
||||
.contains(&sim_mgt::Request::RequestHk)
|
||||
);
|
||||
|
||||
testbench
|
||||
.test_interface()
|
||||
.hk_replies
|
||||
.push_back(sim_mgt::HkSet {
|
||||
dipole: sim_mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
torquing: true,
|
||||
});
|
||||
testbench.handler.periodic_operation();
|
||||
testbench.send_tc(Request::Hk(HkRequestType::OneShot));
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(
|
||||
testbench.next_response(),
|
||||
Response::Hk(HkSet {
|
||||
valid: true,
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
torquing: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hk_set_invalid_after_switch_off() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
testbench
|
||||
.test_interface()
|
||||
.hk_replies
|
||||
.push_back(sim_mgt::HkSet {
|
||||
dipole: sim_mgt::Dipole::default(),
|
||||
torquing: false,
|
||||
});
|
||||
testbench.handler.periodic_operation();
|
||||
|
||||
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Off)));
|
||||
testbench.send_tc(Request::Hk(HkRequestType::OneShot));
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.next_response(), Response::Hk(HkSet::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_periodic_hk() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.send_tc(Request::Hk(HkRequestType::EnablePeriodic(Some(
|
||||
Duration::ZERO,
|
||||
))));
|
||||
testbench.handler.periodic_operation();
|
||||
assert!(matches!(testbench.next_response(), Response::Hk(_)));
|
||||
let tm = testbench.tm_rx.try_recv();
|
||||
assert!(tm.is_err(), "only one HK packet per cycle expected");
|
||||
|
||||
testbench.send_tc(Request::Hk(HkRequestType::DisablePeriodic));
|
||||
testbench.handler.periodic_operation();
|
||||
assert!(testbench.tm_rx.try_recv().is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ fn build_sequence_tables() -> SequenceModeTables {
|
||||
off_step_1.add_entry(SequenceTableEntry::new(
|
||||
"OFF_MGT_OFF",
|
||||
ComponentId::AcsMgt as satrs::ComponentId,
|
||||
types::acs::mgt::Mode::Off.into(),
|
||||
types::DeviceMode::Off.into(),
|
||||
false,
|
||||
));
|
||||
off_table.add_sequence_table(off_step_1);
|
||||
@@ -67,7 +67,7 @@ fn build_sequence_tables() -> SequenceModeTables {
|
||||
safe_step_0.add_entry(SequenceTableEntry::new(
|
||||
"SAFE_MGT_NORMAL",
|
||||
ComponentId::AcsMgt as satrs::ComponentId,
|
||||
types::acs::mgt::Mode::Normal.into(),
|
||||
types::DeviceMode::Normal.into(),
|
||||
false,
|
||||
));
|
||||
safe_table.add_sequence_table(safe_step_0);
|
||||
@@ -126,16 +126,18 @@ fn mgm_assy_response_to_mode_response(
|
||||
}
|
||||
}
|
||||
|
||||
fn mgt_response_to_mode_response(response: types::acs::mgt::response::ModeReport) -> ModeResponse {
|
||||
fn mgt_response_to_mode_response(
|
||||
response: types::acs::mgt::response::ModeResponse,
|
||||
) -> ModeResponse {
|
||||
let sender_id = ComponentId::AcsMgt as satrs::ComponentId;
|
||||
match response {
|
||||
types::acs::mgt::response::ModeReport::Mode(mode) => ModeResponse {
|
||||
types::acs::mgt::response::ModeResponse::Mode(mode) => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: mode.into(),
|
||||
success: true,
|
||||
},
|
||||
types::acs::mgt::response::ModeReport::WrongMode(_) => ModeResponse {
|
||||
types::acs::mgt::response::ModeResponse::SetModeTimeout => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: 0,
|
||||
@@ -155,7 +157,7 @@ pub struct ModeRequestSenders {
|
||||
pub struct ModeReportReceivers {
|
||||
pub mode_response_ctrl: Receiver<types::acs::ctrl::response::ModeReport>,
|
||||
pub mode_response_mgm_assy: Receiver<types::acs::mgm_assembly::response::ModeResponse>,
|
||||
pub mode_response_mgt: Receiver<types::acs::mgt::response::ModeReport>,
|
||||
pub mode_response_mgt: Receiver<types::acs::mgt::response::ModeResponse>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -191,7 +193,7 @@ impl Subsystem {
|
||||
mode_store_vec
|
||||
.add_component(
|
||||
ComponentId::AcsMgt as satrs::ComponentId,
|
||||
types::acs::mgt::Mode::Off.into(),
|
||||
types::DeviceMode::Off.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -319,7 +321,7 @@ impl Subsystem {
|
||||
.mode_request_senders
|
||||
.mode_request_mgt
|
||||
.send(types::acs::mgt::request::ModeRequest::SetMode(
|
||||
types::acs::mgt::Mode::try_from(request.mode).unwrap(),
|
||||
types::DeviceMode::try_from(request.mode).unwrap(),
|
||||
))
|
||||
.unwrap(),
|
||||
_ => {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::collections::HashSet;
|
||||
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||
use types::{
|
||||
ComponentId, Event, EventId, Message,
|
||||
acs::{mgm, mgm_assembly},
|
||||
acs::{mgm, mgm_assembly, mgt},
|
||||
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||
control,
|
||||
event_manager::{request::Request, response::Response},
|
||||
@@ -18,6 +18,7 @@ pub struct EventManager {
|
||||
/// Shared by all MGM instances, which is why the sender ID is part of the message.
|
||||
pub mgm_rx: std::sync::mpsc::Receiver<(ComponentId, mgm::Event)>,
|
||||
pub mgm_assembly_rx: std::sync::mpsc::Receiver<mgm_assembly::Event>,
|
||||
pub mgt_rx: std::sync::mpsc::Receiver<mgt::Event>,
|
||||
pub pcdu_rx: std::sync::mpsc::Receiver<pcdu::Event>,
|
||||
/// Shared by all TC sources, which is why the sender ID is part of the message.
|
||||
pub tc_source_rx: std::sync::mpsc::Receiver<(ComponentId, tmtc::Event)>,
|
||||
@@ -29,11 +30,13 @@ pub struct EventManager {
|
||||
}
|
||||
|
||||
impl EventManager {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||
ctrl_rx: std::sync::mpsc::Receiver<control::Event>,
|
||||
mgm_rx: std::sync::mpsc::Receiver<(ComponentId, mgm::Event)>,
|
||||
mgm_assembly_rx: std::sync::mpsc::Receiver<mgm_assembly::Event>,
|
||||
mgt_rx: std::sync::mpsc::Receiver<mgt::Event>,
|
||||
pcdu_rx: std::sync::mpsc::Receiver<pcdu::Event>,
|
||||
tc_source_rx: std::sync::mpsc::Receiver<(ComponentId, tmtc::Event)>,
|
||||
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
@@ -43,6 +46,7 @@ impl EventManager {
|
||||
ctrl_rx,
|
||||
mgm_rx,
|
||||
mgm_assembly_rx,
|
||||
mgt_rx,
|
||||
pcdu_rx,
|
||||
tc_source_rx,
|
||||
tm_tx,
|
||||
@@ -87,6 +91,9 @@ impl EventManager {
|
||||
while let Ok(event) = self.mgm_assembly_rx.try_recv() {
|
||||
self.event_to_tm(ComponentId::AcsMgmAssembly, &event);
|
||||
}
|
||||
while let Ok(event) = self.mgt_rx.try_recv() {
|
||||
self.event_to_tm(ComponentId::AcsMgt, &event);
|
||||
}
|
||||
while let Ok(event) = self.pcdu_rx.try_recv() {
|
||||
self.event_to_tm(ComponentId::EpsPcdu, &event);
|
||||
}
|
||||
@@ -187,6 +194,7 @@ mod tests {
|
||||
let (_ctrl_tx, ctrl_rx) = mpsc::sync_channel(5);
|
||||
let (mgm_event_tx, mgm_rx) = mpsc::sync_channel(5);
|
||||
let (_mgm_assembly_tx, mgm_assembly_rx) = mpsc::sync_channel(5);
|
||||
let (_mgt_tx, mgt_rx) = mpsc::sync_channel(5);
|
||||
let (_pcdu_tx, pcdu_rx) = mpsc::sync_channel(5);
|
||||
let (_tc_source_tx, tc_source_rx) = mpsc::sync_channel(5);
|
||||
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
|
||||
@@ -199,6 +207,7 @@ mod tests {
|
||||
ctrl_rx,
|
||||
mgm_rx,
|
||||
mgm_assembly_rx,
|
||||
mgt_rx,
|
||||
pcdu_rx,
|
||||
tc_source_rx,
|
||||
tm_tx,
|
||||
|
||||
@@ -72,11 +72,13 @@ fn main() {
|
||||
let (sim_request_tx, sim_request_rx) = mpsc::channel();
|
||||
let (mgm_0_sim_reply_tx, mgm_0_sim_reply_rx) = mpsc::channel();
|
||||
let (mgm_1_sim_reply_tx, mgm_1_sim_reply_rx) = mpsc::channel();
|
||||
let (mgt_sim_reply_tx, mgt_sim_reply_rx) = mpsc::channel();
|
||||
let (pcdu_sim_reply_tx, pcdu_sim_reply_rx) = mpsc::channel();
|
||||
let mut opt_sim_client = create_sim_client(sim_request_rx);
|
||||
|
||||
let (mgm_0_handler_tc_tx, mgm_0_handler_tc_rx) = mpsc::sync_channel(10);
|
||||
let (mgm_1_handler_tc_tx, mgm_1_handler_tc_rx) = mpsc::sync_channel(10);
|
||||
let (mgt_handler_tc_tx, mgt_handler_tc_rx) = mpsc::sync_channel(10);
|
||||
let (mgm_assembly_tc_tx, mgm_assembly_tc_rx) = mpsc::sync_channel(10);
|
||||
let (acs_subsystem_tc_tx, acs_subsystem_tc_rx) = mpsc::sync_channel(10);
|
||||
let (pcdu_handler_tc_tx, pcdu_handler_tc_rx) = mpsc::sync_channel(30);
|
||||
@@ -104,6 +106,7 @@ fn main() {
|
||||
let (event_ctrl_tx, event_ctrl_rx) = mpsc::sync_channel(10);
|
||||
let (mgm_event_tx, mgm_event_rx) = mpsc::sync_channel(10);
|
||||
let (mgm_assembly_event_tx, mgm_assembly_event_rx) = mpsc::sync_channel(10);
|
||||
let (mgt_event_tx, mgt_event_rx) = mpsc::sync_channel(10);
|
||||
let (pcdu_event_tx, pcdu_event_rx) = mpsc::sync_channel(10);
|
||||
let (tc_source_event_tx, tc_source_event_rx) = mpsc::sync_channel(10);
|
||||
let mut event_manager = EventManager::new(
|
||||
@@ -111,6 +114,7 @@ fn main() {
|
||||
event_ctrl_rx,
|
||||
mgm_event_rx,
|
||||
mgm_assembly_event_rx,
|
||||
mgt_event_rx,
|
||||
pcdu_event_rx,
|
||||
tc_source_event_rx,
|
||||
tm_sink_tx.clone(),
|
||||
@@ -125,6 +129,7 @@ fn main() {
|
||||
tc_source.add_target(ComponentId::AcsMgm0, mgm_0_handler_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsMgm1, mgm_1_handler_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsMgmAssembly, mgm_assembly_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsMgt, mgt_handler_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsSubsystem, acs_subsystem_tc_tx);
|
||||
tc_source.add_target(ComponentId::EventManager, event_manager_tc_tx);
|
||||
|
||||
@@ -252,10 +257,29 @@ fn main() {
|
||||
report_tx: acs_ctrl_response_tx,
|
||||
});
|
||||
|
||||
let mut acs_mgt = mgt::Mgt::new(mgt::ModeLeafHelper {
|
||||
request_rx: mgt_request_rx,
|
||||
report_tx: mgt_report_tx,
|
||||
});
|
||||
let mgt_com = if let Some(sim_client) = opt_sim_client.as_mut() {
|
||||
sim_client.add_reply_recipient(satrs_minisim::ComponentId::Mgt, mgt_sim_reply_tx);
|
||||
mgt::MgtCommunication::Sim(mgt::SimInterface {
|
||||
sim_request_tx: sim_request_tx.clone(),
|
||||
sim_reply_rx: mgt_sim_reply_rx,
|
||||
})
|
||||
} else {
|
||||
mgt::MgtCommunication::Dummy(mgt::DummyInterface::default())
|
||||
};
|
||||
let mut mgt_handler = mgt::MgtHandler::new(
|
||||
TmtcQueues {
|
||||
tc_rx: mgt_handler_tc_rx,
|
||||
tm_tx: tm_sink_tx.clone(),
|
||||
},
|
||||
switch_helper.clone(),
|
||||
mgt_com,
|
||||
mgt::ModeLeafHelper {
|
||||
request_rx: mgt_request_rx,
|
||||
report_tx: mgt_report_tx,
|
||||
},
|
||||
Duration::from_millis(1000),
|
||||
mgt_event_tx,
|
||||
);
|
||||
|
||||
let mut acs_subsystem = subsystem::Subsystem::new(
|
||||
subsystem::ModeRequestSenders {
|
||||
@@ -373,7 +397,7 @@ fn main() {
|
||||
mgm_1_handler.periodic_operation();
|
||||
mgm_assembly.periodic_operation();
|
||||
acs_controller.periodic_operation();
|
||||
acs_mgt.periodic_operation();
|
||||
mgt_handler.periodic_operation();
|
||||
acs_subsystem.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_AOCS));
|
||||
}
|
||||
|
||||
@@ -1,38 +1,104 @@
|
||||
#[derive(
|
||||
serde::Serialize,
|
||||
serde::Deserialize,
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
num_enum::IntoPrimitive,
|
||||
num_enum::TryFromPrimitive,
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum Mode {
|
||||
Off,
|
||||
Normal,
|
||||
use crate::DeviceMode;
|
||||
|
||||
/// Commanded magnetic dipole. Simple model using raw values per axis.
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Dipole {
|
||||
pub x: i16,
|
||||
pub y: i16,
|
||||
pub z: i16,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct HkSet {
|
||||
pub valid: bool,
|
||||
pub dipole: Dipole,
|
||||
pub torquing: bool,
|
||||
}
|
||||
|
||||
pub mod request {
|
||||
use super::*;
|
||||
use crate::{DeviceMode, HkRequestType, Message};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
use super::Dipole;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModeRequest {
|
||||
SetMode(Mode),
|
||||
SetMode(DeviceMode),
|
||||
ReadMode,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||
pub enum Request {
|
||||
Ping,
|
||||
Hk(HkRequestType),
|
||||
Mode(ModeRequest),
|
||||
/// Only accepted in normal mode.
|
||||
ApplyTorque {
|
||||
dipole: Dipole,
|
||||
duration: core::time::Duration,
|
||||
},
|
||||
}
|
||||
|
||||
impl Message for Request {
|
||||
fn message_type(&self) -> crate::MessageType {
|
||||
match self {
|
||||
Request::Ping => crate::MessageType::Verification,
|
||||
Request::Hk(_) => crate::MessageType::Hk,
|
||||
Request::Mode(_) => crate::MessageType::Mode,
|
||||
Request::ApplyTorque { .. } => crate::MessageType::Action,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(strum::EnumDiscriminants, serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||
#[strum_discriminants(derive(num_enum::IntoPrimitive))]
|
||||
#[repr(u16)]
|
||||
pub enum Event {
|
||||
/// A commanded mode transition completed.
|
||||
ModeChanged(DeviceMode),
|
||||
}
|
||||
|
||||
impl crate::Message for Event {
|
||||
fn message_type(&self) -> crate::MessageType {
|
||||
crate::MessageType::Event
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::EventId for Event {
|
||||
fn event_id(&self) -> u16 {
|
||||
EventDiscriminants::from(self).into()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod response {
|
||||
use super::*;
|
||||
use crate::{DeviceMode, Message};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ModeReport {
|
||||
/// Mode of the assembly.
|
||||
Mode(super::Mode),
|
||||
/// Children are in wrong mode after commanding.
|
||||
WrongMode([Option<Mode>; 2]),
|
||||
use super::HkSet;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModeResponse {
|
||||
/// New mode has been set.
|
||||
Mode(DeviceMode),
|
||||
/// Setting a mode timed out.
|
||||
SetModeTimeout,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Response {
|
||||
Ok,
|
||||
Hk(HkSet),
|
||||
Mode(ModeResponse),
|
||||
/// The command requires the device to be in normal mode.
|
||||
NotInNormalMode,
|
||||
}
|
||||
|
||||
impl Message for Response {
|
||||
fn message_type(&self) -> crate::MessageType {
|
||||
match self {
|
||||
Response::Ok | Response::NotInNormalMode => crate::MessageType::Verification,
|
||||
Response::Hk(_) => crate::MessageType::Hk,
|
||||
Response::Mode(_) => crate::MessageType::Mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user