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"} } } ]