isar 1.33.5__py3-none-any.whl → 1.33.6__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 (33) hide show
  1. isar/apis/api.py +34 -0
  2. isar/apis/models/models.py +5 -0
  3. isar/apis/schedule/scheduling_controller.py +34 -0
  4. isar/models/events.py +17 -1
  5. isar/robot/robot.py +32 -0
  6. isar/robot/robot_pause_mission.py +63 -0
  7. isar/robot/robot_stop_mission.py +1 -1
  8. isar/services/utilities/scheduling_utilities.py +50 -4
  9. isar/state_machine/state_machine.py +27 -7
  10. isar/state_machine/states/await_next_mission.py +19 -1
  11. isar/state_machine/states/going_to_lockdown.py +80 -0
  12. isar/state_machine/states/home.py +15 -0
  13. isar/state_machine/states/lockdown.py +37 -0
  14. isar/state_machine/states/monitor.py +16 -0
  15. isar/state_machine/states/paused.py +19 -1
  16. isar/state_machine/states/pausing.py +74 -0
  17. isar/state_machine/states/pausing_return_home.py +74 -0
  18. isar/state_machine/states/recharging.py +16 -0
  19. isar/state_machine/states/return_home_paused.py +17 -1
  20. isar/state_machine/states/returning_home.py +18 -2
  21. isar/state_machine/states/stopping_go_to_lockdown.py +79 -0
  22. isar/state_machine/states_enum.py +5 -0
  23. isar/state_machine/transitions/functions/fail_mission.py +7 -0
  24. isar/state_machine/transitions/functions/pause.py +20 -80
  25. isar/state_machine/transitions/mission.py +55 -10
  26. isar/state_machine/transitions/return_home.py +53 -0
  27. {isar-1.33.5.dist-info → isar-1.33.6.dist-info}/METADATA +1 -1
  28. {isar-1.33.5.dist-info → isar-1.33.6.dist-info}/RECORD +33 -27
  29. robot_interface/models/mission/status.py +2 -0
  30. {isar-1.33.5.dist-info → isar-1.33.6.dist-info}/WHEEL +0 -0
  31. {isar-1.33.5.dist-info → isar-1.33.6.dist-info}/entry_points.txt +0 -0
  32. {isar-1.33.5.dist-info → isar-1.33.6.dist-info}/licenses/LICENSE +0 -0
  33. {isar-1.33.5.dist-info → isar-1.33.6.dist-info}/top_level.txt +0 -0
isar/apis/api.py CHANGED
@@ -245,6 +245,40 @@ class API:
245
245
  },
246
246
  },
247
247
  )
248
+ router.add_api_route(
249
+ path="/schedule/lockdown",
250
+ endpoint=self.scheduling_controller.lockdown,
251
+ methods=["POST"],
252
+ dependencies=[authentication_dependency],
253
+ summary="Send the robot to dock and ensure that it stays there",
254
+ responses={
255
+ HTTPStatus.OK.value: {"description": "Robot going to dock"},
256
+ HTTPStatus.CONFLICT.value: {
257
+ "description": "Conflict - Invalid command in the current state"
258
+ },
259
+ HTTPStatus.INTERNAL_SERVER_ERROR.value: {
260
+ "description": "Internal Server Error - Current state of state machine unknown"
261
+ },
262
+ },
263
+ )
264
+ router.add_api_route(
265
+ path="/schedule/release-lockdown",
266
+ endpoint=self.scheduling_controller.release_lockdown,
267
+ methods=["POST"],
268
+ dependencies=[authentication_dependency],
269
+ summary="Allow the robot to start missions again",
270
+ responses={
271
+ HTTPStatus.OK.value: {
272
+ "description": "Robot is able to receive missions again"
273
+ },
274
+ HTTPStatus.CONFLICT.value: {
275
+ "description": "Conflict - Invalid command in the current state"
276
+ },
277
+ HTTPStatus.INTERNAL_SERVER_ERROR.value: {
278
+ "description": "Internal Server Error - Current state of state machine unknown"
279
+ },
280
+ },
281
+ )
248
282
  router.add_api_route(
249
283
  path="/schedule/release-intervention-needed",
250
284
  endpoint=self.scheduling_controller.release_intervention_needed,
@@ -32,6 +32,11 @@ class MissionStartResponse(BaseModel):
32
32
  mission_not_started_reason: Optional[str] = None
33
33
 
34
34
 
35
+ class LockdownResponse(BaseModel):
36
+ lockdown_started: bool
37
+ failure_reason: Optional[str] = None
38
+
39
+
35
40
  class RobotInfoResponse(BaseModel):
36
41
  robot_package: str
37
42
  isar_id: str
@@ -254,6 +254,40 @@ class SchedulingController:
254
254
  self.scheduling_utilities.release_intervention_needed()
255
255
  self.logger.info("Released intervention needed state successfully")
256
256
 
257
+ @tracer.start_as_current_span("lockdown")
258
+ def lockdown(self) -> None:
259
+ self.logger.info("Received request to lockdown robot")
260
+
261
+ state: States = self.scheduling_utilities.get_state()
262
+
263
+ if state == States.Lockdown:
264
+ error_message = "Conflict - Lockdown command received in lockdown state"
265
+ self.logger.warning(error_message)
266
+ raise HTTPException(
267
+ status_code=HTTPStatus.CONFLICT,
268
+ detail=error_message,
269
+ )
270
+
271
+ self.scheduling_utilities.lock_down_robot()
272
+ self.logger.info("Lockdown started successfully")
273
+
274
+ @tracer.start_as_current_span("release_lockdown")
275
+ def release_lockdown(self) -> None:
276
+ self.logger.info("Received request to release robot lockdown")
277
+
278
+ state: States = self.scheduling_utilities.get_state()
279
+
280
+ if state != States.Lockdown:
281
+ error_message = f"Conflict - Release lockdown command received in invalid state - State: {state}"
282
+ self.logger.warning(error_message)
283
+ raise HTTPException(
284
+ status_code=HTTPStatus.CONFLICT,
285
+ detail=error_message,
286
+ )
287
+
288
+ self.scheduling_utilities.release_robot_lockdown()
289
+ self.logger.info("Released lockdown successfully")
290
+
257
291
  def _api_response(self, mission: Mission) -> StartMissionResponse:
258
292
  return StartMissionResponse(
259
293
  id=mission.id,
isar/models/events.py CHANGED
@@ -4,7 +4,11 @@ from typing import Generic, Optional, TypeVar
4
4
 
5
5
  from transitions import State
6
6
 
7
- from isar.apis.models.models import ControlMissionResponse, MissionStartResponse
7
+ from isar.apis.models.models import (
8
+ ControlMissionResponse,
9
+ LockdownResponse,
10
+ MissionStartResponse,
11
+ )
8
12
  from isar.config.settings import settings
9
13
  from robot_interface.models.exceptions.robot_exceptions import ErrorMessage
10
14
  from robot_interface.models.mission.mission import Mission
@@ -105,6 +109,12 @@ class APIRequests:
105
109
  self.release_intervention_needed: APIEvent[bool, bool] = APIEvent(
106
110
  "release_intervention_needed"
107
111
  )
112
+ self.send_to_lockdown: APIEvent[bool, LockdownResponse] = APIEvent(
113
+ "send_to_lockdown"
114
+ )
115
+ self.release_from_lockdown: APIEvent[bool, bool] = APIEvent(
116
+ "release_from_lockdown"
117
+ )
108
118
 
109
119
 
110
120
  class StateMachineEvents:
@@ -128,6 +138,12 @@ class RobotServiceEvents:
128
138
  self.mission_successfully_stopped: Event[bool] = Event(
129
139
  "mission_successfully_stopped"
130
140
  )
141
+ self.mission_failed_to_pause: Event[ErrorMessage] = Event(
142
+ "mission_failed_to_pause"
143
+ )
144
+ self.mission_successfully_paused: Event[bool] = Event(
145
+ "mission_successfully_paused"
146
+ )
131
147
 
132
148
 
133
149
  class SharedState:
isar/robot/robot.py CHANGED
@@ -9,6 +9,7 @@ from isar.models.events import (
9
9
  SharedState,
10
10
  StateMachineEvents,
11
11
  )
12
+ from isar.robot.robot_pause_mission import RobotPauseMissionThread
12
13
  from isar.robot.robot_start_mission import RobotStartMissionThread
13
14
  from isar.robot.robot_status import RobotStatusThread
14
15
  from isar.robot.robot_stop_mission import RobotStopMissionThread
@@ -31,6 +32,7 @@ class Robot(object):
31
32
  self.robot_status_thread: Optional[RobotStatusThread] = None
32
33
  self.robot_task_status_thread: Optional[RobotTaskStatusThread] = None
33
34
  self.stop_mission_thread: Optional[RobotStopMissionThread] = None
35
+ self.pause_mission_thread: Optional[RobotPauseMissionThread] = None
34
36
  self.signal_thread_quitting: ThreadEvent = ThreadEvent()
35
37
 
36
38
  def stop(self) -> None:
@@ -111,6 +113,34 @@ class Robot(object):
111
113
  )
112
114
  self.stop_mission_thread.start()
113
115
 
116
+ def _pause_mission_request_handler(self, event: Event[bool]) -> None:
117
+ if event.consume_event():
118
+ if (
119
+ self.pause_mission_thread is not None
120
+ and self.pause_mission_thread.is_alive()
121
+ ):
122
+ self.logger.warning(
123
+ "Received pause mission event while trying to pause a mission. Aborting pause attempt."
124
+ )
125
+ return
126
+ if (
127
+ self.start_mission_thread is not None
128
+ and self.start_mission_thread.is_alive()
129
+ ):
130
+ error_description = "Received pause mission event while trying to start a mission. Aborting pause attempt."
131
+ error_message = ErrorMessage(
132
+ error_reason=ErrorReason.RobotStillStartingMissionException,
133
+ error_description=error_description,
134
+ )
135
+ self.robot_service_events.mission_failed_to_stop.trigger_event(
136
+ error_message
137
+ )
138
+ return
139
+ self.pause_mission_thread = RobotPauseMissionThread(
140
+ self.robot_service_events, self.robot, self.signal_thread_quitting
141
+ )
142
+ self.pause_mission_thread.start()
143
+
114
144
  def run(self) -> None:
115
145
  self.robot_status_thread = RobotStatusThread(
116
146
  self.robot, self.signal_thread_quitting, self.shared_state
@@ -124,6 +154,8 @@ class Robot(object):
124
154
  self.state_machine_events.task_status_request
125
155
  )
126
156
 
157
+ self._pause_mission_request_handler(self.state_machine_events.pause_mission)
158
+
127
159
  self._stop_mission_request_handler(self.state_machine_events.stop_mission)
128
160
 
129
161
  self.logger.info("Exiting robot service main thread")
@@ -0,0 +1,63 @@
1
+ import logging
2
+ import time
3
+ from threading import Event, Thread
4
+ from typing import Optional
5
+
6
+ from isar.config.settings import settings
7
+ from isar.models.events import RobotServiceEvents
8
+ from robot_interface.models.exceptions.robot_exceptions import (
9
+ ErrorMessage,
10
+ RobotActionException,
11
+ RobotException,
12
+ )
13
+ from robot_interface.robot_interface import RobotInterface
14
+
15
+
16
+ class RobotPauseMissionThread(Thread):
17
+ def __init__(
18
+ self,
19
+ robot_service_events: RobotServiceEvents,
20
+ robot: RobotInterface,
21
+ signal_thread_quitting: Event,
22
+ ):
23
+ self.logger = logging.getLogger("robot")
24
+ self.robot_service_events: RobotServiceEvents = robot_service_events
25
+ self.robot: RobotInterface = robot
26
+ self.signal_thread_quitting: Event = signal_thread_quitting
27
+ Thread.__init__(self, name="Robot pause mission thread")
28
+
29
+ def run(self) -> None:
30
+ retries = 0
31
+ error: Optional[ErrorMessage] = None
32
+ while retries < settings.STATE_TRANSITION_NUM_RETIRES:
33
+ if self.signal_thread_quitting.wait(0):
34
+ return
35
+
36
+ try:
37
+ self.robot.pause()
38
+ except (RobotActionException, RobotException) as e:
39
+ self.logger.warning(
40
+ f"\nFailed to pause robot because: {e.error_description}"
41
+ f"\nAttempting to pause the robot again"
42
+ )
43
+ retries += 1
44
+ error = ErrorMessage(
45
+ error_reason=e.error_reason, error_description=e.error_description
46
+ )
47
+ time.sleep(settings.FSM_SLEEP_TIME)
48
+ continue
49
+ self.robot_service_events.mission_successfully_paused.trigger_event(True)
50
+ return
51
+
52
+ error_description = (
53
+ f"\nFailed to pause the robot after {retries} attempts because: "
54
+ f"{error.error_description}"
55
+ f"\nBe aware that the robot may still be moving even though a pause has "
56
+ "been attempted"
57
+ )
58
+
59
+ error_message = ErrorMessage(
60
+ error_reason=error.error_reason,
61
+ error_description=error_description,
62
+ )
63
+ self.robot_service_events.mission_failed_to_pause.trigger_event(error_message)
@@ -24,7 +24,7 @@ class RobotStopMissionThread(Thread):
24
24
  self.robot_service_events: RobotServiceEvents = robot_service_events
25
25
  self.robot: RobotInterface = robot
26
26
  self.signal_thread_quitting: Event = signal_thread_quitting
27
- Thread.__init__(self, name="Robot start mission thread")
27
+ Thread.__init__(self, name="Robot stop mission thread")
28
28
 
29
29
  def run(self) -> None:
30
30
  retries = 0
@@ -322,14 +322,60 @@ class SchedulingUtilities:
322
322
  try:
323
323
  self._send_command(True, self.api_events.release_intervention_needed)
324
324
  self.logger.info("OK - Intervention needed state released")
325
+ except EventConflictError:
326
+ error_message = (
327
+ "Previous release intervention needed request is still being processed"
328
+ )
329
+ self.logger.warning(error_message)
330
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
325
331
  except EventTimeoutError:
332
+ error_message = "Cannot release intervention needed as it is not in intervention needed state"
333
+ self.logger.warning(error_message)
334
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
335
+
336
+ def lock_down_robot(self) -> None:
337
+ """Lock down robot
338
+
339
+ Raises
340
+ ------
341
+ HTTPException 500 Internal Server Error
342
+ If the robot could not be locked down
343
+ """
344
+ try:
345
+ self._send_command(True, self.api_events.send_to_lockdown)
346
+ self.logger.info("OK - Robot sent into lockdown")
347
+ except EventConflictError:
348
+ error_message = "Previous lockdown request is still being processed"
349
+ self.logger.warning(error_message)
350
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
351
+ except EventTimeoutError:
352
+ error_message = "Cannot send robot to lockdown as it is already in lockdown"
353
+ self.logger.warning(error_message)
354
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
355
+
356
+ def release_robot_lockdown(self) -> None:
357
+ """Release robot from lockdown
358
+
359
+ Raises
360
+ ------
361
+ HTTPException 500 Internal Server Error
362
+ If the robot could not be released from lockdown
363
+ """
364
+ try:
365
+ self._send_command(True, self.api_events.release_from_lockdown)
366
+ self.logger.info("OK - Robot released form lockdown")
367
+ except EventConflictError:
326
368
  error_message = (
327
- "Internal Server Error - Failed to release intervention needed state"
369
+ "Previous release robot from lockdown request is still being processed"
328
370
  )
329
- self.logger.error(error_message)
330
- raise HTTPException(
331
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
371
+ self.logger.warning(error_message)
372
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
373
+ except EventTimeoutError:
374
+ error_message = (
375
+ "Cannot release robot from lockdown as it is not in lockdown"
332
376
  )
377
+ self.logger.warning(error_message)
378
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
333
379
 
334
380
  def _send_command(self, input: T1, api_event: APIEvent[T1, T2]) -> T2:
335
381
  if api_event.request.has_event():
@@ -18,15 +18,20 @@ from isar.models.events import Events, SharedState
18
18
  from isar.services.service_connections.mqtt.mqtt_client import props_expiry
19
19
  from isar.state_machine.states.await_next_mission import AwaitNextMission
20
20
  from isar.state_machine.states.blocked_protective_stop import BlockedProtectiveStop
21
+ from isar.state_machine.states.going_to_lockdown import GoingToLockdown
21
22
  from isar.state_machine.states.home import Home
22
23
  from isar.state_machine.states.intervention_needed import InterventionNeeded
24
+ from isar.state_machine.states.lockdown import Lockdown
23
25
  from isar.state_machine.states.monitor import Monitor
24
26
  from isar.state_machine.states.offline import Offline
25
27
  from isar.state_machine.states.paused import Paused
28
+ from isar.state_machine.states.pausing import Pausing
29
+ from isar.state_machine.states.pausing_return_home import PausingReturnHome
26
30
  from isar.state_machine.states.recharging import Recharging
27
31
  from isar.state_machine.states.return_home_paused import ReturnHomePaused
28
32
  from isar.state_machine.states.returning_home import ReturningHome
29
33
  from isar.state_machine.states.stopping import Stopping
34
+ from isar.state_machine.states.stopping_go_to_lockdown import StoppingGoToLockdown
30
35
  from isar.state_machine.states.stopping_return_home import StoppingReturnHome
31
36
  from isar.state_machine.states.unknown_status import UnknownStatus
32
37
  from isar.state_machine.states_enum import States
@@ -102,8 +107,12 @@ class StateMachine(object):
102
107
  self.returning_home_state: State = ReturningHome(self)
103
108
  self.stopping_state: State = Stopping(self)
104
109
  self.paused_state: State = Paused(self)
110
+ self.pausing_state: State = Pausing(self)
105
111
  self.return_home_paused_state: State = ReturnHomePaused(self)
106
112
  self.stopping_return_home_state: State = StoppingReturnHome(self)
113
+ self.pausing_return_home_state: State = PausingReturnHome(self)
114
+ self.stopping_go_to_lockdown_state: State = StoppingGoToLockdown(self)
115
+ self.going_to_lockdown_state: State = GoingToLockdown(self)
107
116
 
108
117
  # States Waiting for mission
109
118
  self.await_next_mission_state: State = AwaitNextMission(self)
@@ -114,6 +123,7 @@ class StateMachine(object):
114
123
  self.offline_state: State = Offline(self)
115
124
  self.blocked_protective_stopping_state: State = BlockedProtectiveStop(self)
116
125
  self.recharging_state: State = Recharging(self)
126
+ self.lockdown_state: State = Lockdown(self)
117
127
 
118
128
  # Error and special status states
119
129
  self.unknown_status_state: State = UnknownStatus(self)
@@ -123,7 +133,9 @@ class StateMachine(object):
123
133
  self.returning_home_state,
124
134
  self.stopping_state,
125
135
  self.stopping_return_home_state,
136
+ self.pausing_return_home_state,
126
137
  self.paused_state,
138
+ self.pausing_state,
127
139
  self.return_home_paused_state,
128
140
  self.await_next_mission_state,
129
141
  self.home_state,
@@ -132,6 +144,9 @@ class StateMachine(object):
132
144
  self.unknown_status_state,
133
145
  self.intervention_needed_state,
134
146
  self.recharging_state,
147
+ self.stopping_go_to_lockdown_state,
148
+ self.going_to_lockdown_state,
149
+ self.lockdown_state,
135
150
  ]
136
151
 
137
152
  self.machine = Machine(
@@ -288,13 +303,14 @@ class StateMachine(object):
288
303
  timestamp=datetime.now(timezone.utc),
289
304
  )
290
305
 
291
- self.mqtt_publisher.publish(
292
- topic=settings.TOPIC_ISAR_MISSION + f"/{self.current_mission.id}",
293
- payload=json.dumps(payload, cls=EnhancedJSONEncoder),
294
- qos=1,
295
- retain=True,
296
- properties=props_expiry(settings.MQTT_MISSION_AND_TASK_EXPIRY),
297
- )
306
+ if self.current_mission:
307
+ self.mqtt_publisher.publish(
308
+ topic=settings.TOPIC_ISAR_MISSION + f"/{self.current_mission.id}",
309
+ payload=json.dumps(payload, cls=EnhancedJSONEncoder),
310
+ qos=1,
311
+ retain=True,
312
+ properties=props_expiry(settings.MQTT_MISSION_AND_TASK_EXPIRY),
313
+ )
298
314
 
299
315
  def publish_task_status(self, task: TASKS) -> None:
300
316
  """Publishes the task status to the MQTT Broker"""
@@ -384,6 +400,10 @@ class StateMachine(object):
384
400
  return RobotStatus.InterventionNeeded
385
401
  elif self.current_state == States.Recharging:
386
402
  return RobotStatus.Recharging
403
+ elif self.current_state == States.Lockdown:
404
+ return RobotStatus.Lockdown
405
+ elif self.current_state == States.GoingToLockdown:
406
+ return RobotStatus.GoingToLockdown
387
407
  else:
388
408
  return RobotStatus.Busy
389
409
 
@@ -1,11 +1,13 @@
1
- from typing import TYPE_CHECKING, List
1
+ from typing import TYPE_CHECKING, Callable, List, Optional
2
2
 
3
+ from isar.apis.models.models import LockdownResponse
3
4
  from isar.config.settings import settings
4
5
  from isar.eventhandlers.eventhandler import (
5
6
  EventHandlerBase,
6
7
  EventHandlerMapping,
7
8
  TimeoutHandlerMapping,
8
9
  )
10
+ from isar.models.events import Event
9
11
  from isar.state_machine.utils.common_event_handlers import (
10
12
  return_home_event_handler,
11
13
  start_mission_event_handler,
@@ -21,6 +23,17 @@ class AwaitNextMission(EventHandlerBase):
21
23
  def __init__(self, state_machine: "StateMachine"):
22
24
  events = state_machine.events
23
25
 
26
+ def _send_to_lockdown_event_handler(
27
+ event: Event[bool],
28
+ ) -> Optional[Callable]:
29
+ should_lockdown: bool = event.consume_event()
30
+ if should_lockdown:
31
+ events.api_requests.send_to_lockdown.response.trigger_event(
32
+ LockdownResponse(lockdown_started=True)
33
+ )
34
+ return state_machine.request_lockdown_mission # type: ignore
35
+ return None
36
+
24
37
  event_handlers: List[EventHandlerMapping] = [
25
38
  EventHandlerMapping(
26
39
  name="start_mission_event",
@@ -39,6 +52,11 @@ class AwaitNextMission(EventHandlerBase):
39
52
  event=events.api_requests.return_home.request,
40
53
  handler=lambda event: stop_mission_event_handler(state_machine, event),
41
54
  ),
55
+ EventHandlerMapping(
56
+ name="send_to_lockdown_event",
57
+ event=events.api_requests.send_to_lockdown.request,
58
+ handler=_send_to_lockdown_event_handler,
59
+ ),
42
60
  ]
43
61
 
44
62
  timers: List[TimeoutHandlerMapping] = [
@@ -0,0 +1,80 @@
1
+ from typing import TYPE_CHECKING, Callable, List, Optional
2
+
3
+ from isar.eventhandlers.eventhandler import EventHandlerBase, EventHandlerMapping
4
+ from isar.models.events import Event
5
+ from isar.state_machine.utils.common_event_handlers import (
6
+ mission_started_event_handler,
7
+ task_status_event_handler,
8
+ task_status_failed_event_handler,
9
+ )
10
+ from robot_interface.models.exceptions.robot_exceptions import ErrorMessage, ErrorReason
11
+ from robot_interface.models.mission.status import TaskStatus
12
+
13
+ if TYPE_CHECKING:
14
+ from isar.state_machine.state_machine import StateMachine
15
+
16
+
17
+ class GoingToLockdown(EventHandlerBase):
18
+
19
+ def __init__(self, state_machine: "StateMachine"):
20
+ events = state_machine.events
21
+
22
+ def _handle_task_completed(status: TaskStatus):
23
+ if status != TaskStatus.Successful:
24
+ state_machine.current_mission.error_message = ErrorMessage(
25
+ error_reason=ErrorReason.RobotActionException,
26
+ error_description="Lock down mission failed.",
27
+ )
28
+ return state_machine.lockdown_mission_failed # type: ignore
29
+ return state_machine.reached_lockdown # type: ignore
30
+
31
+ def _mission_failed_event_handler(
32
+ event: Event[Optional[ErrorMessage]],
33
+ ) -> Optional[Callable]:
34
+ mission_failed: Optional[ErrorMessage] = event.consume_event()
35
+ if mission_failed is not None:
36
+ state_machine.logger.warning(
37
+ f"Failed to initiate mission "
38
+ f"{str(state_machine.current_mission.id)[:8]} because: "
39
+ f"{mission_failed.error_description}"
40
+ )
41
+ state_machine.current_mission.error_message = ErrorMessage(
42
+ error_reason=mission_failed.error_reason,
43
+ error_description=mission_failed.error_description,
44
+ )
45
+ return state_machine.lockdown_mission_failed # type: ignore
46
+ return None
47
+
48
+ event_handlers: List[EventHandlerMapping] = [
49
+ EventHandlerMapping(
50
+ name="mission_started_event",
51
+ event=events.robot_service_events.mission_started,
52
+ handler=lambda event: mission_started_event_handler(
53
+ state_machine, event
54
+ ),
55
+ ),
56
+ EventHandlerMapping(
57
+ name="mission_failed_event",
58
+ event=events.robot_service_events.mission_failed,
59
+ handler=_mission_failed_event_handler,
60
+ ),
61
+ EventHandlerMapping(
62
+ name="task_status_failed_event",
63
+ event=events.robot_service_events.task_status_failed,
64
+ handler=lambda event: task_status_failed_event_handler(
65
+ state_machine, _handle_task_completed, event
66
+ ),
67
+ ),
68
+ EventHandlerMapping(
69
+ name="task_status_event",
70
+ event=events.robot_service_events.task_status_updated,
71
+ handler=lambda event: task_status_event_handler(
72
+ state_machine, _handle_task_completed, event
73
+ ),
74
+ ),
75
+ ]
76
+ super().__init__(
77
+ state_name="going_to_lockdown",
78
+ state_machine=state_machine,
79
+ event_handler_mappings=event_handlers,
80
+ )
@@ -1,5 +1,6 @@
1
1
  from typing import TYPE_CHECKING, Callable, List, Optional
2
2
 
3
+ from isar.apis.models.models import LockdownResponse
3
4
  from isar.eventhandlers.eventhandler import EventHandlerBase, EventHandlerMapping
4
5
  from isar.models.events import Event
5
6
  from isar.state_machine.utils.common_event_handlers import (
@@ -30,6 +31,15 @@ class Home(EventHandlerBase):
30
31
  return state_machine.robot_status_changed # type: ignore
31
32
  return None
32
33
 
34
+ def _send_to_lockdown_event_handler(event: Event[bool]):
35
+ should_send_robot_home: bool = event.consume_event()
36
+ if should_send_robot_home:
37
+ events.api_requests.send_to_lockdown.response.trigger_event(
38
+ LockdownResponse(lockdown_started=True)
39
+ )
40
+ return state_machine.reached_lockdown # type: ignore
41
+ return None
42
+
33
43
  event_handlers: List[EventHandlerMapping] = [
34
44
  EventHandlerMapping(
35
45
  name="start_mission_event",
@@ -53,6 +63,11 @@ class Home(EventHandlerBase):
53
63
  event=shared_state.robot_status,
54
64
  handler=_robot_status_event_handler,
55
65
  ),
66
+ EventHandlerMapping(
67
+ name="send_to_lockdown_event",
68
+ event=events.api_requests.send_to_lockdown.request,
69
+ handler=_send_to_lockdown_event_handler,
70
+ ),
56
71
  ]
57
72
  super().__init__(
58
73
  state_name="home",
@@ -0,0 +1,37 @@
1
+ from typing import TYPE_CHECKING, List
2
+
3
+ from isar.eventhandlers.eventhandler import EventHandlerBase, EventHandlerMapping
4
+ from isar.models.events import Event
5
+
6
+ if TYPE_CHECKING:
7
+ from isar.state_machine.state_machine import StateMachine
8
+
9
+
10
+ class Lockdown(EventHandlerBase):
11
+
12
+ def __init__(self, state_machine: "StateMachine"):
13
+ events = state_machine.events
14
+
15
+ def _release_from_lockdown_handler(event: Event[bool]):
16
+ should_release_from_lockdown: bool = event.consume_event()
17
+ if should_release_from_lockdown:
18
+ events.api_requests.release_from_lockdown.response.trigger_event(True)
19
+ if state_machine.battery_level_is_above_mission_start_threshold():
20
+ return state_machine.release_from_lockdown # type: ignore
21
+ else:
22
+ return state_machine.starting_recharging # type: ignore
23
+ return None
24
+
25
+ event_handlers: List[EventHandlerMapping] = [
26
+ EventHandlerMapping(
27
+ name="release_from_lockdown",
28
+ event=events.api_requests.release_from_lockdown.request,
29
+ handler=_release_from_lockdown_handler,
30
+ ),
31
+ ]
32
+
33
+ super().__init__(
34
+ state_name="lockdown",
35
+ state_machine=state_machine,
36
+ event_handler_mappings=event_handlers,
37
+ )
@@ -63,6 +63,17 @@ class Monitor(EventHandlerBase):
63
63
  return state_machine.stop # type: ignore
64
64
  return None
65
65
 
66
+ def _send_to_lockdown_event_handler(
67
+ event: Event[bool],
68
+ ) -> Optional[Callable]:
69
+ should_lockdown: bool = event.consume_event()
70
+ if should_lockdown:
71
+ state_machine.logger.warning(
72
+ "Cancelling current mission due to robot going to lockdown"
73
+ )
74
+ return state_machine.stop_go_to_lockdown # type: ignore
75
+ return None
76
+
66
77
  event_handlers: List[EventHandlerMapping] = [
67
78
  EventHandlerMapping(
68
79
  name="stop_mission_event",
@@ -107,6 +118,11 @@ class Monitor(EventHandlerBase):
107
118
  event=shared_state.robot_battery_level,
108
119
  handler=_robot_battery_level_updated_handler,
109
120
  ),
121
+ EventHandlerMapping(
122
+ name="send_to_lockdown_event",
123
+ event=events.api_requests.send_to_lockdown.request,
124
+ handler=_send_to_lockdown_event_handler,
125
+ ),
110
126
  ]
111
127
  super().__init__(
112
128
  state_name="monitor",