MGT handler

This commit is contained in:
Robin Mueller
2026-09-24 16:23:31 +02:00
parent ba9ea3a12c
commit db906565c8
12 changed files with 929 additions and 133 deletions
+131 -11
View File
@@ -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 -2
View File
@@ -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"/>
+4
View File
@@ -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 {
+82 -4
View File
@@ -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
);
}
}
+6
View File
@@ -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
View File
@@ -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());
}
}
+10 -8
View File
@@ -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(),
_ => {
+10 -1
View File
@@ -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,
+29 -5
View File
@@ -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));
}
+91 -25
View File
@@ -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,
}
}
}
}