diff --git a/images/minisim-arch/minisim-arch.graphml b/images/minisim-arch/minisim-arch.graphml index 6eff044..b2e8d57 100644 --- a/images/minisim-arch/minisim-arch.graphml +++ b/images/minisim-arch/minisim-arch.graphml @@ -1,6 +1,6 @@ - + @@ -15,7 +15,6 @@ - @@ -39,7 +38,7 @@ - Simulation + Simulation @@ -50,7 +49,7 @@ - PCDU + PCDU @@ -61,7 +60,7 @@ - Magnetometer + Magnetometers @@ -72,7 +71,7 @@ - Magnetorquer + Magnetorquer @@ -83,7 +82,7 @@ - SimController + SimController @@ -94,7 +93,7 @@ - UDP TC Receiver + UDP TC Receiver @@ -105,7 +104,7 @@ - UDP TM Sender + UDP TM Sender @@ -116,7 +115,18 @@ - Client + Client + + + + + + + + + + + Magnetometers @@ -153,7 +163,7 @@ - schedule_event + schedule_event @@ -167,7 +177,7 @@ - SimRequest + SimRequest @@ -185,10 +195,10 @@ - + - + @@ -207,7 +217,7 @@ - SimReply + SimReply @@ -221,7 +231,7 @@ - SimRequest in UDP + SimRequest in UDP @@ -235,7 +245,7 @@ - SimReply in UDP + SimReply in UDP @@ -248,7 +258,7 @@ - step + step diff --git a/images/minisim-arch/minisim-arch.png b/images/minisim-arch/minisim-arch.png index 8e384ad..4b3f6af 100644 Binary files a/images/minisim-arch/minisim-arch.png and b/images/minisim-arch/minisim-arch.png differ diff --git a/satrs-example/client/src/main.rs b/satrs-example/client/src/main.rs index f301d2c..5a08238 100644 --- a/satrs-example/client/src/main.rs +++ b/satrs-example/client/src/main.rs @@ -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 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, } +#[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, + /// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`. + #[arg(long)] + hk_interval_ms: Option, + #[arg(short, long)] + mode: Option, + /// 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, + #[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, +) -> anyhow::Result { + 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 { + let axes: Vec = value + .split(',') + .map(|axis| axis.trim().parse::().map_err(|e| e.to_string())) + .collect::>()?; + 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::(sender_id, data) } + types::ComponentId::AcsMgt => log_event::(sender_id, data), types::ComponentId::EpsPcdu => log_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::(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)); + } +} diff --git a/satrs-example/minisim/README.md b/satrs-example/minisim/README.md index 42f949d..b55d3e9 100644 --- a/satrs-example/minisim/README.md +++ b/satrs-example/minisim/README.md @@ -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. -Mini simulator architecture +Mini simulator architecture diff --git a/satrs-example/minisim/src/acs/mgm.rs b/satrs-example/minisim/src/acs/mgm.rs index 095c139..4684f66 100644 --- a/satrs-example/minisim/src/acs/mgm.rs +++ b/satrs-example/minisim/src/acs/mgm.rs @@ -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 { diff --git a/satrs-example/minisim/src/acs/mgt.rs b/satrs-example/minisim/src/acs/mgt.rs index fe97297..dbd9b96 100644 --- a/satrs-example/minisim/src/acs/mgt.rs +++ b/satrs-example/minisim/src/acs/mgt.rs @@ -15,6 +15,7 @@ pub struct MgtModel { torquing: bool, torque_dipole: mgt::Dipole, pub gen_magnetic_field: Output, + pub clear_magnetic_field: Output<()>, reply_sender: mpsc::Sender, } @@ -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) { @@ -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) { + 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 + ); + } } diff --git a/satrs-example/minisim/src/controller.rs b/satrs-example/minisim/src/controller.rs index 8c6eeb5..80cdd66 100644 --- a/satrs-example/minisim/src/controller.rs +++ b/satrs-example/minisim/src/controller.rs @@ -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) diff --git a/satrs-example/src/acs/mgt.rs b/satrs-example/src/acs/mgt.rs index 8145e56..2044ef4 100644 --- a/satrs-example/src/acs/mgt.rs +++ b/satrs-example/src/acs/mgt.rs @@ -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, + 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 { + 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, + pub hk_replies: VecDeque, +} + +pub struct SimInterface { + pub sim_request_tx: mpsc::Sender, + pub sim_reply_rx: mpsc::Receiver, +} + +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 { + 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 { + 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, - pub report_tx: SyncSender, + pub request_rx: mpsc::Receiver, + pub report_tx: mpsc::SyncSender, } -/// 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, mode_leaf_helper: ModeLeafHelper, + event_tx: mpsc::SyncSender, } -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, + ) -> 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::(&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, + ) { + 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) { + 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) { + 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, 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, - report_rx: Receiver, - mgt: Mgt, + parent_request_tx: mpsc::SyncSender, + parent_report_rx: mpsc::Receiver, + shared_switch_set: SharedSwitchSet, + switch_rx: mpsc::Receiver, + tc_tx: mpsc::SyncSender, + tm_rx: mpsc::Receiver, + event_rx: mpsc::Receiver, + 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()); } } diff --git a/satrs-example/src/acs/subsystem.rs b/satrs-example/src/acs/subsystem.rs index f90e598..aec43e2 100644 --- a/satrs-example/src/acs/subsystem.rs +++ b/satrs-example/src/acs/subsystem.rs @@ -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, pub mode_response_mgm_assy: Receiver, - pub mode_response_mgt: Receiver, + pub mode_response_mgt: Receiver, } #[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(), _ => { diff --git a/satrs-example/src/event_manager.rs b/satrs-example/src/event_manager.rs index 27346f1..af84537 100644 --- a/satrs-example/src/event_manager.rs +++ b/satrs-example/src/event_manager.rs @@ -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, + pub mgt_rx: std::sync::mpsc::Receiver, pub pcdu_rx: std::sync::mpsc::Receiver, /// 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, ctrl_rx: std::sync::mpsc::Receiver, mgm_rx: std::sync::mpsc::Receiver<(ComponentId, mgm::Event)>, mgm_assembly_rx: std::sync::mpsc::Receiver, + mgt_rx: std::sync::mpsc::Receiver, pcdu_rx: std::sync::mpsc::Receiver, tc_source_rx: std::sync::mpsc::Receiver<(ComponentId, tmtc::Event)>, tm_tx: std::sync::mpsc::SyncSender, @@ -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, diff --git a/satrs-example/src/main.rs b/satrs-example/src/main.rs index 900498a..b00ba09 100644 --- a/satrs-example/src/main.rs +++ b/satrs-example/src/main.rs @@ -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)); } diff --git a/satrs-example/types/src/acs/mgt.rs b/satrs-example/types/src/acs/mgt.rs index 90122a0..8d562c4 100644 --- a/satrs-example/types/src/acs/mgt.rs +++ b/satrs-example/types/src/acs/mgt.rs @@ -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; 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, + } + } } }