fix FDIR reaction
This commit is contained in:
@@ -30,41 +30,12 @@ pub struct Cli {
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
enum Commands {
|
||||
/// Commands addressed to a component in the OBSW.
|
||||
#[command(subcommand)]
|
||||
Obsw(ObswCommand),
|
||||
/// Commands that talk straight to minisim's own control port, bypassing the OBSW entirely.
|
||||
///
|
||||
/// This is the same control port the OBSW's internal sim client uses. Kept separate from
|
||||
/// `Obsw`, since these commands have no OBSW component to address and only make sense in a
|
||||
/// test/simulation environment, never something the real flight software could ask for.
|
||||
#[command(subcommand)]
|
||||
Sim(SimCommand),
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
enum ObswCommand {
|
||||
Mgm0(MgmArgs),
|
||||
Mgm1(MgmArgs),
|
||||
MgmAssy(MgmAssemblyArgs),
|
||||
AcsSubsystem(SubsystemArgs),
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
enum SimCommand {
|
||||
Fault(SimFaultArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||
struct SimFaultArgs {
|
||||
/// Fault mode to inject on the simulated MGM0 SPI bus.
|
||||
#[arg(value_enum)]
|
||||
mode: SpiFaultModeSelect,
|
||||
}
|
||||
|
||||
// MGM1 is not selectable here: MgmRequestLis3Mdl::TARGET is hardcoded to Mgm0Lis3Mdl, so a
|
||||
// request built from this payload always reaches the MGM0 model regardless of intent. This
|
||||
// appears to be a pre-existing minisim limitation, not something specific to fault injection.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
enum SpiFaultModeSelect {
|
||||
None,
|
||||
@@ -90,6 +61,12 @@ struct MgmArgs {
|
||||
request_hk: bool,
|
||||
#[arg(short, long)]
|
||||
mode: Option<DeviceModeSelect>,
|
||||
/// Inject (or clear) an SPI bus failure on the simulated device, bypassing the OBSW.
|
||||
///
|
||||
/// Only takes effect for MGM0: minisim always routes this fault to the MGM0 model
|
||||
/// regardless of which MGM the request names (a pre-existing minisim limitation).
|
||||
#[arg(long, value_enum)]
|
||||
spi_fault: Option<SpiFaultModeSelect>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||
@@ -132,7 +109,15 @@ fn handle_mgm_command(
|
||||
addr: SocketAddr,
|
||||
target_id: types::ComponentId,
|
||||
args: MgmArgs,
|
||||
) {
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(mode) = args.spi_fault {
|
||||
if target_id != types::ComponentId::AcsMgm0 {
|
||||
bail!(
|
||||
"SPI fault injection is only supported for MGM0 right now (minisim limitation)"
|
||||
);
|
||||
}
|
||||
inject_mgm_failure(mode.into())?;
|
||||
}
|
||||
if args.ping {
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
@@ -188,6 +173,7 @@ fn handle_mgm_command(
|
||||
let request_packet = request.to_vec();
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_logger(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
||||
@@ -247,16 +233,13 @@ fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
if let Some(cmd) = cli.commands {
|
||||
match cmd {
|
||||
Commands::Sim(SimCommand::Fault(sim_fault_args)) => {
|
||||
inject_sim_fault(sim_fault_args.mode.into())?;
|
||||
Commands::Mgm0(args) => {
|
||||
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm0, args)?
|
||||
}
|
||||
Commands::Obsw(ObswCommand::Mgm0(args)) => {
|
||||
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm0, args)
|
||||
Commands::Mgm1(args) => {
|
||||
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm1, args)?
|
||||
}
|
||||
Commands::Obsw(ObswCommand::Mgm1(args)) => {
|
||||
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm1, args)
|
||||
}
|
||||
Commands::Obsw(ObswCommand::MgmAssy(mgm_assembly_args)) => {
|
||||
Commands::MgmAssy(mgm_assembly_args) => {
|
||||
let target_id = types::ComponentId::AcsMgmAssembly;
|
||||
if mgm_assembly_args.ping {
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
@@ -303,7 +286,7 @@ fn main() -> anyhow::Result<()> {
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
}
|
||||
Commands::Obsw(ObswCommand::AcsSubsystem(subsystem_args)) => {
|
||||
Commands::AcsSubsystem(subsystem_args) => {
|
||||
let target_id = types::ComponentId::AcsSubsystem;
|
||||
if subsystem_args.ping {
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
@@ -373,7 +356,7 @@ fn main() -> anyhow::Result<()> {
|
||||
/// Confirms the simulator is actually reachable first (same ping/pong check the OBSW's own
|
||||
/// internal sim client does, see `SimClientUdp::attempt_connection`), since a fire-and-forget
|
||||
/// UDP send would otherwise silently do nothing if minisim is not running.
|
||||
fn inject_sim_fault(mode: SpiFaultMode) -> anyhow::Result<()> {
|
||||
fn inject_mgm_failure(mode: SpiFaultMode) -> anyhow::Result<()> {
|
||||
let sim_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), SIM_CTRL_PORT);
|
||||
let sim_socket = UdpSocket::bind("127.0.0.1:0")?;
|
||||
sim_socket.set_read_timeout(Some(Duration::from_millis(200)))?;
|
||||
|
||||
@@ -124,6 +124,7 @@ impl SimController {
|
||||
}
|
||||
match sim_ctrl_request {
|
||||
SimCtrlRequest::Ping => {
|
||||
log::info!("received ping request, a client is connecting");
|
||||
self.reply_sender
|
||||
.send(SimReply::new(&SimCtrlReply::Pong))
|
||||
.expect("sending reply from sim controller failed");
|
||||
@@ -160,6 +161,7 @@ impl SimController {
|
||||
|
||||
_ => panic!("invalid mgm index"),
|
||||
};
|
||||
log::info!("MGM{mgm_idx}: setting SPI fault mode to {fault_mode:?}");
|
||||
self.simulation
|
||||
.process_event(MagnetometerModel::set_spi_fault, fault_mode, addr)
|
||||
.expect("event execution error for mgm");
|
||||
|
||||
@@ -424,6 +424,14 @@ impl MgmHandlerLis3Mdl {
|
||||
// TODO: Event? Health-table changes are currently invisible to the ground
|
||||
// except through this log line. Likely applies to other health/mode
|
||||
// transitions across the example app too, not just this one.
|
||||
// Do not restart an already pending Off transition: poll_sensor still calls
|
||||
// this every cycle the fault persists, and current stays Normal until the
|
||||
// transition completes, so re-triggering here would keep resetting the
|
||||
// transition state machine before it can ever finish.
|
||||
if self.mode_helpers.target != Some(DeviceMode::Off) {
|
||||
log::warn!("{}: commanding device off due to fault", self.id.str());
|
||||
self.start_transition(DeviceMode::Off, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -441,29 +449,35 @@ impl MgmHandlerLis3Mdl {
|
||||
return;
|
||||
}
|
||||
let target_mode = self.mode_helpers.target.unwrap();
|
||||
if target_mode == DeviceMode::On || target_mode == DeviceMode::Normal {
|
||||
if self.mode_helpers.transition_state == TransitionState::Idle {
|
||||
let result = self
|
||||
.switch_helper
|
||||
.send_switch_on_cmd(MessageMetadata::new(0, self.id as u32), self.switch_id());
|
||||
if result.is_err() {
|
||||
// Could not send switch command.. still continue with transition.
|
||||
log::error!("failed to send switch on command");
|
||||
}
|
||||
self.mode_helpers.transition_state = TransitionState::PowerSwitching;
|
||||
let switch_target_on = target_mode != DeviceMode::Off;
|
||||
if self.mode_helpers.transition_state == TransitionState::Idle {
|
||||
let result = if switch_target_on {
|
||||
self.switch_helper
|
||||
.send_switch_on_cmd(MessageMetadata::new(0, self.id as u32), self.switch_id())
|
||||
} else {
|
||||
self.switch_helper
|
||||
.send_switch_off_cmd(MessageMetadata::new(0, self.id as u32), self.switch_id())
|
||||
};
|
||||
if result.is_err() {
|
||||
// Could not send switch command.. still continue with transition.
|
||||
log::error!(
|
||||
"failed to send switch {} command",
|
||||
if switch_target_on { "on" } else { "off" }
|
||||
);
|
||||
}
|
||||
if self.mode_helpers.transition_state == TransitionState::PowerSwitching {
|
||||
if self.switch_helper.is_switch_on(self.switch_id()) {
|
||||
log::info!("switch is on");
|
||||
self.mode_helpers.transition_state = TransitionState::Done;
|
||||
} else if self.mode_helpers.timed_out() {
|
||||
self.handle_mode_transition_failure();
|
||||
}
|
||||
}
|
||||
if self.mode_helpers.transition_state == TransitionState::Done {
|
||||
self.handle_mode_reached();
|
||||
self.mode_helpers.transition_state = TransitionState::PowerSwitching;
|
||||
}
|
||||
if self.mode_helpers.transition_state == TransitionState::PowerSwitching {
|
||||
if self.switch_helper.is_switch_on(self.switch_id()) == switch_target_on {
|
||||
log::info!("switch is {}", if switch_target_on { "on" } else { "off" });
|
||||
self.mode_helpers.transition_state = TransitionState::Done;
|
||||
} else if self.mode_helpers.timed_out() {
|
||||
self.handle_mode_transition_failure();
|
||||
}
|
||||
}
|
||||
if self.mode_helpers.transition_state == TransitionState::Done {
|
||||
self.handle_mode_reached();
|
||||
}
|
||||
}
|
||||
|
||||
// Should be called to complete a mode transition which failed.
|
||||
@@ -889,6 +903,48 @@ mod tests {
|
||||
assert!(!testbench.handler.shared_mgm_set.lock().unwrap().valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spi_fault_above_threshold_commands_device_off() {
|
||||
let mut testbench = MgmTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
// Drain the switch-on request left over from switch_to_normal().
|
||||
testbench
|
||||
.switch_rx
|
||||
.try_recv()
|
||||
.expect("no switch-on request sent");
|
||||
testbench.test_spi_interface().next_mgm_data = MgmLis3RawValues {
|
||||
x: -1,
|
||||
y: -1,
|
||||
z: -1,
|
||||
};
|
||||
for _ in 0..SPI_FAULT_THRESHOLD + 1 {
|
||||
testbench.handler.periodic_operation();
|
||||
}
|
||||
assert_eq!(
|
||||
testbench.health_table.health(ComponentId::AcsMgm0.into()),
|
||||
Some(HealthState::Faulty)
|
||||
);
|
||||
// The Off transition was only started on the last iteration above, so it has not sent
|
||||
// its switch-off request yet: drive one more cycle to let it do so.
|
||||
testbench.handler.periodic_operation();
|
||||
let switch_req = testbench
|
||||
.switch_rx
|
||||
.try_recv()
|
||||
.expect("no switch-off request sent after fault");
|
||||
assert_eq!(switch_req.message.switch_id, SwitchId::Mgm0);
|
||||
assert_eq!(switch_req.message.target_state, SwitchStateBinary::Off);
|
||||
|
||||
// Simulate the PCDU acting on the switch-off request.
|
||||
testbench
|
||||
.shared_switch_set
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_switch_state(SwitchId::Mgm0, SwitchState::Off);
|
||||
testbench.handler.periodic_operation();
|
||||
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spi_fault_does_not_override_external_control() {
|
||||
let mut testbench = MgmTestbench::new();
|
||||
|
||||
Reference in New Issue
Block a user