isar 1.30.1__py3-none-any.whl → 1.30.3__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/config/settings.py +7 -26
- isar/modules.py +0 -7
- isar/robot/robot_start_mission.py +14 -2
- isar/robot/robot_status.py +5 -3
- isar/script.py +0 -1
- isar/state_machine/generic_states/idle.py +133 -0
- isar/state_machine/generic_states/ongoing_mission.py +294 -0
- isar/state_machine/generic_states/robot_unavailable.py +61 -0
- isar/state_machine/state_machine.py +9 -0
- isar/state_machine/states/await_next_mission.py +11 -83
- isar/state_machine/states/blocked_protective_stop.py +13 -39
- isar/state_machine/states/home.py +10 -78
- isar/state_machine/states/monitor.py +10 -242
- isar/state_machine/states/offline.py +11 -36
- isar/state_machine/states/paused.py +1 -1
- isar/state_machine/states/returning_home.py +14 -177
- isar/state_machine/states/robot_standing_still.py +11 -91
- isar/state_machine/transitions/mission.py +1 -1
- {isar-1.30.1.dist-info → isar-1.30.3.dist-info}/METADATA +18 -5
- {isar-1.30.1.dist-info → isar-1.30.3.dist-info}/RECORD +25 -24
- robot_interface/robot_interface.py +15 -7
- isar/config/settings.env +0 -23
- isar/storage/slimm_storage.py +0 -190
- {isar-1.30.1.dist-info → isar-1.30.3.dist-info}/WHEEL +0 -0
- {isar-1.30.1.dist-info → isar-1.30.3.dist-info}/entry_points.txt +0 -0
- {isar-1.30.1.dist-info → isar-1.30.3.dist-info}/licenses/LICENSE +0 -0
- {isar-1.30.1.dist-info → isar-1.30.3.dist-info}/top_level.txt +0 -0
isar/config/settings.py
CHANGED
|
@@ -12,15 +12,6 @@ from robot_interface.telemetry.payloads import DocumentInfo
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Settings(BaseSettings):
|
|
15
|
-
def __init__(self) -> None:
|
|
16
|
-
try:
|
|
17
|
-
source = files("isar").joinpath("config").joinpath("settings.env")
|
|
18
|
-
with as_file(source) as eml:
|
|
19
|
-
env_file = eml
|
|
20
|
-
except ModuleNotFoundError:
|
|
21
|
-
env_file = None
|
|
22
|
-
super().__init__(_env_file=env_file)
|
|
23
|
-
|
|
24
15
|
# Determines which robot package ISAR will attempt to import
|
|
25
16
|
# Name must match with an installed python package in the local environment
|
|
26
17
|
ROBOT_PACKAGE: str = Field(default="isar_robot")
|
|
@@ -75,6 +66,7 @@ class Settings(BaseSettings):
|
|
|
75
66
|
ROBOT_HEARTBEAT_PUBLISH_INTERVAL: float = Field(default=1)
|
|
76
67
|
ROBOT_INFO_PUBLISH_INTERVAL: float = Field(default=5)
|
|
77
68
|
ROBOT_API_STATUS_POLL_INTERVAL: float = Field(default=5)
|
|
69
|
+
THREAD_CHECK_INTERVAL: float = Field(default=0.01)
|
|
78
70
|
|
|
79
71
|
# FastAPI host
|
|
80
72
|
API_HOST_VIEWED_EXTERNALLY: str = Field(default="0.0.0.0")
|
|
@@ -98,7 +90,6 @@ class Settings(BaseSettings):
|
|
|
98
90
|
# Selecting a different storage module than local may require certain access rights
|
|
99
91
|
STORAGE_LOCAL_ENABLED: bool = Field(default=True)
|
|
100
92
|
STORAGE_BLOB_ENABLED: bool = Field(default=False)
|
|
101
|
-
STORAGE_SLIMM_ENABLED: bool = Field(default=False)
|
|
102
93
|
|
|
103
94
|
# Determines whether the MQTT publishing module should be enabled or not
|
|
104
95
|
# The publishing module will attempt to connect to the MQTT broker configured in
|
|
@@ -153,21 +144,6 @@ class Settings(BaseSettings):
|
|
|
153
144
|
# Name of blob container in Azure Blob Storage [slimm test]
|
|
154
145
|
BLOB_CONTAINER: str = Field(default="test")
|
|
155
146
|
|
|
156
|
-
# Client ID for SLIMM App Registration
|
|
157
|
-
SLIMM_CLIENT_ID: str = Field(default="c630ca4d-d8d6-45ab-8cc6-68a363d0de9e")
|
|
158
|
-
|
|
159
|
-
# Scope for access to SLIMM Ingestion API
|
|
160
|
-
SLIMM_APP_SCOPE: str = Field(default=".default")
|
|
161
|
-
|
|
162
|
-
# URL for SLIMM endpoint
|
|
163
|
-
SLIMM_API_URL: str = Field(
|
|
164
|
-
default="https://scinspectioningestapitest.azurewebsites.net/Ingest"
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
# Whether the results should be copied directly into the SLIMM datalake or only the
|
|
168
|
-
# metadata
|
|
169
|
-
COPY_FILES_TO_SLIMM_DATALAKE: bool = Field(default=False)
|
|
170
|
-
|
|
171
147
|
# The configuration of this section is tightly coupled with the metadata that is
|
|
172
148
|
# submitted with the results once they have been uploaded.
|
|
173
149
|
|
|
@@ -298,7 +274,12 @@ class Settings(BaseSettings):
|
|
|
298
274
|
)
|
|
299
275
|
|
|
300
276
|
|
|
301
|
-
|
|
277
|
+
env = os.environ.get("ISAR_ENV")
|
|
278
|
+
|
|
279
|
+
if env == "test":
|
|
280
|
+
load_dotenv(".env.test", override=True)
|
|
281
|
+
else:
|
|
282
|
+
load_dotenv()
|
|
302
283
|
settings = Settings()
|
|
303
284
|
|
|
304
285
|
|
isar/modules.py
CHANGED
|
@@ -14,13 +14,11 @@ from isar.mission_planner.sequential_task_selector import SequentialTaskSelector
|
|
|
14
14
|
from isar.mission_planner.task_selector_interface import TaskSelectorInterface
|
|
15
15
|
from isar.models.communication.queues.events import Events, SharedState
|
|
16
16
|
from isar.robot.robot import Robot
|
|
17
|
-
from isar.services.service_connections.request_handler import RequestHandler
|
|
18
17
|
from isar.services.utilities.robot_utilities import RobotUtilities
|
|
19
18
|
from isar.services.utilities.scheduling_utilities import SchedulingUtilities
|
|
20
19
|
from isar.state_machine.state_machine import StateMachine
|
|
21
20
|
from isar.storage.blob_storage import BlobStorage
|
|
22
21
|
from isar.storage.local_storage import LocalStorage
|
|
23
|
-
from isar.storage.slimm_storage import SlimmStorage
|
|
24
22
|
from isar.storage.uploader import Uploader
|
|
25
23
|
from robot_interface.telemetry.mqtt_client import MqttPublisher
|
|
26
24
|
|
|
@@ -77,11 +75,6 @@ class ApplicationContainer(containers.DeclarativeContainer):
|
|
|
77
75
|
if settings.STORAGE_BLOB_ENABLED:
|
|
78
76
|
blob_storage = providers.Singleton(BlobStorage, keyvault=keyvault)
|
|
79
77
|
storage_handlers_temp.append(blob_storage)
|
|
80
|
-
if settings.STORAGE_SLIMM_ENABLED:
|
|
81
|
-
slimm_storage = providers.Singleton(
|
|
82
|
-
SlimmStorage, request_handler=providers.Singleton(RequestHandler)
|
|
83
|
-
)
|
|
84
|
-
storage_handlers_temp.append(slimm_storage)
|
|
85
78
|
storage_handlers = providers.List(*storage_handlers_temp)
|
|
86
79
|
|
|
87
80
|
# Mqtt client
|
|
@@ -40,6 +40,18 @@ class RobotStartMissionThread(Thread):
|
|
|
40
40
|
return
|
|
41
41
|
try:
|
|
42
42
|
self.robot.initiate_mission(self.mission)
|
|
43
|
+
except RobotInfeasibleMissionException as e:
|
|
44
|
+
self.logger.error(
|
|
45
|
+
f"Mission is infeasible and cannot be scheduled because: {e.error_description}"
|
|
46
|
+
)
|
|
47
|
+
trigger_event(
|
|
48
|
+
self.robot_service_events.mission_failed,
|
|
49
|
+
ErrorMessage(
|
|
50
|
+
error_reason=e.error_reason,
|
|
51
|
+
error_description=e.error_description,
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
break
|
|
43
55
|
except RobotException as e:
|
|
44
56
|
retries += 1
|
|
45
57
|
self.logger.warning(
|
|
@@ -48,7 +60,7 @@ class RobotStartMissionThread(Thread):
|
|
|
48
60
|
)
|
|
49
61
|
|
|
50
62
|
if retries >= settings.INITIATE_FAILURE_COUNTER_LIMIT:
|
|
51
|
-
|
|
63
|
+
self.logger.error(
|
|
52
64
|
f"Mission will be cancelled after failing to initiate "
|
|
53
65
|
f"{settings.INITIATE_FAILURE_COUNTER_LIMIT} times because: "
|
|
54
66
|
f"{e.error_description}"
|
|
@@ -58,7 +70,7 @@ class RobotStartMissionThread(Thread):
|
|
|
58
70
|
self.robot_service_events.mission_failed,
|
|
59
71
|
ErrorMessage(
|
|
60
72
|
error_reason=e.error_reason,
|
|
61
|
-
error_description=error_description,
|
|
73
|
+
error_description=e.error_description,
|
|
62
74
|
),
|
|
63
75
|
)
|
|
64
76
|
break
|
isar/robot/robot_status.py
CHANGED
|
@@ -40,14 +40,16 @@ class RobotStatusThread(Thread):
|
|
|
40
40
|
if self.signal_thread_quitting.is_set():
|
|
41
41
|
return
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
thread_check_interval = settings.THREAD_CHECK_INTERVAL
|
|
44
|
+
|
|
45
|
+
while not self.signal_thread_quitting.wait(thread_check_interval):
|
|
44
46
|
if not self._is_ready_to_poll_for_status():
|
|
45
47
|
continue
|
|
46
48
|
try:
|
|
47
|
-
|
|
49
|
+
self.last_robot_status_poll_time = time.time()
|
|
48
50
|
|
|
51
|
+
robot_status = self.robot.robot_status()
|
|
49
52
|
update_shared_state(self.shared_state.robot_status, robot_status)
|
|
50
|
-
self.last_robot_status_poll_time = time.time()
|
|
51
53
|
except RobotException as e:
|
|
52
54
|
self.logger.error(f"Failed to retrieve robot status: {e}")
|
|
53
55
|
continue
|
isar/script.py
CHANGED
|
@@ -68,7 +68,6 @@ def print_startup_info():
|
|
|
68
68
|
print_setting("Mission planner", settings.MISSION_PLANNER)
|
|
69
69
|
print_setting("Using local storage", settings.STORAGE_LOCAL_ENABLED)
|
|
70
70
|
print_setting("Using blob storage", settings.STORAGE_BLOB_ENABLED)
|
|
71
|
-
print_setting("Using SLIMM storage", settings.STORAGE_SLIMM_ENABLED)
|
|
72
71
|
print_setting("Using async inspection uploading", settings.UPLOAD_INSPECTIONS_ASYNC)
|
|
73
72
|
print_setting("Plant code", settings.PLANT_CODE)
|
|
74
73
|
print_setting("Plant name", settings.PLANT_NAME)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from queue import Queue
|
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
|
6
|
+
|
|
7
|
+
from isar.config.settings import settings
|
|
8
|
+
from isar.models.communication.message import StartMissionMessage
|
|
9
|
+
from isar.models.communication.queues.queue_io import QueueIO
|
|
10
|
+
from isar.models.communication.queues.queue_utils import (
|
|
11
|
+
check_for_event,
|
|
12
|
+
check_shared_state,
|
|
13
|
+
)
|
|
14
|
+
from isar.models.communication.queues.status_queue import StatusQueue
|
|
15
|
+
from robot_interface.models.mission.status import RobotStatus
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from isar.state_machine.state_machine import StateMachine
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IdleStates(str, Enum):
|
|
22
|
+
AwaitNextMission = "awaitNextMission"
|
|
23
|
+
Home = "home"
|
|
24
|
+
RobotStandingStill = "robotStandingStill"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from isar.state_machine.state_machine import StateMachine
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Idle:
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
state_machine: "StateMachine",
|
|
35
|
+
state: IdleStates,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.state_machine: "StateMachine" = state_machine
|
|
38
|
+
self.logger = logging.getLogger("state_machine")
|
|
39
|
+
self.events = state_machine.events
|
|
40
|
+
self.shared_state = self.state_machine.shared_state
|
|
41
|
+
self.signal_state_machine_to_stop = state_machine.signal_state_machine_to_stop
|
|
42
|
+
self.state: IdleStates = state
|
|
43
|
+
|
|
44
|
+
# Only used for await_next_mission state
|
|
45
|
+
self.entered_time: float = time.time()
|
|
46
|
+
self.return_home_delay: float = settings.RETURN_HOME_DELAY
|
|
47
|
+
|
|
48
|
+
def start(self) -> None:
|
|
49
|
+
self.state_machine.update_state()
|
|
50
|
+
self.entered_time = time.time()
|
|
51
|
+
self._run()
|
|
52
|
+
|
|
53
|
+
def stop(self) -> None:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
def _check_and_handle_start_mission_event(
|
|
57
|
+
self, event: Queue[StartMissionMessage]
|
|
58
|
+
) -> bool:
|
|
59
|
+
start_mission: Optional[StartMissionMessage] = check_for_event(event)
|
|
60
|
+
if start_mission:
|
|
61
|
+
self.state_machine.start_mission(mission=start_mission.mission)
|
|
62
|
+
self.state_machine.request_mission_start() # type: ignore
|
|
63
|
+
return True
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
def _check_and_handle_return_home_event(self, event: QueueIO) -> bool:
|
|
67
|
+
if check_for_event(event.input):
|
|
68
|
+
event.output.put(True)
|
|
69
|
+
self.state_machine.request_return_home() # type: ignore
|
|
70
|
+
return True
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
def _check_and_handle_robot_status_event(
|
|
74
|
+
self, event: StatusQueue[RobotStatus]
|
|
75
|
+
) -> bool:
|
|
76
|
+
robot_status: RobotStatus = check_shared_state(event)
|
|
77
|
+
|
|
78
|
+
expected_robot_status = (
|
|
79
|
+
RobotStatus.Home if self.state == IdleStates.Home else RobotStatus.Available
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if robot_status != expected_robot_status:
|
|
83
|
+
self.state_machine.robot_status_changed() # type: ignore
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
def _check_and_handle_stop_mission_event(self, event: Queue) -> bool:
|
|
89
|
+
if check_for_event(event):
|
|
90
|
+
self.state_machine.stop() # type: ignore
|
|
91
|
+
return True
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def _should_return_home(self) -> bool:
|
|
95
|
+
time_since_entered = time.time() - self.entered_time
|
|
96
|
+
return time_since_entered > self.return_home_delay
|
|
97
|
+
|
|
98
|
+
def _run(self) -> None:
|
|
99
|
+
while True:
|
|
100
|
+
if self.signal_state_machine_to_stop.is_set():
|
|
101
|
+
self.logger.info(
|
|
102
|
+
"Stopping state machine from %s state", self.state.name
|
|
103
|
+
)
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
if self._check_and_handle_stop_mission_event(
|
|
107
|
+
self.events.api_requests.stop_mission.input
|
|
108
|
+
):
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
if self._check_and_handle_start_mission_event(
|
|
112
|
+
self.events.api_requests.start_mission.input
|
|
113
|
+
):
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
if self._check_and_handle_return_home_event(
|
|
117
|
+
self.events.api_requests.return_home
|
|
118
|
+
):
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
self.state != IdleStates.AwaitNextMission
|
|
123
|
+
and self._check_and_handle_robot_status_event(
|
|
124
|
+
self.shared_state.robot_status
|
|
125
|
+
)
|
|
126
|
+
):
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
if self.state == IdleStates.AwaitNextMission and self._should_return_home():
|
|
130
|
+
self.state_machine.request_return_home() # type: ignore
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
time.sleep(self.state_machine.sleep_time)
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from queue import Queue
|
|
6
|
+
from threading import Event
|
|
7
|
+
from typing import TYPE_CHECKING, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from isar.config.settings import settings
|
|
10
|
+
from isar.models.communication.message import StartMissionMessage
|
|
11
|
+
from isar.models.communication.queues.queue_utils import (
|
|
12
|
+
check_for_event,
|
|
13
|
+
check_for_event_without_consumption,
|
|
14
|
+
trigger_event,
|
|
15
|
+
)
|
|
16
|
+
from isar.services.utilities.threaded_request import ThreadedRequest
|
|
17
|
+
from robot_interface.models.exceptions.robot_exceptions import (
|
|
18
|
+
ErrorMessage,
|
|
19
|
+
RobotException,
|
|
20
|
+
RobotRetrieveInspectionException,
|
|
21
|
+
)
|
|
22
|
+
from robot_interface.models.inspection.inspection import Inspection
|
|
23
|
+
from robot_interface.models.mission.mission import Mission
|
|
24
|
+
from robot_interface.models.mission.status import TaskStatus
|
|
25
|
+
from robot_interface.models.mission.task import InspectionTask, Task
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OngoingMissionStates(str, Enum):
|
|
29
|
+
Monitor = "monitor"
|
|
30
|
+
ReturningHome = "returningHome"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from isar.state_machine.state_machine import StateMachine
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OngoingMission:
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
state_machine: "StateMachine",
|
|
41
|
+
state: OngoingMissionStates,
|
|
42
|
+
) -> None:
|
|
43
|
+
self.state_machine: "StateMachine" = state_machine
|
|
44
|
+
self.logger = logging.getLogger("state_machine")
|
|
45
|
+
self.events = state_machine.events
|
|
46
|
+
self.awaiting_task_status: bool = False
|
|
47
|
+
self.signal_state_machine_to_stop: Event = (
|
|
48
|
+
state_machine.signal_state_machine_to_stop
|
|
49
|
+
)
|
|
50
|
+
self.state: OngoingMissionStates = state
|
|
51
|
+
|
|
52
|
+
def start(self) -> None:
|
|
53
|
+
self.state_machine.update_state()
|
|
54
|
+
self._run()
|
|
55
|
+
|
|
56
|
+
def stop(self) -> None:
|
|
57
|
+
self.state_machine.mission_ongoing = False
|
|
58
|
+
|
|
59
|
+
def _check_and_handle_stop_mission_event(self, event: Queue) -> bool:
|
|
60
|
+
if check_for_event(event):
|
|
61
|
+
self.state_machine.stop() # type: ignore
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def _check_and_handle_pause_mission_event(self, event: Queue) -> bool:
|
|
66
|
+
if check_for_event(event):
|
|
67
|
+
self.state_machine.pause() # type: ignore
|
|
68
|
+
return True
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
def _check_and_handle_mission_started_event(self, event: Queue) -> bool:
|
|
72
|
+
if check_for_event(event):
|
|
73
|
+
self.state_machine.mission_ongoing = True
|
|
74
|
+
return True
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
def _check_and_handle_mission_failed_event(self, event: Queue) -> bool:
|
|
78
|
+
mission_failed: Optional[ErrorMessage] = check_for_event(event)
|
|
79
|
+
if mission_failed is not None:
|
|
80
|
+
self.state_machine.logger.warning(
|
|
81
|
+
f"Failed to initiate mission "
|
|
82
|
+
f"{str(self.state_machine.current_mission.id)[:8]} because: "
|
|
83
|
+
f"{mission_failed.error_description}"
|
|
84
|
+
)
|
|
85
|
+
self.state_machine.current_mission.error_message = ErrorMessage(
|
|
86
|
+
error_reason=mission_failed.error_reason,
|
|
87
|
+
error_description=mission_failed.error_description,
|
|
88
|
+
)
|
|
89
|
+
self.state_machine.mission_failed_to_start() # type: ignore
|
|
90
|
+
return True
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def _check_and_handle_task_status_failed_event(self, event: Queue) -> bool:
|
|
94
|
+
if not self.state_machine.mission_ongoing:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
task_failure: Optional[ErrorMessage] = check_for_event(event)
|
|
98
|
+
if task_failure is not None:
|
|
99
|
+
self.awaiting_task_status = False
|
|
100
|
+
self.state_machine.current_task.error_message = task_failure
|
|
101
|
+
self.logger.error(
|
|
102
|
+
f"Monitoring task {self.state_machine.current_task.id[:8]} failed "
|
|
103
|
+
f"because: {task_failure.error_description}"
|
|
104
|
+
)
|
|
105
|
+
return self._handle_new_task_status(TaskStatus.Failed)
|
|
106
|
+
elif not self.awaiting_task_status:
|
|
107
|
+
trigger_event(
|
|
108
|
+
self.events.state_machine_events.task_status_request,
|
|
109
|
+
self.state_machine.current_task.id,
|
|
110
|
+
)
|
|
111
|
+
self.awaiting_task_status = True
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
def _check_and_handle_task_status_event(self, event: Queue) -> bool:
|
|
115
|
+
if not self.state_machine.mission_ongoing:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
status: Optional[TaskStatus] = check_for_event(event)
|
|
119
|
+
if status is not None:
|
|
120
|
+
self.awaiting_task_status = False
|
|
121
|
+
return self._handle_new_task_status(status)
|
|
122
|
+
elif not self.awaiting_task_status:
|
|
123
|
+
trigger_event(
|
|
124
|
+
self.events.state_machine_events.task_status_request,
|
|
125
|
+
self.state_machine.current_task.id,
|
|
126
|
+
)
|
|
127
|
+
self.awaiting_task_status = True
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
def _handle_new_task_status(self, status: TaskStatus) -> bool:
|
|
131
|
+
if self.state_machine.current_task is None:
|
|
132
|
+
self.state_machine.iterate_current_task()
|
|
133
|
+
|
|
134
|
+
self.state_machine.current_task.status = status
|
|
135
|
+
|
|
136
|
+
if self.state_machine.current_task.is_finished():
|
|
137
|
+
self._report_task_status(self.state_machine.current_task)
|
|
138
|
+
self.state_machine.publish_task_status(task=self.state_machine.current_task)
|
|
139
|
+
|
|
140
|
+
if self.state == OngoingMissionStates.ReturningHome:
|
|
141
|
+
if status != TaskStatus.Successful:
|
|
142
|
+
self.state_machine.return_home_failed() # type: ignore
|
|
143
|
+
return True
|
|
144
|
+
self.state_machine.returned_home() # type: ignore
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
if self._should_upload_inspections():
|
|
148
|
+
get_inspection_thread = ThreadedRequest(
|
|
149
|
+
self._queue_inspections_for_upload
|
|
150
|
+
)
|
|
151
|
+
get_inspection_thread.start_thread(
|
|
152
|
+
deepcopy(self.state_machine.current_mission),
|
|
153
|
+
deepcopy(self.state_machine.current_task),
|
|
154
|
+
name="State Machine Get Inspections",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
self.state_machine.iterate_current_task()
|
|
158
|
+
if self.state_machine.current_task is None:
|
|
159
|
+
self.state_machine.mission_finished() # type: ignore
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
# Report and update next task
|
|
163
|
+
self.state_machine.current_task.update_task_status()
|
|
164
|
+
self.state_machine.publish_task_status(task=self.state_machine.current_task)
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
def _check_and_handle_start_mission_event(
|
|
168
|
+
self, event: Queue[StartMissionMessage]
|
|
169
|
+
) -> bool:
|
|
170
|
+
if check_for_event_without_consumption(event):
|
|
171
|
+
self.state_machine.stop() # type: ignore
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
def _run(self) -> None:
|
|
177
|
+
self.awaiting_task_status = False
|
|
178
|
+
while True:
|
|
179
|
+
if self.signal_state_machine_to_stop.is_set():
|
|
180
|
+
self.logger.info(
|
|
181
|
+
"Stopping state machine from %s state", self.state.name
|
|
182
|
+
)
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
if self._check_and_handle_stop_mission_event(
|
|
186
|
+
self.events.api_requests.stop_mission.input
|
|
187
|
+
):
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
self.state == OngoingMissionStates.Monitor
|
|
192
|
+
and self._check_and_handle_pause_mission_event(
|
|
193
|
+
self.events.api_requests.pause_mission.input
|
|
194
|
+
)
|
|
195
|
+
):
|
|
196
|
+
break
|
|
197
|
+
|
|
198
|
+
self._check_and_handle_mission_started_event(
|
|
199
|
+
self.events.robot_service_events.mission_started
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if self._check_and_handle_mission_failed_event(
|
|
203
|
+
self.events.robot_service_events.mission_failed
|
|
204
|
+
):
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
if (
|
|
208
|
+
self.state == OngoingMissionStates.ReturningHome
|
|
209
|
+
and self._check_and_handle_start_mission_event(
|
|
210
|
+
self.events.api_requests.start_mission.input
|
|
211
|
+
)
|
|
212
|
+
):
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
if self._check_and_handle_task_status_failed_event(
|
|
216
|
+
self.events.robot_service_events.task_status_failed
|
|
217
|
+
):
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
if self._check_and_handle_task_status_event(
|
|
221
|
+
self.events.robot_service_events.task_status_updated
|
|
222
|
+
):
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
time.sleep(settings.FSM_SLEEP_TIME)
|
|
226
|
+
|
|
227
|
+
def _queue_inspections_for_upload(
|
|
228
|
+
self, mission: Mission, current_task: InspectionTask
|
|
229
|
+
) -> None:
|
|
230
|
+
try:
|
|
231
|
+
inspection: Inspection = self.state_machine.robot.get_inspection(
|
|
232
|
+
task=current_task
|
|
233
|
+
)
|
|
234
|
+
if current_task.inspection_id != inspection.id:
|
|
235
|
+
self.logger.warning(
|
|
236
|
+
f"The inspection_id of task ({current_task.inspection_id}) "
|
|
237
|
+
f"and result ({inspection.id}) is not matching. "
|
|
238
|
+
f"This may lead to confusions when accessing the inspection later"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
except (RobotRetrieveInspectionException, RobotException) as e:
|
|
242
|
+
self._set_error_message(e)
|
|
243
|
+
self.logger.error(
|
|
244
|
+
f"Failed to retrieve inspections because: {e.error_description}"
|
|
245
|
+
)
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
except Exception as e:
|
|
249
|
+
self.logger.error(
|
|
250
|
+
f"Failed to retrieve inspections because of unexpected error: {e}"
|
|
251
|
+
)
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
if not inspection:
|
|
255
|
+
self.logger.warning(
|
|
256
|
+
f"No inspection result data retrieved for task {str(current_task.id)[:8]}"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
inspection.metadata.tag_id = current_task.tag_id
|
|
260
|
+
|
|
261
|
+
message: Tuple[Inspection, Mission] = (
|
|
262
|
+
inspection,
|
|
263
|
+
mission,
|
|
264
|
+
)
|
|
265
|
+
self.state_machine.events.upload_queue.put(message)
|
|
266
|
+
self.logger.info(
|
|
267
|
+
f"Inspection result: {str(inspection.id)[:8]} queued for upload"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def _report_task_status(self, task: Task) -> None:
|
|
271
|
+
if task.status == TaskStatus.Failed:
|
|
272
|
+
self.logger.warning(
|
|
273
|
+
f"Task: {str(task.id)[:8]} was reported as failed by the robot"
|
|
274
|
+
)
|
|
275
|
+
elif task.status == TaskStatus.Successful:
|
|
276
|
+
self.logger.info(
|
|
277
|
+
f"{type(task).__name__} task: {str(task.id)[:8]} completed"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def _should_upload_inspections(self) -> bool:
|
|
281
|
+
if settings.UPLOAD_INSPECTIONS_ASYNC:
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
self.state_machine.current_task.is_finished()
|
|
286
|
+
and self.state_machine.current_task.status == TaskStatus.Successful
|
|
287
|
+
and isinstance(self.state_machine.current_task, InspectionTask)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def _set_error_message(self, e: RobotException) -> None:
|
|
291
|
+
error_message: ErrorMessage = ErrorMessage(
|
|
292
|
+
error_reason=e.error_reason, error_description=e.error_description
|
|
293
|
+
)
|
|
294
|
+
self.state_machine.current_task.error_message = error_message
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from isar.models.communication.queues.queue_utils import check_shared_state
|
|
7
|
+
from robot_interface.models.mission.status import RobotStatus
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RobotUnavailableStates(str, Enum):
|
|
11
|
+
BlockedProtectiveStop = "blockedProtectiveStop"
|
|
12
|
+
Offline = "offline"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from isar.state_machine.state_machine import StateMachine
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RobotUnavailable:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
state_machine: "StateMachine",
|
|
23
|
+
state: RobotUnavailableStates,
|
|
24
|
+
) -> None:
|
|
25
|
+
self.state_machine: "StateMachine" = state_machine
|
|
26
|
+
self.logger = logging.getLogger("state_machine")
|
|
27
|
+
self.shared_state = self.state_machine.shared_state
|
|
28
|
+
self.signal_state_machine_to_stop = state_machine.signal_state_machine_to_stop
|
|
29
|
+
self.state: RobotUnavailableStates = state
|
|
30
|
+
|
|
31
|
+
def start(self) -> None:
|
|
32
|
+
self.state_machine.update_state()
|
|
33
|
+
self._run()
|
|
34
|
+
|
|
35
|
+
def stop(self) -> None:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
def _run(self) -> None:
|
|
39
|
+
while True:
|
|
40
|
+
if self.signal_state_machine_to_stop.is_set():
|
|
41
|
+
self.logger.info(
|
|
42
|
+
"Stopping state machine from %s state", self.state.name
|
|
43
|
+
)
|
|
44
|
+
break
|
|
45
|
+
|
|
46
|
+
robot_status: RobotStatus = check_shared_state(
|
|
47
|
+
self.shared_state.robot_status
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
expected_status = (
|
|
51
|
+
RobotStatus.BlockedProtectiveStop
|
|
52
|
+
if self.state == RobotUnavailableStates.BlockedProtectiveStop
|
|
53
|
+
else RobotStatus.Offline
|
|
54
|
+
)
|
|
55
|
+
if robot_status != expected_status:
|
|
56
|
+
transition = self.state_machine.robot_status_changed # type: ignore
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
time.sleep(self.state_machine.sleep_time)
|
|
60
|
+
|
|
61
|
+
transition()
|
|
@@ -168,6 +168,9 @@ class StateMachine(object):
|
|
|
168
168
|
self.signal_state_machine_to_stop.set()
|
|
169
169
|
|
|
170
170
|
def iterate_current_task(self):
|
|
171
|
+
if self.current_task is None:
|
|
172
|
+
raise ValueError("No current task is set")
|
|
173
|
+
|
|
171
174
|
if self.current_task.is_finished():
|
|
172
175
|
try:
|
|
173
176
|
self.current_task = self.task_selector.next_task()
|
|
@@ -313,6 +316,12 @@ class StateMachine(object):
|
|
|
313
316
|
self.logger.info("Mission overview:\n%s", log_statement)
|
|
314
317
|
|
|
315
318
|
def _make_control_mission_response(self) -> ControlMissionResponse:
|
|
319
|
+
if self.current_mission is None:
|
|
320
|
+
raise ValueError("No current mission is set")
|
|
321
|
+
|
|
322
|
+
if self.current_task is None:
|
|
323
|
+
raise ValueError("No current task is set")
|
|
324
|
+
|
|
316
325
|
return ControlMissionResponse(
|
|
317
326
|
mission_id=self.current_mission.id,
|
|
318
327
|
mission_status=self.current_mission.status,
|