Skip to content

Commit b85839e

Browse files
committed
Simulate missions in separate thread
1 parent 6094a15 commit b85839e

4 files changed

Lines changed: 201 additions & 49 deletions

File tree

src/isar_robot/config/settings.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from importlib.resources import as_file, files
2+
23
from pydantic import Field
34
from pydantic_settings import BaseSettings, SettingsConfigDict
45

@@ -13,15 +14,24 @@ def __init__(self) -> None:
1314
env_file = None
1415
super().__init__(_env_file=env_file)
1516

16-
TASK_DURATION_IN_SECONDS: float = Field(default=5.0)
17+
MISSION_SIMULATION_TASK_DURATION: float = Field(default=5.0)
1718
INITIATE_MISSION_DURATION_IN_SECONDS: float = Field(default=0.1)
18-
SHOULD_FAIL_NORMAL_TASK: bool = Field(default=False)
19-
SHOULD_FAIL_RETURN_TO_HOME_TASK: bool = Field(default=False)
2019
SHOULD_HAVE_RANDOM_BATTERY_LEVEL: bool = Field(default=False)
2120
ROBOT_POSE_PUBLISH_INTERVAL: float = Field(default=5)
2221
ROBOT_BATTERY_PUBLISH_INTERVAL: float = Field(default=2)
2322
ROBOT_OBSTACLE_STATUS_PUBLISH_INTERVAL: float = Field(default=10)
2423
ROBOT_PRESSURE_PUBLISH_INTERVAL: float = Field(default=20)
24+
MISSION_SIMULATION_TIME_TO_START: float = Field(default=5.0)
25+
MISSION_SIMULATION_TIME_TO_STOP: float = Field(default=5.0)
26+
27+
# This is the time from the last task finishing to the mission finishing
28+
MISSION_SIMULATION_MISSION_COMPLETION_DELAY: float = Field(default=2.0)
29+
MISSION_SIMULATION_SHOULD_FAIL_RETURN_TO_HOME_TASK: bool = Field(default=False)
30+
MISSION_SIMULATION_SHOULD_FAIL_NORMAL_TASK: bool = Field(default=False)
31+
MISSION_SIMULATION_TASK_FAILURE_PROBABILITY: float = Field(default=0.05)
32+
33+
# This will cause delay between 0 and 5 seconds
34+
MISSION_SIMULATION_API_DELAY_MODIFIER: float = Field(default=5.0)
2535

2636
model_config = SettingsConfigDict(
2737
env_prefix="ROBOT_",

src/isar_robot/robotinterface.py

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,70 @@
11
import logging
2-
import time
32
from datetime import datetime, timezone
43
from logging import Logger
54
from queue import Queue
65
from threading import Thread
76
from typing import Callable, List, Optional
87

8+
from robot_interface.models.exceptions.robot_exceptions import (
9+
RobotCommunicationException,
10+
)
911
from robot_interface.models.inspection.inspection import Inspection
1012
from robot_interface.models.mission.mission import Mission
11-
from robot_interface.models.mission.status import RobotStatus, TaskStatus
13+
from robot_interface.models.mission.status import MissionStatus, RobotStatus, TaskStatus
1214
from robot_interface.models.mission.task import (
1315
InspectionTask,
1416
RecordAudio,
15-
ReturnToHome,
1617
TakeCO2Measurement,
1718
TakeImage,
1819
TakeThermalImage,
1920
TakeThermalVideo,
2021
TakeVideo,
21-
Task,
2222
)
2323
from robot_interface.models.robots.media import MediaConfig
2424
from robot_interface.robot_interface import RobotInterface
2525
from robot_interface.telemetry.mqtt_client import MqttTelemetryPublisher
2626

2727
from isar_robot import inspections, telemetry
2828
from isar_robot.config.settings import settings
29+
from isar_robot.simulation import MissionSimulation
2930

3031

3132
class Robot(RobotInterface):
3233
def __init__(self) -> None:
3334
self.telemetry = telemetry.Telemetry()
3435
self.logger: Logger = logging.getLogger("isar_robot")
35-
self.current_mission: Optional[Mission] = None
36-
self.current_task: Optional[Task] = None
3736
self.last_task_completion_time: datetime = datetime.now(timezone.utc)
3837
self.robot_is_home: bool = False
38+
self.mission_simulation: Optional[MissionSimulation] = None
3939

4040
def initiate_mission(self, mission: Mission) -> None:
41-
time.sleep(settings.INITIATE_MISSION_DURATION_IN_SECONDS)
42-
self.current_mission = mission
43-
self.current_task_ix = 0
44-
self.current_task = mission.tasks[self.current_task_ix]
45-
self.task_len = len(mission.tasks)
46-
self.last_task_completion_time = datetime.now(timezone.utc)
41+
if self.mission_simulation and not self.mission_simulation.mission_done:
42+
raise RobotCommunicationException(
43+
error_description="Could not start mission as one is already running"
44+
)
45+
elif self.mission_simulation:
46+
self.mission_simulation.join()
47+
self.mission_simulation = MissionSimulation(mission)
48+
self.mission_simulation.start()
4749
self.robot_is_home = False
4850

4951
def task_status(self, task_id: str) -> TaskStatus:
50-
now: datetime = datetime.now(timezone.utc)
51-
if (
52-
now - self.last_task_completion_time
53-
).total_seconds() < settings.TASK_DURATION_IN_SECONDS:
54-
return TaskStatus.InProgress
55-
self.last_task_completion_time = now
56-
57-
next_task: Task = None
58-
if self.current_mission:
59-
if self.current_task_ix < self.task_len - 1:
60-
self.current_task_ix = self.current_task_ix + 1
61-
next_task = self.current_mission.tasks[self.current_task_ix]
62-
63-
# This only happens for last task in mission
64-
if isinstance(self.current_task, ReturnToHome):
65-
self.current_task = None
66-
if settings.SHOULD_FAIL_RETURN_TO_HOME_TASK:
67-
return TaskStatus.Failed
68-
self.robot_is_home = True
69-
return TaskStatus.Successful
52+
if not self.mission_simulation:
53+
raise RobotCommunicationException(
54+
error_description="Could not get task status as no mission is running"
55+
)
7056

71-
if next_task:
72-
self.current_task = next_task
73-
else:
74-
self.current_task = None
75-
if settings.SHOULD_FAIL_NORMAL_TASK:
76-
return TaskStatus.Failed
77-
return TaskStatus.Successful
57+
status = self.mission_simulation.task_status(task_id)
58+
if status == TaskStatus.Successful and self.mission_simulation.is_return_home:
59+
self.robot_is_home = True
60+
return status
7861

7962
def stop(self) -> None:
80-
return
63+
if not self.mission_simulation:
64+
raise RobotCommunicationException(
65+
error_description="Attempted to stop non-existent mission"
66+
)
67+
self.mission_simulation.stop_mission()
8168

8269
def get_inspection(self, task: InspectionTask) -> Inspection:
8370
if type(task) in [TakeImage, TakeThermalImage]:
@@ -174,15 +161,29 @@ def get_telemetry_publishers(
174161
return publisher_threads
175162

176163
def robot_status(self) -> RobotStatus:
164+
if self.mission_simulation and self.mission_simulation.is_alive():
165+
mission_status: MissionStatus = self.mission_simulation.mission_status()
166+
if mission_status == MissionStatus.Paused:
167+
return RobotStatus.Paused
168+
elif mission_status in [MissionStatus.InProgress, MissionStatus.NotStarted]:
169+
return RobotStatus.Busy
177170
if self.robot_is_home:
178171
return RobotStatus.Home
179172
return RobotStatus.Available
180173

181174
def pause(self) -> None:
182-
return
175+
if not self.mission_simulation:
176+
raise RobotCommunicationException(
177+
error_description="Attempted to pause non-existent mission"
178+
)
179+
self.mission_simulation.pause_mission()
183180

184181
def resume(self) -> None:
185-
return
182+
if not self.mission_simulation:
183+
raise RobotCommunicationException(
184+
error_description="Attempted to resume non-existent mission"
185+
)
186+
self.mission_simulation.resume_mission()
186187

187188
def generate_media_config(self) -> Optional[MediaConfig]:
188189
return None

src/isar_robot/simulation.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import logging
2+
import random
3+
import time
4+
from threading import Event, Thread
5+
6+
from robot_interface.models.exceptions.robot_exceptions import (
7+
RobotCommunicationException,
8+
RobotTaskStatusException,
9+
)
10+
from robot_interface.models.mission.mission import Mission
11+
from robot_interface.models.mission.status import MissionStatus, TaskStatus
12+
from robot_interface.models.mission.task import ReturnToHome
13+
14+
from isar_robot.config.settings import settings
15+
16+
17+
class MissionSimulation(Thread):
18+
def __init__(
19+
self,
20+
mission: Mission,
21+
):
22+
self.logger = logging.getLogger("isar robot mission simulation")
23+
self.mission = mission
24+
self.task_index = 0
25+
self.n_tasks = len(mission.tasks)
26+
self.robot_is_home = False
27+
self.task_statuses = list(
28+
map(lambda _: TaskStatus.NotStarted, self.mission.tasks)
29+
)
30+
self.task_id_mapping = {}
31+
for i, task in enumerate(self.mission.tasks):
32+
self.task_id_mapping[task.id] = i
33+
34+
self.is_return_home: bool = len(self.mission.tasks) == 1 and isinstance(
35+
self.mission.tasks[0], ReturnToHome
36+
)
37+
self.task_failure_probability: float = (
38+
settings.MISSION_SIMULATION_TASK_FAILURE_PROBABILITY
39+
)
40+
self.api_delay_modifier: float = settings.MISSION_SIMULATION_API_DELAY_MODIFIER
41+
42+
self.mission_done: bool = False
43+
self.all_tasks_done: bool = False
44+
45+
self.signal_resume_mission: Event = Event()
46+
self.signal_resume_mission.set()
47+
self.signal_stop_mission: Event = Event()
48+
Thread.__init__(self, name="Mission simulation thread")
49+
50+
def stop(self) -> None:
51+
return
52+
53+
def _simulate_api_call_delay(self):
54+
time.sleep(random.random() * self.api_delay_modifier)
55+
56+
def pause_mission(self):
57+
if self.mission_done:
58+
raise RobotCommunicationException(
59+
error_description="Could not pause non-existent mission"
60+
)
61+
self.signal_resume_mission.clear()
62+
63+
def resume_mission(self):
64+
if self.mission_done:
65+
raise RobotCommunicationException(
66+
error_description="Could not resume non-existent mission"
67+
)
68+
self.signal_resume_mission.set()
69+
70+
def stop_mission(self):
71+
if self.mission_done:
72+
raise RobotCommunicationException(
73+
error_description="Could not stop non-existent mission"
74+
)
75+
time.sleep(settings.MISSION_SIMULATION_TIME_TO_STOP)
76+
self.signal_stop_mission.set()
77+
78+
def task_status(self, task_id: str):
79+
task_index = self.task_id_mapping[task_id]
80+
if task_index < 0 or task_index > self.n_tasks - 1:
81+
raise RobotTaskStatusException(
82+
error_description="Task ID did not match any ongoing tasks"
83+
)
84+
return self.task_statuses[task_index]
85+
86+
def mission_status(self):
87+
if not self.signal_resume_mission.wait(0):
88+
return MissionStatus.Paused
89+
if all(map(lambda status: status == TaskStatus.Successful, self.task_statuses)):
90+
return MissionStatus.Successful
91+
if all(map(lambda status: status == TaskStatus.NotStarted, self.task_statuses)):
92+
return MissionStatus.NotStarted
93+
if any(
94+
map(
95+
lambda status: status in [TaskStatus.InProgress, TaskStatus.NotStarted],
96+
self.task_statuses,
97+
)
98+
):
99+
return MissionStatus.InProgress
100+
if all(map(lambda status: status == TaskStatus.Failed, self.task_statuses)):
101+
return MissionStatus.Failed
102+
if any(map(lambda status: status == TaskStatus.Cancelled, self.task_statuses)):
103+
return MissionStatus.Cancelled
104+
if any(map(lambda status: status == TaskStatus.Failed, self.task_statuses)):
105+
return MissionStatus.PartiallySuccessful
106+
raise RobotTaskStatusException("Unhandled task status detected")
107+
108+
def _complete_task(self, task_status: TaskStatus):
109+
if self.task_index < self.n_tasks:
110+
self.task_statuses[self.task_index] = task_status
111+
self.task_index = self.task_index + 1
112+
if self.task_index >= self.n_tasks:
113+
self.all_tasks_done = True
114+
else:
115+
self.task_statuses[self.task_index] = TaskStatus.InProgress
116+
117+
def run(self):
118+
time.sleep(settings.MISSION_SIMULATION_TIME_TO_START)
119+
120+
if self.signal_stop_mission.is_set():
121+
return
122+
123+
thread_check_interval = settings.MISSION_SIMULATION_TASK_DURATION
124+
while not self.signal_stop_mission.wait(thread_check_interval):
125+
126+
self.signal_resume_mission.wait()
127+
128+
if (
129+
self.is_return_home
130+
and not settings.MISSION_SIMULATION_SHOULD_FAIL_RETURN_TO_HOME_TASK
131+
) or not settings.MISSION_SIMULATION_SHOULD_FAIL_NORMAL_TASK:
132+
self._complete_task(TaskStatus.Successful)
133+
else:
134+
if random.random() > self.task_failure_probability:
135+
self._complete_task(TaskStatus.Failed)
136+
else:
137+
self._complete_task(TaskStatus.Successful)
138+
if self.all_tasks_done:
139+
break
140+
141+
time.sleep(settings.MISSION_SIMULATION_MISSION_COMPLETION_DELAY)
142+
self.mission_done = True
143+
self.logger.info("Exiting mission simulation thread")

src/isar_robot/telemetry.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import json
22
import random
33
from datetime import datetime, timezone
4+
from typing import Optional
45

56
from alitra import Frame, Orientation, Pose, Position
6-
7-
from isar_robot.config.settings import settings
8-
97
from robot_interface.models.robots.battery_state import BatteryState
108
from robot_interface.telemetry.payloads import (
119
TelemetryBatteryPayload,
@@ -15,7 +13,7 @@
1513
)
1614
from robot_interface.utilities.json_service import EnhancedJSONEncoder
1715

18-
from typing import Optional
16+
from isar_robot.config.settings import settings
1917

2018

2119
def _get_pressure_level() -> float:

0 commit comments

Comments
 (0)