Skip to main content

recovery/
recovery.rs

1//! Recovery flow.
2//!
3//! Safe mode is a first class part of the lifecycle.
4//! This example shows a recovery path from Safe back to Ready.
5
6#![allow(clippy::print_stdout, clippy::enum_glob_use)]
7
8use ready_active_safe::prelude::*;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11enum Mode {
12    Ready,
13    Active,
14    Safe,
15}
16
17impl core::fmt::Display for Mode {
18    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
19        write!(f, "{self:?}")
20    }
21}
22
23#[derive(Debug)]
24enum Event {
25    Activate,
26    TransientFault,
27    CriticalFault,
28    RecoveryApproved,
29    DiagnosticsComplete,
30    Shutdown,
31}
32
33#[derive(Debug, PartialEq)]
34enum Command {
35    StartProcessing,
36    LogFault,
37    EmergencyStop,
38    NotifyOperator,
39    RunRecoveryDiagnostics,
40    ResetSubsystems,
41    PowerDown,
42}
43
44struct SafetyController;
45
46impl Machine for SafetyController {
47    type Mode = Mode;
48    type Event = Event;
49    type Command = Command;
50
51    fn initial_mode(&self) -> Mode {
52        Mode::Ready
53    }
54
55    fn on_event(
56        &self,
57        mode: &Self::Mode,
58        event: &Self::Event,
59    ) -> Decision<Self::Mode, Self::Command> {
60        use Command::*;
61        use Event::*;
62        use Mode::*;
63
64        match (mode, event) {
65            (Ready, Activate) => transition(Active).emit(StartProcessing),
66            (Active, TransientFault) => stay().emit(LogFault),
67            (Active, CriticalFault) => transition(Safe).emit_all([EmergencyStop, NotifyOperator]),
68            (Safe, RecoveryApproved) => stay().emit(RunRecoveryDiagnostics),
69            (Safe, DiagnosticsComplete) => transition(Ready).emit(ResetSubsystems),
70            (Active | Ready, Shutdown) => transition(Safe).emit(PowerDown),
71            _ => ignore(),
72        }
73    }
74}
75
76struct LifecyclePolicy;
77
78impl Policy<Mode> for LifecyclePolicy {
79    fn is_allowed(&self, from: &Mode, to: &Mode) -> bool {
80        matches!(
81            (from, to),
82            (Mode::Ready, Mode::Active | Mode::Safe)
83                | (Mode::Active, Mode::Safe)
84                | (Mode::Safe, Mode::Ready)
85        )
86    }
87
88    fn check(&self, from: &Mode, to: &Mode) -> Result<(), &'static str> {
89        if self.is_allowed(from, to) {
90            return Ok(());
91        }
92
93        Err("this transition is not part of the allowed lifecycle")
94    }
95}
96
97fn main() -> Result<(), LifecycleError<Mode>> {
98    let controller = SafetyController;
99    let policy = LifecyclePolicy;
100    let mut mode = controller.initial_mode();
101
102    let events = [
103        Event::Activate,
104        Event::TransientFault,
105        Event::CriticalFault,
106        Event::RecoveryApproved,
107        Event::DiagnosticsComplete,
108        Event::Activate,
109        Event::Shutdown,
110    ];
111
112    for event in &events {
113        let decision = controller.decide(&mode, event);
114        println!("{mode:?} + {event:?} => {decision}");
115
116        match decision.apply_checked(mode, &policy) {
117            Ok((next_mode, commands)) => {
118                if !commands.is_empty() {
119                    println!("  commands: {commands:?}");
120                }
121                mode = next_mode;
122            }
123            Err(LifecycleError::TransitionDenied { from, to, reason }) => {
124                println!("  denied: {from:?} -> {to:?} ({reason})");
125                mode = from;
126            }
127            Err(err) => return Err(err),
128        }
129    }
130
131    assert!(!policy.is_allowed(&Mode::Active, &Mode::Ready));
132    assert!(!policy.is_allowed(&Mode::Safe, &Mode::Active));
133    assert_eq!(mode, Mode::Safe);
134
135    Ok(())
136}