1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[repr(u8)]
16pub enum LifecycleState {
17 Unconfigured = 1,
19 Inactive = 2,
21 Active = 3,
23 Finalized = 4,
25 ErrorProcessing = 5,
27}
28
29impl LifecycleState {
30 pub const fn is_terminal(&self) -> bool {
32 matches!(self, Self::Finalized)
33 }
34
35 pub const fn is_primary(&self) -> bool {
40 !matches!(self, Self::ErrorProcessing)
41 }
42
43 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 pub const ALL: [Self; 5] = [
62 Self::Unconfigured,
63 Self::Inactive,
64 Self::Active,
65 Self::Finalized,
66 Self::ErrorProcessing,
67 ];
68
69 pub const fn label_cstr(&self) -> &'static core::ffi::CStr {
76 match self {
77 Self::Unconfigured => c"unconfigured",
78 Self::Inactive => c"inactive",
79 Self::Active => c"active",
80 Self::Finalized => c"finalized",
81 Self::ErrorProcessing => c"errorprocessing",
82 }
83 }
84
85 pub fn label(&self) -> &'static str {
92 self.label_cstr().to_str().unwrap_or_default()
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
128#[repr(u8)]
129pub enum LifecycleTransition {
130 Configure = 1,
133 Cleanup = 2,
136 Activate = 3,
139 Deactivate = 4,
142 ShutdownUnconfigured = 5,
145 ShutdownInactive = 6,
148 ShutdownActive = 7,
151 ErrorRecovery = 60,
155}
156
157impl LifecycleTransition {
158 pub fn from_shorthand(state: LifecycleState, name: &str) -> Option<Self> {
163 match name {
164 "configure" => Some(Self::Configure),
165 "activate" => Some(Self::Activate),
166 "deactivate" => Some(Self::Deactivate),
167 "cleanup" => Some(Self::Cleanup),
168 "shutdown" => match state {
169 LifecycleState::Unconfigured => Some(Self::ShutdownUnconfigured),
170 LifecycleState::Inactive => Some(Self::ShutdownInactive),
171 LifecycleState::Active => Some(Self::ShutdownActive),
172 _ => None,
173 },
174 "error_recovery" => Some(Self::ErrorRecovery),
175 _ => None,
176 }
177 }
178
179 pub const fn from_u8(value: u8) -> Option<Self> {
186 match value {
187 1 => Some(Self::Configure),
188 2 => Some(Self::Cleanup),
189 3 => Some(Self::Activate),
190 4 => Some(Self::Deactivate),
191 5 => Some(Self::ShutdownUnconfigured),
192 6 => Some(Self::ShutdownInactive),
193 7 => Some(Self::ShutdownActive),
194 60 => Some(Self::ErrorRecovery),
195 _ => None,
196 }
197 }
198
199 pub const fn source_state(&self) -> LifecycleState {
201 match self {
202 Self::Configure => LifecycleState::Unconfigured,
203 Self::Activate => LifecycleState::Inactive,
204 Self::Deactivate => LifecycleState::Active,
205 Self::Cleanup => LifecycleState::Inactive,
206 Self::ShutdownUnconfigured => LifecycleState::Unconfigured,
207 Self::ShutdownInactive => LifecycleState::Inactive,
208 Self::ShutdownActive => LifecycleState::Active,
209 Self::ErrorRecovery => LifecycleState::ErrorProcessing,
210 }
211 }
212
213 pub const ALL: [Self; 8] = [
222 Self::Configure,
223 Self::Cleanup,
224 Self::Activate,
225 Self::Deactivate,
226 Self::ShutdownUnconfigured,
227 Self::ShutdownInactive,
228 Self::ShutdownActive,
229 Self::ErrorRecovery,
230 ];
231
232 pub const fn label_cstr(&self) -> &'static core::ffi::CStr {
240 match self {
241 Self::Configure => c"configure",
242 Self::Cleanup => c"cleanup",
243 Self::Activate => c"activate",
244 Self::Deactivate => c"deactivate",
245 Self::ShutdownUnconfigured => c"shutdown",
246 Self::ShutdownInactive => c"shutdown",
247 Self::ShutdownActive => c"shutdown",
248 Self::ErrorRecovery => c"error_recovery",
249 }
250 }
251
252 pub fn label(&self) -> &'static str {
254 self.label_cstr().to_str().unwrap_or_default()
255 }
256
257 pub const fn goal_state(&self) -> LifecycleState {
265 apply_transition(self.source_state(), *self, TransitionResult::Success)
266 }
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
274#[repr(u8)]
275pub enum TransitionResult {
276 Success = 0,
278 Failure = 1,
280 Error = 2,
282}
283
284impl TransitionResult {
285 pub const fn from_u8(value: u8) -> Option<Self> {
287 match value {
288 0 => Some(Self::Success),
289 1 => Some(Self::Failure),
290 2 => Some(Self::Error),
291 _ => None,
292 }
293 }
294}
295
296pub const fn can_transition(state: LifecycleState, transition: LifecycleTransition) -> bool {
298 matches!(
299 (state, transition),
300 (LifecycleState::Unconfigured, LifecycleTransition::Configure)
301 | (
302 LifecycleState::Unconfigured,
303 LifecycleTransition::ShutdownUnconfigured
304 )
305 | (LifecycleState::Inactive, LifecycleTransition::Activate)
306 | (LifecycleState::Inactive, LifecycleTransition::Cleanup)
307 | (
308 LifecycleState::Inactive,
309 LifecycleTransition::ShutdownInactive
310 )
311 | (LifecycleState::Active, LifecycleTransition::Deactivate)
312 | (LifecycleState::Active, LifecycleTransition::ShutdownActive)
313 | (
314 LifecycleState::ErrorProcessing,
315 LifecycleTransition::ErrorRecovery
316 )
317 )
318}
319
320pub const fn apply_transition(
327 state: LifecycleState,
328 transition: LifecycleTransition,
329 result: TransitionResult,
330) -> LifecycleState {
331 match result {
332 TransitionResult::Error => LifecycleState::ErrorProcessing,
333 TransitionResult::Success => match transition {
334 LifecycleTransition::Configure => LifecycleState::Inactive,
335 LifecycleTransition::Activate => LifecycleState::Active,
336 LifecycleTransition::Deactivate => LifecycleState::Inactive,
337 LifecycleTransition::Cleanup => LifecycleState::Unconfigured,
338 LifecycleTransition::ShutdownUnconfigured
339 | LifecycleTransition::ShutdownInactive
340 | LifecycleTransition::ShutdownActive => LifecycleState::Finalized,
341 LifecycleTransition::ErrorRecovery => LifecycleState::Unconfigured,
342 },
343 TransitionResult::Failure => {
344 match transition {
348 LifecycleTransition::ErrorRecovery => LifecycleState::ErrorProcessing,
349 _ => state,
350 }
351 }
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn test_lifecycle_state_properties() {
361 assert!(!LifecycleState::Unconfigured.is_terminal());
362 assert!(!LifecycleState::Inactive.is_terminal());
363 assert!(!LifecycleState::Active.is_terminal());
364 assert!(LifecycleState::Finalized.is_terminal());
365 assert!(!LifecycleState::ErrorProcessing.is_terminal());
366
367 assert!(LifecycleState::Unconfigured.is_primary());
368 assert!(LifecycleState::Inactive.is_primary());
369 assert!(LifecycleState::Active.is_primary());
370 assert!(LifecycleState::Finalized.is_primary());
371 assert!(!LifecycleState::ErrorProcessing.is_primary());
372 }
373
374 #[test]
375 fn test_state_from_u8() {
376 assert_eq!(
377 LifecycleState::from_u8(1),
378 Some(LifecycleState::Unconfigured)
379 );
380 assert_eq!(LifecycleState::from_u8(2), Some(LifecycleState::Inactive));
381 assert_eq!(LifecycleState::from_u8(3), Some(LifecycleState::Active));
382 assert_eq!(LifecycleState::from_u8(4), Some(LifecycleState::Finalized));
383 assert_eq!(
384 LifecycleState::from_u8(5),
385 Some(LifecycleState::ErrorProcessing)
386 );
387 assert_eq!(LifecycleState::from_u8(0), None);
388 assert_eq!(LifecycleState::from_u8(6), None);
389 }
390
391 #[test]
403 fn transition_discriminants_are_lifecycle_msgs_ids() {
404 let table = [
406 (1u8, "TRANSITION_CONFIGURE", LifecycleTransition::Configure),
407 (2, "TRANSITION_CLEANUP", LifecycleTransition::Cleanup),
408 (3, "TRANSITION_ACTIVATE", LifecycleTransition::Activate),
409 (4, "TRANSITION_DEACTIVATE", LifecycleTransition::Deactivate),
410 (
411 5,
412 "TRANSITION_UNCONFIGURED_SHUTDOWN",
413 LifecycleTransition::ShutdownUnconfigured,
414 ),
415 (
416 6,
417 "TRANSITION_INACTIVE_SHUTDOWN",
418 LifecycleTransition::ShutdownInactive,
419 ),
420 (
421 7,
422 "TRANSITION_ACTIVE_SHUTDOWN",
423 LifecycleTransition::ShutdownActive,
424 ),
425 (
426 60,
427 "TRANSITION_ON_ERROR_SUCCESS",
428 LifecycleTransition::ErrorRecovery,
429 ),
430 ];
431 for (id, name, variant) in table {
432 assert_eq!(
433 variant as u8, id,
434 "{variant:?} must be lifecycle_msgs::{name} = {id}"
435 );
436 assert_eq!(
437 LifecycleTransition::from_u8(id),
438 Some(variant),
439 "lifecycle_msgs::{name} = {id} must decode to {variant:?}"
440 );
441 }
442 }
443
444 #[test]
447 fn unimplemented_upstream_transition_ids_are_rejected() {
448 assert_eq!(LifecycleTransition::from_u8(0), None);
452 assert_eq!(LifecycleTransition::from_u8(8), None);
453 assert_eq!(LifecycleTransition::from_u8(9), None);
455 }
456
457 #[test]
459 fn transition_ids_are_injective_and_round_trip() {
460 let all = [
461 LifecycleTransition::Configure,
462 LifecycleTransition::Cleanup,
463 LifecycleTransition::Activate,
464 LifecycleTransition::Deactivate,
465 LifecycleTransition::ShutdownUnconfigured,
466 LifecycleTransition::ShutdownInactive,
467 LifecycleTransition::ShutdownActive,
468 LifecycleTransition::ErrorRecovery,
469 ];
470 for (i, a) in all.iter().enumerate() {
471 assert_eq!(LifecycleTransition::from_u8(*a as u8), Some(*a));
472 for b in &all[i + 1..] {
473 assert_ne!(*a as u8, *b as u8, "{a:?} and {b:?} share an id");
474 }
475 }
476 }
477
478 #[test]
479 fn test_transition_result_from_u8() {
480 assert_eq!(
481 TransitionResult::from_u8(0),
482 Some(TransitionResult::Success)
483 );
484 assert_eq!(
485 TransitionResult::from_u8(1),
486 Some(TransitionResult::Failure)
487 );
488 assert_eq!(TransitionResult::from_u8(2), Some(TransitionResult::Error));
489 assert_eq!(TransitionResult::from_u8(3), None);
490 }
491
492 #[test]
493 fn test_valid_transitions() {
494 assert!(can_transition(
496 LifecycleState::Unconfigured,
497 LifecycleTransition::Configure
498 ));
499 assert!(can_transition(
500 LifecycleState::Unconfigured,
501 LifecycleTransition::ShutdownUnconfigured
502 ));
503 assert!(!can_transition(
504 LifecycleState::Unconfigured,
505 LifecycleTransition::Activate
506 ));
507 assert!(!can_transition(
508 LifecycleState::Unconfigured,
509 LifecycleTransition::Deactivate
510 ));
511
512 assert!(can_transition(
514 LifecycleState::Inactive,
515 LifecycleTransition::Activate
516 ));
517 assert!(can_transition(
518 LifecycleState::Inactive,
519 LifecycleTransition::Cleanup
520 ));
521 assert!(can_transition(
522 LifecycleState::Inactive,
523 LifecycleTransition::ShutdownInactive
524 ));
525 assert!(!can_transition(
526 LifecycleState::Inactive,
527 LifecycleTransition::Configure
528 ));
529 assert!(!can_transition(
530 LifecycleState::Inactive,
531 LifecycleTransition::Deactivate
532 ));
533
534 assert!(can_transition(
536 LifecycleState::Active,
537 LifecycleTransition::Deactivate
538 ));
539 assert!(can_transition(
540 LifecycleState::Active,
541 LifecycleTransition::ShutdownActive
542 ));
543 assert!(!can_transition(
544 LifecycleState::Active,
545 LifecycleTransition::Activate
546 ));
547 assert!(!can_transition(
548 LifecycleState::Active,
549 LifecycleTransition::Configure
550 ));
551
552 assert!(!can_transition(
554 LifecycleState::Finalized,
555 LifecycleTransition::Configure
556 ));
557 assert!(!can_transition(
558 LifecycleState::Finalized,
559 LifecycleTransition::ShutdownUnconfigured
560 ));
561
562 assert!(can_transition(
564 LifecycleState::ErrorProcessing,
565 LifecycleTransition::ErrorRecovery
566 ));
567 assert!(!can_transition(
568 LifecycleState::ErrorProcessing,
569 LifecycleTransition::Configure
570 ));
571 }
572
573 #[test]
574 fn test_apply_transition_success() {
575 assert_eq!(
576 apply_transition(
577 LifecycleState::Unconfigured,
578 LifecycleTransition::Configure,
579 TransitionResult::Success
580 ),
581 LifecycleState::Inactive
582 );
583 assert_eq!(
584 apply_transition(
585 LifecycleState::Inactive,
586 LifecycleTransition::Activate,
587 TransitionResult::Success
588 ),
589 LifecycleState::Active
590 );
591 assert_eq!(
592 apply_transition(
593 LifecycleState::Active,
594 LifecycleTransition::Deactivate,
595 TransitionResult::Success
596 ),
597 LifecycleState::Inactive
598 );
599 assert_eq!(
600 apply_transition(
601 LifecycleState::Inactive,
602 LifecycleTransition::Cleanup,
603 TransitionResult::Success
604 ),
605 LifecycleState::Unconfigured
606 );
607 assert_eq!(
608 apply_transition(
609 LifecycleState::Unconfigured,
610 LifecycleTransition::ShutdownUnconfigured,
611 TransitionResult::Success
612 ),
613 LifecycleState::Finalized
614 );
615 assert_eq!(
616 apply_transition(
617 LifecycleState::Inactive,
618 LifecycleTransition::ShutdownInactive,
619 TransitionResult::Success
620 ),
621 LifecycleState::Finalized
622 );
623 assert_eq!(
624 apply_transition(
625 LifecycleState::Active,
626 LifecycleTransition::ShutdownActive,
627 TransitionResult::Success
628 ),
629 LifecycleState::Finalized
630 );
631 assert_eq!(
632 apply_transition(
633 LifecycleState::ErrorProcessing,
634 LifecycleTransition::ErrorRecovery,
635 TransitionResult::Success
636 ),
637 LifecycleState::Unconfigured
638 );
639 }
640
641 #[test]
642 fn test_apply_transition_failure_rolls_back() {
643 assert_eq!(
645 apply_transition(
646 LifecycleState::Unconfigured,
647 LifecycleTransition::Configure,
648 TransitionResult::Failure
649 ),
650 LifecycleState::Unconfigured
651 );
652 assert_eq!(
654 apply_transition(
655 LifecycleState::Inactive,
656 LifecycleTransition::Activate,
657 TransitionResult::Failure
658 ),
659 LifecycleState::Inactive
660 );
661 assert_eq!(
663 apply_transition(
664 LifecycleState::Active,
665 LifecycleTransition::Deactivate,
666 TransitionResult::Failure
667 ),
668 LifecycleState::Active
669 );
670 assert_eq!(
672 apply_transition(
673 LifecycleState::ErrorProcessing,
674 LifecycleTransition::ErrorRecovery,
675 TransitionResult::Failure
676 ),
677 LifecycleState::ErrorProcessing
678 );
679 }
680
681 #[test]
682 fn test_apply_transition_error_goes_to_error_processing() {
683 assert_eq!(
684 apply_transition(
685 LifecycleState::Unconfigured,
686 LifecycleTransition::Configure,
687 TransitionResult::Error
688 ),
689 LifecycleState::ErrorProcessing
690 );
691 assert_eq!(
692 apply_transition(
693 LifecycleState::Inactive,
694 LifecycleTransition::Activate,
695 TransitionResult::Error
696 ),
697 LifecycleState::ErrorProcessing
698 );
699 assert_eq!(
700 apply_transition(
701 LifecycleState::Active,
702 LifecycleTransition::ShutdownActive,
703 TransitionResult::Error
704 ),
705 LifecycleState::ErrorProcessing
706 );
707 }
708
709 #[test]
710 fn test_shutdown_shorthand_disambiguation() {
711 assert_eq!(
712 LifecycleTransition::from_shorthand(LifecycleState::Unconfigured, "shutdown"),
713 Some(LifecycleTransition::ShutdownUnconfigured)
714 );
715 assert_eq!(
716 LifecycleTransition::from_shorthand(LifecycleState::Inactive, "shutdown"),
717 Some(LifecycleTransition::ShutdownInactive)
718 );
719 assert_eq!(
720 LifecycleTransition::from_shorthand(LifecycleState::Active, "shutdown"),
721 Some(LifecycleTransition::ShutdownActive)
722 );
723 assert_eq!(
725 LifecycleTransition::from_shorthand(LifecycleState::Finalized, "shutdown"),
726 None
727 );
728 assert_eq!(
729 LifecycleTransition::from_shorthand(LifecycleState::ErrorProcessing, "shutdown"),
730 None
731 );
732 }
733
734 #[test]
735 fn test_shorthand_other_transitions() {
736 assert_eq!(
737 LifecycleTransition::from_shorthand(LifecycleState::Unconfigured, "configure"),
738 Some(LifecycleTransition::Configure)
739 );
740 assert_eq!(
741 LifecycleTransition::from_shorthand(LifecycleState::Active, "deactivate"),
742 Some(LifecycleTransition::Deactivate)
743 );
744 assert_eq!(
745 LifecycleTransition::from_shorthand(LifecycleState::Inactive, "cleanup"),
746 Some(LifecycleTransition::Cleanup)
747 );
748 assert_eq!(
749 LifecycleTransition::from_shorthand(LifecycleState::ErrorProcessing, "error_recovery"),
750 Some(LifecycleTransition::ErrorRecovery)
751 );
752 assert_eq!(
753 LifecycleTransition::from_shorthand(LifecycleState::Active, "unknown"),
754 None
755 );
756 }
757
758 #[test]
759 fn test_transition_source_state() {
760 assert_eq!(
761 LifecycleTransition::Configure.source_state(),
762 LifecycleState::Unconfigured
763 );
764 assert_eq!(
765 LifecycleTransition::Activate.source_state(),
766 LifecycleState::Inactive
767 );
768 assert_eq!(
769 LifecycleTransition::Deactivate.source_state(),
770 LifecycleState::Active
771 );
772 assert_eq!(
773 LifecycleTransition::Cleanup.source_state(),
774 LifecycleState::Inactive
775 );
776 assert_eq!(
777 LifecycleTransition::ShutdownUnconfigured.source_state(),
778 LifecycleState::Unconfigured
779 );
780 assert_eq!(
781 LifecycleTransition::ShutdownInactive.source_state(),
782 LifecycleState::Inactive
783 );
784 assert_eq!(
785 LifecycleTransition::ShutdownActive.source_state(),
786 LifecycleState::Active
787 );
788 assert_eq!(
789 LifecycleTransition::ErrorRecovery.source_state(),
790 LifecycleState::ErrorProcessing
791 );
792 }
793
794 #[test]
795 fn test_full_lifecycle_happy_path() {
796 let mut state = LifecycleState::Unconfigured;
797
798 assert!(can_transition(state, LifecycleTransition::Configure));
800 state = apply_transition(
801 state,
802 LifecycleTransition::Configure,
803 TransitionResult::Success,
804 );
805 assert_eq!(state, LifecycleState::Inactive);
806
807 assert!(can_transition(state, LifecycleTransition::Activate));
809 state = apply_transition(
810 state,
811 LifecycleTransition::Activate,
812 TransitionResult::Success,
813 );
814 assert_eq!(state, LifecycleState::Active);
815
816 assert!(can_transition(state, LifecycleTransition::Deactivate));
818 state = apply_transition(
819 state,
820 LifecycleTransition::Deactivate,
821 TransitionResult::Success,
822 );
823 assert_eq!(state, LifecycleState::Inactive);
824
825 assert!(can_transition(state, LifecycleTransition::ShutdownInactive));
827 state = apply_transition(
828 state,
829 LifecycleTransition::ShutdownInactive,
830 TransitionResult::Success,
831 );
832 assert_eq!(state, LifecycleState::Finalized);
833 assert!(state.is_terminal());
834 }
835
836 #[test]
837 fn test_error_recovery_path() {
838 let mut state = LifecycleState::Unconfigured;
839
840 state = apply_transition(
842 state,
843 LifecycleTransition::Configure,
844 TransitionResult::Error,
845 );
846 assert_eq!(state, LifecycleState::ErrorProcessing);
847 assert!(!state.is_primary());
848
849 assert!(can_transition(state, LifecycleTransition::ErrorRecovery));
851 state = apply_transition(
852 state,
853 LifecycleTransition::ErrorRecovery,
854 TransitionResult::Success,
855 );
856 assert_eq!(state, LifecycleState::Unconfigured);
857 }
858
859 #[test]
866 fn every_label_is_valid_utf8() {
867 for t in LifecycleTransition::ALL {
868 assert!(!t.label().is_empty(), "transition {t:?} has no label");
869 assert_eq!(t.label().as_bytes(), t.label_cstr().to_bytes());
870 }
871 for s in LifecycleState::ALL {
872 assert!(!s.label().is_empty(), "state {s:?} has no label");
873 assert_eq!(s.label().as_bytes(), s.label_cstr().to_bytes());
874 }
875 }
876
877 #[test]
881 fn all_is_complete_and_ordered() {
882 let ids: heapless::Vec<u8, 8> = LifecycleTransition::ALL
883 .iter()
884 .map(|t| *t as u8)
885 .collect::<heapless::Vec<u8, 8>>();
886 assert_eq!(ids.as_slice(), &[1, 2, 3, 4, 5, 6, 7, 60]);
887 for id in ids.iter() {
888 assert!(LifecycleTransition::from_u8(*id).is_some());
889 }
890 let state_ids: heapless::Vec<u8, 5> = LifecycleState::ALL
891 .iter()
892 .map(|s| *s as u8)
893 .collect::<heapless::Vec<u8, 5>>();
894 assert_eq!(state_ids.as_slice(), &[1, 2, 3, 4, 5]);
895 }
896
897 #[test]
901 fn goal_state_is_the_success_destination() {
902 use LifecycleState as S;
903 use LifecycleTransition as T;
904 assert_eq!(T::Configure.goal_state(), S::Inactive);
905 assert_eq!(T::Cleanup.goal_state(), S::Unconfigured);
906 assert_eq!(T::Activate.goal_state(), S::Active);
907 assert_eq!(T::Deactivate.goal_state(), S::Inactive);
908 assert_eq!(T::ErrorRecovery.goal_state(), S::Unconfigured);
909 for t in [
910 T::ShutdownUnconfigured,
911 T::ShutdownInactive,
912 T::ShutdownActive,
913 ] {
914 assert_eq!(t.goal_state(), S::Finalized);
915 }
916 assert_eq!(T::ShutdownUnconfigured.source_state(), S::Unconfigured);
917 assert_eq!(T::ShutdownInactive.source_state(), S::Inactive);
918 assert_eq!(T::ShutdownActive.source_state(), S::Active);
919 }
920}