diff --git a/aircraft/aircraft_ws/src/mission/mission/mission_actions.py b/aircraft/aircraft_ws/src/mission/mission/mission_actions.py new file mode 100644 index 00000000..c75934e4 --- /dev/null +++ b/aircraft/aircraft_ws/src/mission/mission/mission_actions.py @@ -0,0 +1,17 @@ +"""Mission leaf action name allowlist (import-safe offline).""" + +KNOWN_ACTIONS = frozenset({ + "takeoff", + "land", + "orbit", + "wait", + "offboard", + "reposition", + "speed", + "check_blackboard", +}) + + +def is_known_mission_action(action) -> bool: + """Return True if action is a supported mission tree leaf name.""" + return isinstance(action, str) and action in KNOWN_ACTIONS diff --git a/aircraft/aircraft_ws/src/mission/mission/test_mission_actions.py b/aircraft/aircraft_ws/src/mission/mission/test_mission_actions.py new file mode 100644 index 00000000..9587f580 --- /dev/null +++ b/aircraft/aircraft_ws/src/mission/mission/test_mission_actions.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import unittest + +try: + from mission.mission_actions import KNOWN_ACTIONS, is_known_mission_action +except ImportError: # plain unittest from this directory + from mission_actions import KNOWN_ACTIONS, is_known_mission_action + + +class TestMissionActions(unittest.TestCase): + def test_known(self): + for name in ("takeoff", "land", "wait", "offboard"): + self.assertTrue(is_known_mission_action(name)) + + def test_unknown(self): + self.assertFalse(is_known_mission_action("fly_to_moon")) + self.assertFalse(is_known_mission_action("")) + self.assertFalse(is_known_mission_action(None)) + self.assertFalse(is_known_mission_action(1)) + + def test_frozen_set_size(self): + self.assertEqual(len(KNOWN_ACTIONS), 8) + + +if __name__ == "__main__": + unittest.main() diff --git a/aircraft/aircraft_ws/src/mission/mission/tree_builder.py b/aircraft/aircraft_ws/src/mission/mission/tree_builder.py index 1127d028..73feb556 100644 --- a/aircraft/aircraft_ws/src/mission/mission/tree_builder.py +++ b/aircraft/aircraft_ws/src/mission/mission/tree_builder.py @@ -1,6 +1,7 @@ import uuid import py_trees from mission import behaviors +from mission.mission_actions import is_known_mission_action def create_mission_tree(node_cfg, ros_node): # Recursively parse a YAML dictionary node into a py_trees object @@ -27,7 +28,11 @@ def create_mission_tree(node_cfg, ros_node): elif action == 'check_blackboard': return behaviors.CheckBlackboardBehavior(name, ros_node, params) else: - ros_node.get_logger().error(f"Unknown action: {action}") + # Keep KNOWN_ACTIONS in mission_actions.py in sync with branches above + if not is_known_mission_action(action): + ros_node.get_logger().error(f"Unknown action: {action}") + else: + ros_node.get_logger().error(f"Unhandled known action branching: {action}") return py_trees.behaviours.Failure(name=f"Unknown_{action}") # Is it a composite node (a sequence or fallback/selector branch)?