Skip to content

Commit ebbf89b

Browse files
committed
Auto test valid API states
1 parent e2585c2 commit ebbf89b

12 files changed

Lines changed: 268 additions & 268 deletions

File tree

src/isar/apis/schedule/scheduling_controller.py

Lines changed: 0 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
)
1818
from isar.config.settings import robot_settings
1919
from isar.services.utilities.scheduling_utilities import SchedulingUtilities
20-
from isar.state_machine.states_enum import States
2120
from robot_interface.models.mission.mission import Mission
2221
from robot_interface.models.mission.task import TASKS
2322

@@ -54,9 +53,6 @@ def start_mission(
5453
detail=error_message_no_mission_definition,
5554
)
5655

57-
state: States = self.scheduling_utilities.get_state()
58-
self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state)
59-
6056
try:
6157
mission: Mission = to_isar_mission(
6258
start_mission_definition=mission_definition
@@ -81,32 +77,12 @@ def start_mission(
8177
def return_home(self) -> None:
8278
self.logger.info("Received request to return home")
8379

84-
state: States = self.scheduling_utilities.get_state()
85-
self.scheduling_utilities.verify_state_machine_ready_to_receive_return_home_mission(
86-
state
87-
)
88-
8980
self.scheduling_utilities.return_home()
9081

9182
@tracer.start_as_current_span("pause_mission")
9283
def pause_mission(self) -> ControlMissionResponse:
9384
self.logger.info("Received request to pause current mission")
9485

95-
state: States = self.scheduling_utilities.get_state()
96-
97-
if state not in [
98-
States.Monitor,
99-
States.ReturningHome,
100-
]:
101-
error_message = (
102-
f"Conflict - Pause command received in invalid state - State: {state}"
103-
)
104-
self.logger.warning(error_message)
105-
raise HTTPException(
106-
status_code=HTTPStatus.CONFLICT,
107-
detail=error_message,
108-
)
109-
11086
pause_mission_response: ControlMissionResponse = (
11187
self.scheduling_utilities.pause_mission()
11288
)
@@ -116,15 +92,6 @@ def pause_mission(self) -> ControlMissionResponse:
11692
def resume_mission(self) -> ControlMissionResponse:
11793
self.logger.info("Received request to resume current mission")
11894

119-
state: States = self.scheduling_utilities.get_state()
120-
121-
if state not in [States.Paused, States.ReturnHomePaused]:
122-
error_message = (
123-
f"Conflict - Resume command received in invalid state - State: {state}"
124-
)
125-
self.logger.warning(error_message)
126-
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
127-
12895
resume_mission_response: ControlMissionResponse = (
12996
self.scheduling_utilities.resume_mission()
13097
)
@@ -143,25 +110,6 @@ def stop_mission(
143110

144111
self.logger.info("Received request to stop current mission")
145112

146-
state: States = self.scheduling_utilities.get_state()
147-
148-
if (
149-
state == States.UnknownStatus
150-
or state == States.Stopping
151-
or state == States.Offline
152-
or state == States.Home
153-
or state == States.ReturningHome
154-
or state == States.GoingToLockdown
155-
or state == States.GoingToRecharging
156-
or state == States.Recharging
157-
or state == States.Maintenance
158-
):
159-
error_message = (
160-
f"Conflict - Stop command received in invalid state - State: {state}"
161-
)
162-
self.logger.warning(error_message)
163-
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
164-
165113
stop_mission_response: ControlMissionResponse = (
166114
self.scheduling_utilities.stop_mission(
167115
mission_id.mission_id if mission_id.mission_id else ""
@@ -173,84 +121,34 @@ def stop_mission(
173121
def release_intervention_needed(self) -> None:
174122
self.logger.info("Received request to release intervention needed state")
175123

176-
state: States = self.scheduling_utilities.get_state()
177-
178-
if state != States.InterventionNeeded:
179-
error_message = f"Conflict - Release intervention needed command received in invalid state - State: {state}"
180-
self.logger.warning(error_message)
181-
raise HTTPException(
182-
status_code=HTTPStatus.CONFLICT,
183-
detail=error_message,
184-
)
185-
186124
self.scheduling_utilities.release_intervention_needed()
187125
self.logger.info("Released intervention needed state successfully")
188126

189127
@tracer.start_as_current_span("lockdown")
190128
def lockdown(self) -> None:
191129
self.logger.info("Received request to lockdown robot")
192130

193-
state: States = self.scheduling_utilities.get_state()
194-
195-
if state == States.Lockdown:
196-
error_message = "Conflict - Lockdown command received in lockdown state"
197-
self.logger.warning(error_message)
198-
raise HTTPException(
199-
status_code=HTTPStatus.CONFLICT,
200-
detail=error_message,
201-
)
202-
203131
self.scheduling_utilities.lock_down_robot()
204132
self.logger.info("Lockdown started successfully")
205133

206134
@tracer.start_as_current_span("release_lockdown")
207135
def release_lockdown(self) -> None:
208136
self.logger.info("Received request to release robot lockdown")
209137

210-
state: States = self.scheduling_utilities.get_state()
211-
212-
if state != States.Lockdown:
213-
error_message = f"Conflict - Release lockdown command received in invalid state - State: {state}"
214-
self.logger.warning(error_message)
215-
raise HTTPException(
216-
status_code=HTTPStatus.CONFLICT,
217-
detail=error_message,
218-
)
219-
220138
self.scheduling_utilities.release_robot_lockdown()
221139
self.logger.info("Released lockdown successfully")
222140

223141
@tracer.start_as_current_span("maintenance_mode")
224142
def set_maintenance_mode(self) -> None:
225143
self.logger.info("Received request to set maintenance_mode")
226144

227-
state: States = self.scheduling_utilities.get_state()
228-
229-
if state == States.Maintenance or state == States.StoppingDueToMaintenance:
230-
message = f"Conflict - Call to set maintenance mode was given while in state {state}."
231-
self.logger.info(message)
232-
raise HTTPException(
233-
status_code=HTTPStatus.CONFLICT,
234-
detail=message,
235-
)
236-
237145
self.scheduling_utilities.set_maintenance_mode()
238146
self.logger.info("Maintenance mode has been set")
239147

240148
@tracer.start_as_current_span("release_maintenance_mode")
241149
def release_maintenance_mode(self) -> None:
242150
self.logger.info("Received request to release robot from maintenance mode")
243151

244-
state: States = self.scheduling_utilities.get_state()
245-
246-
if state != States.Maintenance:
247-
message = f"Conflict - Release maintenance mode command received in invalid state - State: {state}"
248-
self.logger.info(message)
249-
raise HTTPException(
250-
status_code=HTTPStatus.CONFLICT,
251-
detail=message,
252-
)
253-
254152
self.scheduling_utilities.release_maintenance_mode()
255153
self.logger.info("Maintenance mode successfully released")
256154

src/isar/models/events.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,14 @@ class APIEvent[T1, T2]:
105105
api to state machine while the response is from state machine to api.
106106
"""
107107

108-
def __init__(self, name: str):
108+
prioritized: bool
109+
110+
def __init__(self, name: str, prioritized: bool = False):
109111
self.request: Event[T1] = Event("api-" + name + "-request")
110112
self.response: Event[T2] = Event("api-" + name + "-request")
113+
self.prioritized = (
114+
prioritized # For when we want to try even if the statemachine is not ready
115+
)
111116
self.lock: Lock = Lock()
112117

113118

@@ -130,13 +135,13 @@ def __init__(self) -> None:
130135
APIEvent("release_intervention_needed")
131136
)
132137
self.send_to_lockdown: APIEvent[EmptyMessage, LockdownResponse] = APIEvent(
133-
"send_to_lockdown"
138+
"send_to_lockdown", prioritized=True
134139
)
135140
self.release_from_lockdown: APIEvent[EmptyMessage, EmptyMessage] = APIEvent(
136141
"release_from_lockdown"
137142
)
138143
self.set_maintenance_mode: APIEvent[EmptyMessage, MaintenanceResponse] = (
139-
APIEvent("set_maintenance_mode")
144+
APIEvent("set_maintenance_mode", prioritized=True)
140145
)
141146
self.release_from_maintenance_mode: APIEvent[EmptyMessage, EmptyMessage] = (
142147
APIEvent("release_from_maintenance_mode")

src/isar/modules.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,19 @@ class ApplicationContainer(containers.DeclarativeContainer):
3939
mqtt_queue=providers.Callable(events.provided.mqtt_queue),
4040
)
4141

42+
# State machine
43+
state_machine = providers.Singleton(
44+
StateMachine,
45+
events=events,
46+
mqtt_publisher=mqtt_client,
47+
)
48+
4249
# API and controllers
4350
authenticator = providers.Singleton(Authenticator)
4451
scheduling_utilities = providers.Singleton(
4552
SchedulingUtilities,
4653
events=events,
54+
state_machine=state_machine,
4755
)
4856
scheduling_controller = providers.Singleton(
4957
SchedulingController, scheduling_utilities=scheduling_utilities
@@ -69,13 +77,6 @@ class ApplicationContainer(containers.DeclarativeContainer):
6977
storage_handlers_temp.append(blob_storage)
7078
storage_handlers = providers.List(*storage_handlers_temp)
7179

72-
# State machine
73-
state_machine = providers.Singleton(
74-
StateMachine,
75-
events=events,
76-
mqtt_publisher=mqtt_client,
77-
)
78-
7980
# Robot
8081
robot = providers.Singleton(
8182
RobotService,

src/isar/services/utilities/scheduling_utilities.py

Lines changed: 16 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111
APIEvent,
1212
APIRequests,
1313
EmptyMessage,
14-
Event,
1514
EventConflictError,
1615
Events,
1716
EventTimeoutError,
1817
)
19-
from isar.state_machine.states_enum import States
18+
from isar.state_machine.state_machine import StateMachine
2019
from robot_interface.models.mission.mission import Mission
2120

2221
T1 = TypeVar("T1")
@@ -32,30 +31,12 @@ class SchedulingUtilities:
3231
def __init__(
3332
self,
3433
events: Events,
34+
state_machine: StateMachine,
3535
):
3636
self.api_events: APIRequests = events.api_requests
37-
self.state_event: Event[States] = events.state
37+
self.state_machine: StateMachine = state_machine
3838
self.logger = logging.getLogger("api")
3939

40-
def get_state(self) -> States:
41-
"""Return the current state of the state machine
42-
43-
Raises
44-
------
45-
HTTPException 500 Internal Server Error
46-
If the current state is not available on the queue
47-
"""
48-
current_state = self.state_event.check()
49-
if current_state is None:
50-
error_message: str = (
51-
"Internal Server Error - Current state of the state machine is unknown"
52-
)
53-
self.logger.error(error_message)
54-
raise HTTPException(
55-
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
56-
)
57-
return current_state
58-
5940
def verify_robot_capable_of_mission(
6041
self, mission: Mission, robot_capabilities: list[str]
6142
) -> bool:
@@ -83,45 +64,6 @@ def verify_robot_capable_of_mission(
8364

8465
return True
8566

86-
def verify_state_machine_ready_to_receive_mission(self, state: States) -> bool:
87-
"""Verify that the state machine is ready to receive a mission
88-
89-
Raises
90-
------
91-
HTTPException 409 Conflict
92-
If state machine is not home, robot standing still, awaiting next mission
93-
return home paused or returning home and therefore cannot start a new mission
94-
"""
95-
if (
96-
state == States.Home
97-
or state == States.AwaitNextMission
98-
or state == States.ReturningHome
99-
or state == States.ReturnHomePaused
100-
):
101-
return True
102-
103-
error_message = f"Conflict - Robot is not home, robot standing still, awaiting next mission or returning home - State: {state}"
104-
self.logger.warning(error_message)
105-
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
106-
107-
def verify_state_machine_ready_to_receive_return_home_mission(
108-
self, state: States
109-
) -> bool:
110-
"""Verify that the state machine is ready to receive a return home mission
111-
112-
Raises
113-
------
114-
HTTPException 409 Conflict
115-
If state machine is not home, robot standing still or awaiting next mission
116-
and therefore cannot start a new return home mission
117-
"""
118-
if state == States.Home or state == States.AwaitNextMission:
119-
return True
120-
121-
error_message = f"Conflict - Robot is not home, robot standing still or awaiting next mission - State: {state}"
122-
self.logger.warning(error_message)
123-
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
124-
12567
def log_mission_overview(self, mission: Mission) -> None:
12668
"""Log an overview of the tasks in a mission"""
12769
log_statements: list[str] = []
@@ -433,7 +375,20 @@ def release_maintenance_mode(self) -> None:
433375
self.logger.warning(error_message)
434376
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
435377

378+
def _verify_valid_state(self, api_event: APIEvent) -> None:
379+
if (
380+
not api_event.prioritized
381+
and not self.state_machine.current_state_handles_event(api_event.request)
382+
):
383+
error_message = (
384+
"Conflict - Robot is not in a state where it can handle the request"
385+
)
386+
self.logger.warning(error_message)
387+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
388+
436389
def _send_command(self, input: T1, api_event: APIEvent[T1, T2]) -> T2:
390+
self._verify_valid_state(api_event)
391+
437392
if not api_event.lock.acquire(blocking=False):
438393
raise EventConflictError("API event has already been sent")
439394

src/isar/state_machine/state.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ def get_event_timer_by_name(
6464
)
6565
return filtered_timers[0] if len(filtered_timers) > 0 else None
6666

67+
def handles_event(self, event: Event) -> bool:
68+
allowed_events: list[Event] = [m.event for m in self.event_handler_mappings]
69+
return event in allowed_events
70+
6771
def run(self) -> Transition | None:
6872
timers = deepcopy(self.timers)
6973
entered_time = time.time()

0 commit comments

Comments
 (0)