Skip to main content

nros_core/
lifecycle.rs

1//! Lifecycle state machine types (REP-2002)
2//!
3//! Provides the core types for ROS 2 lifecycle node management:
4//! - [`LifecycleState`] — the five lifecycle states
5//! - [`LifecycleTransition`] — transitions between states
6//! - [`TransitionResult`] — callback return values
7//!
8//! These types are shared between the Rust and C APIs. All enums use
9//! `#[repr(u8)]` for C interop.
10
11/// Lifecycle state (REP-2002)
12///
13/// A lifecycle node is always in exactly one of these states.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[repr(u8)]
16pub enum LifecycleState {
17    /// Initial state after construction. Node is not yet configured.
18    Unconfigured = 1,
19    /// Node is configured but not processing data.
20    Inactive = 2,
21    /// Node is fully operational and processing data.
22    Active = 3,
23    /// Terminal state. Node cannot be reused.
24    Finalized = 4,
25    /// Error occurred during a transition. Must recover or shut down.
26    ErrorProcessing = 5,
27}
28
29impl LifecycleState {
30    /// Returns true if this is a terminal state (Finalized).
31    pub const fn is_terminal(&self) -> bool {
32        matches!(self, Self::Finalized)
33    }
34
35    /// Returns true if this is a primary state (not a transition state).
36    ///
37    /// Primary states are: Unconfigured, Inactive, Active, Finalized.
38    /// ErrorProcessing is the only non-primary state.
39    pub const fn is_primary(&self) -> bool {
40        !matches!(self, Self::ErrorProcessing)
41    }
42
43    /// Try to convert from a u8 value.
44    pub const fn from_u8(value: u8) -> Option<Self> {
45        match value {
46            1 => Some(Self::Unconfigured),
47            2 => Some(Self::Inactive),
48            3 => Some(Self::Active),
49            4 => Some(Self::Finalized),
50            5 => Some(Self::ErrorProcessing),
51            _ => None,
52        }
53    }
54}
55
56/// Lifecycle transition (REP-2002)
57///
58/// Each transition has a specific source state. Shutdown has three variants
59/// because it can originate from Unconfigured, Inactive, or Active.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61#[repr(u8)]
62pub enum LifecycleTransition {
63    /// Unconfigured -> (configuring) -> Inactive
64    Configure = 1,
65    /// Inactive -> (activating) -> Active
66    Activate = 2,
67    /// Active -> (deactivating) -> Inactive
68    Deactivate = 3,
69    /// Inactive -> (cleaning up) -> Unconfigured
70    Cleanup = 4,
71    /// Unconfigured -> (shutting down) -> Finalized
72    ShutdownUnconfigured = 5,
73    /// Inactive -> (shutting down) -> Finalized
74    ShutdownInactive = 6,
75    /// Active -> (shutting down) -> Finalized
76    ShutdownActive = 7,
77    /// ErrorProcessing -> (error recovery) -> Unconfigured
78    ErrorRecovery = 8,
79}
80
81impl LifecycleTransition {
82    /// Resolve a shorthand transition name from the current state.
83    ///
84    /// "shutdown" maps to the correct variant based on the current state.
85    /// Returns `None` if the shorthand is not valid from the given state.
86    pub fn from_shorthand(state: LifecycleState, name: &str) -> Option<Self> {
87        match name {
88            "configure" => Some(Self::Configure),
89            "activate" => Some(Self::Activate),
90            "deactivate" => Some(Self::Deactivate),
91            "cleanup" => Some(Self::Cleanup),
92            "shutdown" => match state {
93                LifecycleState::Unconfigured => Some(Self::ShutdownUnconfigured),
94                LifecycleState::Inactive => Some(Self::ShutdownInactive),
95                LifecycleState::Active => Some(Self::ShutdownActive),
96                _ => None,
97            },
98            "error_recovery" => Some(Self::ErrorRecovery),
99            _ => None,
100        }
101    }
102
103    /// Try to convert from a u8 value.
104    pub const fn from_u8(value: u8) -> Option<Self> {
105        match value {
106            1 => Some(Self::Configure),
107            2 => Some(Self::Activate),
108            3 => Some(Self::Deactivate),
109            4 => Some(Self::Cleanup),
110            5 => Some(Self::ShutdownUnconfigured),
111            6 => Some(Self::ShutdownInactive),
112            7 => Some(Self::ShutdownActive),
113            8 => Some(Self::ErrorRecovery),
114            _ => None,
115        }
116    }
117
118    /// Get the source state required for this transition.
119    pub const fn source_state(&self) -> LifecycleState {
120        match self {
121            Self::Configure => LifecycleState::Unconfigured,
122            Self::Activate => LifecycleState::Inactive,
123            Self::Deactivate => LifecycleState::Active,
124            Self::Cleanup => LifecycleState::Inactive,
125            Self::ShutdownUnconfigured => LifecycleState::Unconfigured,
126            Self::ShutdownInactive => LifecycleState::Inactive,
127            Self::ShutdownActive => LifecycleState::Active,
128            Self::ErrorRecovery => LifecycleState::ErrorProcessing,
129        }
130    }
131}
132
133/// Result of a lifecycle transition callback.
134///
135/// Matches the rclc convention where callbacks return a status
136/// indicating whether the transition should proceed.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
138#[repr(u8)]
139pub enum TransitionResult {
140    /// Transition succeeded; move to the target state.
141    Success = 0,
142    /// Transition failed; roll back to the previous primary state.
143    Failure = 1,
144    /// An error occurred; move to ErrorProcessing state.
145    Error = 2,
146}
147
148impl TransitionResult {
149    /// Try to convert from a u8 value.
150    pub const fn from_u8(value: u8) -> Option<Self> {
151        match value {
152            0 => Some(Self::Success),
153            1 => Some(Self::Failure),
154            2 => Some(Self::Error),
155            _ => None,
156        }
157    }
158}
159
160/// Check whether a transition is valid from the given state.
161pub const fn can_transition(state: LifecycleState, transition: LifecycleTransition) -> bool {
162    matches!(
163        (state, transition),
164        (LifecycleState::Unconfigured, LifecycleTransition::Configure)
165            | (
166                LifecycleState::Unconfigured,
167                LifecycleTransition::ShutdownUnconfigured
168            )
169            | (LifecycleState::Inactive, LifecycleTransition::Activate)
170            | (LifecycleState::Inactive, LifecycleTransition::Cleanup)
171            | (
172                LifecycleState::Inactive,
173                LifecycleTransition::ShutdownInactive
174            )
175            | (LifecycleState::Active, LifecycleTransition::Deactivate)
176            | (LifecycleState::Active, LifecycleTransition::ShutdownActive)
177            | (
178                LifecycleState::ErrorProcessing,
179                LifecycleTransition::ErrorRecovery
180            )
181    )
182}
183
184/// Apply a transition given the callback result.
185///
186/// Implements the REP-2002 transition table:
187/// - **Success**: move to the target state
188/// - **Failure**: roll back to the previous primary state
189/// - **Error**: move to ErrorProcessing
190pub const fn apply_transition(
191    state: LifecycleState,
192    transition: LifecycleTransition,
193    result: TransitionResult,
194) -> LifecycleState {
195    match result {
196        TransitionResult::Error => LifecycleState::ErrorProcessing,
197        TransitionResult::Success => match transition {
198            LifecycleTransition::Configure => LifecycleState::Inactive,
199            LifecycleTransition::Activate => LifecycleState::Active,
200            LifecycleTransition::Deactivate => LifecycleState::Inactive,
201            LifecycleTransition::Cleanup => LifecycleState::Unconfigured,
202            LifecycleTransition::ShutdownUnconfigured
203            | LifecycleTransition::ShutdownInactive
204            | LifecycleTransition::ShutdownActive => LifecycleState::Finalized,
205            LifecycleTransition::ErrorRecovery => LifecycleState::Unconfigured,
206        },
207        TransitionResult::Failure => {
208            // Roll back to the source state (previous primary state)
209            // For error recovery, failure means we stay in ErrorProcessing
210            // since there's nowhere to roll back to
211            match transition {
212                LifecycleTransition::ErrorRecovery => LifecycleState::ErrorProcessing,
213                _ => state,
214            }
215        }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_lifecycle_state_properties() {
225        assert!(!LifecycleState::Unconfigured.is_terminal());
226        assert!(!LifecycleState::Inactive.is_terminal());
227        assert!(!LifecycleState::Active.is_terminal());
228        assert!(LifecycleState::Finalized.is_terminal());
229        assert!(!LifecycleState::ErrorProcessing.is_terminal());
230
231        assert!(LifecycleState::Unconfigured.is_primary());
232        assert!(LifecycleState::Inactive.is_primary());
233        assert!(LifecycleState::Active.is_primary());
234        assert!(LifecycleState::Finalized.is_primary());
235        assert!(!LifecycleState::ErrorProcessing.is_primary());
236    }
237
238    #[test]
239    fn test_state_from_u8() {
240        assert_eq!(
241            LifecycleState::from_u8(1),
242            Some(LifecycleState::Unconfigured)
243        );
244        assert_eq!(LifecycleState::from_u8(2), Some(LifecycleState::Inactive));
245        assert_eq!(LifecycleState::from_u8(3), Some(LifecycleState::Active));
246        assert_eq!(LifecycleState::from_u8(4), Some(LifecycleState::Finalized));
247        assert_eq!(
248            LifecycleState::from_u8(5),
249            Some(LifecycleState::ErrorProcessing)
250        );
251        assert_eq!(LifecycleState::from_u8(0), None);
252        assert_eq!(LifecycleState::from_u8(6), None);
253    }
254
255    #[test]
256    fn test_transition_from_u8() {
257        assert_eq!(
258            LifecycleTransition::from_u8(1),
259            Some(LifecycleTransition::Configure)
260        );
261        assert_eq!(
262            LifecycleTransition::from_u8(8),
263            Some(LifecycleTransition::ErrorRecovery)
264        );
265        assert_eq!(LifecycleTransition::from_u8(0), None);
266        assert_eq!(LifecycleTransition::from_u8(9), None);
267    }
268
269    #[test]
270    fn test_transition_result_from_u8() {
271        assert_eq!(
272            TransitionResult::from_u8(0),
273            Some(TransitionResult::Success)
274        );
275        assert_eq!(
276            TransitionResult::from_u8(1),
277            Some(TransitionResult::Failure)
278        );
279        assert_eq!(TransitionResult::from_u8(2), Some(TransitionResult::Error));
280        assert_eq!(TransitionResult::from_u8(3), None);
281    }
282
283    #[test]
284    fn test_valid_transitions() {
285        // From Unconfigured
286        assert!(can_transition(
287            LifecycleState::Unconfigured,
288            LifecycleTransition::Configure
289        ));
290        assert!(can_transition(
291            LifecycleState::Unconfigured,
292            LifecycleTransition::ShutdownUnconfigured
293        ));
294        assert!(!can_transition(
295            LifecycleState::Unconfigured,
296            LifecycleTransition::Activate
297        ));
298        assert!(!can_transition(
299            LifecycleState::Unconfigured,
300            LifecycleTransition::Deactivate
301        ));
302
303        // From Inactive
304        assert!(can_transition(
305            LifecycleState::Inactive,
306            LifecycleTransition::Activate
307        ));
308        assert!(can_transition(
309            LifecycleState::Inactive,
310            LifecycleTransition::Cleanup
311        ));
312        assert!(can_transition(
313            LifecycleState::Inactive,
314            LifecycleTransition::ShutdownInactive
315        ));
316        assert!(!can_transition(
317            LifecycleState::Inactive,
318            LifecycleTransition::Configure
319        ));
320        assert!(!can_transition(
321            LifecycleState::Inactive,
322            LifecycleTransition::Deactivate
323        ));
324
325        // From Active
326        assert!(can_transition(
327            LifecycleState::Active,
328            LifecycleTransition::Deactivate
329        ));
330        assert!(can_transition(
331            LifecycleState::Active,
332            LifecycleTransition::ShutdownActive
333        ));
334        assert!(!can_transition(
335            LifecycleState::Active,
336            LifecycleTransition::Activate
337        ));
338        assert!(!can_transition(
339            LifecycleState::Active,
340            LifecycleTransition::Configure
341        ));
342
343        // From Finalized (terminal — no transitions)
344        assert!(!can_transition(
345            LifecycleState::Finalized,
346            LifecycleTransition::Configure
347        ));
348        assert!(!can_transition(
349            LifecycleState::Finalized,
350            LifecycleTransition::ShutdownUnconfigured
351        ));
352
353        // From ErrorProcessing
354        assert!(can_transition(
355            LifecycleState::ErrorProcessing,
356            LifecycleTransition::ErrorRecovery
357        ));
358        assert!(!can_transition(
359            LifecycleState::ErrorProcessing,
360            LifecycleTransition::Configure
361        ));
362    }
363
364    #[test]
365    fn test_apply_transition_success() {
366        assert_eq!(
367            apply_transition(
368                LifecycleState::Unconfigured,
369                LifecycleTransition::Configure,
370                TransitionResult::Success
371            ),
372            LifecycleState::Inactive
373        );
374        assert_eq!(
375            apply_transition(
376                LifecycleState::Inactive,
377                LifecycleTransition::Activate,
378                TransitionResult::Success
379            ),
380            LifecycleState::Active
381        );
382        assert_eq!(
383            apply_transition(
384                LifecycleState::Active,
385                LifecycleTransition::Deactivate,
386                TransitionResult::Success
387            ),
388            LifecycleState::Inactive
389        );
390        assert_eq!(
391            apply_transition(
392                LifecycleState::Inactive,
393                LifecycleTransition::Cleanup,
394                TransitionResult::Success
395            ),
396            LifecycleState::Unconfigured
397        );
398        assert_eq!(
399            apply_transition(
400                LifecycleState::Unconfigured,
401                LifecycleTransition::ShutdownUnconfigured,
402                TransitionResult::Success
403            ),
404            LifecycleState::Finalized
405        );
406        assert_eq!(
407            apply_transition(
408                LifecycleState::Inactive,
409                LifecycleTransition::ShutdownInactive,
410                TransitionResult::Success
411            ),
412            LifecycleState::Finalized
413        );
414        assert_eq!(
415            apply_transition(
416                LifecycleState::Active,
417                LifecycleTransition::ShutdownActive,
418                TransitionResult::Success
419            ),
420            LifecycleState::Finalized
421        );
422        assert_eq!(
423            apply_transition(
424                LifecycleState::ErrorProcessing,
425                LifecycleTransition::ErrorRecovery,
426                TransitionResult::Success
427            ),
428            LifecycleState::Unconfigured
429        );
430    }
431
432    #[test]
433    fn test_apply_transition_failure_rolls_back() {
434        // Failure on configure -> stay Unconfigured
435        assert_eq!(
436            apply_transition(
437                LifecycleState::Unconfigured,
438                LifecycleTransition::Configure,
439                TransitionResult::Failure
440            ),
441            LifecycleState::Unconfigured
442        );
443        // Failure on activate -> stay Inactive
444        assert_eq!(
445            apply_transition(
446                LifecycleState::Inactive,
447                LifecycleTransition::Activate,
448                TransitionResult::Failure
449            ),
450            LifecycleState::Inactive
451        );
452        // Failure on deactivate -> stay Active
453        assert_eq!(
454            apply_transition(
455                LifecycleState::Active,
456                LifecycleTransition::Deactivate,
457                TransitionResult::Failure
458            ),
459            LifecycleState::Active
460        );
461        // Failure on error recovery -> stay ErrorProcessing
462        assert_eq!(
463            apply_transition(
464                LifecycleState::ErrorProcessing,
465                LifecycleTransition::ErrorRecovery,
466                TransitionResult::Failure
467            ),
468            LifecycleState::ErrorProcessing
469        );
470    }
471
472    #[test]
473    fn test_apply_transition_error_goes_to_error_processing() {
474        assert_eq!(
475            apply_transition(
476                LifecycleState::Unconfigured,
477                LifecycleTransition::Configure,
478                TransitionResult::Error
479            ),
480            LifecycleState::ErrorProcessing
481        );
482        assert_eq!(
483            apply_transition(
484                LifecycleState::Inactive,
485                LifecycleTransition::Activate,
486                TransitionResult::Error
487            ),
488            LifecycleState::ErrorProcessing
489        );
490        assert_eq!(
491            apply_transition(
492                LifecycleState::Active,
493                LifecycleTransition::ShutdownActive,
494                TransitionResult::Error
495            ),
496            LifecycleState::ErrorProcessing
497        );
498    }
499
500    #[test]
501    fn test_shutdown_shorthand_disambiguation() {
502        assert_eq!(
503            LifecycleTransition::from_shorthand(LifecycleState::Unconfigured, "shutdown"),
504            Some(LifecycleTransition::ShutdownUnconfigured)
505        );
506        assert_eq!(
507            LifecycleTransition::from_shorthand(LifecycleState::Inactive, "shutdown"),
508            Some(LifecycleTransition::ShutdownInactive)
509        );
510        assert_eq!(
511            LifecycleTransition::from_shorthand(LifecycleState::Active, "shutdown"),
512            Some(LifecycleTransition::ShutdownActive)
513        );
514        // Cannot shutdown from Finalized or ErrorProcessing
515        assert_eq!(
516            LifecycleTransition::from_shorthand(LifecycleState::Finalized, "shutdown"),
517            None
518        );
519        assert_eq!(
520            LifecycleTransition::from_shorthand(LifecycleState::ErrorProcessing, "shutdown"),
521            None
522        );
523    }
524
525    #[test]
526    fn test_shorthand_other_transitions() {
527        assert_eq!(
528            LifecycleTransition::from_shorthand(LifecycleState::Unconfigured, "configure"),
529            Some(LifecycleTransition::Configure)
530        );
531        assert_eq!(
532            LifecycleTransition::from_shorthand(LifecycleState::Active, "deactivate"),
533            Some(LifecycleTransition::Deactivate)
534        );
535        assert_eq!(
536            LifecycleTransition::from_shorthand(LifecycleState::Inactive, "cleanup"),
537            Some(LifecycleTransition::Cleanup)
538        );
539        assert_eq!(
540            LifecycleTransition::from_shorthand(LifecycleState::ErrorProcessing, "error_recovery"),
541            Some(LifecycleTransition::ErrorRecovery)
542        );
543        assert_eq!(
544            LifecycleTransition::from_shorthand(LifecycleState::Active, "unknown"),
545            None
546        );
547    }
548
549    #[test]
550    fn test_transition_source_state() {
551        assert_eq!(
552            LifecycleTransition::Configure.source_state(),
553            LifecycleState::Unconfigured
554        );
555        assert_eq!(
556            LifecycleTransition::Activate.source_state(),
557            LifecycleState::Inactive
558        );
559        assert_eq!(
560            LifecycleTransition::Deactivate.source_state(),
561            LifecycleState::Active
562        );
563        assert_eq!(
564            LifecycleTransition::Cleanup.source_state(),
565            LifecycleState::Inactive
566        );
567        assert_eq!(
568            LifecycleTransition::ShutdownUnconfigured.source_state(),
569            LifecycleState::Unconfigured
570        );
571        assert_eq!(
572            LifecycleTransition::ShutdownInactive.source_state(),
573            LifecycleState::Inactive
574        );
575        assert_eq!(
576            LifecycleTransition::ShutdownActive.source_state(),
577            LifecycleState::Active
578        );
579        assert_eq!(
580            LifecycleTransition::ErrorRecovery.source_state(),
581            LifecycleState::ErrorProcessing
582        );
583    }
584
585    #[test]
586    fn test_full_lifecycle_happy_path() {
587        let mut state = LifecycleState::Unconfigured;
588
589        // Configure
590        assert!(can_transition(state, LifecycleTransition::Configure));
591        state = apply_transition(
592            state,
593            LifecycleTransition::Configure,
594            TransitionResult::Success,
595        );
596        assert_eq!(state, LifecycleState::Inactive);
597
598        // Activate
599        assert!(can_transition(state, LifecycleTransition::Activate));
600        state = apply_transition(
601            state,
602            LifecycleTransition::Activate,
603            TransitionResult::Success,
604        );
605        assert_eq!(state, LifecycleState::Active);
606
607        // Deactivate
608        assert!(can_transition(state, LifecycleTransition::Deactivate));
609        state = apply_transition(
610            state,
611            LifecycleTransition::Deactivate,
612            TransitionResult::Success,
613        );
614        assert_eq!(state, LifecycleState::Inactive);
615
616        // Shutdown
617        assert!(can_transition(state, LifecycleTransition::ShutdownInactive));
618        state = apply_transition(
619            state,
620            LifecycleTransition::ShutdownInactive,
621            TransitionResult::Success,
622        );
623        assert_eq!(state, LifecycleState::Finalized);
624        assert!(state.is_terminal());
625    }
626
627    #[test]
628    fn test_error_recovery_path() {
629        let mut state = LifecycleState::Unconfigured;
630
631        // Configure with error
632        state = apply_transition(
633            state,
634            LifecycleTransition::Configure,
635            TransitionResult::Error,
636        );
637        assert_eq!(state, LifecycleState::ErrorProcessing);
638        assert!(!state.is_primary());
639
640        // Error recovery
641        assert!(can_transition(state, LifecycleTransition::ErrorRecovery));
642        state = apply_transition(
643            state,
644            LifecycleTransition::ErrorRecovery,
645            TransitionResult::Success,
646        );
647        assert_eq!(state, LifecycleState::Unconfigured);
648    }
649}