isar 1.15.0__py3-none-any.whl → 1.34.9__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.
- isar/__init__.py +2 -5
- isar/apis/api.py +159 -66
- isar/apis/models/__init__.py +0 -1
- isar/apis/models/models.py +22 -12
- isar/apis/models/start_mission_definition.py +128 -123
- isar/apis/robot_control/robot_controller.py +41 -0
- isar/apis/schedule/scheduling_controller.py +135 -121
- isar/apis/security/authentication.py +5 -5
- isar/config/certs/ca-cert.pem +32 -32
- isar/config/keyvault/keyvault_service.py +1 -2
- isar/config/log.py +47 -39
- isar/config/logging.conf +16 -31
- isar/config/open_telemetry.py +102 -0
- isar/config/predefined_mission_definition/default_exr.json +49 -0
- isar/config/predefined_mission_definition/default_mission.json +1 -5
- isar/config/predefined_mission_definition/default_turtlebot.json +4 -11
- isar/config/predefined_missions/default.json +67 -87
- isar/config/predefined_missions/default_extra_capabilities.json +107 -0
- isar/config/settings.py +119 -142
- isar/eventhandlers/eventhandler.py +123 -0
- isar/mission_planner/local_planner.py +6 -20
- isar/mission_planner/mission_planner_interface.py +1 -1
- isar/models/events.py +184 -0
- isar/models/status.py +18 -0
- isar/modules.py +118 -205
- isar/robot/robot.py +377 -0
- isar/robot/robot_battery.py +60 -0
- isar/robot/robot_monitor_mission.py +357 -0
- isar/robot/robot_pause_mission.py +74 -0
- isar/robot/robot_resume_mission.py +67 -0
- isar/robot/robot_start_mission.py +66 -0
- isar/robot/robot_status.py +61 -0
- isar/robot/robot_stop_mission.py +68 -0
- isar/robot/robot_upload_inspection.py +75 -0
- isar/script.py +171 -0
- isar/services/service_connections/mqtt/mqtt_client.py +47 -11
- isar/services/service_connections/mqtt/robot_heartbeat_publisher.py +32 -0
- isar/services/service_connections/mqtt/robot_info_publisher.py +4 -3
- isar/services/service_connections/persistent_memory.py +69 -0
- isar/services/utilities/mqtt_utilities.py +93 -0
- isar/services/utilities/robot_utilities.py +20 -0
- isar/services/utilities/scheduling_utilities.py +393 -65
- isar/state_machine/state_machine.py +227 -486
- isar/state_machine/states/__init__.py +0 -7
- isar/state_machine/states/await_next_mission.py +114 -0
- isar/state_machine/states/blocked_protective_stop.py +60 -0
- isar/state_machine/states/going_to_lockdown.py +95 -0
- isar/state_machine/states/going_to_recharging.py +92 -0
- isar/state_machine/states/home.py +115 -0
- isar/state_machine/states/intervention_needed.py +77 -0
- isar/state_machine/states/lockdown.py +38 -0
- isar/state_machine/states/maintenance.py +36 -0
- isar/state_machine/states/monitor.py +137 -166
- isar/state_machine/states/offline.py +60 -0
- isar/state_machine/states/paused.py +92 -23
- isar/state_machine/states/pausing.py +48 -0
- isar/state_machine/states/pausing_return_home.py +48 -0
- isar/state_machine/states/recharging.py +80 -0
- isar/state_machine/states/resuming.py +57 -0
- isar/state_machine/states/resuming_return_home.py +64 -0
- isar/state_machine/states/return_home_paused.py +109 -0
- isar/state_machine/states/returning_home.py +217 -0
- isar/state_machine/states/stopping.py +61 -0
- isar/state_machine/states/stopping_due_to_maintenance.py +61 -0
- isar/state_machine/states/stopping_go_to_lockdown.py +60 -0
- isar/state_machine/states/stopping_go_to_recharge.py +51 -0
- isar/state_machine/states/stopping_return_home.py +77 -0
- isar/state_machine/states/unknown_status.py +72 -0
- isar/state_machine/states_enum.py +22 -5
- isar/state_machine/transitions/mission.py +192 -0
- isar/state_machine/transitions/return_home.py +106 -0
- isar/state_machine/transitions/robot_status.py +80 -0
- isar/state_machine/utils/common_event_handlers.py +73 -0
- isar/storage/blob_storage.py +71 -45
- isar/storage/local_storage.py +28 -14
- isar/storage/storage_interface.py +28 -6
- isar/storage/uploader.py +184 -55
- isar/storage/utilities.py +35 -27
- isar-1.34.9.dist-info/METADATA +496 -0
- isar-1.34.9.dist-info/RECORD +135 -0
- {isar-1.15.0.dist-info → isar-1.34.9.dist-info}/WHEEL +1 -1
- isar-1.34.9.dist-info/entry_points.txt +3 -0
- robot_interface/models/exceptions/__init__.py +0 -7
- robot_interface/models/exceptions/robot_exceptions.py +274 -4
- robot_interface/models/initialize/__init__.py +0 -1
- robot_interface/models/inspection/__init__.py +0 -13
- robot_interface/models/inspection/inspection.py +43 -34
- robot_interface/models/mission/mission.py +18 -14
- robot_interface/models/mission/status.py +20 -25
- robot_interface/models/mission/task.py +156 -92
- robot_interface/models/robots/battery_state.py +6 -0
- robot_interface/models/robots/media.py +13 -0
- robot_interface/models/robots/robot_model.py +7 -7
- robot_interface/robot_interface.py +135 -66
- robot_interface/telemetry/mqtt_client.py +84 -12
- robot_interface/telemetry/payloads.py +111 -12
- robot_interface/utilities/json_service.py +7 -1
- isar/config/predefined_missions/default_turtlebot.json +0 -110
- isar/config/predefined_poses/__init__.py +0 -0
- isar/config/predefined_poses/predefined_poses.py +0 -616
- isar/config/settings.env +0 -26
- isar/mission_planner/sequential_task_selector.py +0 -23
- isar/mission_planner/task_selector_interface.py +0 -31
- isar/models/communication/__init__.py +0 -0
- isar/models/communication/message.py +0 -12
- isar/models/communication/queues/__init__.py +0 -4
- isar/models/communication/queues/queue_io.py +0 -12
- isar/models/communication/queues/queue_timeout_error.py +0 -2
- isar/models/communication/queues/queues.py +0 -19
- isar/models/communication/queues/status_queue.py +0 -20
- isar/models/mission_metadata/__init__.py +0 -0
- isar/services/readers/__init__.py +0 -0
- isar/services/readers/base_reader.py +0 -37
- isar/services/service_connections/mqtt/robot_status_publisher.py +0 -93
- isar/services/service_connections/stid/__init__.py +0 -0
- isar/services/service_connections/stid/stid_service.py +0 -45
- isar/services/utilities/queue_utilities.py +0 -39
- isar/state_machine/states/idle.py +0 -40
- isar/state_machine/states/initialize.py +0 -60
- isar/state_machine/states/initiate.py +0 -129
- isar/state_machine/states/off.py +0 -18
- isar/state_machine/states/stop.py +0 -78
- isar/storage/slimm_storage.py +0 -181
- isar-1.15.0.dist-info/METADATA +0 -417
- isar-1.15.0.dist-info/RECORD +0 -113
- robot_interface/models/initialize/initialize_params.py +0 -9
- robot_interface/models/mission/step.py +0 -211
- {isar-1.15.0.dist-info → isar-1.34.9.dist-info/licenses}/LICENSE +0 -0
- {isar-1.15.0.dist-info → isar-1.34.9.dist-info}/top_level.txt +0 -0
|
@@ -1,590 +1,331 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
-
import queue
|
|
4
|
-
import time
|
|
5
3
|
from collections import deque
|
|
6
|
-
from datetime import datetime
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from threading import Event
|
|
7
6
|
from typing import Deque, List, Optional
|
|
8
7
|
|
|
9
|
-
from alitra import Pose
|
|
10
|
-
from injector import inject
|
|
11
8
|
from transitions import Machine
|
|
12
9
|
from transitions.core import State
|
|
13
10
|
|
|
14
|
-
from isar.apis.models.models import ControlMissionResponse
|
|
15
11
|
from isar.config.settings import settings
|
|
16
|
-
from isar.
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
from isar.models.events import Events, SharedState
|
|
13
|
+
from isar.models.status import IsarStatus
|
|
14
|
+
from isar.services.service_connections.persistent_memory import (
|
|
15
|
+
NoSuchRobotException,
|
|
16
|
+
create_persistent_robot_state,
|
|
17
|
+
read_persistent_robot_state_is_maintenance_mode,
|
|
19
18
|
)
|
|
20
|
-
from isar.
|
|
21
|
-
from isar.
|
|
22
|
-
from isar.state_machine.states import
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
from isar.services.utilities.mqtt_utilities import publish_isar_status
|
|
20
|
+
from isar.state_machine.states.await_next_mission import AwaitNextMission
|
|
21
|
+
from isar.state_machine.states.blocked_protective_stop import BlockedProtectiveStop
|
|
22
|
+
from isar.state_machine.states.going_to_lockdown import GoingToLockdown
|
|
23
|
+
from isar.state_machine.states.going_to_recharging import GoingToRecharging
|
|
24
|
+
from isar.state_machine.states.home import Home
|
|
25
|
+
from isar.state_machine.states.intervention_needed import InterventionNeeded
|
|
26
|
+
from isar.state_machine.states.lockdown import Lockdown
|
|
27
|
+
from isar.state_machine.states.maintenance import Maintenance
|
|
28
|
+
from isar.state_machine.states.monitor import Monitor
|
|
29
|
+
from isar.state_machine.states.offline import Offline
|
|
30
|
+
from isar.state_machine.states.paused import Paused
|
|
31
|
+
from isar.state_machine.states.pausing import Pausing
|
|
32
|
+
from isar.state_machine.states.pausing_return_home import PausingReturnHome
|
|
33
|
+
from isar.state_machine.states.recharging import Recharging
|
|
34
|
+
from isar.state_machine.states.resuming import Resuming
|
|
35
|
+
from isar.state_machine.states.resuming_return_home import ResumingReturnHome
|
|
36
|
+
from isar.state_machine.states.return_home_paused import ReturnHomePaused
|
|
37
|
+
from isar.state_machine.states.returning_home import ReturningHome
|
|
38
|
+
from isar.state_machine.states.stopping import Stopping
|
|
39
|
+
from isar.state_machine.states.stopping_due_to_maintenance import (
|
|
40
|
+
StoppingDueToMaintenance,
|
|
30
41
|
)
|
|
42
|
+
from isar.state_machine.states.stopping_go_to_lockdown import StoppingGoToLockdown
|
|
43
|
+
from isar.state_machine.states.stopping_go_to_recharge import StoppingGoToRecharge
|
|
44
|
+
from isar.state_machine.states.stopping_return_home import StoppingReturnHome
|
|
45
|
+
from isar.state_machine.states.unknown_status import UnknownStatus
|
|
31
46
|
from isar.state_machine.states_enum import States
|
|
32
|
-
from
|
|
47
|
+
from isar.state_machine.transitions.mission import get_mission_transitions
|
|
48
|
+
from isar.state_machine.transitions.return_home import get_return_home_transitions
|
|
49
|
+
from isar.state_machine.transitions.robot_status import get_robot_status_transitions
|
|
33
50
|
from robot_interface.models.mission.mission import Mission
|
|
34
|
-
from robot_interface.models.mission.
|
|
35
|
-
from robot_interface.models.mission.step import Step
|
|
36
|
-
from robot_interface.models.mission.task import Task
|
|
51
|
+
from robot_interface.models.mission.task import ReturnToHome
|
|
37
52
|
from robot_interface.robot_interface import RobotInterface
|
|
38
53
|
from robot_interface.telemetry.mqtt_client import MqttClientInterface
|
|
54
|
+
from robot_interface.telemetry.payloads import (
|
|
55
|
+
InterventionNeededPayload,
|
|
56
|
+
MissionAbortedPayload,
|
|
57
|
+
)
|
|
39
58
|
from robot_interface.utilities.json_service import EnhancedJSONEncoder
|
|
40
59
|
|
|
41
60
|
|
|
42
61
|
class StateMachine(object):
|
|
43
62
|
"""Handles state transitions for supervisory robot control."""
|
|
44
63
|
|
|
45
|
-
@inject
|
|
46
64
|
def __init__(
|
|
47
65
|
self,
|
|
48
|
-
|
|
66
|
+
events: Events,
|
|
67
|
+
shared_state: SharedState,
|
|
49
68
|
robot: RobotInterface,
|
|
50
69
|
mqtt_publisher: MqttClientInterface,
|
|
51
|
-
task_selector: TaskSelectorInterface,
|
|
52
|
-
sleep_time: float = settings.FSM_SLEEP_TIME,
|
|
53
|
-
stepwise_mission: bool = settings.RUN_MISSION_STEPWISE,
|
|
54
|
-
stop_robot_attempts_limit: int = settings.STOP_ROBOT_ATTEMPTS_LIMIT,
|
|
55
|
-
transitions_log_length: int = settings.STATE_TRANSITIONS_LOG_LENGTH,
|
|
56
70
|
):
|
|
57
71
|
"""Initializes the state machine.
|
|
58
72
|
|
|
59
73
|
Parameters
|
|
60
74
|
----------
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
events : Events
|
|
76
|
+
Events used for API and robot service communication.
|
|
63
77
|
robot : RobotInterface
|
|
64
78
|
Instance of robot interface.
|
|
65
79
|
mqtt_publisher : MqttClientInterface
|
|
66
80
|
Instance of MQTT client interface which has a publish function
|
|
67
|
-
sleep_time : float
|
|
68
|
-
Time to sleep in between state machine iterations.
|
|
69
|
-
stop_robot_attempts_limit : int
|
|
70
|
-
Maximum attempts to stop the robot when stop command is received
|
|
71
|
-
transitions_log_length : int
|
|
72
|
-
Length of state transition log list.
|
|
73
81
|
|
|
74
82
|
"""
|
|
75
83
|
self.logger = logging.getLogger("state_machine")
|
|
76
84
|
|
|
77
|
-
self.
|
|
85
|
+
self.events: Events = events
|
|
86
|
+
self.shared_state: SharedState = shared_state
|
|
78
87
|
self.robot: RobotInterface = robot
|
|
79
88
|
self.mqtt_publisher: Optional[MqttClientInterface] = mqtt_publisher
|
|
80
|
-
|
|
81
|
-
self.
|
|
89
|
+
|
|
90
|
+
self.signal_state_machine_to_stop: Event = Event()
|
|
82
91
|
|
|
83
92
|
# List of states
|
|
84
|
-
|
|
85
|
-
self.paused_state: State = Paused(self)
|
|
86
|
-
self.idle_state: State = Idle(self)
|
|
87
|
-
self.initialize_state: State = Initialize(self)
|
|
93
|
+
# States running mission
|
|
88
94
|
self.monitor_state: State = Monitor(self)
|
|
89
|
-
self.
|
|
90
|
-
self.
|
|
95
|
+
self.returning_home_state: State = ReturningHome(self)
|
|
96
|
+
self.stopping_state: State = Stopping(self)
|
|
97
|
+
self.paused_state: State = Paused(self)
|
|
98
|
+
self.pausing_state: State = Pausing(self)
|
|
99
|
+
self.return_home_paused_state: State = ReturnHomePaused(self)
|
|
100
|
+
self.stopping_return_home_state: State = StoppingReturnHome(self)
|
|
101
|
+
self.pausing_return_home_state: State = PausingReturnHome(self)
|
|
102
|
+
self.resuming_state: State = Resuming(self)
|
|
103
|
+
self.resuming_return_home_state: State = ResumingReturnHome(self)
|
|
104
|
+
self.stopping_go_to_lockdown_state: State = StoppingGoToLockdown(self)
|
|
105
|
+
self.stopping_go_to_recharge_state: State = StoppingGoToRecharge(self)
|
|
106
|
+
self.going_to_lockdown_state: State = GoingToLockdown(self)
|
|
107
|
+
self.going_to_recharging_state: State = GoingToRecharging(self)
|
|
108
|
+
self.stopping_due_to_maintenance_state: State = StoppingDueToMaintenance(self)
|
|
109
|
+
|
|
110
|
+
# States Waiting for mission
|
|
111
|
+
self.await_next_mission_state: State = AwaitNextMission(self)
|
|
112
|
+
self.home_state: State = Home(self)
|
|
113
|
+
self.intervention_needed_state: State = InterventionNeeded(self)
|
|
114
|
+
|
|
115
|
+
# Status states
|
|
116
|
+
self.offline_state: State = Offline(self)
|
|
117
|
+
self.blocked_protective_stopping_state: State = BlockedProtectiveStop(self)
|
|
118
|
+
self.recharging_state: State = Recharging(self)
|
|
119
|
+
self.lockdown_state: State = Lockdown(self)
|
|
120
|
+
self.maintenance_state: State = Maintenance(self)
|
|
121
|
+
|
|
122
|
+
# Error and special status states
|
|
123
|
+
self.unknown_status_state: State = UnknownStatus(self)
|
|
91
124
|
|
|
92
125
|
self.states: List[State] = [
|
|
93
|
-
self.off_state,
|
|
94
|
-
self.idle_state,
|
|
95
|
-
self.initialize_state,
|
|
96
|
-
self.initiate_state,
|
|
97
126
|
self.monitor_state,
|
|
98
|
-
self.
|
|
127
|
+
self.returning_home_state,
|
|
128
|
+
self.stopping_state,
|
|
129
|
+
self.stopping_return_home_state,
|
|
130
|
+
self.pausing_return_home_state,
|
|
99
131
|
self.paused_state,
|
|
132
|
+
self.pausing_state,
|
|
133
|
+
self.resuming_state,
|
|
134
|
+
self.return_home_paused_state,
|
|
135
|
+
self.await_next_mission_state,
|
|
136
|
+
self.home_state,
|
|
137
|
+
self.offline_state,
|
|
138
|
+
self.blocked_protective_stopping_state,
|
|
139
|
+
self.unknown_status_state,
|
|
140
|
+
self.intervention_needed_state,
|
|
141
|
+
self.recharging_state,
|
|
142
|
+
self.stopping_go_to_lockdown_state,
|
|
143
|
+
self.resuming_return_home_state,
|
|
144
|
+
self.going_to_lockdown_state,
|
|
145
|
+
self.lockdown_state,
|
|
146
|
+
self.going_to_recharging_state,
|
|
147
|
+
self.stopping_go_to_recharge_state,
|
|
148
|
+
self.stopping_due_to_maintenance_state,
|
|
149
|
+
self.maintenance_state,
|
|
100
150
|
]
|
|
101
151
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"trigger": "start_machine",
|
|
107
|
-
"source": self.off_state,
|
|
108
|
-
"dest": self.idle_state,
|
|
109
|
-
"before": self._off,
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
"trigger": "initiated",
|
|
113
|
-
"source": self.initiate_state,
|
|
114
|
-
"dest": self.monitor_state,
|
|
115
|
-
"before": self._initiated,
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
"trigger": "pause",
|
|
119
|
-
"source": [self.initiate_state, self.monitor_state],
|
|
120
|
-
"dest": self.stop_state,
|
|
121
|
-
"before": self._pause,
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
"trigger": "stop",
|
|
125
|
-
"source": [self.initiate_state, self.monitor_state],
|
|
126
|
-
"dest": self.stop_state,
|
|
127
|
-
"before": self._stop,
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
"trigger": "mission_finished",
|
|
131
|
-
"source": [
|
|
132
|
-
self.initiate_state,
|
|
133
|
-
],
|
|
134
|
-
"dest": self.idle_state,
|
|
135
|
-
"before": self._mission_finished,
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
"trigger": "mission_started",
|
|
139
|
-
"source": self.idle_state,
|
|
140
|
-
"dest": self.initialize_state,
|
|
141
|
-
"before": self._mission_started,
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
"trigger": "initialization_successful",
|
|
145
|
-
"source": self.initialize_state,
|
|
146
|
-
"dest": self.initiate_state,
|
|
147
|
-
"before": self._initialization_successful,
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
"trigger": "initialization_failed",
|
|
151
|
-
"source": self.initialize_state,
|
|
152
|
-
"dest": self.idle_state,
|
|
153
|
-
"before": self._initialization_failed,
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
"trigger": "resume",
|
|
157
|
-
"source": self.paused_state,
|
|
158
|
-
"dest": self.initiate_state,
|
|
159
|
-
"before": self._resume,
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
"trigger": "step_finished",
|
|
163
|
-
"source": self.monitor_state,
|
|
164
|
-
"dest": self.initiate_state,
|
|
165
|
-
"before": self._step_finished,
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
"trigger": "full_mission_finished",
|
|
169
|
-
"source": self.monitor_state,
|
|
170
|
-
"dest": self.initiate_state,
|
|
171
|
-
"before": self._full_mission_finished,
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
"trigger": "mission_paused",
|
|
175
|
-
"source": self.stop_state,
|
|
176
|
-
"dest": self.paused_state,
|
|
177
|
-
"before": self._mission_paused,
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
"trigger": "initiate_infeasible",
|
|
181
|
-
"source": self.initiate_state,
|
|
182
|
-
"dest": self.initiate_state,
|
|
183
|
-
"before": self._initiate_infeasible,
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
"trigger": "initiate_failed",
|
|
187
|
-
"source": self.initiate_state,
|
|
188
|
-
"dest": self.idle_state,
|
|
189
|
-
"before": self._initiate_failed,
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
"trigger": "mission_stopped",
|
|
193
|
-
"source": [self.stop_state, self.paused_state],
|
|
194
|
-
"dest": self.idle_state,
|
|
195
|
-
"before": self._mission_stopped,
|
|
196
|
-
},
|
|
197
|
-
]
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
self.stop_robot_attempts_limit: int = stop_robot_attempts_limit
|
|
201
|
-
self.sleep_time: float = sleep_time
|
|
202
|
-
|
|
203
|
-
self.stopped: bool = False
|
|
204
|
-
self.current_mission: Optional[Mission] = None
|
|
205
|
-
self.current_task: Optional[Task] = None
|
|
206
|
-
self.current_step: Optional[Step] = None
|
|
207
|
-
self.initial_pose: Optional[Pose] = None
|
|
208
|
-
|
|
209
|
-
self.current_state: State = States(self.state) # type: ignore
|
|
210
|
-
|
|
211
|
-
self.predefined_mission_id: Optional[int] = None
|
|
212
|
-
|
|
213
|
-
self.transitions_log_length: int = transitions_log_length
|
|
214
|
-
self.transitions_list: Deque[States] = deque([], self.transitions_log_length)
|
|
215
|
-
|
|
216
|
-
#################################################################################
|
|
217
|
-
# Transition Callbacks
|
|
218
|
-
def _initialization_successful(self) -> None:
|
|
219
|
-
self.queues.start_mission.output.put(True)
|
|
220
|
-
self.logger.info(
|
|
221
|
-
f"Initialization successful. Starting new mission: {self.current_mission.id}"
|
|
222
|
-
)
|
|
223
|
-
self.log_step_overview(mission=self.current_mission)
|
|
224
|
-
|
|
225
|
-
# This is a workaround to enable the Flotilla repository to write the mission to
|
|
226
|
-
# its database before the publishing from ISAR starts. This is not a permanent
|
|
227
|
-
# solution and should be further addressed in the following issue.
|
|
228
|
-
# https://github.com/equinor/flotilla/issues/226
|
|
229
|
-
time.sleep(2)
|
|
230
|
-
|
|
231
|
-
self.current_mission.status = MissionStatus.InProgress
|
|
232
|
-
self.publish_mission_status()
|
|
233
|
-
self.current_task = self.task_selector.next_task()
|
|
234
|
-
self.current_task.status = TaskStatus.InProgress
|
|
235
|
-
self.publish_task_status(task=self.current_task)
|
|
236
|
-
self.update_current_step()
|
|
237
|
-
|
|
238
|
-
def _initialization_failed(self) -> None:
|
|
239
|
-
self.queues.start_mission.output.put(False)
|
|
240
|
-
self._finalize()
|
|
241
|
-
|
|
242
|
-
def _initiated(self) -> None:
|
|
243
|
-
if self.stepwise_mission:
|
|
244
|
-
self.current_step.status = StepStatus.InProgress
|
|
245
|
-
self.publish_step_status(step=self.current_step)
|
|
246
|
-
self.logger.info(
|
|
247
|
-
f"Successfully initiated "
|
|
248
|
-
f"{type(self.current_step).__name__} "
|
|
249
|
-
f"step: {str(self.current_step.id)[:8]}"
|
|
152
|
+
if settings.PERSISTENT_STORAGE_CONNECTION_STRING == "":
|
|
153
|
+
initial_state = "unknown_status"
|
|
154
|
+
self.logger.warning(
|
|
155
|
+
"PERSISTENT_STORAGE_CONNECTION_STRING is not set. Restarting ISAR will forget the state, including maintenance mode. "
|
|
250
156
|
)
|
|
251
157
|
else:
|
|
158
|
+
is_maintenance_mode = read_or_create_persistent_maintenance_mode()
|
|
252
159
|
self.logger.info(
|
|
253
|
-
f"
|
|
254
|
-
f"{str(self.current_mission.id)[:8]}"
|
|
160
|
+
f"Connected to robot status database and the maintenance mode was: {is_maintenance_mode}. "
|
|
255
161
|
)
|
|
162
|
+
if is_maintenance_mode:
|
|
163
|
+
initial_state = "maintenance"
|
|
164
|
+
else:
|
|
165
|
+
initial_state = "unknown_status"
|
|
256
166
|
|
|
257
|
-
|
|
258
|
-
|
|
167
|
+
self.machine = Machine(
|
|
168
|
+
self, states=self.states, initial=initial_state, queued=True
|
|
169
|
+
)
|
|
259
170
|
|
|
260
|
-
|
|
261
|
-
return
|
|
171
|
+
self.transitions: List[dict] = []
|
|
262
172
|
|
|
263
|
-
|
|
264
|
-
self.
|
|
265
|
-
self.
|
|
266
|
-
self.current_task.status = TaskStatus.InProgress
|
|
267
|
-
self.publish_mission_status()
|
|
268
|
-
self.publish_task_status(task=self.current_task)
|
|
173
|
+
self.transitions.extend(get_mission_transitions(self))
|
|
174
|
+
self.transitions.extend(get_return_home_transitions(self))
|
|
175
|
+
self.transitions.extend(get_robot_status_transitions(self))
|
|
269
176
|
|
|
270
|
-
|
|
271
|
-
self._make_control_mission_response()
|
|
272
|
-
)
|
|
273
|
-
self.queues.resume_mission.output.put(resume_mission_response)
|
|
177
|
+
self.machine.add_transitions(self.transitions)
|
|
274
178
|
|
|
275
|
-
self.
|
|
276
|
-
self.update_current_step()
|
|
179
|
+
self.current_state: State = States(self.state) # type: ignore
|
|
277
180
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
TaskStatus.Cancelled,
|
|
281
|
-
TaskStatus.Failed,
|
|
282
|
-
]
|
|
283
|
-
partially_fail_statuses = fail_statuses + [TaskStatus.PartiallySuccessful]
|
|
284
|
-
|
|
285
|
-
if all(task.status in fail_statuses for task in self.current_mission.tasks):
|
|
286
|
-
self.current_mission.status = MissionStatus.Failed
|
|
287
|
-
elif any(
|
|
288
|
-
task.status in partially_fail_statuses
|
|
289
|
-
for task in self.current_mission.tasks
|
|
290
|
-
):
|
|
291
|
-
self.current_mission.status = MissionStatus.PartiallySuccessful
|
|
292
|
-
else:
|
|
293
|
-
self.current_mission.status = MissionStatus.Successful
|
|
294
|
-
self._finalize()
|
|
295
|
-
|
|
296
|
-
def _mission_started(self) -> None:
|
|
297
|
-
return
|
|
298
|
-
|
|
299
|
-
def _step_finished(self) -> None:
|
|
300
|
-
self.publish_step_status(step=self.current_step)
|
|
301
|
-
self.update_current_task()
|
|
302
|
-
self.update_current_step()
|
|
303
|
-
|
|
304
|
-
def _full_mission_finished(self) -> None:
|
|
305
|
-
self.current_task = None
|
|
306
|
-
step_status: StepStatus = StepStatus.Failed
|
|
307
|
-
task_status: TaskStatus = TaskStatus.Failed
|
|
308
|
-
|
|
309
|
-
if self.current_mission.status == MissionStatus.Failed:
|
|
310
|
-
step_status = StepStatus.Failed
|
|
311
|
-
task_status = TaskStatus.Failed
|
|
312
|
-
elif self.current_mission.status == MissionStatus.Cancelled:
|
|
313
|
-
step_status = StepStatus.Cancelled
|
|
314
|
-
task_status = TaskStatus.Cancelled
|
|
315
|
-
elif (
|
|
316
|
-
self.current_mission.status == MissionStatus.Successful
|
|
317
|
-
or self.current_mission.status == MissionStatus.PartiallySuccessful
|
|
318
|
-
):
|
|
319
|
-
step_status = StepStatus.Successful
|
|
320
|
-
task_status = TaskStatus.Successful
|
|
321
|
-
|
|
322
|
-
for task in self.current_mission.tasks:
|
|
323
|
-
task.status = task_status
|
|
324
|
-
for step in task.steps:
|
|
325
|
-
step.status = step_status
|
|
326
|
-
self.publish_step_status(step=step)
|
|
327
|
-
self.publish_task_status(task=task)
|
|
328
|
-
|
|
329
|
-
def _mission_paused(self) -> None:
|
|
330
|
-
self.logger.info(f"Pausing mission: {self.current_mission.id}")
|
|
331
|
-
self.current_mission.status = MissionStatus.Paused
|
|
332
|
-
self.current_task.status = TaskStatus.Paused
|
|
333
|
-
self.current_step.status = StepStatus.NotStarted
|
|
334
|
-
|
|
335
|
-
paused_mission_response: ControlMissionResponse = (
|
|
336
|
-
self._make_control_mission_response()
|
|
337
|
-
)
|
|
338
|
-
self.queues.pause_mission.output.put(paused_mission_response)
|
|
339
|
-
|
|
340
|
-
self.publish_mission_status()
|
|
341
|
-
self.publish_task_status(task=self.current_task)
|
|
342
|
-
self.publish_step_status(step=self.current_step)
|
|
343
|
-
|
|
344
|
-
def _stop(self) -> None:
|
|
345
|
-
self.stopped = True
|
|
346
|
-
|
|
347
|
-
def _initiate_failed(self) -> None:
|
|
348
|
-
self.current_step.status = StepStatus.Failed
|
|
349
|
-
self.current_task.update_task_status()
|
|
350
|
-
self.current_mission.status = MissionStatus.Failed
|
|
351
|
-
self.publish_step_status(step=self.current_step)
|
|
352
|
-
self.publish_task_status(task=self.current_task)
|
|
353
|
-
self._finalize()
|
|
354
|
-
|
|
355
|
-
def _initiate_infeasible(self) -> None:
|
|
356
|
-
if self.stepwise_mission:
|
|
357
|
-
self.current_step.status = StepStatus.Failed
|
|
358
|
-
self.publish_step_status(step=self.current_step)
|
|
359
|
-
self.update_current_task()
|
|
360
|
-
self.update_current_step()
|
|
361
|
-
|
|
362
|
-
def _mission_stopped(self) -> None:
|
|
363
|
-
self.current_mission.status = MissionStatus.Cancelled
|
|
364
|
-
for task in self.current_mission.tasks:
|
|
365
|
-
for step in task.steps:
|
|
366
|
-
if step.status in [StepStatus.NotStarted, StepStatus.InProgress]:
|
|
367
|
-
step.status = StepStatus.Cancelled
|
|
368
|
-
if task.status in [
|
|
369
|
-
TaskStatus.NotStarted,
|
|
370
|
-
TaskStatus.InProgress,
|
|
371
|
-
TaskStatus.Paused,
|
|
372
|
-
]:
|
|
373
|
-
task.status = TaskStatus.Cancelled
|
|
374
|
-
|
|
375
|
-
stopped_mission_response: ControlMissionResponse = (
|
|
376
|
-
self._make_control_mission_response()
|
|
181
|
+
self.transitions_list: Deque[States] = deque(
|
|
182
|
+
[], settings.STATE_TRANSITIONS_LOG_LENGTH
|
|
377
183
|
)
|
|
378
|
-
self.queues.stop_mission.output.put(stopped_mission_response)
|
|
379
|
-
|
|
380
|
-
self.publish_task_status(task=self.current_task)
|
|
381
|
-
self.publish_step_status(step=self.current_step)
|
|
382
|
-
self._finalize()
|
|
383
184
|
|
|
384
185
|
#################################################################################
|
|
385
186
|
|
|
386
|
-
def
|
|
387
|
-
self.publish_mission_status()
|
|
388
|
-
self.log_step_overview(mission=self.current_mission)
|
|
187
|
+
def print_transitions(self) -> None:
|
|
389
188
|
state_transitions: str = ", ".join(
|
|
390
189
|
[
|
|
391
190
|
f"\n {transition}" if (i + 1) % 10 == 0 else f"{transition}"
|
|
392
191
|
for i, transition in enumerate(list(self.transitions_list))
|
|
393
192
|
]
|
|
394
193
|
)
|
|
395
|
-
self.logger.info(
|
|
396
|
-
self.
|
|
194
|
+
self.logger.info("State transitions:\n %s", state_transitions)
|
|
195
|
+
self.transitions = []
|
|
397
196
|
|
|
398
197
|
def begin(self):
|
|
399
|
-
"""Starts the state machine.
|
|
198
|
+
"""Starts the state machine. Transitions into unknown status state."""
|
|
199
|
+
self.initial_transition() # type: ignore
|
|
400
200
|
|
|
401
|
-
|
|
201
|
+
def terminate(self):
|
|
202
|
+
self.logger.info("Stopping state machine")
|
|
203
|
+
self.signal_state_machine_to_stop.set()
|
|
402
204
|
|
|
403
|
-
|
|
404
|
-
self.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
self.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
self.current_task = self.task_selector.next_task()
|
|
412
|
-
self.current_task.status = TaskStatus.InProgress
|
|
413
|
-
self.publish_task_status(task=self.current_task)
|
|
414
|
-
except TaskSelectorStop:
|
|
415
|
-
# Indicates that all tasks are finished
|
|
416
|
-
self.current_task = None
|
|
417
|
-
|
|
418
|
-
def update_current_step(self):
|
|
419
|
-
if self.current_task:
|
|
420
|
-
self.current_step = self.current_task.next_step()
|
|
205
|
+
def battery_level_is_above_mission_start_threshold(self):
|
|
206
|
+
if not self.shared_state.robot_battery_level.check():
|
|
207
|
+
self.logger.warning("Battery level is None")
|
|
208
|
+
return False
|
|
209
|
+
return (
|
|
210
|
+
not self.shared_state.robot_battery_level.check()
|
|
211
|
+
< settings.ROBOT_MISSION_BATTERY_START_THRESHOLD
|
|
212
|
+
)
|
|
421
213
|
|
|
422
214
|
def update_state(self):
|
|
423
215
|
"""Updates the current state of the state machine."""
|
|
424
|
-
self.current_state = States(self.state)
|
|
425
|
-
self.
|
|
426
|
-
self.
|
|
427
|
-
self.logger.info(
|
|
428
|
-
self.
|
|
429
|
-
|
|
430
|
-
def reset_state_machine(self) -> None:
|
|
431
|
-
self.stopped = False
|
|
432
|
-
self.current_step = None
|
|
433
|
-
self.current_task = None
|
|
434
|
-
self.current_mission = None
|
|
435
|
-
self.initial_pose = None
|
|
436
|
-
|
|
437
|
-
def start_mission(self, mission: Mission, initial_pose: Pose):
|
|
438
|
-
"""Starts a scheduled mission."""
|
|
439
|
-
self.current_mission = mission
|
|
440
|
-
self.initial_pose = initial_pose
|
|
441
|
-
|
|
442
|
-
self.task_selector.initialize(tasks=self.current_mission.tasks)
|
|
443
|
-
|
|
444
|
-
def get_initialize_params(self):
|
|
445
|
-
return InitializeParams(initial_pose=self.initial_pose)
|
|
446
|
-
|
|
447
|
-
def should_start_mission(self) -> Optional[StartMissionMessage]:
|
|
448
|
-
try:
|
|
449
|
-
return self.queues.start_mission.input.get(block=False)
|
|
450
|
-
except queue.Empty:
|
|
451
|
-
return None
|
|
216
|
+
self.current_state = States(self.state) # type: ignore
|
|
217
|
+
self.shared_state.state.update(self.current_state)
|
|
218
|
+
self.transitions_list.append(self.current_state)
|
|
219
|
+
self.logger.info("State: %s", self.current_state)
|
|
220
|
+
self.publish_status()
|
|
452
221
|
|
|
453
|
-
def
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
except queue.Empty:
|
|
457
|
-
return False
|
|
458
|
-
|
|
459
|
-
def should_pause_mission(self) -> bool:
|
|
460
|
-
try:
|
|
461
|
-
return self.queues.pause_mission.input.get(block=False)
|
|
462
|
-
except queue.Empty:
|
|
463
|
-
return False
|
|
464
|
-
|
|
465
|
-
def should_resume_mission(self) -> bool:
|
|
466
|
-
try:
|
|
467
|
-
return self.queues.resume_mission.input.get(block=False)
|
|
468
|
-
except queue.Empty:
|
|
469
|
-
return False
|
|
222
|
+
def start_mission(self, mission: Mission):
|
|
223
|
+
"""Starts a scheduled mission."""
|
|
224
|
+
self.events.state_machine_events.start_mission.trigger_event(mission)
|
|
470
225
|
|
|
471
|
-
def
|
|
472
|
-
|
|
226
|
+
def start_return_home_mission(self):
|
|
227
|
+
"""Starts a return to home mission."""
|
|
228
|
+
mission = Mission(
|
|
229
|
+
tasks=[ReturnToHome()],
|
|
230
|
+
name="Return Home",
|
|
231
|
+
)
|
|
232
|
+
self.events.state_machine_events.start_mission.trigger_event(mission)
|
|
473
233
|
|
|
474
|
-
def
|
|
234
|
+
def publish_mission_aborted(self, reason: str, can_be_continued: bool) -> None:
|
|
475
235
|
if not self.mqtt_publisher:
|
|
476
236
|
return
|
|
477
|
-
payload: str = json.dumps(
|
|
478
|
-
{
|
|
479
|
-
"isar_id": settings.ISAR_ID,
|
|
480
|
-
"robot_name": settings.ROBOT_NAME,
|
|
481
|
-
"mission_id": self.current_mission.id if self.current_mission else None,
|
|
482
|
-
"status": self.current_mission.status if self.current_mission else None,
|
|
483
|
-
"timestamp": datetime.utcnow(),
|
|
484
|
-
},
|
|
485
|
-
cls=EnhancedJSONEncoder,
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
self.mqtt_publisher.publish(
|
|
489
|
-
topic=settings.TOPIC_ISAR_MISSION,
|
|
490
|
-
payload=payload,
|
|
491
|
-
retain=False,
|
|
492
|
-
)
|
|
493
237
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
238
|
+
if self.shared_state.mission_id.check() is None:
|
|
239
|
+
self.logger.warning(
|
|
240
|
+
"Could not publish mission aborted message. No ongoing mission."
|
|
241
|
+
)
|
|
497
242
|
return
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
},
|
|
507
|
-
cls=EnhancedJSONEncoder,
|
|
243
|
+
|
|
244
|
+
payload: MissionAbortedPayload = MissionAbortedPayload(
|
|
245
|
+
isar_id=settings.ISAR_ID,
|
|
246
|
+
robot_name=settings.ROBOT_NAME,
|
|
247
|
+
mission_id=self.shared_state.mission_id.check(),
|
|
248
|
+
reason=reason,
|
|
249
|
+
can_be_continued=can_be_continued,
|
|
250
|
+
timestamp=datetime.now(timezone.utc),
|
|
508
251
|
)
|
|
509
252
|
|
|
510
253
|
self.mqtt_publisher.publish(
|
|
511
|
-
topic=settings.
|
|
512
|
-
payload=payload,
|
|
513
|
-
|
|
254
|
+
topic=settings.TOPIC_ISAR_MISSION_ABORTED,
|
|
255
|
+
payload=json.dumps(payload, cls=EnhancedJSONEncoder),
|
|
256
|
+
qos=1,
|
|
257
|
+
retain=True,
|
|
514
258
|
)
|
|
515
259
|
|
|
516
|
-
def
|
|
517
|
-
"""Publishes the
|
|
260
|
+
def publish_intervention_needed(self, error_message: str) -> None:
|
|
261
|
+
"""Publishes the intervention needed message to the MQTT Broker"""
|
|
518
262
|
if not self.mqtt_publisher:
|
|
519
263
|
return
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
"step_id": step.id if step else None,
|
|
527
|
-
"step_type": step.__class__.__name__ if step else None,
|
|
528
|
-
"status": step.status if step else None,
|
|
529
|
-
"timestamp": datetime.utcnow(),
|
|
530
|
-
},
|
|
531
|
-
cls=EnhancedJSONEncoder,
|
|
264
|
+
|
|
265
|
+
payload: InterventionNeededPayload = InterventionNeededPayload(
|
|
266
|
+
isar_id=settings.ISAR_ID,
|
|
267
|
+
robot_name=settings.ROBOT_NAME,
|
|
268
|
+
reason=error_message,
|
|
269
|
+
timestamp=datetime.now(timezone.utc),
|
|
532
270
|
)
|
|
533
271
|
|
|
534
272
|
self.mqtt_publisher.publish(
|
|
535
|
-
topic=settings.
|
|
536
|
-
payload=payload,
|
|
537
|
-
|
|
273
|
+
topic=settings.TOPIC_ISAR_INTERVENTION_NEEDED,
|
|
274
|
+
payload=json.dumps(payload, cls=EnhancedJSONEncoder),
|
|
275
|
+
qos=1,
|
|
276
|
+
retain=True,
|
|
538
277
|
)
|
|
539
278
|
|
|
540
|
-
def
|
|
279
|
+
def publish_status(self) -> None:
|
|
541
280
|
if not self.mqtt_publisher:
|
|
542
281
|
return
|
|
543
|
-
payload: str = json.dumps(
|
|
544
|
-
{
|
|
545
|
-
"isar_id": settings.ISAR_ID,
|
|
546
|
-
"robot_name": settings.ROBOT_NAME,
|
|
547
|
-
"state": self.current_state,
|
|
548
|
-
"timestamp": datetime.utcnow(),
|
|
549
|
-
},
|
|
550
|
-
cls=EnhancedJSONEncoder,
|
|
551
|
-
)
|
|
552
282
|
|
|
553
|
-
self.mqtt_publisher.
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
283
|
+
publish_isar_status(self.mqtt_publisher, self._current_status())
|
|
284
|
+
|
|
285
|
+
def _current_status(self) -> IsarStatus:
|
|
286
|
+
if self.current_state == States.AwaitNextMission:
|
|
287
|
+
return IsarStatus.Available
|
|
288
|
+
elif self.current_state == States.ReturnHomePaused:
|
|
289
|
+
return IsarStatus.ReturnHomePaused
|
|
290
|
+
elif self.current_state == States.Paused:
|
|
291
|
+
return IsarStatus.Paused
|
|
292
|
+
elif self.current_state == States.Home:
|
|
293
|
+
return IsarStatus.Home
|
|
294
|
+
elif self.current_state == States.ReturningHome:
|
|
295
|
+
return IsarStatus.ReturningHome
|
|
296
|
+
elif self.current_state == States.Offline:
|
|
297
|
+
return IsarStatus.Offline
|
|
298
|
+
elif self.current_state == States.BlockedProtectiveStop:
|
|
299
|
+
return IsarStatus.BlockedProtectiveStop
|
|
300
|
+
elif self.current_state == States.InterventionNeeded:
|
|
301
|
+
return IsarStatus.InterventionNeeded
|
|
302
|
+
elif self.current_state == States.Recharging:
|
|
303
|
+
return IsarStatus.Recharging
|
|
304
|
+
elif self.current_state == States.Lockdown:
|
|
305
|
+
return IsarStatus.Lockdown
|
|
306
|
+
elif self.current_state == States.GoingToLockdown:
|
|
307
|
+
return IsarStatus.GoingToLockdown
|
|
308
|
+
elif self.current_state == States.GoingToRecharging:
|
|
309
|
+
return IsarStatus.GoingToRecharging
|
|
310
|
+
elif self.current_state == States.Maintenance:
|
|
311
|
+
return IsarStatus.Maintenance
|
|
312
|
+
else:
|
|
313
|
+
return IsarStatus.Busy
|
|
558
314
|
|
|
559
|
-
def _log_state_transition(self, next_state):
|
|
560
|
-
"""Logs all state transitions that are not self-transitions."""
|
|
561
|
-
self.transitions_list.append(next_state)
|
|
562
315
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
log_statement: str = "\n".join(log_statements)
|
|
576
|
-
|
|
577
|
-
self.logger.info(f"Mission overview:\n{log_statement}")
|
|
578
|
-
|
|
579
|
-
def _make_control_mission_response(self) -> ControlMissionResponse:
|
|
580
|
-
return ControlMissionResponse(
|
|
581
|
-
mission_id=self.current_mission.id,
|
|
582
|
-
mission_status=self.current_mission.status,
|
|
583
|
-
task_id=self.current_task.id,
|
|
584
|
-
task_status=self.current_task.status,
|
|
585
|
-
step_id=self.current_step.id,
|
|
586
|
-
step_status=self.current_step.status,
|
|
316
|
+
def read_or_create_persistent_maintenance_mode():
|
|
317
|
+
try:
|
|
318
|
+
is_maintenance_mode = read_persistent_robot_state_is_maintenance_mode(
|
|
319
|
+
settings.PERSISTENT_STORAGE_CONNECTION_STRING, settings.ISAR_ID
|
|
320
|
+
)
|
|
321
|
+
except NoSuchRobotException:
|
|
322
|
+
create_persistent_robot_state(
|
|
323
|
+
settings.PERSISTENT_STORAGE_CONNECTION_STRING, settings.ISAR_ID
|
|
324
|
+
)
|
|
325
|
+
is_maintenance_mode = read_persistent_robot_state_is_maintenance_mode(
|
|
326
|
+
settings.PERSISTENT_STORAGE_CONNECTION_STRING, settings.ISAR_ID
|
|
587
327
|
)
|
|
328
|
+
return is_maintenance_mode
|
|
588
329
|
|
|
589
330
|
|
|
590
331
|
def main(state_machine: StateMachine):
|