isar 1.32.3__py3-none-any.whl → 1.33.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.
- isar/apis/models/models.py +6 -0
- isar/apis/schedule/scheduling_controller.py +21 -52
- isar/config/open_telemetry.py +52 -12
- isar/config/settings.py +17 -3
- isar/eventhandlers/eventhandler.py +22 -0
- isar/models/events.py +56 -27
- isar/robot/robot_status.py +3 -0
- isar/services/utilities/scheduling_utilities.py +52 -21
- isar/state_machine/state_machine.py +39 -1
- isar/state_machine/states/await_next_mission.py +3 -1
- isar/state_machine/states/home.py +3 -1
- isar/state_machine/states/monitor.py +22 -0
- isar/state_machine/states/recharging.py +44 -0
- isar/state_machine/states/returning_home.py +15 -1
- isar/state_machine/states/robot_standing_still.py +3 -1
- isar/state_machine/states/stopping.py +33 -0
- isar/state_machine/states_enum.py +1 -0
- isar/state_machine/transitions/functions/pause.py +1 -1
- isar/state_machine/transitions/functions/resume.py +1 -1
- isar/state_machine/transitions/functions/start_mission.py +11 -3
- isar/state_machine/transitions/functions/stop.py +3 -30
- isar/state_machine/transitions/mission.py +0 -2
- isar/state_machine/transitions/return_home.py +11 -1
- isar/state_machine/transitions/robot_status.py +10 -0
- isar/state_machine/utils/common_event_handlers.py +16 -4
- {isar-1.32.3.dist-info → isar-1.33.1.dist-info}/METADATA +1 -1
- {isar-1.32.3.dist-info → isar-1.33.1.dist-info}/RECORD +35 -34
- robot_interface/models/inspection/inspection.py +6 -15
- robot_interface/models/mission/status.py +1 -0
- robot_interface/robot_interface.py +27 -0
- robot_interface/telemetry/payloads.py +10 -0
- {isar-1.32.3.dist-info → isar-1.33.1.dist-info}/WHEEL +0 -0
- {isar-1.32.3.dist-info → isar-1.33.1.dist-info}/entry_points.txt +0 -0
- {isar-1.32.3.dist-info → isar-1.33.1.dist-info}/licenses/LICENSE +0 -0
- {isar-1.32.3.dist-info → isar-1.33.1.dist-info}/top_level.txt +0 -0
isar/apis/models/models.py
CHANGED
|
@@ -26,6 +26,12 @@ class ControlMissionResponse(BaseModel):
|
|
|
26
26
|
task_status: Optional[str]
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
class MissionStartResponse(BaseModel):
|
|
30
|
+
mission_id: Optional[str] = None
|
|
31
|
+
mission_started: bool
|
|
32
|
+
mission_not_started_reason: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
|
|
29
35
|
class RobotInfoResponse(BaseModel):
|
|
30
36
|
robot_package: str
|
|
31
37
|
isar_id: str
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from http import HTTPStatus
|
|
3
|
-
from threading import Lock
|
|
4
3
|
|
|
5
4
|
from fastapi import Body, HTTPException, Path
|
|
6
5
|
|
|
@@ -29,7 +28,6 @@ class SchedulingController:
|
|
|
29
28
|
):
|
|
30
29
|
self.scheduling_utilities: SchedulingUtilities = scheduling_utilities
|
|
31
30
|
self.logger = logging.getLogger("api")
|
|
32
|
-
self.start_mission_lock: Lock = Lock()
|
|
33
31
|
|
|
34
32
|
def start_mission_by_id(
|
|
35
33
|
self,
|
|
@@ -77,67 +75,38 @@ class SchedulingController:
|
|
|
77
75
|
detail=error_message_no_mission_definition,
|
|
78
76
|
)
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"Conflict - Another mission is currently being started"
|
|
83
|
-
)
|
|
84
|
-
self.logger.warning(error_message_another_mission_starting)
|
|
85
|
-
raise HTTPException(
|
|
86
|
-
status_code=HTTPStatus.CONFLICT,
|
|
87
|
-
detail=error_message_another_mission_starting,
|
|
88
|
-
)
|
|
78
|
+
state: States = self.scheduling_utilities.get_state()
|
|
79
|
+
self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state)
|
|
89
80
|
|
|
90
81
|
try:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
state
|
|
82
|
+
mission: Mission = to_isar_mission(
|
|
83
|
+
start_mission_definition=mission_definition
|
|
94
84
|
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
error_message = f"Bad Request - Cannot create ISAR mission: {e}"
|
|
102
|
-
self.logger.warning(error_message)
|
|
103
|
-
raise HTTPException(
|
|
104
|
-
status_code=HTTPStatus.BAD_REQUEST,
|
|
105
|
-
detail=error_message,
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
self.scheduling_utilities.verify_robot_capable_of_mission(
|
|
109
|
-
mission=mission, robot_capabilities=robot_settings.CAPABILITIES
|
|
85
|
+
except MissionPlannerError as e:
|
|
86
|
+
error_message = f"Bad Request - Cannot create ISAR mission: {e}"
|
|
87
|
+
self.logger.warning(error_message)
|
|
88
|
+
raise HTTPException(
|
|
89
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
90
|
+
detail=error_message,
|
|
110
91
|
)
|
|
111
92
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
93
|
+
self.scheduling_utilities.verify_robot_capable_of_mission(
|
|
94
|
+
mission=mission, robot_capabilities=robot_settings.CAPABILITIES
|
|
95
|
+
)
|
|
115
96
|
|
|
116
|
-
|
|
117
|
-
|
|
97
|
+
self.logger.info("Starting mission: %s", mission.id)
|
|
98
|
+
self.scheduling_utilities.start_mission(mission=mission)
|
|
99
|
+
return self._api_response(mission)
|
|
118
100
|
|
|
119
101
|
def return_home(self) -> None:
|
|
120
102
|
self.logger.info("Received request to return home")
|
|
121
103
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
self.logger.warning(error_message_another_mission_starting)
|
|
127
|
-
raise HTTPException(
|
|
128
|
-
status_code=HTTPStatus.CONFLICT,
|
|
129
|
-
detail=error_message_another_mission_starting,
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
try:
|
|
133
|
-
state: States = self.scheduling_utilities.get_state()
|
|
134
|
-
self.scheduling_utilities.verify_state_machine_ready_to_receive_return_home_mission(
|
|
135
|
-
state
|
|
136
|
-
)
|
|
104
|
+
state: States = self.scheduling_utilities.get_state()
|
|
105
|
+
self.scheduling_utilities.verify_state_machine_ready_to_receive_return_home_mission(
|
|
106
|
+
state
|
|
107
|
+
)
|
|
137
108
|
|
|
138
|
-
|
|
139
|
-
finally:
|
|
140
|
-
self.start_mission_lock.release()
|
|
109
|
+
self.scheduling_utilities.return_home()
|
|
141
110
|
|
|
142
111
|
def pause_mission(self) -> ControlMissionResponse:
|
|
143
112
|
self.logger.info("Received request to pause current mission")
|
isar/config/open_telemetry.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from urllib.parse import urljoin
|
|
2
3
|
|
|
3
4
|
from azure.monitor.opentelemetry.exporter import (
|
|
4
5
|
AzureMonitorLogExporter,
|
|
@@ -7,6 +8,12 @@ from azure.monitor.opentelemetry.exporter import (
|
|
|
7
8
|
from fastapi import FastAPI
|
|
8
9
|
from opentelemetry import trace
|
|
9
10
|
from opentelemetry._logs import set_logger_provider
|
|
11
|
+
from opentelemetry.exporter.otlp.proto.http._log_exporter import (
|
|
12
|
+
OTLPLogExporter as OTLPHttpLogExporter,
|
|
13
|
+
)
|
|
14
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
15
|
+
OTLPSpanExporter as OTLPHttpSpanExporter,
|
|
16
|
+
)
|
|
10
17
|
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
11
18
|
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
|
12
19
|
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
|
@@ -17,22 +24,45 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
|
17
24
|
from isar.config.log import load_log_config
|
|
18
25
|
from isar.config.settings import settings
|
|
19
26
|
|
|
27
|
+
logging.getLogger("opentelemetry.sdk").setLevel(logging.CRITICAL)
|
|
28
|
+
|
|
20
29
|
|
|
21
30
|
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
31
|
|
|
26
|
-
service_name = settings.
|
|
32
|
+
service_name = settings.ROBOT_NAME
|
|
27
33
|
resource = Resource.create({SERVICE_NAME: service_name})
|
|
28
34
|
|
|
29
35
|
tracer_provider = TracerProvider(resource=resource)
|
|
30
|
-
tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter))
|
|
31
|
-
trace.set_tracer_provider(tracer_provider)
|
|
32
|
-
|
|
33
36
|
log_provider = LoggerProvider(resource=resource)
|
|
37
|
+
|
|
38
|
+
if settings.LOG_HANDLER_APPLICATION_INSIGHTS_ENABLED:
|
|
39
|
+
print("[OTEL] Azure Monitor exporters enabled")
|
|
40
|
+
azure_monitor_trace_exporter, azure_monitor_log_exporter = (
|
|
41
|
+
get_azure_monitor_exporters()
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
tracer_provider.add_span_processor(
|
|
45
|
+
BatchSpanProcessor(azure_monitor_trace_exporter)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
log_provider.add_log_record_processor(
|
|
49
|
+
BatchLogRecordProcessor(azure_monitor_log_exporter)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
otlp_exporter_endpoint = settings.OPEN_TELEMETRY_OTLP_EXPORTER_ENDPOINT
|
|
53
|
+
if otlp_exporter_endpoint:
|
|
54
|
+
print(f"[OTEL] OTLP exporters enabled, endpoint={otlp_exporter_endpoint}")
|
|
55
|
+
otlp_trace_exporter, otlp_log_exporter = get_otlp_exporters(
|
|
56
|
+
otlp_exporter_endpoint
|
|
57
|
+
)
|
|
58
|
+
tracer_provider.add_span_processor(BatchSpanProcessor(otlp_trace_exporter))
|
|
59
|
+
|
|
60
|
+
log_provider.add_log_record_processor(
|
|
61
|
+
BatchLogRecordProcessor(otlp_log_exporter)
|
|
62
|
+
)
|
|
63
|
+
|
|
34
64
|
set_logger_provider(log_provider)
|
|
35
|
-
|
|
65
|
+
trace.set_tracer_provider(tracer_provider)
|
|
36
66
|
|
|
37
67
|
handler = LoggingHandler(logger_provider=log_provider)
|
|
38
68
|
attach_loggers_for_open_telemetry(handler)
|
|
@@ -51,12 +81,22 @@ def attach_loggers_for_open_telemetry(handler: LoggingHandler):
|
|
|
51
81
|
def get_azure_monitor_exporters() -> (
|
|
52
82
|
tuple[AzureMonitorTraceExporter, AzureMonitorLogExporter]
|
|
53
83
|
):
|
|
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
84
|
connection_string = settings.APPLICATIONINSIGHTS_CONNECTION_STRING
|
|
59
85
|
trace_exporter = AzureMonitorTraceExporter(connection_string=connection_string)
|
|
60
86
|
log_exporter = AzureMonitorLogExporter(connection_string=connection_string)
|
|
61
87
|
|
|
62
88
|
return trace_exporter, log_exporter
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_otlp_exporters(
|
|
92
|
+
endpoint: str,
|
|
93
|
+
) -> tuple[OTLPHttpSpanExporter, OTLPHttpLogExporter]:
|
|
94
|
+
base = endpoint.rstrip("/") + "/"
|
|
95
|
+
trace_ep = urljoin(base, "v1/traces")
|
|
96
|
+
log_ep = urljoin(base, "v1/logs")
|
|
97
|
+
|
|
98
|
+
print("[OTEL] Using HTTP/Protobuf protocol for OpenTelemetry export")
|
|
99
|
+
print(f"[OTEL] traces → {trace_ep}")
|
|
100
|
+
print(f"[OTEL] logs → {log_ep}")
|
|
101
|
+
|
|
102
|
+
return OTLPHttpSpanExporter(endpoint=trace_ep), OTLPHttpLogExporter(endpoint=log_ep)
|
isar/config/settings.py
CHANGED
|
@@ -12,8 +12,9 @@ from robot_interface.telemetry.payloads import DocumentInfo
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Settings(BaseSettings):
|
|
15
|
-
#
|
|
16
|
-
|
|
15
|
+
# Endpoint open telemetry will export telemetry in the otlp protocol to
|
|
16
|
+
OPEN_TELEMETRY_OTLP_EXPORTER_ENDPOINT: str = Field(default="http://localhost:4318")
|
|
17
|
+
|
|
17
18
|
# Connection string for Azure Application Insights
|
|
18
19
|
# This is optional and it will use managed identity if not set
|
|
19
20
|
APPLICATIONINSIGHTS_CONNECTION_STRING: str = Field(default="")
|
|
@@ -29,7 +30,7 @@ class Settings(BaseSettings):
|
|
|
29
30
|
REQUEST_TIMEOUT: int = Field(default=30)
|
|
30
31
|
|
|
31
32
|
# Timeout in seconds for checking whether there is a message on a queue
|
|
32
|
-
QUEUE_TIMEOUT: int = Field(default=
|
|
33
|
+
QUEUE_TIMEOUT: int = Field(default=3)
|
|
33
34
|
|
|
34
35
|
# Sleep time for while loops in the finite state machine in seconds
|
|
35
36
|
# The sleep is used to throttle the system on every iteration in the loop
|
|
@@ -80,6 +81,15 @@ class Settings(BaseSettings):
|
|
|
80
81
|
ROBOT_API_STATUS_POLL_INTERVAL: float = Field(default=5)
|
|
81
82
|
THREAD_CHECK_INTERVAL: float = Field(default=0.01)
|
|
82
83
|
|
|
84
|
+
# Determines the minimum battery level the robot must have to start a mission
|
|
85
|
+
# If it drops below this level it will recharge to the value set by
|
|
86
|
+
# ROBOT_BATTERY_RECHARGE_THRESHOLD before starting new missions
|
|
87
|
+
ROBOT_MISSION_BATTERY_START_THRESHOLD: float = Field(default=25.0)
|
|
88
|
+
|
|
89
|
+
# Determines the minimum battery threshold to consider the robot recharged
|
|
90
|
+
# and ready for more missions, after having run low on charge
|
|
91
|
+
ROBOT_BATTERY_RECHARGE_THRESHOLD: float = Field(default=80.0)
|
|
92
|
+
|
|
83
93
|
# FastAPI host
|
|
84
94
|
API_HOST_VIEWED_EXTERNALLY: str = Field(default="0.0.0.0")
|
|
85
95
|
|
|
@@ -215,6 +225,9 @@ class Settings(BaseSettings):
|
|
|
215
225
|
# List of MQTT Topics
|
|
216
226
|
TOPIC_ISAR_STATUS: str = Field(default="status", validate_default=True)
|
|
217
227
|
TOPIC_ISAR_MISSION: str = Field(default="mission", validate_default=True)
|
|
228
|
+
TOPIC_ISAR_MISSION_ABORTED: str = Field(
|
|
229
|
+
default="aborted_mission", validate_default=True
|
|
230
|
+
)
|
|
218
231
|
TOPIC_ISAR_TASK: str = Field(default="task", validate_default=True)
|
|
219
232
|
TOPIC_ISAR_INSPECTION_RESULT: str = Field(
|
|
220
233
|
default="inspection_result", validate_default=True
|
|
@@ -282,6 +295,7 @@ class Settings(BaseSettings):
|
|
|
282
295
|
"TOPIC_ISAR_INSPECTION_VALUE",
|
|
283
296
|
"TOPIC_ISAR_STARTUP",
|
|
284
297
|
"TOPIC_ISAR_INTERVENTION_NEEDED",
|
|
298
|
+
"TOPIC_ISAR_MISSION_ABORTED",
|
|
285
299
|
)
|
|
286
300
|
@classmethod
|
|
287
301
|
def prefix_isar_topics(cls, v: Any, info: ValidationInfo):
|
|
@@ -58,6 +58,28 @@ class EventHandlerBase(State):
|
|
|
58
58
|
def stop(self) -> None:
|
|
59
59
|
return
|
|
60
60
|
|
|
61
|
+
def get_event_handler_by_name(
|
|
62
|
+
self, event_handler_name: str
|
|
63
|
+
) -> Optional[EventHandlerMapping]:
|
|
64
|
+
filtered_handlers = list(
|
|
65
|
+
filter(
|
|
66
|
+
lambda mapping: mapping.name == event_handler_name,
|
|
67
|
+
self.event_handler_mappings,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
return filtered_handlers[0] if len(filtered_handlers) > 0 else None
|
|
71
|
+
|
|
72
|
+
def get_event_timer_by_name(
|
|
73
|
+
self, event_timer_name: str
|
|
74
|
+
) -> Optional[TimeoutHandlerMapping]:
|
|
75
|
+
filtered_timers = list(
|
|
76
|
+
filter(
|
|
77
|
+
lambda mapping: mapping.name == event_timer_name,
|
|
78
|
+
self.timers,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return filtered_timers[0] if len(filtered_timers) > 0 else None
|
|
82
|
+
|
|
61
83
|
def _run(self) -> None:
|
|
62
84
|
should_exit_state: bool = False
|
|
63
85
|
timers = deepcopy(self.timers)
|
isar/models/events.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Generic, Optional, TypeVar
|
|
|
4
4
|
|
|
5
5
|
from transitions import State
|
|
6
6
|
|
|
7
|
-
from isar.apis.models.models import ControlMissionResponse
|
|
7
|
+
from isar.apis.models.models import ControlMissionResponse, MissionStartResponse
|
|
8
8
|
from isar.config.settings import settings
|
|
9
9
|
from robot_interface.models.exceptions.robot_exceptions import ErrorMessage
|
|
10
10
|
from robot_interface.models.mission.mission import Mission
|
|
@@ -17,11 +17,19 @@ T2 = TypeVar("T2")
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class Event(Queue[T]):
|
|
20
|
-
def __init__(self) -> None:
|
|
20
|
+
def __init__(self, name: str) -> None:
|
|
21
21
|
super().__init__(maxsize=1)
|
|
22
|
+
self.name = name
|
|
22
23
|
|
|
23
|
-
def trigger_event(self, data: T) -> None:
|
|
24
|
-
|
|
24
|
+
def trigger_event(self, data: T, timeout: int = None) -> None:
|
|
25
|
+
try:
|
|
26
|
+
# We always want a timeout when blocking for results, so that
|
|
27
|
+
# the thread will never get stuck waiting for a result
|
|
28
|
+
self.put(data, block=timeout is not None, timeout=timeout)
|
|
29
|
+
except Exception:
|
|
30
|
+
if timeout is not None:
|
|
31
|
+
raise EventTimeoutError
|
|
32
|
+
return None
|
|
25
33
|
|
|
26
34
|
def consume_event(self, timeout: int = None) -> Optional[T]:
|
|
27
35
|
try:
|
|
@@ -74,46 +82,67 @@ class APIEvent(Generic[T1, T2]):
|
|
|
74
82
|
api to state machine while the response is from state machine to api.
|
|
75
83
|
"""
|
|
76
84
|
|
|
77
|
-
def __init__(self):
|
|
78
|
-
self.request: Event[T1] = Event()
|
|
79
|
-
self.response: Event[T2] = Event()
|
|
85
|
+
def __init__(self, name: str):
|
|
86
|
+
self.request: Event[T1] = Event("api-" + name + "-request")
|
|
87
|
+
self.response: Event[T2] = Event("api-" + name + "-request")
|
|
80
88
|
|
|
81
89
|
|
|
82
90
|
class APIRequests:
|
|
83
91
|
def __init__(self) -> None:
|
|
84
|
-
self.start_mission: APIEvent[Mission,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
self.
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
self.start_mission: APIEvent[Mission, MissionStartResponse] = APIEvent(
|
|
93
|
+
"start_mission"
|
|
94
|
+
)
|
|
95
|
+
self.stop_mission: APIEvent[str, ControlMissionResponse] = APIEvent(
|
|
96
|
+
"stop_mission"
|
|
97
|
+
)
|
|
98
|
+
self.pause_mission: APIEvent[bool, ControlMissionResponse] = APIEvent(
|
|
99
|
+
"pause_mission"
|
|
100
|
+
)
|
|
101
|
+
self.resume_mission: APIEvent[bool, ControlMissionResponse] = APIEvent(
|
|
102
|
+
"resume_mission"
|
|
103
|
+
)
|
|
104
|
+
self.return_home: APIEvent[bool, bool] = APIEvent("return_home")
|
|
105
|
+
self.release_intervention_needed: APIEvent[bool, bool] = APIEvent(
|
|
106
|
+
"release_intervention_needed"
|
|
107
|
+
)
|
|
90
108
|
|
|
91
109
|
|
|
92
110
|
class StateMachineEvents:
|
|
93
111
|
def __init__(self) -> None:
|
|
94
|
-
self.start_mission: Event[Mission] = Event()
|
|
95
|
-
self.stop_mission: Event[bool] = Event()
|
|
96
|
-
self.pause_mission: Event[bool] = Event()
|
|
97
|
-
self.task_status_request: Event[str] = Event()
|
|
112
|
+
self.start_mission: Event[Mission] = Event("start_mission")
|
|
113
|
+
self.stop_mission: Event[bool] = Event("stop_mission")
|
|
114
|
+
self.pause_mission: Event[bool] = Event("pause_mission")
|
|
115
|
+
self.task_status_request: Event[str] = Event("task_status_request")
|
|
98
116
|
|
|
99
117
|
|
|
100
118
|
class RobotServiceEvents:
|
|
101
119
|
def __init__(self) -> None:
|
|
102
|
-
self.task_status_updated: Event[TaskStatus] = Event()
|
|
103
|
-
self.task_status_failed: Event[ErrorMessage] = Event()
|
|
104
|
-
self.mission_started: Event[bool] = Event()
|
|
105
|
-
self.mission_failed: Event[ErrorMessage] = Event()
|
|
106
|
-
self.robot_status_changed: Event[bool] = Event()
|
|
107
|
-
self.mission_failed_to_stop: Event[ErrorMessage] = Event(
|
|
108
|
-
|
|
120
|
+
self.task_status_updated: Event[TaskStatus] = Event("task_status_updated")
|
|
121
|
+
self.task_status_failed: Event[ErrorMessage] = Event("task_status_failed")
|
|
122
|
+
self.mission_started: Event[bool] = Event("mission_started")
|
|
123
|
+
self.mission_failed: Event[ErrorMessage] = Event("mission_failed")
|
|
124
|
+
self.robot_status_changed: Event[bool] = Event("robot_status_changed")
|
|
125
|
+
self.mission_failed_to_stop: Event[ErrorMessage] = Event(
|
|
126
|
+
"mission_failed_to_stop"
|
|
127
|
+
)
|
|
128
|
+
self.mission_successfully_stopped: Event[bool] = Event(
|
|
129
|
+
"mission_successfully_stopped"
|
|
130
|
+
)
|
|
109
131
|
|
|
110
132
|
|
|
111
133
|
class SharedState:
|
|
112
134
|
def __init__(self) -> None:
|
|
113
|
-
self.state: Event[State] = Event()
|
|
114
|
-
self.robot_status: Event[RobotStatus] = Event()
|
|
115
|
-
self.state_machine_current_task: Event[TASKS] = Event(
|
|
135
|
+
self.state: Event[State] = Event("state")
|
|
136
|
+
self.robot_status: Event[RobotStatus] = Event("robot_status")
|
|
137
|
+
self.state_machine_current_task: Event[TASKS] = Event(
|
|
138
|
+
"state_machine_current_task"
|
|
139
|
+
)
|
|
140
|
+
self.robot_battery_level: Event[float] = Event("robot_battery_level")
|
|
116
141
|
|
|
117
142
|
|
|
118
143
|
class EventTimeoutError(Exception):
|
|
119
144
|
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class EventConflictError(Exception):
|
|
148
|
+
pass
|
isar/robot/robot_status.py
CHANGED
|
@@ -48,7 +48,10 @@ class RobotStatusThread(Thread):
|
|
|
48
48
|
self.last_robot_status_poll_time = time.time()
|
|
49
49
|
|
|
50
50
|
robot_status = self.robot.robot_status()
|
|
51
|
+
robot_battery_level = self.robot.get_battery_level()
|
|
52
|
+
|
|
51
53
|
self.shared_state.robot_status.update(robot_status)
|
|
54
|
+
self.shared_state.robot_battery_level.update(robot_battery_level)
|
|
52
55
|
except RobotException as e:
|
|
53
56
|
self.logger.error(f"Failed to retrieve robot status: {e}")
|
|
54
57
|
continue
|
|
@@ -16,6 +16,7 @@ from isar.mission_planner.mission_planner_interface import (
|
|
|
16
16
|
from isar.models.events import (
|
|
17
17
|
APIEvent,
|
|
18
18
|
APIRequests,
|
|
19
|
+
EventConflictError,
|
|
19
20
|
Events,
|
|
20
21
|
EventTimeoutError,
|
|
21
22
|
SharedState,
|
|
@@ -176,16 +177,28 @@ class SchedulingUtilities:
|
|
|
176
177
|
If there is a timeout while communicating with the state machine
|
|
177
178
|
"""
|
|
178
179
|
try:
|
|
179
|
-
self._send_command(
|
|
180
|
+
mission_start_response = self._send_command(
|
|
180
181
|
deepcopy(mission),
|
|
181
182
|
self.api_events.start_mission,
|
|
182
183
|
)
|
|
184
|
+
if not mission_start_response.mission_started:
|
|
185
|
+
self.logger.warning(
|
|
186
|
+
f"Mission failed to start - {mission_start_response.mission_not_started_reason}"
|
|
187
|
+
)
|
|
188
|
+
raise HTTPException(
|
|
189
|
+
status_code=HTTPStatus.CONFLICT,
|
|
190
|
+
detail=mission_start_response.mission_not_started_reason,
|
|
191
|
+
)
|
|
192
|
+
except EventConflictError:
|
|
193
|
+
error_message = "Previous mission request is still being processed"
|
|
194
|
+
self.logger.warning(error_message)
|
|
195
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
183
196
|
except EventTimeoutError:
|
|
184
|
-
error_message =
|
|
185
|
-
|
|
186
|
-
raise HTTPException(
|
|
187
|
-
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
|
|
197
|
+
error_message = (
|
|
198
|
+
"State machine has entered a state which cannot start a mission"
|
|
188
199
|
)
|
|
200
|
+
self.logger.warning(error_message)
|
|
201
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
189
202
|
self.logger.info("OK - Mission started in ISAR")
|
|
190
203
|
|
|
191
204
|
def return_home(
|
|
@@ -203,14 +216,14 @@ class SchedulingUtilities:
|
|
|
203
216
|
True,
|
|
204
217
|
self.api_events.return_home,
|
|
205
218
|
)
|
|
219
|
+
except EventConflictError:
|
|
220
|
+
error_message = "Previous return home request is still being processed"
|
|
221
|
+
self.logger.warning(error_message)
|
|
222
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
206
223
|
except EventTimeoutError:
|
|
207
|
-
error_message =
|
|
208
|
-
|
|
209
|
-
)
|
|
210
|
-
self.logger.error(error_message)
|
|
211
|
-
raise HTTPException(
|
|
212
|
-
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
|
|
213
|
-
)
|
|
224
|
+
error_message = "State machine has entered a state which cannot start a return home mission"
|
|
225
|
+
self.logger.warning(error_message)
|
|
226
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
214
227
|
self.logger.info("OK - Return home mission started in ISAR")
|
|
215
228
|
|
|
216
229
|
def pause_mission(self) -> ControlMissionResponse:
|
|
@@ -225,12 +238,16 @@ class SchedulingUtilities:
|
|
|
225
238
|
response = self._send_command(True, self.api_events.pause_mission)
|
|
226
239
|
self.logger.info("OK - Mission successfully paused")
|
|
227
240
|
return response
|
|
241
|
+
except EventConflictError:
|
|
242
|
+
error_message = "Previous pause mission request is still being processed"
|
|
243
|
+
self.logger.warning(error_message)
|
|
244
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
228
245
|
except EventTimeoutError:
|
|
229
|
-
error_message =
|
|
230
|
-
|
|
231
|
-
raise HTTPException(
|
|
232
|
-
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
|
|
246
|
+
error_message = (
|
|
247
|
+
"State machine has entered a state which cannot pause a mission"
|
|
233
248
|
)
|
|
249
|
+
self.logger.warning(error_message)
|
|
250
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
234
251
|
|
|
235
252
|
def resume_mission(self) -> ControlMissionResponse:
|
|
236
253
|
"""Resume mission
|
|
@@ -244,6 +261,10 @@ class SchedulingUtilities:
|
|
|
244
261
|
response = self._send_command(True, self.api_events.resume_mission)
|
|
245
262
|
self.logger.info("OK - Mission successfully resumed")
|
|
246
263
|
return response
|
|
264
|
+
except EventConflictError:
|
|
265
|
+
error_message = "Previous resume mission request is still being processed"
|
|
266
|
+
self.logger.warning(error_message)
|
|
267
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
247
268
|
except EventTimeoutError:
|
|
248
269
|
error_message = "Internal Server Error - Failed to resume mission"
|
|
249
270
|
self.logger.error(error_message)
|
|
@@ -281,12 +302,16 @@ class SchedulingUtilities:
|
|
|
281
302
|
raise HTTPException(
|
|
282
303
|
status_code=HTTPStatus.CONFLICT, detail=error_message
|
|
283
304
|
)
|
|
305
|
+
except EventConflictError:
|
|
306
|
+
error_message = "Previous stop mission request is still being processed"
|
|
307
|
+
self.logger.warning(error_message)
|
|
308
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
284
309
|
except EventTimeoutError:
|
|
285
|
-
error_message =
|
|
286
|
-
|
|
287
|
-
raise HTTPException(
|
|
288
|
-
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
|
|
310
|
+
error_message = (
|
|
311
|
+
"State machine has entered a state which cannot stop a mission"
|
|
289
312
|
)
|
|
313
|
+
self.logger.warning(error_message)
|
|
314
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
290
315
|
self.logger.info("OK - Mission successfully stopped")
|
|
291
316
|
return stop_mission_response
|
|
292
317
|
|
|
@@ -311,11 +336,17 @@ class SchedulingUtilities:
|
|
|
311
336
|
)
|
|
312
337
|
|
|
313
338
|
def _send_command(self, input: T1, api_event: APIEvent[T1, T2]) -> T2:
|
|
314
|
-
api_event.request.
|
|
339
|
+
if api_event.request.has_event() or api_event.response.has_event():
|
|
340
|
+
raise EventConflictError("API event has already been sent")
|
|
341
|
+
|
|
315
342
|
try:
|
|
343
|
+
api_event.request.trigger_event(input, timeout=1)
|
|
316
344
|
return api_event.response.consume_event(timeout=self.queue_timeout)
|
|
317
345
|
except EventTimeoutError as e:
|
|
318
346
|
self.logger.error("Queue timed out")
|
|
319
347
|
api_event.request.clear_event()
|
|
320
348
|
self.logger.error("No output received for command to state machine")
|
|
321
349
|
raise e
|
|
350
|
+
finally:
|
|
351
|
+
api_event.request.clear_event()
|
|
352
|
+
api_event.response.clear_event()
|
|
@@ -22,6 +22,7 @@ from isar.state_machine.states.intervention_needed import InterventionNeeded
|
|
|
22
22
|
from isar.state_machine.states.monitor import Monitor
|
|
23
23
|
from isar.state_machine.states.offline import Offline
|
|
24
24
|
from isar.state_machine.states.paused import Paused
|
|
25
|
+
from isar.state_machine.states.recharging import Recharging
|
|
25
26
|
from isar.state_machine.states.returning_home import ReturningHome
|
|
26
27
|
from isar.state_machine.states.robot_standing_still import RobotStandingStill
|
|
27
28
|
from isar.state_machine.states.stopping import Stopping
|
|
@@ -43,6 +44,7 @@ from robot_interface.robot_interface import RobotInterface
|
|
|
43
44
|
from robot_interface.telemetry.mqtt_client import MqttClientInterface
|
|
44
45
|
from robot_interface.telemetry.payloads import (
|
|
45
46
|
InterventionNeededPayload,
|
|
47
|
+
MissionAbortedPayload,
|
|
46
48
|
MissionPayload,
|
|
47
49
|
RobotStatusPayload,
|
|
48
50
|
TaskPayload,
|
|
@@ -108,6 +110,7 @@ class StateMachine(object):
|
|
|
108
110
|
# Status states
|
|
109
111
|
self.offline_state: State = Offline(self)
|
|
110
112
|
self.blocked_protective_stopping_state: State = BlockedProtectiveStop(self)
|
|
113
|
+
self.recharging_state: State = Recharging(self)
|
|
111
114
|
|
|
112
115
|
# Error and special status states
|
|
113
116
|
self.unknown_status_state: State = UnknownStatus(self)
|
|
@@ -124,6 +127,7 @@ class StateMachine(object):
|
|
|
124
127
|
self.blocked_protective_stopping_state,
|
|
125
128
|
self.unknown_status_state,
|
|
126
129
|
self.intervention_needed_state,
|
|
130
|
+
self.recharging_state,
|
|
127
131
|
]
|
|
128
132
|
|
|
129
133
|
self.machine = Machine(
|
|
@@ -189,6 +193,12 @@ class StateMachine(object):
|
|
|
189
193
|
self.current_task = None
|
|
190
194
|
self.send_task_status()
|
|
191
195
|
|
|
196
|
+
def battery_level_is_above_mission_start_threshold(self):
|
|
197
|
+
return (
|
|
198
|
+
not self.shared_state.robot_battery_level.check()
|
|
199
|
+
< settings.ROBOT_MISSION_BATTERY_START_THRESHOLD
|
|
200
|
+
)
|
|
201
|
+
|
|
192
202
|
def update_state(self):
|
|
193
203
|
"""Updates the current state of the state machine."""
|
|
194
204
|
self.current_state = States(self.state) # type: ignore
|
|
@@ -227,6 +237,32 @@ class StateMachine(object):
|
|
|
227
237
|
f"Task: {str(task.id)[:8]} was reported as task.status by the robot"
|
|
228
238
|
)
|
|
229
239
|
|
|
240
|
+
def publish_mission_aborted(self, reason: str, can_be_continued: bool) -> None:
|
|
241
|
+
if not self.mqtt_publisher:
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
if self.current_mission is None:
|
|
245
|
+
self.logger.warning(
|
|
246
|
+
"Could not publish mission aborted message. No ongoing mission."
|
|
247
|
+
)
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
payload: MissionAbortedPayload = MissionAbortedPayload(
|
|
251
|
+
isar_id=settings.ISAR_ID,
|
|
252
|
+
robot_name=settings.ROBOT_NAME,
|
|
253
|
+
mission_id=self.current_mission.id,
|
|
254
|
+
reason=reason,
|
|
255
|
+
can_be_continued=can_be_continued,
|
|
256
|
+
timestamp=datetime.now(timezone.utc),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
self.mqtt_publisher.publish(
|
|
260
|
+
topic=settings.TOPIC_ISAR_MISSION_ABORTED,
|
|
261
|
+
payload=json.dumps(payload, cls=EnhancedJSONEncoder),
|
|
262
|
+
qos=1,
|
|
263
|
+
retain=True,
|
|
264
|
+
)
|
|
265
|
+
|
|
230
266
|
def publish_mission_status(self) -> None:
|
|
231
267
|
if not self.mqtt_publisher:
|
|
232
268
|
return
|
|
@@ -339,6 +375,8 @@ class StateMachine(object):
|
|
|
339
375
|
return RobotStatus.BlockedProtectiveStop
|
|
340
376
|
elif self.current_state == States.InterventionNeeded:
|
|
341
377
|
return RobotStatus.InterventionNeeded
|
|
378
|
+
elif self.current_state == States.Recharging:
|
|
379
|
+
return RobotStatus.Recharging
|
|
342
380
|
else:
|
|
343
381
|
return RobotStatus.Busy
|
|
344
382
|
|
|
@@ -368,7 +406,7 @@ class StateMachine(object):
|
|
|
368
406
|
)
|
|
369
407
|
|
|
370
408
|
def _queue_empty_response(self) -> None:
|
|
371
|
-
self.events.api_requests.stop_mission.response.
|
|
409
|
+
self.events.api_requests.stop_mission.response.trigger_event(
|
|
372
410
|
ControlMissionResponse(
|
|
373
411
|
mission_id="None",
|
|
374
412
|
mission_status="None",
|
|
@@ -25,7 +25,9 @@ class AwaitNextMission(EventHandlerBase):
|
|
|
25
25
|
EventHandlerMapping(
|
|
26
26
|
name="start_mission_event",
|
|
27
27
|
event=events.api_requests.start_mission.request,
|
|
28
|
-
handler=lambda event: start_mission_event_handler(
|
|
28
|
+
handler=lambda event: start_mission_event_handler(
|
|
29
|
+
state_machine, event, events.api_requests.start_mission.response
|
|
30
|
+
),
|
|
29
31
|
),
|
|
30
32
|
EventHandlerMapping(
|
|
31
33
|
name="return_home_event",
|