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
isar/config/settings.py
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
|
-
import importlib.resources as pkg_resources
|
|
2
1
|
import os
|
|
3
|
-
from
|
|
2
|
+
from importlib.resources import as_file, files
|
|
3
|
+
from typing import Any, List
|
|
4
4
|
|
|
5
5
|
from dotenv import load_dotenv
|
|
6
|
-
from pydantic import
|
|
6
|
+
from pydantic import Field, ValidationInfo, field_validator
|
|
7
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
7
8
|
|
|
8
9
|
from isar.config import predefined_missions
|
|
9
10
|
from robot_interface.models.robots.robot_model import RobotModel
|
|
10
|
-
from robot_interface.telemetry.payloads import
|
|
11
|
+
from robot_interface.telemetry.payloads import DocumentInfo
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class Settings(BaseSettings):
|
|
15
|
+
# Endpoint open telemetry will export telemetry in the otlp protocol to
|
|
16
|
+
OPEN_TELEMETRY_OTLP_EXPORTER_ENDPOINT: str = Field(default="http://localhost:4318")
|
|
17
|
+
|
|
18
|
+
# Connection string for Azure Application Insights
|
|
19
|
+
# This is optional and it will use managed identity if not set
|
|
20
|
+
APPLICATIONINSIGHTS_CONNECTION_STRING: str = Field(default="")
|
|
21
|
+
|
|
14
22
|
# Determines which robot package ISAR will attempt to import
|
|
15
23
|
# Name must match with an installed python package in the local environment
|
|
16
24
|
ROBOT_PACKAGE: str = Field(default="isar_robot")
|
|
17
25
|
|
|
18
|
-
# The run mode of the robot (stepwise or full mission)
|
|
19
|
-
RUN_MISSION_STEPWISE: bool = Field(default=True)
|
|
20
|
-
|
|
21
26
|
# Determines the local path in which results from missions are stored
|
|
22
27
|
LOCAL_STORAGE_PATH: str = Field(default="./results")
|
|
23
28
|
|
|
@@ -32,23 +37,27 @@ class Settings(BaseSettings):
|
|
|
32
37
|
FSM_SLEEP_TIME: float = Field(default=0.1)
|
|
33
38
|
|
|
34
39
|
# Location of JSON files containing predefined missions for the Local Planner to use
|
|
35
|
-
path = os.path.dirname(predefined_missions.__file__)
|
|
40
|
+
path: str = os.path.dirname(predefined_missions.__file__)
|
|
36
41
|
PREDEFINED_MISSIONS_FOLDER: str = Field(default=path + "/")
|
|
37
42
|
|
|
38
43
|
# Name of default map transformation
|
|
39
44
|
DEFAULT_MAP: str = Field(default="default_map")
|
|
40
45
|
|
|
41
|
-
# Location of JSON files containing predefined maps
|
|
42
|
-
MAPS_FOLDER: str = Field(default="src/isar/config/maps/")
|
|
43
|
-
|
|
44
46
|
# Determines the number of state transitions that are kept in the log
|
|
45
47
|
STATE_TRANSITIONS_LOG_LENGTH: int = Field(default=20)
|
|
46
48
|
|
|
47
|
-
# Number of attempts to
|
|
48
|
-
|
|
49
|
+
# Number of attempts to request a task status in monitor before cancelling
|
|
50
|
+
REQUEST_STATUS_FAILURE_COUNTER_LIMIT: int = Field(default=3)
|
|
51
|
+
|
|
52
|
+
# Time allocated to reconnect when failing to retrieve status due to communication
|
|
53
|
+
# issues
|
|
54
|
+
REQUEST_STATUS_COMMUNICATION_RECONNECT_DELAY: float = Field(default=10)
|
|
55
|
+
|
|
56
|
+
# Number of attempts for state transitions resume and pause if failed
|
|
57
|
+
STATE_TRANSITION_NUM_RETIRES: int = Field(default=10)
|
|
49
58
|
|
|
50
59
|
# Number of attempts to stop the robot before giving up
|
|
51
|
-
STOP_ROBOT_ATTEMPTS_LIMIT: int = Field(default=
|
|
60
|
+
STOP_ROBOT_ATTEMPTS_LIMIT: int = Field(default=3)
|
|
52
61
|
|
|
53
62
|
# Number of attempts to stop the robot before giving up
|
|
54
63
|
UPLOAD_FAILURE_ATTEMPTS_LIMIT: int = Field(default=10)
|
|
@@ -57,9 +66,20 @@ class Settings(BaseSettings):
|
|
|
57
66
|
UPLOAD_FAILURE_MAX_WAIT: int = Field(default=60)
|
|
58
67
|
|
|
59
68
|
# ISAR telemetry intervals
|
|
60
|
-
|
|
61
|
-
ROBOT_INFO_PUBLISH_INTERVAL:
|
|
62
|
-
|
|
69
|
+
ROBOT_HEARTBEAT_PUBLISH_INTERVAL: float = Field(default=1)
|
|
70
|
+
ROBOT_INFO_PUBLISH_INTERVAL: float = Field(default=5)
|
|
71
|
+
ROBOT_API_BATTERY_POLL_INTERVAL: float = Field(default=5)
|
|
72
|
+
ROBOT_API_STATUS_POLL_INTERVAL: float = Field(default=5)
|
|
73
|
+
THREAD_CHECK_INTERVAL: float = Field(default=0.01)
|
|
74
|
+
|
|
75
|
+
# Determines the minimum battery level the robot must have to start a mission
|
|
76
|
+
# If it drops below this level it will recharge to the value set by
|
|
77
|
+
# ROBOT_BATTERY_RECHARGE_THRESHOLD before starting new missions
|
|
78
|
+
ROBOT_MISSION_BATTERY_START_THRESHOLD: float = Field(default=25.0)
|
|
79
|
+
|
|
80
|
+
# Determines the minimum battery threshold to consider the robot recharged
|
|
81
|
+
# and ready for more missions, after having run low on charge
|
|
82
|
+
ROBOT_BATTERY_RECHARGE_THRESHOLD: float = Field(default=80.0)
|
|
63
83
|
|
|
64
84
|
# FastAPI host
|
|
65
85
|
API_HOST_VIEWED_EXTERNALLY: str = Field(default="0.0.0.0")
|
|
@@ -67,20 +87,21 @@ class Settings(BaseSettings):
|
|
|
67
87
|
# FastAPI port
|
|
68
88
|
API_PORT: int = Field(default=3000)
|
|
69
89
|
|
|
90
|
+
# Determines how long delay time should be allowed before returning home
|
|
91
|
+
RETURN_HOME_DELAY: int = Field(default=10)
|
|
92
|
+
|
|
93
|
+
# Sets how many times the robot should try to return home if a return home fails
|
|
94
|
+
RETURN_HOME_RETRY_LIMIT: int = Field(default=5)
|
|
95
|
+
|
|
70
96
|
# Determines which mission planner module is used by ISAR
|
|
71
97
|
MISSION_PLANNER: str = Field(default="local")
|
|
72
98
|
|
|
73
|
-
# Determines which task selector module is used by ISAR
|
|
74
|
-
# Options: [sequential]
|
|
75
|
-
TASK_SELECTOR: str = Field(default="sequential")
|
|
76
|
-
|
|
77
99
|
# Determines which storage modules are used by ISAR
|
|
78
100
|
# Multiple storage modules can be chosen
|
|
79
101
|
# Each module will be called when storing results from inspections
|
|
80
102
|
# Selecting a different storage module than local may require certain access rights
|
|
81
103
|
STORAGE_LOCAL_ENABLED: bool = Field(default=True)
|
|
82
104
|
STORAGE_BLOB_ENABLED: bool = Field(default=False)
|
|
83
|
-
STORAGE_SLIMM_ENABLED: bool = Field(default=False)
|
|
84
105
|
|
|
85
106
|
# Determines whether the MQTT publishing module should be enabled or not
|
|
86
107
|
# The publishing module will attempt to connect to the MQTT broker configured in
|
|
@@ -96,7 +117,7 @@ class Settings(BaseSettings):
|
|
|
96
117
|
# Enabling this requires certain resources available for OAuth2 authentication
|
|
97
118
|
# Currently supported authentication is Azure AD
|
|
98
119
|
# (https://github.com/Intility/fastapi-azure-auth)
|
|
99
|
-
AUTHENTICATION_ENABLED: bool = Field(default=
|
|
120
|
+
AUTHENTICATION_ENABLED: bool = Field(default=True)
|
|
100
121
|
|
|
101
122
|
# Tenant ID for the Azure tenant with your Azure Active Directory application
|
|
102
123
|
AZURE_TENANT_ID: str = Field(default="3aa4a235-b6e2-48d5-9195-7fcf05b459b0")
|
|
@@ -126,58 +147,28 @@ class Settings(BaseSettings):
|
|
|
126
147
|
# Keyvault name
|
|
127
148
|
KEYVAULT_NAME: str = Field(default="IsarDevKv")
|
|
128
149
|
|
|
150
|
+
# Determines whether inspections are uploaded asynchronously or get_inspections in robotinterface
|
|
151
|
+
UPLOAD_INSPECTIONS_ASYNC: bool = Field(default=False)
|
|
152
|
+
|
|
129
153
|
# URL to storage account for Azure Blob Storage
|
|
130
|
-
|
|
131
|
-
default="https://eqrobotdevstorage.blob.core.windows.net"
|
|
132
|
-
)
|
|
154
|
+
BLOB_STORAGE_ACCOUNT: str = Field(default="")
|
|
133
155
|
|
|
134
156
|
# Name of blob container in Azure Blob Storage [slimm test]
|
|
135
157
|
BLOB_CONTAINER: str = Field(default="test")
|
|
136
158
|
|
|
137
|
-
|
|
138
|
-
STID_CLIENT_ID: str = Field(default="1734406c-3449-4192-a50d-7c3a63d3f57d")
|
|
139
|
-
|
|
140
|
-
# Scope for access to STID API
|
|
141
|
-
STID_APP_SCOPE: str = Field(default=".default")
|
|
142
|
-
|
|
143
|
-
# URL for STID endpoint
|
|
144
|
-
STID_API_URL: str = Field(default="https://stidapi.equinor.com")
|
|
145
|
-
|
|
146
|
-
# Plant name for the facility which STID should look for tags in
|
|
147
|
-
STID_PLANT_NAME: str = Field(default="kaa")
|
|
148
|
-
|
|
149
|
-
# Client ID for SLIMM App Registration
|
|
150
|
-
SLIMM_CLIENT_ID: str = Field(default="c630ca4d-d8d6-45ab-8cc6-68a363d0de9e")
|
|
151
|
-
|
|
152
|
-
# Scope for access to SLIMM Ingestion API
|
|
153
|
-
SLIMM_APP_SCOPE: str = Field(default=".default")
|
|
154
|
-
|
|
155
|
-
# URL for SLIMM endpoint
|
|
156
|
-
SLIMM_API_URL: str = Field(
|
|
157
|
-
default="https://scinspectioningestapitest.azurewebsites.net/Ingest"
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
# Whether the results should be copied directly into the SLIMM datalake or only the
|
|
161
|
-
# metadata
|
|
162
|
-
COPY_FILES_TO_SLIMM_DATALAKE: bool = Field(default=False)
|
|
159
|
+
PERSISTENT_STORAGE_CONNECTION_STRING: str = Field(default="")
|
|
163
160
|
|
|
164
161
|
# The configuration of this section is tightly coupled with the metadata that is
|
|
165
162
|
# submitted with the results once they have been uploaded.
|
|
166
163
|
|
|
167
164
|
# Four digit code indicating facility
|
|
168
|
-
PLANT_CODE: str = Field(default="
|
|
165
|
+
PLANT_CODE: str = Field(default="1210")
|
|
169
166
|
|
|
170
167
|
# Name of the facility the robot is operating in
|
|
171
|
-
PLANT_NAME: str = Field(default="
|
|
168
|
+
PLANT_NAME: str = Field(default="Huldra")
|
|
172
169
|
|
|
173
170
|
# Shortname of the facility the robot is operating in
|
|
174
|
-
PLANT_SHORT_NAME: str = Field(default="
|
|
175
|
-
|
|
176
|
-
# Country the robot is operating in
|
|
177
|
-
COUNTRY: str = Field(default="Norway")
|
|
178
|
-
|
|
179
|
-
# Type of robot ISAR is monitoring
|
|
180
|
-
ROBOT_TYPE: str = Field(default="robot")
|
|
171
|
+
PLANT_SHORT_NAME: str = Field(default="HUA")
|
|
181
172
|
|
|
182
173
|
# Name of robot
|
|
183
174
|
ROBOT_NAME: str = Field(default="Placebot")
|
|
@@ -188,25 +179,8 @@ class Settings(BaseSettings):
|
|
|
188
179
|
# Serial number of the robot ISAR is connected to
|
|
189
180
|
SERIAL_NUMBER: str = Field(default="0001")
|
|
190
181
|
|
|
191
|
-
#
|
|
192
|
-
|
|
193
|
-
default=[
|
|
194
|
-
VideoStream(
|
|
195
|
-
name="Front camera",
|
|
196
|
-
url="http://localhost:5000/videostream/front",
|
|
197
|
-
type="turtlebot",
|
|
198
|
-
),
|
|
199
|
-
VideoStream(
|
|
200
|
-
name="Rear camera",
|
|
201
|
-
url="http://localhost:5000/videostream/rear",
|
|
202
|
-
type="turtlebot",
|
|
203
|
-
),
|
|
204
|
-
]
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
# Data scheme the robot should adhere to
|
|
208
|
-
# Options [DS0001]
|
|
209
|
-
DATA_SCHEME: str = Field(default="DS0001")
|
|
182
|
+
# Info about robot documentation
|
|
183
|
+
DOCUMENTATION: List[DocumentInfo] = Field(default=[])
|
|
210
184
|
|
|
211
185
|
# Coordinate reference system of facility
|
|
212
186
|
COORDINATE_REFERENCE_SYSTEM: str = Field(default="EQUINOR:4100001")
|
|
@@ -218,9 +192,6 @@ class Settings(BaseSettings):
|
|
|
218
192
|
# Options [quaternion]
|
|
219
193
|
MEDIA_ORIENTATION_REFERENCE_SYSTEM: str = Field(default="quaternion")
|
|
220
194
|
|
|
221
|
-
# Contractor who is responsible for robot missions
|
|
222
|
-
CONTRACTOR: str = Field(default="equinor")
|
|
223
|
-
|
|
224
195
|
# Timezone
|
|
225
196
|
TIMEZONE: str = Field(default="UTC+00:00")
|
|
226
197
|
|
|
@@ -228,13 +199,31 @@ class Settings(BaseSettings):
|
|
|
228
199
|
DATA_CLASSIFICATION: str = Field(default="internal")
|
|
229
200
|
|
|
230
201
|
# List of MQTT Topics
|
|
231
|
-
|
|
232
|
-
TOPIC_ISAR_MISSION: str = Field(default="mission")
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
202
|
+
TOPIC_ISAR_STATUS: str = Field(default="status", validate_default=True)
|
|
203
|
+
TOPIC_ISAR_MISSION: str = Field(default="mission", validate_default=True)
|
|
204
|
+
TOPIC_ISAR_MISSION_ABORTED: str = Field(
|
|
205
|
+
default="aborted_mission", validate_default=True
|
|
206
|
+
)
|
|
207
|
+
TOPIC_ISAR_TASK: str = Field(default="task", validate_default=True)
|
|
208
|
+
TOPIC_ISAR_INSPECTION_RESULT: str = Field(
|
|
209
|
+
default="inspection_result", validate_default=True
|
|
210
|
+
)
|
|
211
|
+
TOPIC_ISAR_INSPECTION_VALUE: str = Field(
|
|
212
|
+
default="inspection_value", validate_default=True
|
|
213
|
+
)
|
|
214
|
+
TOPIC_ISAR_ROBOT_INFO: str = Field(default="robot_info", validate_default=True)
|
|
215
|
+
TOPIC_ISAR_ROBOT_HEARTBEAT: str = Field(
|
|
216
|
+
default="robot_heartbeat", validate_default=True
|
|
217
|
+
)
|
|
218
|
+
TOPIC_ISAR_STARTUP: str = Field(default="startup", validate_default=True)
|
|
219
|
+
TOPIC_ISAR_INTERVENTION_NEEDED: str = Field(
|
|
220
|
+
default="intervention_needed", validate_default=True
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# List of MQTT Topics Expiry
|
|
224
|
+
MQTT_ROBOT_HEARTBEAT_EXPIRY: int = Field(default=5)
|
|
225
|
+
MQTT_TELEMETRY_EXPIRY: int = Field(default=10)
|
|
226
|
+
MQTT_MISSION_AND_TASK_EXPIRY: int = Field(default=86400)
|
|
238
227
|
|
|
239
228
|
# Logging
|
|
240
229
|
|
|
@@ -244,88 +233,76 @@ class Settings(BaseSettings):
|
|
|
244
233
|
# Each handler will be called when logging
|
|
245
234
|
# Selecting a different log handler than local may require certain access rights:
|
|
246
235
|
# - The Azure AI logger requires the 'APPLICATIONINSIGHTS_CONNECTION_STRING' to be set as an environment variable.
|
|
236
|
+
# In general logging is configured in logging.conf
|
|
247
237
|
LOG_HANDLER_LOCAL_ENABLED: bool = Field(default=True)
|
|
248
238
|
LOG_HANDLER_APPLICATION_INSIGHTS_ENABLED: bool = Field(default=False)
|
|
249
239
|
|
|
250
|
-
|
|
251
|
-
API_LOG_LEVEL: str = Field(default="INFO")
|
|
252
|
-
MAIN_LOG_LEVEL: str = Field(default="INFO")
|
|
253
|
-
MQTT_LOG_LEVEL: str = Field(default="INFO")
|
|
254
|
-
STATE_MACHINE_LOG_LEVEL: str = Field(default="INFO")
|
|
255
|
-
UPLOADER_LOG_LEVEL: str = Field(default="INFO")
|
|
256
|
-
CONSOLE_LOG_LEVEL: str = Field(default="INFO")
|
|
257
|
-
URLLIB3_LOG_LEVEL: str = Field(default="WARNING")
|
|
258
|
-
UVICORN_LOG_LEVEL: str = Field(default="WARNING")
|
|
259
|
-
AZURE_LOG_LEVEL: str = Field(default="WARNING")
|
|
240
|
+
DEBUG_LOG_FORMATTER: bool = Field(default=False)
|
|
260
241
|
|
|
261
|
-
|
|
242
|
+
# You can set logger levels from environment variables ending with _LOG_LEVEL
|
|
243
|
+
# For example, MQTT_LOG_LEVEL="DEBUG" will set the logger MQTT_LOGGER to DEBUG level
|
|
244
|
+
# Handeled in log.py and only work for loggers defined in logging.conf
|
|
262
245
|
|
|
263
246
|
REQUIRED_ROLE: str = Field(default="Mission.Control")
|
|
264
247
|
|
|
265
|
-
@
|
|
266
|
-
|
|
267
|
-
return {
|
|
268
|
-
"api": values["API_LOG_LEVEL"],
|
|
269
|
-
"main": values["MAIN_LOG_LEVEL"],
|
|
270
|
-
"mqtt": values["MQTT_LOG_LEVEL"],
|
|
271
|
-
"state_machine": values["STATE_MACHINE_LOG_LEVEL"],
|
|
272
|
-
"uploader": values["UPLOADER_LOG_LEVEL"],
|
|
273
|
-
"console": values["CONSOLE_LOG_LEVEL"],
|
|
274
|
-
"urllib3": values["URLLIB3_LOG_LEVEL"],
|
|
275
|
-
"uvicorn": values["UVICORN_LOG_LEVEL"],
|
|
276
|
-
"azure": values["AZURE_LOG_LEVEL"],
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
@validator(
|
|
280
|
-
"TOPIC_ISAR_STATE",
|
|
248
|
+
@field_validator(
|
|
249
|
+
"TOPIC_ISAR_STATUS",
|
|
281
250
|
"TOPIC_ISAR_MISSION",
|
|
282
251
|
"TOPIC_ISAR_TASK",
|
|
283
|
-
"TOPIC_ISAR_STEP",
|
|
284
|
-
"TOPIC_ISAR_ROBOT_STATUS",
|
|
285
252
|
"TOPIC_ISAR_ROBOT_INFO",
|
|
253
|
+
"TOPIC_ISAR_ROBOT_HEARTBEAT",
|
|
286
254
|
"TOPIC_ISAR_INSPECTION_RESULT",
|
|
287
|
-
|
|
288
|
-
|
|
255
|
+
"TOPIC_ISAR_INSPECTION_VALUE",
|
|
256
|
+
"TOPIC_ISAR_STARTUP",
|
|
257
|
+
"TOPIC_ISAR_INTERVENTION_NEEDED",
|
|
258
|
+
"TOPIC_ISAR_MISSION_ABORTED",
|
|
259
|
+
)
|
|
260
|
+
@classmethod
|
|
261
|
+
def prefix_isar_topics(cls, v: Any, info: ValidationInfo):
|
|
262
|
+
return f"isar/{info.data['ISAR_ID']}/{v}"
|
|
263
|
+
|
|
264
|
+
model_config = SettingsConfigDict(
|
|
265
|
+
env_prefix="ISAR_",
|
|
266
|
+
env_file_encoding="utf-8",
|
|
267
|
+
case_sensitive=True,
|
|
268
|
+
extra="ignore",
|
|
289
269
|
)
|
|
290
|
-
def prefix_isar_topics(cls, v, values):
|
|
291
|
-
return f"isar/{values['ISAR_ID']}/{v}"
|
|
292
|
-
|
|
293
|
-
class Config:
|
|
294
|
-
with pkg_resources.path("isar.config", "settings.env") as path:
|
|
295
|
-
package_path = path
|
|
296
270
|
|
|
297
|
-
env_prefix = "ISAR_"
|
|
298
|
-
env_file = package_path
|
|
299
|
-
env_file_encoding = "utf-8"
|
|
300
|
-
case_sensitive = True
|
|
301
271
|
|
|
272
|
+
env = os.environ.get("ISAR_ENV")
|
|
302
273
|
|
|
303
|
-
|
|
274
|
+
if env == "test":
|
|
275
|
+
load_dotenv(".env.test", override=True)
|
|
276
|
+
else:
|
|
277
|
+
load_dotenv()
|
|
304
278
|
settings = Settings()
|
|
305
279
|
|
|
306
280
|
|
|
307
281
|
class RobotSettings(BaseSettings):
|
|
308
282
|
def __init__(self) -> None:
|
|
309
283
|
try:
|
|
310
|
-
|
|
311
|
-
f"{settings.ROBOT_PACKAGE}
|
|
312
|
-
|
|
313
|
-
|
|
284
|
+
source = (
|
|
285
|
+
files(f"{settings.ROBOT_PACKAGE}")
|
|
286
|
+
.joinpath("config")
|
|
287
|
+
.joinpath("settings.env")
|
|
288
|
+
)
|
|
289
|
+
with as_file(source) as eml:
|
|
290
|
+
env_file = eml
|
|
314
291
|
except ModuleNotFoundError:
|
|
315
|
-
|
|
316
|
-
super().__init__(_env_file=
|
|
292
|
+
env_file = None
|
|
293
|
+
super().__init__(_env_file=env_file)
|
|
317
294
|
|
|
318
295
|
# ISAR steps the robot is capable of performing
|
|
319
296
|
# This should be set in the robot package settings.env file
|
|
320
|
-
CAPABILITIES: List[str] = Field(default=["
|
|
297
|
+
CAPABILITIES: List[str] = Field(default=["take_image"])
|
|
321
298
|
|
|
322
299
|
# Model of the robot which ISAR is connected to
|
|
323
300
|
# This should be set in the robot package settings.env file
|
|
324
301
|
ROBOT_MODEL: RobotModel = Field(default=RobotModel.Robot) # type: ignore
|
|
325
302
|
|
|
326
|
-
|
|
327
|
-
env_file_encoding
|
|
328
|
-
|
|
303
|
+
model_config = SettingsConfigDict(
|
|
304
|
+
env_file_encoding="utf-8", case_sensitive=True, extra="ignore"
|
|
305
|
+
)
|
|
329
306
|
|
|
330
307
|
|
|
331
308
|
robot_settings = RobotSettings()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from threading import Event as ThreadEvent
|
|
6
|
+
from typing import TYPE_CHECKING, Callable, Generic, List, Optional, TypeVar
|
|
7
|
+
|
|
8
|
+
from transitions import State
|
|
9
|
+
|
|
10
|
+
from isar.config.settings import settings
|
|
11
|
+
from isar.models.events import Event
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class EventHandlerMapping(Generic[T]):
|
|
18
|
+
name: str
|
|
19
|
+
event: Event[T]
|
|
20
|
+
handler: Callable[[Event[T]], Optional[Callable]]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class TimeoutHandlerMapping:
|
|
25
|
+
name: str
|
|
26
|
+
timeout_in_seconds: float
|
|
27
|
+
handler: Callable[[], Optional[Callable]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from isar.state_machine.state_machine import StateMachine
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EventHandlerBase(State):
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
state_machine: "StateMachine",
|
|
38
|
+
state_name: str,
|
|
39
|
+
event_handler_mappings: List[EventHandlerMapping],
|
|
40
|
+
timers: List[TimeoutHandlerMapping] = [],
|
|
41
|
+
on_entry: Optional[Callable[[], None]] = None,
|
|
42
|
+
on_transition: Optional[Callable[[], None]] = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
|
|
45
|
+
super().__init__(name=state_name, on_enter=self.start)
|
|
46
|
+
self.state_machine: "StateMachine" = state_machine
|
|
47
|
+
self.logger = logging.getLogger("state_machine")
|
|
48
|
+
self.events = state_machine.events
|
|
49
|
+
self.signal_state_machine_to_stop: ThreadEvent = (
|
|
50
|
+
state_machine.signal_state_machine_to_stop
|
|
51
|
+
)
|
|
52
|
+
self.event_handler_mappings = event_handler_mappings
|
|
53
|
+
self.state_name: str = state_name
|
|
54
|
+
self.timers = timers
|
|
55
|
+
self.on_entry = on_entry
|
|
56
|
+
self.on_transition = on_transition
|
|
57
|
+
|
|
58
|
+
def start(self) -> None:
|
|
59
|
+
self.state_machine.update_state()
|
|
60
|
+
if self.on_entry:
|
|
61
|
+
self.on_entry()
|
|
62
|
+
self._run()
|
|
63
|
+
|
|
64
|
+
def stop(self) -> None:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
def get_event_handler_by_name(
|
|
68
|
+
self, event_handler_name: str
|
|
69
|
+
) -> Optional[EventHandlerMapping]:
|
|
70
|
+
filtered_handlers = list(
|
|
71
|
+
filter(
|
|
72
|
+
lambda mapping: mapping.name == event_handler_name,
|
|
73
|
+
self.event_handler_mappings,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
return filtered_handlers[0] if len(filtered_handlers) > 0 else None
|
|
77
|
+
|
|
78
|
+
def get_event_timer_by_name(
|
|
79
|
+
self, event_timer_name: str
|
|
80
|
+
) -> Optional[TimeoutHandlerMapping]:
|
|
81
|
+
filtered_timers = list(
|
|
82
|
+
filter(
|
|
83
|
+
lambda mapping: mapping.name == event_timer_name,
|
|
84
|
+
self.timers,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
return filtered_timers[0] if len(filtered_timers) > 0 else None
|
|
88
|
+
|
|
89
|
+
def _run(self) -> None:
|
|
90
|
+
should_exit_state: bool = False
|
|
91
|
+
timers = deepcopy(self.timers)
|
|
92
|
+
entered_time = time.time()
|
|
93
|
+
while True:
|
|
94
|
+
if self.signal_state_machine_to_stop.is_set():
|
|
95
|
+
self.logger.info(
|
|
96
|
+
"Stopping state machine from %s state", self.state_name
|
|
97
|
+
)
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
for timer in timers:
|
|
101
|
+
if time.time() - entered_time > timer.timeout_in_seconds:
|
|
102
|
+
transition_func = timer.handler()
|
|
103
|
+
timers.remove(timer)
|
|
104
|
+
if transition_func is not None:
|
|
105
|
+
transition_func()
|
|
106
|
+
should_exit_state = True
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
if should_exit_state:
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
for handler_mapping in self.event_handler_mappings:
|
|
113
|
+
transition_func = handler_mapping.handler(handler_mapping.event)
|
|
114
|
+
if transition_func is not None:
|
|
115
|
+
transition_func()
|
|
116
|
+
should_exit_state = True
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
if should_exit_state:
|
|
120
|
+
break
|
|
121
|
+
time.sleep(settings.FSM_SLEEP_TIME)
|
|
122
|
+
if self.on_transition:
|
|
123
|
+
self.on_transition()
|
|
@@ -1,23 +1,19 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
|
-
from alitra import Frame
|
|
5
|
-
from injector import inject
|
|
6
|
-
|
|
7
5
|
from isar.config.settings import settings
|
|
8
6
|
from isar.mission_planner.mission_planner_interface import (
|
|
9
7
|
MissionNotFoundError,
|
|
10
8
|
MissionPlannerError,
|
|
11
9
|
MissionPlannerInterface,
|
|
12
10
|
)
|
|
13
|
-
from isar.services.readers.base_reader import BaseReader, BaseReaderError
|
|
14
11
|
from robot_interface.models.mission.mission import Mission
|
|
15
12
|
|
|
16
13
|
logger = logging.getLogger("api")
|
|
17
14
|
|
|
18
15
|
|
|
19
16
|
class LocalPlanner(MissionPlannerInterface):
|
|
20
|
-
@inject
|
|
21
17
|
def __init__(self):
|
|
22
18
|
self.predefined_mission_folder = Path(settings.PREDEFINED_MISSIONS_FOLDER)
|
|
23
19
|
|
|
@@ -37,15 +33,10 @@ class LocalPlanner(MissionPlannerInterface):
|
|
|
37
33
|
|
|
38
34
|
@staticmethod
|
|
39
35
|
def read_mission_from_file(mission_path: Path) -> Mission:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
dataclass_dict=mission_dict,
|
|
43
|
-
target_dataclass=Mission,
|
|
44
|
-
cast_config=[Frame],
|
|
45
|
-
strict_config=True,
|
|
46
|
-
)
|
|
36
|
+
with open(mission_path) as json_file:
|
|
37
|
+
mission_dict = json.load(json_file)
|
|
47
38
|
|
|
48
|
-
return
|
|
39
|
+
return Mission(**mission_dict)
|
|
49
40
|
|
|
50
41
|
def get_predefined_missions(self) -> dict:
|
|
51
42
|
missions: dict = {}
|
|
@@ -54,13 +45,8 @@ class LocalPlanner(MissionPlannerInterface):
|
|
|
54
45
|
for file in json_files:
|
|
55
46
|
mission_name = file.stem
|
|
56
47
|
path_to_file = self.predefined_mission_folder.joinpath(file.name)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
except BaseReaderError as e:
|
|
60
|
-
logger.warning(
|
|
61
|
-
f"Failed to read predefined mission {path_to_file} \n {e}"
|
|
62
|
-
)
|
|
63
|
-
continue
|
|
48
|
+
|
|
49
|
+
mission: Mission = self.read_mission_from_file(path_to_file)
|
|
64
50
|
if mission.id in invalid_mission_ids:
|
|
65
51
|
logger.warning(
|
|
66
52
|
f"Duplicate mission id {mission.id} : {path_to_file.as_posix()}"
|