isar 1.30.5__py3-none-any.whl → 1.31.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of isar might be problematic. Click here for more details.

Files changed (62) hide show
  1. isar/apis/api.py +7 -51
  2. isar/apis/models/models.py +1 -0
  3. isar/apis/models/start_mission_definition.py +4 -0
  4. isar/apis/robot_control/robot_controller.py +0 -2
  5. isar/apis/schedule/scheduling_controller.py +12 -4
  6. isar/config/log.py +8 -29
  7. isar/config/open_telemetry.py +62 -0
  8. isar/config/settings.py +12 -0
  9. isar/eventhandlers/eventhandler.py +93 -0
  10. isar/mission_planner/local_planner.py +0 -3
  11. isar/models/events.py +118 -0
  12. isar/modules.py +1 -1
  13. isar/robot/robot.py +16 -21
  14. isar/robot/robot_start_mission.py +8 -14
  15. isar/robot/robot_status.py +2 -3
  16. isar/robot/robot_stop_mission.py +3 -9
  17. isar/robot/robot_task_status.py +3 -7
  18. isar/script.py +4 -1
  19. isar/services/utilities/robot_utilities.py +0 -3
  20. isar/services/utilities/scheduling_utilities.py +45 -35
  21. isar/state_machine/state_machine.py +79 -11
  22. isar/state_machine/states/await_next_mission.py +46 -11
  23. isar/state_machine/states/blocked_protective_stop.py +24 -15
  24. isar/state_machine/states/home.py +40 -9
  25. isar/state_machine/states/monitor.py +83 -14
  26. isar/state_machine/states/offline.py +25 -13
  27. isar/state_machine/states/paused.py +24 -38
  28. isar/state_machine/states/returning_home.py +75 -14
  29. isar/state_machine/states/robot_standing_still.py +41 -11
  30. isar/state_machine/states/stopping.py +52 -67
  31. isar/state_machine/states/unknown_status.py +37 -64
  32. isar/state_machine/transitions/functions/pause.py +39 -10
  33. isar/state_machine/transitions/functions/resume.py +44 -15
  34. isar/state_machine/transitions/functions/robot_status.py +4 -5
  35. isar/state_machine/transitions/functions/stop.py +3 -12
  36. isar/state_machine/transitions/mission.py +12 -2
  37. isar/state_machine/transitions/return_home.py +1 -1
  38. isar/state_machine/utils/common_event_handlers.py +166 -0
  39. isar/storage/blob_storage.py +0 -2
  40. isar/storage/uploader.py +1 -4
  41. {isar-1.30.5.dist-info → isar-1.31.1.dist-info}/METADATA +7 -4
  42. {isar-1.30.5.dist-info → isar-1.31.1.dist-info}/RECORD +48 -58
  43. robot_interface/models/mission/task.py +0 -3
  44. robot_interface/robot_interface.py +1 -4
  45. isar/models/communication/__init__.py +0 -0
  46. isar/models/communication/message.py +0 -8
  47. isar/models/communication/queues/__init__.py +0 -0
  48. isar/models/communication/queues/events.py +0 -58
  49. isar/models/communication/queues/queue_io.py +0 -12
  50. isar/models/communication/queues/queue_timeout_error.py +0 -2
  51. isar/models/communication/queues/queue_utils.py +0 -38
  52. isar/models/communication/queues/status_queue.py +0 -22
  53. isar/models/mission_metadata/__init__.py +0 -0
  54. isar/services/service_connections/stid/__init__.py +0 -0
  55. isar/services/utilities/queue_utilities.py +0 -39
  56. isar/state_machine/generic_states/idle.py +0 -133
  57. isar/state_machine/generic_states/ongoing_mission.py +0 -294
  58. isar/state_machine/generic_states/robot_unavailable.py +0 -61
  59. {isar-1.30.5.dist-info → isar-1.31.1.dist-info}/WHEEL +0 -0
  60. {isar-1.30.5.dist-info → isar-1.31.1.dist-info}/entry_points.txt +0 -0
  61. {isar-1.30.5.dist-info → isar-1.31.1.dist-info}/licenses/LICENSE +0 -0
  62. {isar-1.30.5.dist-info → isar-1.31.1.dist-info}/top_level.txt +0 -0
isar/apis/api.py CHANGED
@@ -8,35 +8,23 @@ from typing import List, Union
8
8
 
9
9
  import click
10
10
  import uvicorn
11
- from dependency_injector.wiring import inject
12
- from fastapi import FastAPI, Request, Security
11
+ from fastapi import FastAPI, Security
13
12
  from fastapi.middleware.cors import CORSMiddleware
14
13
  from fastapi.routing import APIRouter
15
- from opencensus.ext.azure.trace_exporter import AzureExporter
16
- from opencensus.trace.attributes_helper import COMMON_ATTRIBUTES
17
- from opencensus.trace.samplers import ProbabilitySampler
18
- from opencensus.trace.span import SpanKind
19
- from opencensus.trace.tracer import Tracer
20
14
  from pydantic import AnyHttpUrl
21
15
 
22
16
  from isar.apis.models.models import ControlMissionResponse, StartMissionResponse
23
17
  from isar.apis.robot_control.robot_controller import RobotController
24
18
  from isar.apis.schedule.scheduling_controller import SchedulingController
25
19
  from isar.apis.security.authentication import Authenticator
26
- from isar.config.configuration_error import ConfigurationError
27
- from isar.config.keyvault.keyvault_error import KeyvaultError
28
20
  from isar.config.keyvault.keyvault_service import Keyvault
29
21
  from isar.config.settings import settings
30
22
  from robot_interface.telemetry.mqtt_client import MqttClientInterface
31
23
  from robot_interface.telemetry.payloads import StartUpMessagePayload
32
24
  from robot_interface.utilities.json_service import EnhancedJSONEncoder
33
25
 
34
- HTTP_URL = COMMON_ATTRIBUTES["HTTP_URL"]
35
- HTTP_STATUS_CODE = COMMON_ATTRIBUTES["HTTP_STATUS_CODE"]
36
-
37
26
 
38
27
  class API:
39
- @inject
40
28
  def __init__(
41
29
  self,
42
30
  authenticator: Authenticator,
@@ -45,7 +33,6 @@ class API:
45
33
  keyvault: Keyvault,
46
34
  mqtt_publisher: MqttClientInterface,
47
35
  port: int = settings.API_PORT,
48
- azure_ai_logging_enabled: bool = settings.LOG_HANDLER_APPLICATION_INSIGHTS_ENABLED,
49
36
  ) -> None:
50
37
  self.authenticator: Authenticator = authenticator
51
38
  self.scheduling_controller: SchedulingController = scheduling_controller
@@ -53,7 +40,6 @@ class API:
53
40
  self.keyvault: Keyvault = keyvault
54
41
  self.host: str = "0.0.0.0" # Locking uvicorn to use 0.0.0.0
55
42
  self.port: int = port
56
- self.azure_ai_logging_enabled: bool = azure_ai_logging_enabled
57
43
  self.mqtt_publisher: MqttClientInterface = mqtt_publisher
58
44
 
59
45
  self.logger: Logger = logging.getLogger("api")
@@ -113,9 +99,6 @@ class API:
113
99
  allow_headers=["*"],
114
100
  )
115
101
 
116
- if self.azure_ai_logging_enabled:
117
- self._add_request_logging_middleware(app)
118
-
119
102
  app.include_router(router=self._create_scheduler_router())
120
103
 
121
104
  app.include_router(router=self._create_info_router())
@@ -210,9 +193,15 @@ class API:
210
193
  "description": "Mission succesfully stopped",
211
194
  "model": ControlMissionResponse,
212
195
  },
196
+ HTTPStatus.UNPROCESSABLE_ENTITY.value: {
197
+ "description": "Invalid body - The JSON is incorrect",
198
+ },
213
199
  HTTPStatus.CONFLICT.value: {
214
200
  "description": "Conflict - Invalid command in the current state",
215
201
  },
202
+ HTTPStatus.BAD_REQUEST.value: {
203
+ "description": "Bad request - Robot does not have the capabilities to perform the mission",
204
+ },
216
205
  HTTPStatus.INTERNAL_SERVER_ERROR.value: {
217
206
  "description": "Internal Server Error - Current state of state machine unknown",
218
207
  },
@@ -336,39 +325,6 @@ class API:
336
325
  extra={"color_message": color_message},
337
326
  )
338
327
 
339
- def _add_request_logging_middleware(self, app: FastAPI) -> None:
340
- connection_string: str
341
- try:
342
- connection_string = self.keyvault.get_secret(
343
- "application-insights-connection-string"
344
- ).value
345
- except KeyvaultError:
346
- message: str = (
347
- "Missing connection string for Application Insights in key vault. "
348
- )
349
- self.logger.critical(message)
350
- raise ConfigurationError(message)
351
-
352
- @app.middleware("http")
353
- async def middlewareOpencensus(request: Request, call_next):
354
- tracer = Tracer(
355
- exporter=AzureExporter(connection_string=connection_string),
356
- sampler=ProbabilitySampler(1.0),
357
- )
358
- with tracer.span("main") as span:
359
- span.span_kind = SpanKind.SERVER
360
-
361
- response = await call_next(request)
362
-
363
- tracer.add_attribute_to_current_span(
364
- attribute_key=HTTP_STATUS_CODE, attribute_value=response.status_code
365
- )
366
- tracer.add_attribute_to_current_span(
367
- attribute_key=HTTP_URL, attribute_value=str(request.url)
368
- )
369
-
370
- return response
371
-
372
328
  def _publish_startup_message(self) -> None:
373
329
  if not self.mqtt_publisher:
374
330
  return
@@ -21,6 +21,7 @@ class StartMissionResponse(BaseModel):
21
21
  class ControlMissionResponse(BaseModel):
22
22
  mission_id: Optional[str]
23
23
  mission_status: Optional[str]
24
+ mission_not_found: Optional[bool] = False
24
25
  task_id: Optional[str]
25
26
  task_status: Optional[str]
26
27
 
@@ -58,6 +58,10 @@ class StartMissionDefinition(BaseModel):
58
58
  start_pose: Optional[InputPose] = None
59
59
 
60
60
 
61
+ class StopMissionDefinition(BaseModel):
62
+ mission_id: Optional[str] = None
63
+
64
+
61
65
  def to_isar_mission(
62
66
  start_mission_definition: StartMissionDefinition,
63
67
  ) -> Mission:
@@ -1,6 +1,5 @@
1
1
  import logging
2
2
 
3
- from dependency_injector.wiring import inject
4
3
  from fastapi import HTTPException
5
4
 
6
5
  from isar.apis.models.models import RobotInfoResponse
@@ -10,7 +9,6 @@ from robot_interface.models.robots.media import MediaConfig
10
9
 
11
10
 
12
11
  class RobotController:
13
- @inject
14
12
  def __init__(
15
13
  self,
16
14
  robot_utilities: RobotUtilities,
@@ -2,7 +2,6 @@ import logging
2
2
  from http import HTTPStatus
3
3
  from threading import Lock
4
4
 
5
- from dependency_injector.wiring import inject
6
5
  from fastapi import Body, HTTPException, Path
7
6
 
8
7
  from isar.apis.models.models import (
@@ -12,6 +11,7 @@ from isar.apis.models.models import (
12
11
  )
13
12
  from isar.apis.models.start_mission_definition import (
14
13
  StartMissionDefinition,
14
+ StopMissionDefinition,
15
15
  to_isar_mission,
16
16
  )
17
17
  from isar.config.settings import robot_settings, settings
@@ -23,7 +23,6 @@ from robot_interface.models.mission.task import TASKS, InspectionTask, MoveArm
23
23
 
24
24
 
25
25
  class SchedulingController:
26
- @inject
27
26
  def __init__(
28
27
  self,
29
28
  scheduling_utilities: SchedulingUtilities,
@@ -179,7 +178,16 @@ class SchedulingController:
179
178
  )
180
179
  return resume_mission_response
181
180
 
182
- def stop_mission(self) -> ControlMissionResponse:
181
+ def stop_mission(
182
+ self,
183
+ mission_id: StopMissionDefinition = Body(
184
+ default=None,
185
+ embed=True,
186
+ title="Mission ID to stop",
187
+ description="The mission ID of the mission being stopped, in json format",
188
+ ),
189
+ ) -> ControlMissionResponse:
190
+
183
191
  self.logger.info("Received request to stop current mission")
184
192
 
185
193
  state: States = self.scheduling_utilities.get_state()
@@ -198,7 +206,7 @@ class SchedulingController:
198
206
  raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
199
207
 
200
208
  stop_mission_response: ControlMissionResponse = (
201
- self.scheduling_utilities.stop_mission()
209
+ self.scheduling_utilities.stop_mission(mission_id.mission_id)
202
210
  )
203
211
  return stop_mission_response
204
212
 
isar/config/log.py CHANGED
@@ -3,30 +3,21 @@ import logging.config
3
3
  from importlib.resources import as_file, files
4
4
 
5
5
  import yaml
6
- from opencensus.ext.azure.log_exporter import AzureLogHandler
7
6
  from uvicorn.logging import ColourizedFormatter
8
7
 
9
- from isar.config.configuration_error import ConfigurationError
10
- from isar.config.keyvault.keyvault_error import KeyvaultError
11
8
  from isar.config.keyvault.keyvault_service import Keyvault
12
9
  from isar.config.settings import settings
13
10
 
14
11
 
15
12
  def setup_loggers(keyvault: Keyvault) -> None:
16
13
  log_levels: dict = settings.LOG_LEVELS
17
- source = files("isar").joinpath("config").joinpath("logging.conf")
18
- with as_file(source) as f:
19
- log_config = yaml.safe_load(open(f))
14
+ log_config = load_log_config()
20
15
 
21
16
  logging.config.dictConfig(log_config)
22
17
 
23
18
  handlers = []
24
19
  if settings.LOG_HANDLER_LOCAL_ENABLED:
25
20
  handlers.append(configure_console_handler(log_config=log_config))
26
- if settings.LOG_HANDLER_APPLICATION_INSIGHTS_ENABLED:
27
- handlers.append(
28
- configure_azure_handler(log_config=log_config, keyvault=keyvault)
29
- )
30
21
 
31
22
  for log_handler in handlers:
32
23
  for loggers in log_config["loggers"].keys():
@@ -35,6 +26,13 @@ def setup_loggers(keyvault: Keyvault) -> None:
35
26
  logging.getLogger().addHandler(log_handler)
36
27
 
37
28
 
29
+ def load_log_config():
30
+ source = files("isar").joinpath("config").joinpath("logging.conf")
31
+ with as_file(source) as f:
32
+ log_config = yaml.safe_load(open(f))
33
+ return log_config
34
+
35
+
38
36
  def configure_console_handler(log_config: dict) -> logging.Handler:
39
37
  handler = logging.StreamHandler()
40
38
  handler.setLevel(log_config["root"]["level"])
@@ -46,22 +44,3 @@ def configure_console_handler(log_config: dict) -> logging.Handler:
46
44
  )
47
45
  )
48
46
  return handler
49
-
50
-
51
- def configure_azure_handler(log_config: dict, keyvault: Keyvault) -> logging.Handler:
52
- connection_string: str
53
- try:
54
- connection_string = keyvault.get_secret(
55
- "application-insights-connection-string"
56
- ).value
57
- except KeyvaultError:
58
- message: str = (
59
- "CRITICAL ERROR: Missing connection string for"
60
- f" Application Insights in key vault '{keyvault.name}'."
61
- )
62
- print(f"\n{message} \n")
63
- raise ConfigurationError(message)
64
-
65
- handler = AzureLogHandler(connection_string=connection_string)
66
- handler.setLevel(log_config["root"]["level"])
67
- return handler
@@ -0,0 +1,62 @@
1
+ import logging
2
+
3
+ from azure.monitor.opentelemetry.exporter import (
4
+ AzureMonitorLogExporter,
5
+ AzureMonitorTraceExporter,
6
+ )
7
+ from fastapi import FastAPI
8
+ from opentelemetry import trace
9
+ from opentelemetry._logs import set_logger_provider
10
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
11
+ from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
12
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
13
+ from opentelemetry.sdk.resources import SERVICE_NAME, Resource
14
+ from opentelemetry.sdk.trace import TracerProvider
15
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
16
+
17
+ from isar.config.log import load_log_config
18
+ from isar.config.settings import settings
19
+
20
+
21
+ def setup_open_telemetry(app: FastAPI) -> None:
22
+ if not settings.LOG_HANDLER_APPLICATION_INSIGHTS_ENABLED:
23
+ return
24
+ trace_exporter, log_exporter = get_azure_monitor_exporters()
25
+
26
+ service_name = settings.OPEN_TELEMETRY_SERVICE_NAME
27
+ resource = Resource.create({SERVICE_NAME: service_name})
28
+
29
+ tracer_provider = TracerProvider(resource=resource)
30
+ tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter))
31
+ trace.set_tracer_provider(tracer_provider)
32
+
33
+ log_provider = LoggerProvider(resource=resource)
34
+ set_logger_provider(log_provider)
35
+ log_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
36
+
37
+ handler = LoggingHandler(logger_provider=log_provider)
38
+ attach_loggers_for_open_telemetry(handler)
39
+
40
+ FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer_provider)
41
+
42
+
43
+ def attach_loggers_for_open_telemetry(handler: LoggingHandler):
44
+ log_config = load_log_config()
45
+
46
+ for logger_name in log_config["loggers"].keys():
47
+ logger = logging.getLogger(logger_name)
48
+ logger.addHandler(handler)
49
+
50
+
51
+ def get_azure_monitor_exporters() -> (
52
+ tuple[AzureMonitorTraceExporter, AzureMonitorLogExporter]
53
+ ):
54
+ """
55
+ If connection string is defined in environment variables, then use it to create Azure Monitor Exporters.
56
+ Else use Azure Managed Identity to create Azure Monitor Exporters.
57
+ """
58
+ connection_string = settings.APPLICATIONINSIGHTS_CONNECTION_STRING
59
+ trace_exporter = AzureMonitorTraceExporter(connection_string=connection_string)
60
+ log_exporter = AzureMonitorLogExporter(connection_string=connection_string)
61
+
62
+ return trace_exporter, log_exporter
isar/config/settings.py CHANGED
@@ -12,6 +12,12 @@ from robot_interface.telemetry.payloads import DocumentInfo
12
12
 
13
13
 
14
14
  class Settings(BaseSettings):
15
+ # Name of the OpenTelemetry service
16
+ OPEN_TELEMETRY_SERVICE_NAME: str = Field(default="isar")
17
+ # Connection string for Azure Application Insights
18
+ # This is optional and it will use managed identity if not set
19
+ APPLICATIONINSIGHTS_CONNECTION_STRING: str = Field(default="")
20
+
15
21
  # Determines which robot package ISAR will attempt to import
16
22
  # Name must match with an installed python package in the local environment
17
23
  ROBOT_PACKAGE: str = Field(default="isar_robot")
@@ -52,6 +58,12 @@ class Settings(BaseSettings):
52
58
  # issues
53
59
  REQUEST_STATUS_COMMUNICATION_RECONNECT_DELAY: float = Field(default=10)
54
60
 
61
+ # Number of attempts for state transitions resume and pause if failed
62
+ STATE_TRANSITION_NUM_RETIRES: int = Field(default=10)
63
+
64
+ # Interval between attempt of state transition
65
+ STATE_TRANSITION_RETRY_INTERVAL_SEC: float = Field(default=1)
66
+
55
67
  # Number of attempts to stop the robot before giving up
56
68
  STOP_ROBOT_ATTEMPTS_LIMIT: int = Field(default=3)
57
69
 
@@ -0,0 +1,93 @@
1
+ import logging
2
+ import time
3
+ from copy import deepcopy
4
+ from dataclasses import dataclass
5
+ from threading import Event as ThreadEvent
6
+ from typing import TYPE_CHECKING, Callable, Generic, List, Optional, TypeVar
7
+
8
+ from transitions import State
9
+
10
+ from isar.config.settings import settings
11
+ from isar.models.events import Event
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ @dataclass
17
+ class EventHandlerMapping(Generic[T]):
18
+ name: str
19
+ event: Event[T]
20
+ handler: Callable[[Event[T]], Optional[Callable]]
21
+
22
+
23
+ @dataclass
24
+ class TimeoutHandlerMapping:
25
+ name: str
26
+ timeout_in_seconds: float
27
+ handler: Callable[[], Optional[Callable]]
28
+
29
+
30
+ if TYPE_CHECKING:
31
+ from isar.state_machine.state_machine import StateMachine
32
+
33
+
34
+ class EventHandlerBase(State):
35
+ def __init__(
36
+ self,
37
+ state_machine: "StateMachine",
38
+ state_name: str,
39
+ event_handler_mappings: List[EventHandlerMapping],
40
+ timers: List[TimeoutHandlerMapping] = [],
41
+ ) -> None:
42
+
43
+ super().__init__(name=state_name, on_enter=self.start)
44
+ self.state_machine: "StateMachine" = state_machine
45
+ self.logger = logging.getLogger("state_machine")
46
+ self.events = state_machine.events
47
+ self.signal_state_machine_to_stop: ThreadEvent = (
48
+ state_machine.signal_state_machine_to_stop
49
+ )
50
+ self.event_handler_mappings = event_handler_mappings
51
+ self.state_name: str = state_name
52
+ self.timers = timers
53
+
54
+ def start(self) -> None:
55
+ self.state_machine.update_state()
56
+ self._run()
57
+
58
+ def stop(self) -> None:
59
+ return
60
+
61
+ def _run(self) -> None:
62
+ should_exit_state: bool = False
63
+ timers = deepcopy(self.timers)
64
+ entered_time = time.time()
65
+ while True:
66
+ if self.signal_state_machine_to_stop.is_set():
67
+ self.logger.info(
68
+ "Stopping state machine from %s state", self.state_name
69
+ )
70
+ break
71
+
72
+ for timer in timers:
73
+ if time.time() - entered_time > timer.timeout_in_seconds:
74
+ transition_func = timer.handler()
75
+ timers.remove(timer)
76
+ if transition_func is not None:
77
+ transition_func()
78
+ should_exit_state = True
79
+ break
80
+
81
+ if should_exit_state:
82
+ break
83
+
84
+ for handler_mapping in self.event_handler_mappings:
85
+ transition_func = handler_mapping.handler(handler_mapping.event)
86
+ if transition_func is not None:
87
+ transition_func()
88
+ should_exit_state = True
89
+ break
90
+
91
+ if should_exit_state:
92
+ break
93
+ time.sleep(settings.FSM_SLEEP_TIME)
@@ -2,8 +2,6 @@ import json
2
2
  import logging
3
3
  from pathlib import Path
4
4
 
5
- from dependency_injector.wiring import inject
6
-
7
5
  from isar.config.settings import settings
8
6
  from isar.mission_planner.mission_planner_interface import (
9
7
  MissionNotFoundError,
@@ -16,7 +14,6 @@ logger = logging.getLogger("api")
16
14
 
17
15
 
18
16
  class LocalPlanner(MissionPlannerInterface):
19
- @inject
20
17
  def __init__(self):
21
18
  self.predefined_mission_folder = Path(settings.PREDEFINED_MISSIONS_FOLDER)
22
19
 
isar/models/events.py ADDED
@@ -0,0 +1,118 @@
1
+ from collections import deque
2
+ from queue import Empty, Queue
3
+ from typing import Generic, Optional, TypeVar
4
+
5
+ from transitions import State
6
+
7
+ from isar.apis.models.models import ControlMissionResponse
8
+ from isar.config.settings import settings
9
+ from robot_interface.models.exceptions.robot_exceptions import ErrorMessage
10
+ from robot_interface.models.mission.mission import Mission
11
+ from robot_interface.models.mission.status import RobotStatus, TaskStatus
12
+ from robot_interface.models.mission.task import TASKS
13
+
14
+ T = TypeVar("T")
15
+ T1 = TypeVar("T1")
16
+ T2 = TypeVar("T2")
17
+
18
+
19
+ class Event(Queue[T]):
20
+ def __init__(self) -> None:
21
+ super().__init__(maxsize=1)
22
+
23
+ def trigger_event(self, data: T) -> None:
24
+ self.put(data)
25
+
26
+ def consume_event(self, timeout: int = None) -> Optional[T]:
27
+ try:
28
+ return self.get(block=timeout is not None, timeout=timeout)
29
+ except Empty:
30
+ if timeout is not None:
31
+ raise EventTimeoutError
32
+ return None
33
+
34
+ def clear_event(self) -> None:
35
+ while True:
36
+ try:
37
+ self.get(block=False)
38
+ except Empty:
39
+ break
40
+
41
+ def has_event(self) -> bool:
42
+ return (
43
+ self.qsize() != 0
44
+ ) # Queue size is not reliable, but should be sufficient for this case
45
+
46
+ def check(self) -> Optional[T]:
47
+ if not self._qsize():
48
+ return None
49
+ with self.mutex:
50
+ queueList = list(self.queue)
51
+ return queueList.pop()
52
+
53
+ def update(self, item: T):
54
+ with self.mutex:
55
+ self.queue: deque[T] = deque()
56
+ self.queue.append(item)
57
+
58
+
59
+ class Events:
60
+ def __init__(self) -> None:
61
+ self.api_requests: APIRequests = APIRequests()
62
+ self.state_machine_events: StateMachineEvents = StateMachineEvents()
63
+ self.robot_service_events: RobotServiceEvents = RobotServiceEvents()
64
+
65
+ self.upload_queue: Queue = Queue(maxsize=10)
66
+
67
+ if settings.MQTT_ENABLED:
68
+ self.mqtt_queue: Queue = Queue()
69
+
70
+
71
+ class APIEvent(Generic[T1, T2]):
72
+ """
73
+ Creates input and output event. The events are defined such that the input is from
74
+ api to state machine while the output is from state machine to api.
75
+ """
76
+
77
+ def __init__(self):
78
+ self.input: Event[T1] = Event()
79
+ self.output: Event[T2] = Event()
80
+
81
+
82
+ class APIRequests:
83
+ def __init__(self) -> None:
84
+ self.start_mission: APIEvent[Mission, bool] = APIEvent()
85
+ self.stop_mission: APIEvent[str, ControlMissionResponse] = APIEvent()
86
+ self.pause_mission: APIEvent[bool, ControlMissionResponse] = APIEvent()
87
+ self.resume_mission: APIEvent[bool, ControlMissionResponse] = APIEvent()
88
+ self.return_home: APIEvent[bool, bool] = APIEvent()
89
+
90
+
91
+ class StateMachineEvents:
92
+ def __init__(self) -> None:
93
+ self.start_mission: Event[Mission] = Event()
94
+ self.stop_mission: Event[bool] = Event()
95
+ self.pause_mission: Event[bool] = Event()
96
+ self.task_status_request: Event[str] = Event()
97
+
98
+
99
+ class RobotServiceEvents:
100
+ def __init__(self) -> None:
101
+ self.task_status_updated: Event[TaskStatus] = Event()
102
+ self.task_status_failed: Event[ErrorMessage] = Event()
103
+ self.mission_started: Event[bool] = Event()
104
+ self.mission_failed: Event[ErrorMessage] = Event()
105
+ self.robot_status_changed: Event[bool] = Event()
106
+ self.mission_failed_to_stop: Event[ErrorMessage] = Event()
107
+ self.mission_successfully_stopped: Event[bool] = Event()
108
+
109
+
110
+ class SharedState:
111
+ def __init__(self) -> None:
112
+ self.state: Event[State] = Event()
113
+ self.robot_status: Event[RobotStatus] = Event()
114
+ self.state_machine_current_task: Event[TASKS] = Event()
115
+
116
+
117
+ class EventTimeoutError(Exception):
118
+ pass
isar/modules.py CHANGED
@@ -12,7 +12,7 @@ from isar.config.settings import settings
12
12
  from isar.mission_planner.local_planner import LocalPlanner
13
13
  from isar.mission_planner.sequential_task_selector import SequentialTaskSelector
14
14
  from isar.mission_planner.task_selector_interface import TaskSelectorInterface
15
- from isar.models.communication.queues.events import Events, SharedState
15
+ from isar.models.events import Events, SharedState
16
16
  from isar.robot.robot import Robot
17
17
  from isar.services.utilities.robot_utilities import RobotUtilities
18
18
  from isar.services.utilities.scheduling_utilities import SchedulingUtilities