diff --git a/src/isar/apis/api.py b/src/isar/apis/api.py index ef8e56b5..23a08758 100644 --- a/src/isar/apis/api.py +++ b/src/isar/apis/api.py @@ -17,6 +17,7 @@ from pydantic import AnyHttpUrl from isar.apis.models.models import ControlMissionResponse, StartMissionResponse +from isar.apis.robot_control.robot_controller import RobotController from isar.apis.schedule.scheduling_controller import SchedulingController from isar.apis.security.authentication import Authenticator from isar.config.configuration_error import ConfigurationError @@ -34,12 +35,14 @@ def __init__( self, authenticator: Authenticator, scheduling_controller: SchedulingController, + robot_controller: RobotController, keyvault_client: Keyvault, port: int = settings.API_PORT, azure_ai_logging_enabled: bool = settings.LOG_HANDLER_APPLICATION_INSIGHTS_ENABLED, ) -> None: self.authenticator: Authenticator = authenticator self.scheduling_controller: SchedulingController = scheduling_controller + self.robot_controller: RobotController = robot_controller self.keyvault_client: Keyvault = keyvault_client self.host: str = "0.0.0.0" # Locking uvicorn to use 0.0.0.0 self.port: int = port @@ -98,6 +101,8 @@ def _create_app(self) -> FastAPI: app.include_router(router=self._create_info_router()) + app.include_router(router=self._create_media_control_router()) + return app def _create_scheduler_router(self) -> APIRouter: @@ -277,7 +282,7 @@ def _create_info_router(self) -> APIRouter: router.add_api_route( path="/info/robot-settings", - endpoint=self.scheduling_controller.get_info, + endpoint=self.robot_controller.get_info, methods=["GET"], dependencies=[authentication_dependency], summary="Information about the robot-settings", @@ -285,6 +290,21 @@ def _create_info_router(self) -> APIRouter: return router + def _create_media_control_router(self) -> APIRouter: + router: APIRouter = APIRouter(tags=["Media"]) + + authentication_dependency: Security = Security(self.authenticator.get_scheme()) + + router.add_api_route( + path="/media/media-stream-config", + endpoint=self.robot_controller.generate_media_config, + methods=["GET"], + dependencies=[authentication_dependency], + summary="Generates a media stream connection config", + ) + + return router + def _log_startup_message(self) -> None: address_format = "%s://%s:%d/docs" message = f"Uvicorn running on {address_format} (Press CTRL+C to quit)" diff --git a/src/robot_interface/telemetry/media_connection_type.py b/src/isar/apis/models/media_connection_type.py similarity index 100% rename from src/robot_interface/telemetry/media_connection_type.py rename to src/isar/apis/models/media_connection_type.py diff --git a/src/isar/apis/models/models.py b/src/isar/apis/models/models.py index 48289016..be024b20 100644 --- a/src/isar/apis/models/models.py +++ b/src/isar/apis/models/models.py @@ -1,6 +1,8 @@ +from dataclasses import dataclass from typing import List, Optional from alitra import Frame, Orientation, Pose, Position +from isar.apis.models.media_connection_type import MediaConnectionType from pydantic import BaseModel, Field from robot_interface.models.mission.task import TaskTypes @@ -33,6 +35,13 @@ class RobotInfoResponse(BaseModel): plant_short_name: str +@dataclass +class MediaConfig: + url: str + token: str + media_connection_type: MediaConnectionType + + class InputOrientation(BaseModel): x: float y: float diff --git a/src/isar/apis/robot_control/robot_controller.py b/src/isar/apis/robot_control/robot_controller.py new file mode 100644 index 00000000..05b75358 --- /dev/null +++ b/src/isar/apis/robot_control/robot_controller.py @@ -0,0 +1,35 @@ +import logging +from typing import List + +from injector import inject + +from isar.apis.models.models import ( + RobotInfoResponse, + TaskResponse, +) +from isar.config.settings import robot_settings, settings +from isar.services.utilities.robot_utilities import RobotUtilities +from robot_interface.models.mission.task import Task + + +class RobotController: + @inject + def __init__( + self, + robot_utilities: RobotUtilities, + ): + self.robot_utilities: RobotUtilities = robot_utilities + self.logger = logging.getLogger("api") + + def generate_media_config(self): + return self.robot_utilities.generate_media_config() + + def get_info(self): + return RobotInfoResponse( + robot_package=settings.ROBOT_PACKAGE, + isar_id=settings.ISAR_ID, + robot_name=settings.ROBOT_NAME, + robot_capabilities=robot_settings.CAPABILITIES, + robot_map_name=settings.DEFAULT_MAP, + plant_short_name=settings.PLANT_SHORT_NAME, + ) diff --git a/src/isar/apis/schedule/scheduling_controller.py b/src/isar/apis/schedule/scheduling_controller.py index 1562353b..3d9d3209 100644 --- a/src/isar/apis/schedule/scheduling_controller.py +++ b/src/isar/apis/schedule/scheduling_controller.py @@ -301,16 +301,6 @@ def start_move_arm_mission( ) return self._api_response(mission) - def get_info(self): - return RobotInfoResponse( - robot_package=settings.ROBOT_PACKAGE, - isar_id=settings.ISAR_ID, - robot_name=settings.ROBOT_NAME, - robot_capabilities=robot_settings.CAPABILITIES, - robot_map_name=settings.DEFAULT_MAP, - plant_short_name=settings.PLANT_SHORT_NAME, - ) - def _api_response(self, mission: Mission) -> StartMissionResponse: return StartMissionResponse( id=mission.id, diff --git a/src/isar/modules.py b/src/isar/modules.py index a577aefc..467cd2d6 100644 --- a/src/isar/modules.py +++ b/src/isar/modules.py @@ -8,6 +8,7 @@ from isar.apis.api import API from isar.apis.schedule.scheduling_controller import SchedulingController +from isar.apis.robot_control.robot_controller import RobotController from isar.apis.security.authentication import Authenticator from isar.config.keyvault.keyvault_service import Keyvault from isar.config.settings import settings @@ -18,6 +19,7 @@ from isar.models.communication.queues.queues import Queues from isar.services.service_connections.request_handler import RequestHandler from isar.services.utilities.scheduling_utilities import SchedulingUtilities +from isar.services.utilities.robot_utilities import RobotUtilities from isar.state_machine.state_machine import StateMachine from isar.storage.blob_storage import BlobStorage from isar.storage.local_storage import LocalStorage @@ -35,9 +37,10 @@ def provide_api( self, authenticator: Authenticator, scheduling_controller: SchedulingController, + robot_controller: RobotController, keyvault: Keyvault, ) -> API: - return API(authenticator, scheduling_controller, keyvault) + return API(authenticator, scheduling_controller, robot_controller, keyvault) @provider @singleton @@ -47,6 +50,14 @@ def provide_scheduling_controller( ) -> SchedulingController: return SchedulingController(scheduling_utilities) + @provider + @singleton + def provide_robot_controller( + self, + robot_utilities: RobotUtilities, + ) -> SchedulingController: + return RobotController(robot_utilities) + class AuthenticationModule(Module): @provider @@ -142,7 +153,7 @@ def provide_uploader( ) -class UtilitiesModule(Module): +class SchedulingUtilitiesModule(Module): @provider @singleton def provide_scheduling_utilities( @@ -151,6 +162,13 @@ def provide_scheduling_utilities( return SchedulingUtilities(queues, mission_planner) +class RobotUtilitiesModule(Module): + @provider + @singleton + def provide_robot_utilities(self, robot: RobotInterface) -> RobotUtilities: + return RobotUtilities(robot) + + class ServiceModule(Module): @provider @singleton @@ -193,7 +211,8 @@ def provide_task_selector(self) -> TaskSelectorInterface: "storage_blob": (BlobStorageModule, settings.STORAGE_BLOB_ENABLED), "storage_slimm": (SlimmStorageModule, settings.STORAGE_SLIMM_ENABLED), "mqtt": (MqttModule, "required"), - "utilities": (UtilitiesModule, "required"), + "scheduling_utilities": (SchedulingUtilitiesModule, "required"), + "robot_utilities": (RobotUtilitiesModule, "required"), } diff --git a/src/isar/services/utilities/robot_utilities.py b/src/isar/services/utilities/robot_utilities.py new file mode 100644 index 00000000..c9498f67 --- /dev/null +++ b/src/isar/services/utilities/robot_utilities.py @@ -0,0 +1,27 @@ +import logging + +from injector import inject + +from isar.config.settings import settings + +from isar.config.settings import settings + +from isar.apis.models.models import MediaConfig +from robot_interface.robot_interface import RobotInterface + + +class RobotUtilities: + """ + Contains utility functions getting robot information from the API. + """ + + @inject + def __init__( + self, + robot: RobotInterface, + ): + self.robot: RobotInterface = robot + self.logger = logging.getLogger("api") + + def generate_media_config(self) -> MediaConfig: + return self.robot.generate_media_config() diff --git a/src/robot_interface/robot_interface.py b/src/robot_interface/robot_interface.py index f10083ba..97023a21 100644 --- a/src/robot_interface/robot_interface.py +++ b/src/robot_interface/robot_interface.py @@ -3,6 +3,7 @@ from threading import Thread from typing import Callable, List +from isar.apis.models.models 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 @@ -224,6 +225,19 @@ def initialize(self, params: InitializeParams) -> None: """ raise NotImplementedError + @abstractmethod + def generate_media_config(self) -> MediaConfig: + """ + Generate a JSON containing the url and token needed to establish a media stream + connection to a robot. + + Returns + ------- + MediaConfig + An object containing the connection information for a media stream connection + """ + raise NotImplementedError + @abstractmethod def get_telemetry_publishers( self, queue: Queue, isar_id: str, robot_name: str diff --git a/src/robot_interface/telemetry/payloads.py b/src/robot_interface/telemetry/payloads.py index e175606d..a90a1ac6 100644 --- a/src/robot_interface/telemetry/payloads.py +++ b/src/robot_interface/telemetry/payloads.py @@ -6,7 +6,6 @@ from transitions import State from robot_interface.models.mission.status import RobotStatus -from robot_interface.telemetry.media_connection_type import MediaConnectionType @dataclass @@ -56,13 +55,6 @@ class VideoStream: type: str -@dataclass -class MediaConfig(TelemetryPayload): - url: str - token: str - media_connection_type: MediaConnectionType - - @dataclass class RobotStatusPayload: isar_id: str diff --git a/tests/conftest.py b/tests/conftest.py index 4de6d9f1..9eb04e8a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,7 @@ SequentialTaskSelectorModule, ServiceModule, StateMachineModule, - UtilitiesModule, + SchedulingUtilitiesModule, ) from isar.services.service_connections.request_handler import RequestHandler from isar.services.utilities.scheduling_utilities import SchedulingUtilities @@ -47,7 +47,7 @@ def injector(): ServiceModule, StateMachineModule, SequentialTaskSelectorModule, - UtilitiesModule, + SchedulingUtilitiesModule, ] ) @@ -66,7 +66,7 @@ def injector_auth(): RequestHandlerModule, ServiceModule, StateMachineModule, - UtilitiesModule, + SchedulingUtilitiesModule, ] ) diff --git a/tests/integration/turtlebot/test_successful_mission.py b/tests/integration/turtlebot/test_successful_mission.py index fadd51b2..89e340f5 100644 --- a/tests/integration/turtlebot/test_successful_mission.py +++ b/tests/integration/turtlebot/test_successful_mission.py @@ -23,7 +23,7 @@ RobotModule, ServiceModule, StateMachineModule, - UtilitiesModule, + SchedulingUtilitiesModule, ) from isar.services.readers.base_reader import BaseReader from isar.state_machine.states_enum import States @@ -57,7 +57,7 @@ def injector_turtlebot(): StateMachineModule, LocalPlannerModule, LocalStorageModule, - UtilitiesModule, + SchedulingUtilitiesModule, MockMqttModule, ] ) diff --git a/tests/mocks/robot_interface.py b/tests/mocks/robot_interface.py index 10dd5811..b27ecd12 100644 --- a/tests/mocks/robot_interface.py +++ b/tests/mocks/robot_interface.py @@ -6,6 +6,8 @@ from alitra import Frame, Orientation, Pose, Position +from isar.apis.models.models import MediaConfig +from isar.apis.models.media_connection_type import MediaConnectionType from robot_interface.models.initialize import InitializeParams from robot_interface.models.inspection.inspection import ( Image, @@ -62,6 +64,11 @@ def get_inspection(self, task: InspectionTask) -> Inspection: image.data = b"Some binary image data" return image + def generate_media_config(self) -> MediaConfig: + return MediaConfig( + url="", token="", media_connection_type=MediaConnectionType.LiveKit + ) + def register_inspection_callback( self, callback_function: Callable[[Inspection, Mission], None] ) -> None: