From c8ff71cdf9bd409c3aceb59567c877251a15d71d Mon Sep 17 00:00:00 2001 From: Thomas Sundvoll <35451859+tsundvoll@users.noreply.github.com> Date: Tue, 10 Dec 2024 22:18:43 +0100 Subject: [PATCH] Give ISAR responsibility to create ids for mission, task and inspection. Therefore the ids are removed from the StartMissionDefinitions and instead generated when the Mission and Tasks are created Some refactors are also applied, most notably replacing dataclass with pydantic BaseModels --- src/isar/apis/models/models.py | 1 + .../apis/models/start_mission_definition.py | 196 +++++++----------- .../apis/schedule/scheduling_controller.py | 36 +++- .../config/predefined_missions/default.json | 23 +- .../default_turtlebot.json | 33 +-- src/isar/mission_planner/local_planner.py | 26 +-- .../mission_planner_interface.py | 2 +- src/isar/script.py | 2 +- src/isar/services/readers/base_reader.py | 37 ---- src/isar/state_machine/state_machine.py | 4 +- src/isar/state_machine/states/monitor.py | 14 +- src/isar/storage/slimm_storage.py | 5 +- src/isar/storage/utilities.py | 7 +- .../models/inspection/inspection.py | 15 +- src/robot_interface/models/mission/mission.py | 26 +-- src/robot_interface/models/mission/task.py | 66 +++--- src/robot_interface/robot_interface.py | 8 +- .../turtlebot/test_successful_mission.py | 5 +- .../isar/apis/models}/__init__.py | 0 .../models/example_mission_definition.json | 0 .../models/test_start_mission_definition.py | 81 ++++++++ .../apis/scheduler/test_scheduler_router.py | 60 ------ tests/isar/mission/test_mission.py | 12 +- .../models/test_start_mission_definition.py | 63 ------ .../isar/services/readers/test_base_reader.py | 23 -- .../services/readers/test_mission_reader.py | 40 ++-- .../utilities/test_queue_utilities.py | 8 +- .../isar/state_machine/states/test_monitor.py | 3 +- .../isar/state_machine/test_state_machine.py | 14 +- tests/isar/storage/test_uploader.py | 15 +- tests/mocks/mission_definition.py | 39 +--- tests/mocks/robot_interface.py | 14 +- tests/mocks/task.py | 8 +- tests/test_data/test_mission_working.json | 29 +-- .../test_mission_working_no_tasks.json | 1 + .../test_data/test_thermal_image_mission.json | 9 +- 36 files changed, 371 insertions(+), 554 deletions(-) delete mode 100644 src/isar/services/readers/base_reader.py rename {src/isar/services/readers => tests/isar/apis/models}/__init__.py (100%) rename tests/isar/{ => apis}/models/example_mission_definition.json (100%) create mode 100644 tests/isar/apis/models/test_start_mission_definition.py delete mode 100644 tests/isar/models/test_start_mission_definition.py delete mode 100644 tests/isar/services/readers/test_base_reader.py diff --git a/src/isar/apis/models/models.py b/src/isar/apis/models/models.py index 48289016..f1a89a61 100644 --- a/src/isar/apis/models/models.py +++ b/src/isar/apis/models/models.py @@ -9,6 +9,7 @@ class TaskResponse(BaseModel): id: str tag_id: Optional[str] = None + inspection_id: Optional[str] = None type: TaskTypes diff --git a/src/isar/apis/models/start_mission_definition.py b/src/isar/apis/models/start_mission_definition.py index 67d95b6b..bc572593 100644 --- a/src/isar/apis/models/start_mission_definition.py +++ b/src/isar/apis/models/start_mission_definition.py @@ -2,7 +2,6 @@ from enum import Enum from typing import List, Optional -from alitra import Frame, Orientation, Pose, Position from pydantic import BaseModel, Field from isar.apis.models.models import InputPose, InputPosition @@ -44,7 +43,6 @@ class StartMissionInspectionDefinition(BaseModel): analysis_type: Optional[str] = None duration: Optional[float] = None metadata: Optional[dict] = None - id: Optional[str] = None class StartMissionTaskDefinition(BaseModel): @@ -52,180 +50,132 @@ class StartMissionTaskDefinition(BaseModel): pose: InputPose inspection: Optional[StartMissionInspectionDefinition] = None tag: Optional[str] = None - id: Optional[str] = None zoom: Optional[ZoomDescription] = None class StartMissionDefinition(BaseModel): tasks: List[StartMissionTaskDefinition] - id: Optional[str] = None name: Optional[str] = None start_pose: Optional[InputPose] = None - dock: Optional[bool] = None - undock: Optional[bool] = None + dock: Optional[bool] = Field(default=False) + undock: Optional[bool] = Field(default=False) -def to_isar_mission(start_mission_definition: StartMissionDefinition) -> Mission: +def to_isar_mission( + start_mission_definition: StartMissionDefinition, + return_pose: Optional[InputPose] = None, +) -> Mission: isar_tasks: List[TASKS] = [] - for start_mission_task_definition in start_mission_definition.tasks: - task: TASKS = create_isar_task(start_mission_task_definition) - if start_mission_task_definition.id: - task.id = start_mission_task_definition.id + for task_definition in start_mission_definition.tasks: + task: TASKS = to_isar_task(task_definition) isar_tasks.append(task) + if return_pose: + isar_tasks.append(ReturnToHome(pose=return_pose.to_alitra_pose())) + if not isar_tasks: raise MissionPlannerError("Mission does not contain any valid tasks") - check_for_duplicate_ids(isar_tasks) - - isar_mission: Mission = Mission(tasks=isar_tasks) - - isar_mission.dock = start_mission_definition.dock - isar_mission.undock = start_mission_definition.undock - + isar_mission_name: str if start_mission_definition.name: - isar_mission.name = start_mission_definition.name + isar_mission_name = start_mission_definition.name else: - isar_mission.name = _build_mission_name() - - if start_mission_definition.id: - isar_mission.id = start_mission_definition.id + isar_mission_name = _build_mission_name() + start_pose = None if start_mission_definition.start_pose: - input_pose: InputPose = start_mission_definition.start_pose - input_frame: Frame = Frame(name=input_pose.frame_name) - input_position: Position = Position( - input_pose.position.x, - input_pose.position.y, - input_pose.position.z, - input_frame, - ) - input_orientation: Orientation = Orientation( - input_pose.orientation.x, - input_pose.orientation.y, - input_pose.orientation.z, - input_pose.orientation.w, - input_frame, - ) - isar_mission.start_pose = Pose( - position=input_position, orientation=input_orientation, frame=input_frame - ) - - return isar_mission - - -def check_for_duplicate_ids(items: List[TASKS]): - duplicate_ids = get_duplicate_ids(items=items) - if len(duplicate_ids) > 0: - raise MissionPlannerError( - f"Failed to create as there were duplicate IDs which is not allowed " - f"({duplicate_ids})" - ) - + start_pose = start_mission_definition.start_pose.to_alitra_pose() + + return Mission( + tasks=isar_tasks, + name=isar_mission_name, + start_pose=start_pose, + dock=start_mission_definition.dock, + undock=start_mission_definition.undock, + ) -def create_isar_task(start_mission_task_definition) -> TASKS: - if start_mission_task_definition.type == TaskType.Inspection: - return create_inspection_task(start_mission_task_definition) - elif start_mission_task_definition.type == TaskType.Localization: - return create_localization_task(start_mission_task_definition) - elif start_mission_task_definition.type == TaskType.ReturnToHome: - return create_return_to_home_task(start_mission_task_definition) - elif start_mission_task_definition.type == TaskType.Dock: +def to_isar_task(task_definition: StartMissionTaskDefinition) -> TASKS: + if task_definition.type == TaskType.Inspection: + return to_inspection_task(task_definition) + elif task_definition.type == TaskType.Localization: + return to_localization_task(task_definition) + elif task_definition.type == TaskType.ReturnToHome: + return create_return_to_home_task(task_definition) + elif task_definition.type == TaskType.Dock: return create_dock_task() else: raise MissionPlannerError( - f"Failed to create task: '{start_mission_task_definition.type}' is not a valid" + f"Failed to create task: '{task_definition.type}' is not a valid" ) -def create_inspection_task( - start_mission_task_definition: StartMissionTaskDefinition, -) -> TASKS: +def to_inspection_task(task_definition: StartMissionTaskDefinition) -> TASKS: + inspection_definition = task_definition.inspection - if start_mission_task_definition.inspection.type == InspectionTypes.image: + if inspection_definition.type == InspectionTypes.image: return TakeImage( - target=start_mission_task_definition.inspection.inspection_target.to_alitra_position(), - tag_id=start_mission_task_definition.tag, - robot_pose=start_mission_task_definition.pose.to_alitra_pose(), - metadata=start_mission_task_definition.inspection.metadata, - zoom=start_mission_task_definition.zoom, + robot_pose=task_definition.pose.to_alitra_pose(), + tag_id=task_definition.tag, + target=task_definition.inspection.inspection_target.to_alitra_position(), + metadata=task_definition.inspection.metadata, + zoom=task_definition.zoom, ) - elif start_mission_task_definition.inspection.type == InspectionTypes.video: + elif inspection_definition.type == InspectionTypes.video: return TakeVideo( - target=start_mission_task_definition.inspection.inspection_target.to_alitra_position(), - duration=start_mission_task_definition.inspection.duration, - tag_id=start_mission_task_definition.tag, - robot_pose=start_mission_task_definition.pose.to_alitra_pose(), - metadata=start_mission_task_definition.inspection.metadata, - zoom=start_mission_task_definition.zoom, + robot_pose=task_definition.pose.to_alitra_pose(), + tag_id=task_definition.tag, + target=task_definition.inspection.inspection_target.to_alitra_position(), + duration=inspection_definition.duration, + metadata=task_definition.inspection.metadata, + zoom=task_definition.zoom, ) - - elif start_mission_task_definition.inspection.type == InspectionTypes.thermal_image: + elif inspection_definition.type == InspectionTypes.thermal_image: return TakeThermalImage( - target=start_mission_task_definition.inspection.inspection_target.to_alitra_position(), - tag_id=start_mission_task_definition.tag, - robot_pose=start_mission_task_definition.pose.to_alitra_pose(), - metadata=start_mission_task_definition.inspection.metadata, - zoom=start_mission_task_definition.zoom, + robot_pose=task_definition.pose.to_alitra_pose(), + tag_id=task_definition.tag, + target=task_definition.inspection.inspection_target.to_alitra_position(), + metadata=task_definition.inspection.metadata, + zoom=task_definition.zoom, ) - - elif start_mission_task_definition.inspection.type == InspectionTypes.thermal_video: + elif inspection_definition.type == InspectionTypes.thermal_video: return TakeThermalVideo( - target=start_mission_task_definition.inspection.inspection_target.to_alitra_position(), - duration=start_mission_task_definition.inspection.duration, - tag_id=start_mission_task_definition.tag, - robot_pose=start_mission_task_definition.pose.to_alitra_pose(), - metadata=start_mission_task_definition.inspection.metadata, - zoom=start_mission_task_definition.zoom, + robot_pose=task_definition.pose.to_alitra_pose(), + tag_id=task_definition.tag, + target=task_definition.inspection.inspection_target.to_alitra_position(), + duration=inspection_definition.duration, + metadata=task_definition.inspection.metadata, + zoom=task_definition.zoom, ) - - elif start_mission_task_definition.inspection.type == InspectionTypes.audio: + elif inspection_definition.type == InspectionTypes.audio: return RecordAudio( - target=start_mission_task_definition.inspection.inspection_target.to_alitra_position(), - duration=start_mission_task_definition.inspection.duration, - tag_id=start_mission_task_definition.tag, - robot_pose=start_mission_task_definition.pose.to_alitra_pose(), - metadata=start_mission_task_definition.inspection.metadata, - zoom=start_mission_task_definition.zoom, + robot_pose=task_definition.pose.to_alitra_pose(), + tag_id=task_definition.tag, + target=task_definition.inspection.inspection_target.to_alitra_position(), + duration=inspection_definition.duration, + metadata=task_definition.inspection.metadata, + zoom=task_definition.zoom, ) else: raise ValueError( - f"Inspection type '{start_mission_task_definition.inspection.type}' not supported" + f"Inspection type '{inspection_definition.type}' not supported" ) -def create_localization_task( - start_mission_task_definition: StartMissionTaskDefinition, -) -> Localize: - return Localize( - localization_pose=start_mission_task_definition.pose.to_alitra_pose() - ) +def to_localization_task(task_definition: StartMissionTaskDefinition) -> Localize: + return Localize(localization_pose=task_definition.pose.to_alitra_pose()) def create_return_to_home_task( - start_mission_task_definition: StartMissionTaskDefinition, + task_definition: StartMissionTaskDefinition, ) -> ReturnToHome: - return ReturnToHome(pose=start_mission_task_definition.pose.to_alitra_pose()) + return ReturnToHome(pose=task_definition.pose.to_alitra_pose()) def create_dock_task() -> DockingProcedure: return DockingProcedure(behavior="dock") -def get_duplicate_ids(items: List[TASKS]) -> List[str]: - unique_ids: List[str] = [] - duplicate_ids: List[str] = [] - for item in items: - id: str = item.id - if id not in unique_ids: - unique_ids.append(id) - else: - duplicate_ids.append(id) - - return duplicate_ids - - def _build_mission_name() -> str: return f"{settings.PLANT_SHORT_NAME}{settings.ROBOT_NAME}{int(time.time())}" diff --git a/src/isar/apis/schedule/scheduling_controller.py b/src/isar/apis/schedule/scheduling_controller.py index 97d88e48..b950bbfc 100644 --- a/src/isar/apis/schedule/scheduling_controller.py +++ b/src/isar/apis/schedule/scheduling_controller.py @@ -21,7 +21,13 @@ from isar.services.utilities.scheduling_utilities import SchedulingUtilities from isar.state_machine.states_enum import States from robot_interface.models.mission.mission import Mission -from robot_interface.models.mission.task import TASKS, Localize, MoveArm, ReturnToHome +from robot_interface.models.mission.task import ( + TASKS, + InspectionTask, + Localize, + MoveArm, + ReturnToHome, +) class SchedulingController: @@ -115,7 +121,9 @@ def start_mission( self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state) try: - mission: Mission = to_isar_mission(mission_definition) + mission: Mission = to_isar_mission( + start_mission_definition=mission_definition, return_pose=return_pose + ) except MissionPlannerError as e: error_message = f"Bad Request - Cannot create ISAR mission: {e}" self.logger.warning(error_message) @@ -127,9 +135,6 @@ def start_mission( self.scheduling_utilities.verify_robot_capable_of_mission( mission=mission, robot_capabilities=robot_settings.CAPABILITIES ) - if return_pose: - pose: Pose = return_pose.to_alitra_pose() - mission.tasks.append(ReturnToHome(pose=pose)) initial_pose_alitra: Optional[Pose] = ( initial_pose.to_alitra_pose() if initial_pose else None @@ -213,7 +218,9 @@ def drive_to( self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state) pose: Pose = target_pose.to_alitra_pose() - mission: Mission = Mission(tasks=[ReturnToHome(pose=pose)]) + mission: Mission = Mission( + name="Drive to pose", tasks=[ReturnToHome(pose=pose)] + ) self.logger.info( f"Starting drive to mission with ISAR Mission ID: '{mission.id}'" @@ -237,7 +244,9 @@ def start_localization_mission( self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state) pose: Pose = localization_pose.to_alitra_pose() - mission: Mission = Mission(tasks=[Localize(localization_pose=pose)]) + mission: Mission = Mission( + name="Localization mission", tasks=[Localize(localization_pose=pose)] + ) self.logger.info( f"Starting localization mission with ISAR Mission ID: '{mission.id}'" @@ -284,7 +293,9 @@ def start_move_arm_mission( self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state) - mission: Mission = Mission(tasks=[MoveArm(arm_pose=arm_pose_literal)]) + mission: Mission = Mission( + name="Move arm mission", tasks=[MoveArm(arm_pose=arm_pose_literal)] + ) self.logger.info( f"Starting move arm mission with ISAR Mission ID: '{mission.id}'" @@ -302,4 +313,11 @@ def _api_response(self, mission: Mission) -> StartMissionResponse: ) def _task_api_response(self, task: TASKS) -> TaskResponse: - return TaskResponse(id=task.id, tag_id=task.tag_id, type=task.type) + if isinstance(task, InspectionTask): + inspection_id = task.inspection_id + else: + inspection_id = None + + return TaskResponse( + id=task.id, tag_id=task.tag_id, inspection_id=inspection_id, type=task.type + ) diff --git a/src/isar/config/predefined_missions/default.json b/src/isar/config/predefined_missions/default.json index fd1dd033..76672314 100644 --- a/src/isar/config/predefined_missions/default.json +++ b/src/isar/config/predefined_missions/default.json @@ -1,5 +1,6 @@ { "id": "1", + "name": "Default mission", "tasks": [ { "type": "take_image", @@ -8,22 +9,22 @@ "x": -2, "y": -2, "z": 0, - "frame": "asset" + "frame": {"name": "asset"} }, "orientation": { "x": 0, "y": 0, "z": 0.4794255, "w": 0.8775826, - "frame": "asset" + "frame": {"name": "asset"} }, - "frame": "asset" + "frame": {"name": "asset"} }, "target": { "x": 2, "y": 2, "z": 0, - "frame": "robot" + "frame": {"name": "asset"} } }, { @@ -33,22 +34,22 @@ "x": -2, "y": 2, "z": 0, - "frame": "asset" + "frame": {"name": "asset"} }, "orientation": { "x": 0, "y": 0, "z": 0.4794255, "w": 0.8775826, - "frame": "asset" + "frame": {"name": "asset"} }, - "frame": "asset" + "frame": {"name": "asset"} }, "target": { "x": 2, "y": 2, "z": 0, - "frame": "robot" + "frame": {"name": "asset"} } }, { @@ -58,16 +59,16 @@ "x": 2, "y": 2, "z": 0, - "frame": "asset" + "frame": {"name": "asset"} }, "orientation": { "x": 0, "y": 0, "z": 0.4794255, "w": 0.8775826, - "frame": "asset" + "frame": {"name": "asset"} }, - "frame": "asset" + "frame": {"name": "asset"} } } ] diff --git a/src/isar/config/predefined_missions/default_turtlebot.json b/src/isar/config/predefined_missions/default_turtlebot.json index 080d8f98..c2005784 100644 --- a/src/isar/config/predefined_missions/default_turtlebot.json +++ b/src/isar/config/predefined_missions/default_turtlebot.json @@ -1,5 +1,6 @@ { "id": "2", + "name": "Default mission Turtlebot", "tasks": [ { "type": "take_image", @@ -8,22 +9,22 @@ "x": -3.6, "y": 4, "z": 0, - "frame": "asset" + "frame": {"name": "asset"} }, "orientation": { "x": 0, "y": 0, "z": -0.7286672256879113, "w": -0.6848660759820616, - "frame": "asset" + "frame": {"name": "asset"} }, - "frame": "asset" + "frame": {"name": "asset"} }, "target": { "x": -4.7, "y": 4.9, "z": 0, - "frame": "robot" + "frame": {"name": "asset"} } }, @@ -34,22 +35,22 @@ "x": 4.7, "y": 3, "z": 0, - "frame": "asset" + "frame": {"name": "asset"} }, "orientation": { "x": 0, "y": 0, "z": 0.5769585, "w": 0.8167734, - "frame": "asset" + "frame": {"name": "asset"} }, - "frame": "asset" + "frame": {"name": "asset"} }, "target": { "x": 5.6, "y": 5.2, "z": 0, - "frame": "robot" + "frame": {"name": "asset"} } }, { @@ -59,22 +60,22 @@ "x": 4.7, "y": 3, "z": 0, - "frame": "asset" + "frame": {"name": "asset"} }, "orientation": { "x": 0, "y": 0, "z": 0.5769585, "w": 0.8167734, - "frame": "asset" + "frame": {"name": "asset"} }, - "frame": "asset" + "frame": {"name": "asset"} }, "target": { "x": 3.1, "y": 5.2, "z": 0, - "frame": "robot" + "frame": {"name": "asset"} } }, { @@ -84,22 +85,22 @@ "x": 0.95, "y": 2.6, "z": 0, - "frame": "asset" + "frame": {"name": "asset"} }, "orientation": { "x": 0, "y": 0, "z": -0.6992469, "w": 0.7148802, - "frame": "asset" + "frame": {"name": "asset"} }, - "frame": "asset" + "frame": {"name": "asset"} }, "target": { "x": 1.9, "y": 1.9, "z": 0, - "frame": "robot" + "frame": {"name": "asset"} } } ] diff --git a/src/isar/mission_planner/local_planner.py b/src/isar/mission_planner/local_planner.py index 7af96df0..dd4c119e 100644 --- a/src/isar/mission_planner/local_planner.py +++ b/src/isar/mission_planner/local_planner.py @@ -1,8 +1,7 @@ +import json import logging from pathlib import Path -from typing import List, Optional -from alitra import Frame from injector import inject from isar.config.settings import settings @@ -11,10 +10,8 @@ MissionPlannerError, MissionPlannerInterface, ) -from isar.services.readers.base_reader import BaseReader, BaseReaderError from robot_interface.models.mission.mission import Mission - logger = logging.getLogger("api") @@ -39,16 +36,10 @@ def get_mission(self, mission_id) -> Mission: @staticmethod def read_mission_from_file(mission_path: Path) -> Mission: - mission_dict: dict = BaseReader.read_json(location=mission_path) - - mission: Mission = BaseReader.dict_to_dataclass( - dataclass_dict=mission_dict, - target_dataclass=Mission, - cast_config=[Frame], - strict_config=True, - ) + with open(mission_path) as json_file: + mission_dict = json.load(json_file) - return mission + return Mission(**mission_dict) def get_predefined_missions(self) -> dict: missions: dict = {} @@ -57,13 +48,8 @@ def get_predefined_missions(self) -> dict: for file in json_files: mission_name = file.stem path_to_file = self.predefined_mission_folder.joinpath(file.name) - try: - mission: Mission = self.read_mission_from_file(path_to_file) - except BaseReaderError as e: - logger.warning( - f"Failed to read predefined mission {path_to_file} \n {e}" - ) - continue + + mission: Mission = self.read_mission_from_file(path_to_file) if mission.id in invalid_mission_ids: logger.warning( f"Duplicate mission id {mission.id} : {path_to_file.as_posix()}" diff --git a/src/isar/mission_planner/mission_planner_interface.py b/src/isar/mission_planner/mission_planner_interface.py index 76ccebc4..bcd0327e 100644 --- a/src/isar/mission_planner/mission_planner_interface.py +++ b/src/isar/mission_planner/mission_planner_interface.py @@ -9,7 +9,7 @@ def get_mission(self, mission_id: str) -> Mission: """ Parameters ---------- - mission_id : int + mission_id : str Returns ------- diff --git a/src/isar/script.py b/src/isar/script.py index 534d4502..a1e2f85a 100644 --- a/src/isar/script.py +++ b/src/isar/script.py @@ -82,7 +82,7 @@ def print_setting(setting: str = "", value: Any = "", fillchar: str = " "): print() -def start(): +def start() -> None: injector: Injector = get_injector() keyvault_client = injector.get(Keyvault) diff --git a/src/isar/services/readers/base_reader.py b/src/isar/services/readers/base_reader.py deleted file mode 100644 index 92a4b495..00000000 --- a/src/isar/services/readers/base_reader.py +++ /dev/null @@ -1,37 +0,0 @@ -import json -import logging -from dataclasses import is_dataclass -from logging import Logger -from pathlib import Path -from typing import Any, Optional - -from dacite import Config, from_dict - -logger: Logger = logging.getLogger("api") - - -class BaseReader: - @staticmethod - def read_json(location: Path) -> dict: - with open(location) as json_file: - return json.load(json_file) - - @staticmethod - def dict_to_dataclass( - dataclass_dict: dict, - target_dataclass: Any, - cast_config: list = [], - strict_config: bool = False, - ) -> Optional[Any]: - if not is_dataclass(target_dataclass): - raise BaseReaderError("{target_dataclass} is not a dataclass") - generated_dataclass = from_dict( - data_class=target_dataclass, - data=dataclass_dict, - config=Config(cast=cast_config, strict=strict_config), - ) - return generated_dataclass - - -class BaseReaderError(Exception): - pass diff --git a/src/isar/state_machine/state_machine.py b/src/isar/state_machine/state_machine.py index 210aacd3..50106d32 100644 --- a/src/isar/state_machine/state_machine.py +++ b/src/isar/state_machine/state_machine.py @@ -412,7 +412,7 @@ def begin(self): Transitions into idle state. """ - self.to_idle() + self.to_idle() # type: ignore def iterate_current_task(self): if self.current_task.is_finished(): @@ -426,7 +426,7 @@ def iterate_current_task(self): def update_state(self): """Updates the current state of the state machine.""" - self.current_state = States(self.state) + self.current_state = States(self.state) # type: ignore self.send_state_status() self._log_state_transition(self.current_state) self.logger.info(f"State: {self.current_state}") diff --git a/src/isar/state_machine/states/monitor.py b/src/isar/state_machine/states/monitor.py index aa431a67..99da316f 100644 --- a/src/isar/state_machine/states/monitor.py +++ b/src/isar/state_machine/states/monitor.py @@ -13,11 +13,11 @@ ) from robot_interface.models.exceptions.robot_exceptions import ( ErrorMessage, + RobotCommunicationException, RobotCommunicationTimeoutException, RobotException, RobotRetrieveInspectionException, RobotTaskStatusException, - RobotCommunicationException, ) from robot_interface.models.inspection.inspection import Inspection from robot_interface.models.mission.mission import Mission @@ -173,6 +173,12 @@ def _queue_inspections_for_upload( inspection: Inspection = self.state_machine.robot.get_inspection( task=current_task ) + if current_task.inspection_id == inspection.id: + self.logger.warning( + f"The inspection_id of task ({current_task.inspection_id}) " + f"and result ({inspection.id}) is not matching. " + f"This may lead to confusions when accessing the inspection later" + ) except (RobotRetrieveInspectionException, RobotException) as e: self._set_error_message(e) @@ -183,7 +189,7 @@ def _queue_inspections_for_upload( if not inspection: self.logger.warning( - f"No inspection data retrieved for task {str(current_task.id)[:8]}" + f"No inspection result data retrieved for task {str(current_task.id)[:8]}" ) inspection.metadata.tag_id = current_task.tag_id @@ -193,7 +199,9 @@ def _queue_inspections_for_upload( mission, ) self.state_machine.queues.upload_queue.put(message) - self.logger.info(f"Inspection: {str(inspection.id)[:8]} queued for upload") + self.logger.info( + f"Inspection result: {str(inspection.id)[:8]} queued for upload" + ) def _report_task_status(self, task: Task) -> None: self.request_status_failure_counter = 0 diff --git a/src/isar/storage/slimm_storage.py b/src/isar/storage/slimm_storage.py index 8026e44e..c674e007 100644 --- a/src/isar/storage/slimm_storage.py +++ b/src/isar/storage/slimm_storage.py @@ -71,7 +71,10 @@ def _store_video( return inspection_path def _ingest( - self, inspection: Inspection, multiform_body: MultipartEncoder, request_url: str + self, + inspection: Inspection, + multiform_body: MultipartEncoder, + request_url: str, ) -> str: token: str = self.credentials.get_token(self.request_scope).token try: diff --git a/src/isar/storage/utilities.py b/src/isar/storage/utilities.py index 1e060081..ce3d8dc9 100644 --- a/src/isar/storage/utilities.py +++ b/src/isar/storage/utilities.py @@ -34,10 +34,11 @@ def construct_metadata_file( "plant_code": settings.PLANT_CODE, "media_orientation_reference_system": settings.MEDIA_ORIENTATION_REFERENCE_SYSTEM, # noqa: E501 "additional_meta": { + "inspection_id": inspection.id, "mission_id": mission.id, "mission_name": mission.name, - "plant_name": settings.PLANT_NAME, "mission_date": datetime.now(timezone.utc).date(), + "plant_name": settings.PLANT_NAME, "isar_id": settings.ISAR_ID, "robot_name": settings.ROBOT_NAME, "analysis_type": ( @@ -69,9 +70,7 @@ def construct_metadata_file( return json.dumps(data, cls=EnhancedJSONEncoder, indent=4).encode() -def get_filename( - inspection: Inspection, -) -> str: +def get_filename(inspection: Inspection) -> str: inspection_type: str = type(inspection).__name__ tag: str = inspection.metadata.tag_id if inspection.metadata.tag_id else "no-tag" epoch_time: int = int(time.time()) diff --git a/src/robot_interface/models/inspection/inspection.py b/src/robot_interface/models/inspection/inspection.py index 9ada0372..b12924d9 100644 --- a/src/robot_interface/models/inspection/inspection.py +++ b/src/robot_interface/models/inspection/inspection.py @@ -4,8 +4,7 @@ from typing import Optional, Type from alitra import Pose - -from robot_interface.utilities.uuid_string_factory import uuid4_string +from pydantic import BaseModel, Field @dataclass @@ -43,18 +42,16 @@ class AudioMetadata(InspectionMetadata): duration: Optional[float] = field(default=None) -@dataclass -class Inspection: +class Inspection(BaseModel): metadata: InspectionMetadata - id: str = field(default_factory=uuid4_string, init=True) - data: Optional[bytes] = field(default=None, init=False) + id: str = Field(frozen=True) + data: Optional[bytes] = Field(default=None, frozen=True) @staticmethod def get_metadata_type() -> Type[InspectionMetadata]: return InspectionMetadata -@dataclass class Image(Inspection): metadata: ImageMetadata @@ -63,7 +60,6 @@ def get_metadata_type() -> Type[InspectionMetadata]: return ImageMetadata -@dataclass class ThermalImage(Inspection): metadata: ThermalImageMetadata @@ -72,7 +68,6 @@ def get_metadata_type() -> Type[InspectionMetadata]: return ThermalImageMetadata -@dataclass class Video(Inspection): metadata: VideoMetadata @@ -81,7 +76,6 @@ def get_metadata_type() -> Type[InspectionMetadata]: return VideoMetadata -@dataclass class ThermalVideo(Inspection): metadata: ThermalVideoMetadata @@ -90,7 +84,6 @@ def get_metadata_type() -> Type[InspectionMetadata]: return ThermalVideoMetadata -@dataclass class Audio(Inspection): metadata: AudioMetadata diff --git a/src/robot_interface/models/mission/mission.py b/src/robot_interface/models/mission/mission.py index 4fb03a49..757ee9e4 100644 --- a/src/robot_interface/models/mission/mission.py +++ b/src/robot_interface/models/mission/mission.py @@ -1,7 +1,7 @@ -from dataclasses import dataclass, field from typing import List, Optional from alitra import Pose +from pydantic import BaseModel, Field from robot_interface.models.exceptions.robot_exceptions import ErrorMessage from robot_interface.models.mission.status import MissionStatus @@ -9,20 +9,12 @@ from robot_interface.utilities.uuid_string_factory import uuid4_string -@dataclass -class Mission: - tasks: List[TASKS] - id: str = field(default_factory=uuid4_string, init=True) - name: str = "" - start_pose: Optional[Pose] = None - dock: Optional[bool] = None - undock: Optional[bool] = None +class Mission(BaseModel): + id: str = Field(default_factory=uuid4_string, frozen=True) + tasks: List[TASKS] = Field(default_factory=list, frozen=True) + name: str = Field(frozen=True) + start_pose: Optional[Pose] = Field(default=None, frozen=True) + dock: bool = Field(default=False, frozen=True) + undock: bool = Field(default=False, frozen=True) status: MissionStatus = MissionStatus.NotStarted - error_message: Optional[ErrorMessage] = field(default=None, init=False) - - def _set_unique_id(self) -> None: - self.id: str = uuid4_string() - - def __post_init__(self) -> None: - if self.id is None: - self._set_unique_id() + error_message: Optional[ErrorMessage] = Field(default=None) diff --git a/src/robot_interface/models/mission/task.py b/src/robot_interface/models/mission/task.py index 60e86552..27c85981 100644 --- a/src/robot_interface/models/mission/task.py +++ b/src/robot_interface/models/mission/task.py @@ -1,8 +1,8 @@ -from dataclasses import dataclass, field from enum import Enum from typing import Literal, Optional, Type, Union from alitra import Pose, Position +from pydantic import BaseModel, Field from robot_interface.models.exceptions.robot_exceptions import ErrorMessage from robot_interface.models.inspection import ( @@ -29,18 +29,16 @@ class TaskTypes(str, Enum): DockingProcedure = "docking_procedure" -@dataclass -class ZoomDescription: +class ZoomDescription(BaseModel): objectWidth: float objectHeight: float -@dataclass -class Task: - status: TaskStatus = field(default=TaskStatus.NotStarted, init=False) - error_message: Optional[ErrorMessage] = field(default=None, init=False) - tag_id: Optional[str] = field(default=None) - id: str = field(default_factory=uuid4_string, init=True) +class Task(BaseModel): + status: TaskStatus = Field(default=TaskStatus.NotStarted) + error_message: Optional[ErrorMessage] = Field(default=None) + tag_id: Optional[str] = Field(default=None) + id: str = Field(default_factory=uuid4_string) def is_finished(self) -> bool: if ( @@ -56,69 +54,63 @@ def update_task_status(self) -> TaskStatus: return self.status -@dataclass class InspectionTask(Task): """ Base class for all inspection tasks which produce results to be uploaded. """ - inspection: Inspection = field(default=None, init=True) - robot_pose: Pose = field(default=None, init=True) - metadata: Optional[dict] = field(default_factory=dict, init=True) - zoom: Optional[ZoomDescription] = field(default=None) + inspection_id: str = Field(default_factory=uuid4_string) + robot_pose: Pose = Field(default=None, init=True) + metadata: Optional[dict] = Field(default_factory=dict) + zoom: Optional[ZoomDescription] = Field(default=None) @staticmethod def get_inspection_type() -> Type[Inspection]: return Inspection -@dataclass class DockingProcedure(Task): """ Task which causes the robot to dock or undock """ - behavior: Literal["dock", "undock"] = field(default=None, init=True) + behavior: Literal["dock", "undock"] = Field(default=None) type: Literal[TaskTypes.DockingProcedure] = TaskTypes.DockingProcedure -@dataclass class ReturnToHome(Task): """ Task which cases the robot to return home """ - pose: Pose = field(default=None, init=True) + pose: Pose = Field(default=None) type: Literal[TaskTypes.ReturnToHome] = TaskTypes.ReturnToHome -@dataclass class Localize(Task): """ Task which causes the robot to localize """ - localization_pose: Pose = field(default=None, init=True) + localization_pose: Pose = Field(default=None) type: Literal[TaskTypes.Localize] = TaskTypes.Localize -@dataclass class MoveArm(Task): """ Task which causes the robot to move its arm """ - arm_pose: str = field(default=None, init=True) + arm_pose: str = Field(default=None) type: Literal[TaskTypes.MoveArm] = TaskTypes.MoveArm -@dataclass class TakeImage(InspectionTask): """ - Task which causes the robot to take an image towards the given coordinate. + Task which causes the robot to take an image towards the given target. """ - target: Position = field(default=None, init=True) + target: Position = Field(default=None) type: Literal[TaskTypes.TakeImage] = TaskTypes.TakeImage @staticmethod @@ -126,13 +118,12 @@ def get_inspection_type() -> Type[Inspection]: return Image -@dataclass class TakeThermalImage(InspectionTask): """ - Task which causes the robot to take a thermal image towards the given coordinate. + Task which causes the robot to take a thermal image towards the given target. """ - target: Position = field(default=None, init=True) + target: Position = Field(default=None) type: Literal[TaskTypes.TakeThermalImage] = TaskTypes.TakeThermalImage @staticmethod @@ -140,16 +131,15 @@ def get_inspection_type() -> Type[Inspection]: return ThermalImage -@dataclass class TakeVideo(InspectionTask): """ - Task which causes the robot to take a video towards the given coordinate. + Task which causes the robot to take a video towards the given target. Duration of video is given in seconds. """ - target: Position = field(default=None, init=True) - duration: float = field(default=None, init=True) + target: Position = Field(default=None) + duration: float = Field(default=None) type: Literal[TaskTypes.TakeVideo] = TaskTypes.TakeVideo @staticmethod @@ -157,16 +147,15 @@ def get_inspection_type() -> Type[Inspection]: return Video -@dataclass class TakeThermalVideo(InspectionTask): """ - Task which causes the robot to record thermal video towards the given coordinate + Task which causes the robot to record thermal video towards the given target Duration of video is given in seconds. """ - target: Position = field(default=None, init=True) - duration: float = field(default=None, init=True) + target: Position = Field(default=None) + duration: float = Field(default=None) type: Literal[TaskTypes.TakeThermalVideo] = TaskTypes.TakeThermalVideo @staticmethod @@ -174,7 +163,6 @@ def get_inspection_type() -> Type[Inspection]: return ThermalVideo -@dataclass class RecordAudio(InspectionTask): """ Task which causes the robot to record a video at its position, facing the target. @@ -182,8 +170,8 @@ class RecordAudio(InspectionTask): Duration of audio is given in seconds. """ - target: Position = field(default=None, init=True) - duration: float = field(default=None, init=True) + target: Position = Field(default=None) + duration: float = Field(default=None) type: Literal[TaskTypes.RecordAudio] = TaskTypes.RecordAudio @staticmethod diff --git a/src/robot_interface/robot_interface.py b/src/robot_interface/robot_interface.py index c6ce8e98..5a2c3fa2 100644 --- a/src/robot_interface/robot_interface.py +++ b/src/robot_interface/robot_interface.py @@ -3,12 +3,12 @@ from threading import Thread from typing import Callable, List, Optional -from robot_interface.models.robots.media import MediaConfig from robot_interface.models.initialize import InitializeParams from robot_interface.models.inspection.inspection import Inspection from robot_interface.models.mission.mission import Mission from robot_interface.models.mission.status import RobotStatus, TaskStatus from robot_interface.models.mission.task import InspectionTask, Task +from robot_interface.models.robots.media import MediaConfig class RobotInterface(metaclass=ABCMeta): @@ -166,8 +166,10 @@ def get_inspection(self, task: InspectionTask) -> Inspection: Returns ------- - Sequence[InspectionResult] - List containing all the inspection results connected to the given task + Sequence[Inspection] + List containing all the inspection connected to the given task. + get_inspection has responsibility to assign the inspection_id of the task + to the inspection that it returns. Raises ------ diff --git a/tests/integration/turtlebot/test_successful_mission.py b/tests/integration/turtlebot/test_successful_mission.py index 89e340f5..f9fbeae1 100644 --- a/tests/integration/turtlebot/test_successful_mission.py +++ b/tests/integration/turtlebot/test_successful_mission.py @@ -1,3 +1,4 @@ +import json import shutil import time from copy import deepcopy @@ -25,7 +26,6 @@ StateMachineModule, SchedulingUtilitiesModule, ) -from isar.services.readers.base_reader import BaseReader from isar.state_machine.states_enum import States from robot_interface.models.mission.mission import Mission from robot_interface.models.mission.task import ReturnToHome @@ -118,7 +118,8 @@ def test_successful_mission( paths = mission_result_folder.rglob("*.json") for path in paths: - metadata: dict = BaseReader.read_json(path) + with open(path) as json_file: + metadata = json.load(json_file) files_metadata: dict = metadata["data"][0]["files"][0] filename: str = files_metadata["file_name"] inspection_file: Path = mission_result_folder.joinpath(filename) diff --git a/src/isar/services/readers/__init__.py b/tests/isar/apis/models/__init__.py similarity index 100% rename from src/isar/services/readers/__init__.py rename to tests/isar/apis/models/__init__.py diff --git a/tests/isar/models/example_mission_definition.json b/tests/isar/apis/models/example_mission_definition.json similarity index 100% rename from tests/isar/models/example_mission_definition.json rename to tests/isar/apis/models/example_mission_definition.json diff --git a/tests/isar/apis/models/test_start_mission_definition.py b/tests/isar/apis/models/test_start_mission_definition.py new file mode 100644 index 00000000..f4377293 --- /dev/null +++ b/tests/isar/apis/models/test_start_mission_definition.py @@ -0,0 +1,81 @@ +import json +import os + +from alitra import Frame, Orientation, Pose, Position + +from isar.apis.models.models import InputOrientation, InputPose, InputPosition +from isar.apis.models.start_mission_definition import ( + InspectionTypes, + StartMissionDefinition, + StartMissionInspectionDefinition, + StartMissionTaskDefinition, + TaskType, + to_isar_mission, +) +from robot_interface.models.mission.mission import Mission +from robot_interface.models.mission.task import TakeImage + + +def test_to_isar_mission() -> None: + DUMMY_MISSION_NAME = "mission_name" + + inspection_definition = StartMissionInspectionDefinition( + type=InspectionTypes.image, + inspection_target=InputPosition(x=1, y=1, z=1), + ) + task_pose = InputPose( + position=InputPosition(x=1, y=1, z=1), + orientation=InputOrientation(x=1, y=1, z=1, w=1), + ) + task_definition = StartMissionTaskDefinition( + type=TaskType.Inspection, + pose=task_pose, + inspection=inspection_definition, + ) + mission_definition = StartMissionDefinition( + tasks=[task_definition], name=DUMMY_MISSION_NAME + ) + + isar_mission: Mission = to_isar_mission(mission_definition) + + assert len(isar_mission.id) > 1 + assert isar_mission.name == DUMMY_MISSION_NAME + assert len(isar_mission.tasks) == 1 + + first_task = isar_mission.tasks[0] + assert len(first_task.id) > 1 + + assert isinstance(first_task, TakeImage) + + assert first_task.target == Position(x=1, y=1, z=1, frame=Frame(name="robot")) + assert len(first_task.inspection_id) > 1 + assert first_task.robot_pose == Pose( + position=Position(x=1, y=1, z=1, frame=Frame(name="robot")), + orientation=Orientation(x=1, y=1, z=1, w=1, frame=Frame(name="robot")), + frame=Frame(name="robot"), + ) + + +def test_mission_definition_from_json_to_isar_mission() -> None: + dirname = os.path.dirname(__file__) + filepath = os.path.join(dirname, "example_mission_definition.json") + + with open(filepath) as f: + datax = json.load(f) + mission_definition = StartMissionDefinition(**datax) + + isar_mission: Mission = to_isar_mission(mission_definition) + assert len(isar_mission.id) > 1 + assert isar_mission.name == "my-mission" + + assert len(isar_mission.tasks) == 1 + task = isar_mission.tasks[0] + assert len(task.id) > 1 + assert isinstance(task, TakeImage) + assert task.robot_pose == Pose( + position=Position(0.0, 0.0, 0.0, frame=Frame("robot")), + orientation=Orientation(0.0, 0.0, 0.0, 0.0, frame=Frame("robot")), + frame=Frame("robot"), + ) + assert task.type == "take_image" + assert task.target == Position(0.0, 0.0, 0.0, frame=Frame("robot")) diff --git a/tests/isar/apis/scheduler/test_scheduler_router.py b/tests/isar/apis/scheduler/test_scheduler_router.py index ff0a3280..0b1c7cad 100644 --- a/tests/isar/apis/scheduler/test_scheduler_router.py +++ b/tests/isar/apis/scheduler/test_scheduler_router.py @@ -1,7 +1,6 @@ import json import re from http import HTTPStatus -from typing import List from unittest import mock import pytest @@ -15,7 +14,6 @@ from isar.models.communication.queues.queue_timeout_error import QueueTimeoutError from isar.services.utilities.scheduling_utilities import SchedulingUtilities from isar.state_machine.states_enum import States -from robot_interface.models.mission.task import TaskTypes from tests.mocks.mission_definition import MockMissionDefinition mock_mission = MockMissionDefinition.default_mission @@ -104,12 +102,6 @@ class TestStartMission: schedule_start_mission_path = "/schedule/start-mission" mock_start_mission_definition = MockMissionDefinition.mock_start_mission_definition mock_start_mission_content = {"mission_definition": mock_start_mission_definition} - mock_start_mission_with_task_ids_content = { - "mission_definition": MockMissionDefinition.mock_start_mission_definition_task_ids - } - mock_start_mission_duplicate_task_ids_content = { - "mission_definition": MockMissionDefinition.mock_start_mission_definition_with_duplicate_task_ids - } @mock.patch.object(SchedulingUtilities, "get_state", mock_return_idle) @mock.patch.object(SchedulingUtilities, "start_mission", mock_void) @@ -157,58 +149,6 @@ def test_robot_not_capable(self, client: TestClient): assert re.search("return_to_home", response_detail) assert re.search("take_image", response_detail) - @mock.patch.object(SchedulingUtilities, "get_state", mock_return_idle) - @mock.patch.object(SchedulingUtilities, "start_mission", mock_void) - def test_mission_with_input_task_ids(self, client: TestClient): - expected_ids: List[str] = [] - for task in self.mock_start_mission_with_task_ids_content[ - "mission_definition" - ].tasks: - if task.id: - expected_ids.append(task.id) - - response = client.post( - url=self.schedule_start_mission_path, - json=jsonable_encoder(self.mock_start_mission_with_task_ids_content), - ) - assert response.status_code == HTTPStatus.OK - start_mission_response: dict = response.json() - for task in start_mission_response["tasks"]: - assert task["id"] in expected_ids - - @mock.patch.object(SchedulingUtilities, "get_state", mock_return_idle) - @mock.patch.object(SchedulingUtilities, "start_mission", mock_void) - def test_mission_with_input_inspection_task_ids(self, client: TestClient): - expected_inspection_ids: List[str] = [] - for task in self.mock_start_mission_with_task_ids_content[ - "mission_definition" - ].tasks: - expected_inspection_ids.append(task.inspection.id) - - response = client.post( - url=self.schedule_start_mission_path, - json=jsonable_encoder(self.mock_start_mission_with_task_ids_content), - ) - assert response.status_code == HTTPStatus.OK - start_mission_response: dict = response.json() - for task in start_mission_response["tasks"]: - if ( - task["type"] == TaskTypes.ReturnToHome == False - and task["type"] == TaskTypes.Localize == False - and task["type"] == TaskTypes.DockingProcedure == False - and task["type"] == TaskTypes.MoveArm == False - ): - assert task["id"] in expected_inspection_ids - - @mock.patch.object(SchedulingUtilities, "get_state", mock_return_idle) - @mock.patch.object(SchedulingUtilities, "start_mission", mock_void) - def test_mission_with_duplicate_task_ids(self, client: TestClient): - response = client.post( - url=self.schedule_start_mission_path, - json=jsonable_encoder(self.mock_start_mission_duplicate_task_ids_content), - ) - assert response.status_code == HTTPStatus.BAD_REQUEST - class TestPauseMission: schedule_pause_mission_path = "/schedule/pause-mission" diff --git a/tests/isar/mission/test_mission.py b/tests/isar/mission/test_mission.py index 9bf6bccd..33a112cd 100644 --- a/tests/isar/mission/test_mission.py +++ b/tests/isar/mission/test_mission.py @@ -1,12 +1,11 @@ from alitra import Frame, Orientation, Pose, Position -from isar.services.readers.base_reader import BaseReader from robot_interface.models.mission.mission import Mission from robot_interface.models.mission.task import ( TASKS, + ReturnToHome, TakeImage, TakeThermalImage, - ReturnToHome, ) robot_pose_1 = Pose( @@ -46,11 +45,13 @@ expected_mission = Mission( id="1", + name="Test mission", tasks=[task_take_image, task_take_thermal_image, task_return_to_home], ) example_mission_dict = { "id": "1", + "name": "Test mission", "tasks": [ { "type": "take_image", @@ -116,12 +117,9 @@ def test_mission_definition() -> None: - loaded_mission: Mission = BaseReader.dict_to_dataclass( - dataclass_dict=example_mission_dict, - target_dataclass=Mission, - strict_config=False, - ) + loaded_mission: Mission = Mission(**example_mission_dict) + assert loaded_mission.id == expected_mission.id assert loaded_mission.id == expected_mission.id assert loaded_mission.status == expected_mission.status diff --git a/tests/isar/models/test_start_mission_definition.py b/tests/isar/models/test_start_mission_definition.py deleted file mode 100644 index dee0e983..00000000 --- a/tests/isar/models/test_start_mission_definition.py +++ /dev/null @@ -1,63 +0,0 @@ -import json -import os -from typing import List - -import pytest -from alitra import Frame, Orientation, Pose, Position - -from isar.apis.models.start_mission_definition import ( - StartMissionDefinition, - get_duplicate_ids, - to_isar_mission, -) -from robot_interface.models.mission.mission import Mission -from robot_interface.models.mission.task import TASKS, Task - -task_1: Task = Task(tag_id=None, id="123") -task_2: Task = Task(tag_id=None, id="123") -task_3: Task = Task(tag_id=None, id="123456") -task_4: Task = Task() -task_5: Task = Task() - - -@pytest.mark.parametrize( - "item_list, expected_boolean", - [ - ( - [task_1, task_2, task_3], - True, - ), - ( - [task_1, task_3, task_4, task_5], - False, - ), - ], -) -def test_duplicate_id_check(item_list: List[TASKS], expected_boolean: bool): - duplicates: List[str] = get_duplicate_ids(item_list) - has_duplicates: bool = len(duplicates) > 0 - assert has_duplicates == expected_boolean - - -def test_mission_definition_to_isar_mission(): - dirname = os.path.dirname(__file__) - filepath = os.path.join(dirname, "example_mission_definition.json") - - with open(filepath) as f: - datax = json.load(f) - mission_definition = StartMissionDefinition(**datax) - - generated_mission: Mission = to_isar_mission(mission_definition) - assert generated_mission.id == "generated_mission_id" - assert generated_mission.name == "my-mission" - assert len(generated_mission.tasks) == 1 - - task = generated_mission.tasks[0] - assert task.id == "generated_task_id" - assert task.robot_pose == Pose( - position=Position(0.0, 0.0, 0.0, frame=Frame("robot")), - orientation=Orientation(0.0, 0.0, 0.0, 0.0, frame=Frame("robot")), - frame=Frame("robot"), - ) - assert task.type == "take_image" - assert task.target == Position(0.0, 0.0, 0.0, frame=Frame("robot")) diff --git a/tests/isar/services/readers/test_base_reader.py b/tests/isar/services/readers/test_base_reader.py deleted file mode 100644 index b694a4b0..00000000 --- a/tests/isar/services/readers/test_base_reader.py +++ /dev/null @@ -1,23 +0,0 @@ -from dataclasses import asdict -from typing import Any - -import pytest -from alitra import Pose - -from isar.services.readers.base_reader import BaseReader -from robot_interface.models.mission.task import ReturnToHome, TakeImage -from tests.mocks.pose import MockPose -from tests.mocks.task import MockTask - - -class TestBaseReader: - @pytest.mark.parametrize( - "dataclass_dict, expected_dataclass", - [ - (asdict(MockTask.return_home()), ReturnToHome), - (asdict(MockPose.default_pose()), Pose), - ], - ) - def test_dict_to_dataclass(self, dataclass_dict: dict, expected_dataclass: Any): - content = BaseReader.dict_to_dataclass(dataclass_dict, expected_dataclass) - assert type(content) is expected_dataclass diff --git a/tests/isar/services/readers/test_mission_reader.py b/tests/isar/services/readers/test_mission_reader.py index 80c88376..618688da 100644 --- a/tests/isar/services/readers/test_mission_reader.py +++ b/tests/isar/services/readers/test_mission_reader.py @@ -1,10 +1,11 @@ from pathlib import Path -from typing import List, Union +from typing import List import pytest from alitra import Frame, Orientation, Pose, Position from isar.config.settings import settings +from isar.mission_planner.local_planner import LocalPlanner from isar.mission_planner.mission_planner_interface import MissionNotFoundError from robot_interface.models.mission.mission import Mission from robot_interface.models.mission.task import ( @@ -13,22 +14,21 @@ TakeImage, TakeThermalImage, ) -from robot_interface.models.mission.task import Task -@pytest.mark.parametrize( - "mission_path", - [ - Path("./tests/test_data/test_mission_working_no_tasks.json"), - Path("./tests/test_data/test_mission_working.json"), - ], -) -def test_get_mission(mission_reader, mission_path) -> None: - output: Mission = mission_reader.read_mission_from_file(mission_path) - assert isinstance(output, Mission) +def test_get_working_mission(mission_reader: LocalPlanner) -> None: + mission_path = Path("./tests/test_data/test_mission_working.json") + mission: Mission = mission_reader.read_mission_from_file(mission_path) + assert isinstance(mission, Mission) + + +def test_get_mission_with_no_tasks(mission_reader: LocalPlanner) -> None: + mission_path = Path("./tests/test_data/test_mission_working_no_tasks.json") + mission: Mission = mission_reader.read_mission_from_file(mission_path) + assert isinstance(mission, Mission) -def test_read_mission_from_file(mission_reader) -> None: +def test_read_mission_from_file(mission_reader: LocalPlanner) -> None: expected_robot_pose_1 = Pose( position=Position(-2, -2, 0, Frame("asset")), orientation=Orientation(0, 0, 0.4794255, 0.8775826, Frame("asset")), @@ -41,7 +41,7 @@ def test_read_mission_from_file(mission_reader) -> None: orientation=Orientation(0, 0, 0.4794255, 0.8775826, Frame("asset")), frame=Frame("asset"), ) - expected_inspection_target_1 = Position(2, 2, 0, Frame("robot")) + expected_inspection_target_1 = Position(2, 2, 0, Frame("asset")) task_2: TakeImage = TakeImage( target=expected_inspection_target_1, robot_pose=expected_robot_pose_2 ) @@ -51,7 +51,7 @@ def test_read_mission_from_file(mission_reader) -> None: orientation=Orientation(0, 0, 0.4794255, 0.8775826, Frame("asset")), frame=Frame("asset"), ) - expected_inspection_target_2 = Position(2, 2, 0, Frame("robot")) + expected_inspection_target_2 = Position(2, 2, 0, Frame("asset")) task_3: TakeImage = TakeImage( target=expected_inspection_target_2, robot_pose=expected_robot_pose_3 ) @@ -88,22 +88,22 @@ def test_read_mission_from_file(mission_reader) -> None: (Path("./tests/test_data/test_mission_not_working.json")), ], ) -def test_get_invalid_mission(mission_reader, mission_path) -> None: +def test_get_invalid_mission(mission_reader: LocalPlanner, mission_path) -> None: with pytest.raises(Exception): mission_reader.read_mission_from_file(mission_path) -def test_get_mission_by_id(mission_reader) -> None: +def test_get_mission_by_id(mission_reader: LocalPlanner) -> None: output = mission_reader.get_mission("1") assert isinstance(output, Mission) -def test_get_mission_by_invalid_id(mission_reader) -> None: +def test_get_mission_by_invalid_id(mission_reader: LocalPlanner) -> None: with pytest.raises(MissionNotFoundError): mission_reader.get_mission("12345") -def test_valid_predefined_missions_files(mission_reader) -> None: +def test_valid_predefined_missions_files(mission_reader: LocalPlanner) -> None: # Checks that the predefined mission folder contains only valid missions! mission_list_dict = mission_reader.get_predefined_missions() predefined_mission_folder = Path(settings.PREDEFINED_MISSIONS_FOLDER) @@ -116,7 +116,7 @@ def test_valid_predefined_missions_files(mission_reader) -> None: assert mission is not None -def test_thermal_image_task(mission_reader) -> None: +def test_thermal_image_task(mission_reader: LocalPlanner) -> None: mission_path: Path = Path("./tests/test_data/test_thermal_image_mission.json") output: Mission = mission_reader.read_mission_from_file(mission_path) diff --git a/tests/isar/services/utilities/test_queue_utilities.py b/tests/isar/services/utilities/test_queue_utilities.py index a35e34e6..0ca0c8b3 100644 --- a/tests/isar/services/utilities/test_queue_utilities.py +++ b/tests/isar/services/utilities/test_queue_utilities.py @@ -20,8 +20,8 @@ class TestQueueUtilities: ) def test_check_queue_with_queue_size_one( self, message, queue_timeout, expected_message - ): - test_queue = Queue(maxsize=1) + ) -> None: + test_queue: Queue = Queue(maxsize=1) if message is not None: test_queue.put(message) message = QueueUtilities.check_queue(test_queue, queue_timeout) @@ -30,8 +30,8 @@ def test_check_queue_with_queue_size_one( with pytest.raises(QueueTimeoutError): QueueUtilities.check_queue(test_queue, queue_timeout) - def test_clear_queue(self): - test_queue = Queue(maxsize=2) + def test_clear_queue(self) -> None: + test_queue: Queue = Queue(maxsize=2) test_queue.put(1) test_queue.put(2) QueueUtilities.clear_queue(test_queue) diff --git a/tests/isar/state_machine/states/test_monitor.py b/tests/isar/state_machine/states/test_monitor.py index 1e1409ff..6e447950 100644 --- a/tests/isar/state_machine/states/test_monitor.py +++ b/tests/isar/state_machine/states/test_monitor.py @@ -2,7 +2,6 @@ from isar.state_machine.states.monitor import Monitor from robot_interface.models.mission.mission import Mission - from robot_interface.models.mission.status import MissionStatus, TaskStatus from robot_interface.models.mission.task import ReturnToHome, TakeImage from tests.mocks.task import MockTask @@ -35,7 +34,7 @@ def test_should_only_upload_if_status_is_completed( ): task: TakeImage = MockTask.take_image() task.status = TaskStatus.Successful if is_status_successful else TaskStatus.Failed - mission: Mission = Mission(tasks=[task]) + mission: Mission = Mission(name="Dummy misson", tasks=[task]) mission.status = ( MissionStatus.Successful if is_status_successful else MissionStatus.Failed ) diff --git a/tests/isar/state_machine/test_state_machine.py b/tests/isar/state_machine/test_state_machine.py index b6f8ddb1..889aa8d2 100644 --- a/tests/isar/state_machine/test_state_machine.py +++ b/tests/isar/state_machine/test_state_machine.py @@ -89,7 +89,7 @@ def test_state_machine_transitions_when_running_mission_by_task( target=MockPose.default_pose().position, robot_pose=MockPose.default_pose() ) task_2: Task = ReturnToHome(pose=MockPose.default_pose()) - mission: Mission = Mission(tasks=[task_1, task_2]) # type: ignore + mission: Mission = Mission(name="Dummy misson", tasks=[task_1, task_2]) state_machine_thread.state_machine.run_mission_by_task = True state_machine_thread.start() @@ -122,7 +122,7 @@ def test_state_machine_transitions_when_running_full_mission( target=MockPose.default_pose().position, robot_pose=MockPose.default_pose() ) task_2: Task = ReturnToHome(pose=MockPose.default_pose()) - mission: Mission = Mission(tasks=[task_1, task_2]) # type: ignore + mission: Mission = Mission(name="Dummy misson", tasks=[task_1, task_2]) scheduling_utilities: SchedulingUtilities = injector.get(SchedulingUtilities) scheduling_utilities.start_mission(mission=mission, initial_pose=None) @@ -147,7 +147,7 @@ def test_state_machine_failed_dependency( target=MockPose.default_pose().position, robot_pose=MockPose.default_pose() ) task_2: Task = ReturnToHome(pose=MockPose.default_pose()) - mission: Mission = Mission(tasks=[task_1, task_2]) # type: ignore + mission: Mission = Mission(name="Dummy misson", tasks=[task_1, task_2]) mocker.patch.object(MockRobot, "task_status", return_value=TaskStatus.Failed) @@ -180,7 +180,7 @@ def test_state_machine_with_successful_collection( storage_mock: StorageInterface = injector.get(List[StorageInterface])[0] - mission: Mission = Mission(tasks=[MockTask.take_image()]) + mission: Mission = Mission(name="Dummy misson", tasks=[MockTask.take_image()]) scheduling_utilities: SchedulingUtilities = injector.get(SchedulingUtilities) scheduling_utilities.start_mission(mission=mission, initial_pose=None) @@ -209,7 +209,7 @@ def test_state_machine_with_unsuccessful_collection( state_machine_thread.start() - mission: Mission = Mission(tasks=[MockTask.take_image()]) + mission: Mission = Mission(name="Dummy misson", tasks=[MockTask.take_image()]) scheduling_utilities: SchedulingUtilities = injector.get(SchedulingUtilities) scheduling_utilities.start_mission(mission=mission, initial_pose=None) @@ -236,7 +236,7 @@ def test_state_machine_with_successful_mission_stop( ) -> None: state_machine_thread.start() - mission: Mission = Mission(tasks=[MockTask.take_image()]) + mission: Mission = Mission(name="Dummy misson", tasks=[MockTask.take_image()]) scheduling_utilities: SchedulingUtilities = injector.get(SchedulingUtilities) scheduling_utilities.start_mission(mission=mission, initial_pose=None) @@ -253,7 +253,7 @@ def test_state_machine_with_unsuccessful_mission_stop( state_machine_thread: StateMachineThread, caplog: pytest.LogCaptureFixture, ) -> None: - mission: Mission = Mission(tasks=[MockTask.take_image()]) + mission: Mission = Mission(name="Dummy misson", tasks=[MockTask.take_image()]) scheduling_utilities: SchedulingUtilities = injector.get(SchedulingUtilities) mocker.patch.object(MockRobot, "task_status", return_value=TaskStatus.InProgress) diff --git a/tests/isar/storage/test_uploader.py b/tests/isar/storage/test_uploader.py index c02c0805..e3aaf3b9 100644 --- a/tests/isar/storage/test_uploader.py +++ b/tests/isar/storage/test_uploader.py @@ -11,6 +11,7 @@ from isar.storage.uploader import Uploader from robot_interface.models.inspection.inspection import ImageMetadata, Inspection from robot_interface.models.mission.mission import Mission +from robot_interface.models.mission.task import TakeImage from robot_interface.telemetry.mqtt_client import MqttClientInterface MISSION_ID = "some-mission-id" @@ -42,8 +43,13 @@ def uploader(injector) -> Uploader: def test_should_upload_from_queue(uploader) -> None: - mission: Mission = Mission([]) - inspection: Inspection = Inspection(metadata=ARBITRARY_IMAGE_METADATA) + take_image_task = TakeImage() + mission: Mission = Mission(name="Dummy misson", tasks=[take_image_task]) + + assert isinstance(mission.tasks[0], TakeImage) + inspection = Inspection( + metadata=ARBITRARY_IMAGE_METADATA, id=mission.tasks[0].inspection_id + ) message: Tuple[Inspection, Mission] = ( inspection, @@ -56,8 +62,9 @@ def test_should_upload_from_queue(uploader) -> None: def test_should_retry_failed_upload_from_queue(uploader) -> None: - mission: Mission = Mission([]) - inspection: Inspection = Inspection(metadata=ARBITRARY_IMAGE_METADATA) + INSPECTION_ID = "123-456" + inspection = Inspection(metadata=ARBITRARY_IMAGE_METADATA, id=INSPECTION_ID) + mission: Mission = Mission(name="Dummy Mission") message: Tuple[Inspection, Mission] = ( inspection, diff --git a/tests/mocks/mission_definition.py b/tests/mocks/mission_definition.py index 546b48ff..473346f4 100644 --- a/tests/mocks/mission_definition.py +++ b/tests/mocks/mission_definition.py @@ -28,6 +28,7 @@ class MockMissionDefinition: mock_task_return_home = MockTask.return_home() default_mission = Mission( id="default_mission", + name="Dummy misson", tasks=[ mock_task_take_image, mock_task_return_home, @@ -37,21 +38,10 @@ class MockMissionDefinition: type=InspectionTypes.image, inspection_target=mock_input_target_position, ) - mock_start_mission_inspection_definition_id_123 = StartMissionInspectionDefinition( - type=InspectionTypes.image, - inspection_target=mock_input_target_position, - id="123", - ) - mock_start_mission_inspection_definition_id_123456 = ( - StartMissionInspectionDefinition( - type=InspectionTypes.image, - inspection_target=mock_input_target_position, - id="123456", - ) - ) mock_task_response_take_image = TaskResponse( id=mock_task_take_image.id, tag_id=mock_task_take_image.tag_id, + inspection_id=mock_task_take_image.inspection_id, type=mock_task_take_image.type, ) @@ -74,46 +64,21 @@ class MockMissionDefinition: ] ) mock_start_mission_definition_task_ids = StartMissionDefinition( - tasks=[ - StartMissionTaskDefinition( - pose=mock_input_pose, - tag="dummy_tag", - inspection=mock_start_mission_inspection_definition_id_123, - id="123", - ), - StartMissionTaskDefinition( - pose=mock_input_pose, - tag="dummy_tag", - inspection=mock_start_mission_inspection_definition, - id="123456", - ), - StartMissionTaskDefinition( - pose=mock_input_pose, - tag="dummy_tag", - inspection=mock_start_mission_inspection_definition, - id="123456789", - ), - ] - ) - mock_start_mission_definition_with_duplicate_task_ids = StartMissionDefinition( tasks=[ StartMissionTaskDefinition( pose=mock_input_pose, tag="dummy_tag", inspection=mock_start_mission_inspection_definition, - id="123", ), StartMissionTaskDefinition( pose=mock_input_pose, tag="dummy_tag", inspection=mock_start_mission_inspection_definition, - id="123456", ), StartMissionTaskDefinition( pose=mock_input_pose, tag="dummy_tag", inspection=mock_start_mission_inspection_definition, - id="123", ), ] ) diff --git a/tests/mocks/robot_interface.py b/tests/mocks/robot_interface.py index 5b995ad3..f5442f7b 100644 --- a/tests/mocks/robot_interface.py +++ b/tests/mocks/robot_interface.py @@ -1,13 +1,10 @@ -from dataclasses import field from datetime import datetime from queue import Queue from threading import Thread -from typing import Callable, List, Sequence +from typing import Callable, List from alitra import Frame, Orientation, Pose, Position -from robot_interface.models.robots.media import MediaConnectionType -from robot_interface.models.robots.media import MediaConfig from robot_interface.models.initialize import InitializeParams from robot_interface.models.inspection.inspection import ( Image, @@ -17,6 +14,7 @@ from robot_interface.models.mission.mission import Mission from robot_interface.models.mission.status import MissionStatus, RobotStatus, TaskStatus from robot_interface.models.mission.task import InspectionTask, Task +from robot_interface.models.robots.media import MediaConfig, MediaConnectionType from robot_interface.robot_interface import RobotInterface @@ -58,9 +56,11 @@ def resume(self) -> None: return def get_inspection(self, task: InspectionTask) -> Inspection: - image: Image = Image(mock_image_metadata()) - image.data = b"Some binary image data" - return image + return Image( + metadata=mock_image_metadata(), + id=task.inspection_id, + data=b"Some binary image data", + ) def generate_media_config(self) -> MediaConfig: return MediaConfig( diff --git a/tests/mocks/task.py b/tests/mocks/task.py index 5eca2f15..cb083b26 100644 --- a/tests/mocks/task.py +++ b/tests/mocks/task.py @@ -1,4 +1,4 @@ -from alitra import Frame, Position +from alitra import Frame, Orientation, Pose, Position from robot_interface.models.mission.task import ReturnToHome, TakeImage from tests.mocks.pose import MockPose @@ -12,5 +12,9 @@ def return_home() -> ReturnToHome: @staticmethod def take_image() -> TakeImage: target_pose = Position(x=1, y=1, z=1, frame=Frame("robot")) - robot_pose = Position(x=0, y=0, z=1, frame=Frame("robot")) + robot_pose = Pose( + position=Position(x=0, y=0, z=1, frame=Frame("robot")), + orientation=Orientation(x=0, y=0, z=0, w=1, frame=Frame("robot")), + frame=Frame("robot"), + ) return TakeImage(target=target_pose, robot_pose=robot_pose) diff --git a/tests/test_data/test_mission_working.json b/tests/test_data/test_mission_working.json index 9c72258e..4699acbe 100644 --- a/tests/test_data/test_mission_working.json +++ b/tests/test_data/test_mission_working.json @@ -1,5 +1,6 @@ { "id": "1", + "name": "Well defined mission", "tasks": [ { "type": "return_to_home", @@ -8,16 +9,16 @@ "x": -2, "y": -2, "z": 0, - "frame": "asset" + "frame": {"name": "asset"} }, "orientation": { "x": 0, "y": 0, "z": 0.4794255, "w": 0.8775826, - "frame": "asset" + "frame": {"name": "asset"} }, - "frame": "asset" + "frame": {"name": "asset"} } }, { @@ -27,22 +28,22 @@ "x": -2, "y": 2, "z": 0, - "frame": "asset" + "frame": {"name": "asset"} }, "orientation": { "x": 0, "y": 0, "z": 0.4794255, "w": 0.8775826, - "frame": "asset" + "frame": {"name": "asset"} }, - "frame": "asset" + "frame": {"name": "asset"} }, "target": { "x": 2, "y": 2, "z": 0, - "frame": "robot" + "frame": {"name": "asset"} } }, { @@ -52,22 +53,22 @@ "x": 2, "y": 2, "z": 0, - "frame": "asset" + "frame": {"name": "asset"} }, "orientation": { "x": 0, "y": 0, "z": 0.4794255, "w": 0.8775826, - "frame": "asset" + "frame": {"name": "asset"} }, - "frame": "asset" + "frame": {"name": "asset"} }, "target": { "x": 2, "y": 2, "z": 0, - "frame": "robot" + "frame": {"name": "asset"} } }, { @@ -77,16 +78,16 @@ "x": 0, "y": 0, "z": 0, - "frame": "asset" + "frame": {"name": "asset"} }, "orientation": { "x": 0, "y": 0, "z": 0.4794255, "w": 0.8775826, - "frame": "asset" + "frame": {"name": "asset"} }, - "frame": "asset" + "frame": {"name": "asset"} } } ] diff --git a/tests/test_data/test_mission_working_no_tasks.json b/tests/test_data/test_mission_working_no_tasks.json index 87912ba1..3a371be6 100644 --- a/tests/test_data/test_mission_working_no_tasks.json +++ b/tests/test_data/test_mission_working_no_tasks.json @@ -1,4 +1,5 @@ { "id": "41", + "name": "Empty mission", "tasks": [] } diff --git a/tests/test_data/test_thermal_image_mission.json b/tests/test_data/test_thermal_image_mission.json index 56107918..865b4d3b 100644 --- a/tests/test_data/test_thermal_image_mission.json +++ b/tests/test_data/test_thermal_image_mission.json @@ -1,5 +1,6 @@ { "id": "1", + "name": "Thermal image mission", "tasks": [ { "type": "take_thermal_image", @@ -7,23 +8,23 @@ "x": 2, "y": 2, "z": 0, - "frame": "robot" + "frame": {"name": "asset"} }, "robot_pose": { "position": { "x": -2, "y": 2, "z": 0, - "frame": "asset" + "frame": {"name": "asset"} }, "orientation": { "x": 0, "y": 0, "z": 0.4794255, "w": 0.8775826, - "frame": "asset" + "frame": {"name": "asset"} }, - "frame": "asset" + "frame": {"name": "asset"} } } ]