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.
Files changed (129) hide show
  1. isar/__init__.py +2 -5
  2. isar/apis/api.py +159 -66
  3. isar/apis/models/__init__.py +0 -1
  4. isar/apis/models/models.py +22 -12
  5. isar/apis/models/start_mission_definition.py +128 -123
  6. isar/apis/robot_control/robot_controller.py +41 -0
  7. isar/apis/schedule/scheduling_controller.py +135 -121
  8. isar/apis/security/authentication.py +5 -5
  9. isar/config/certs/ca-cert.pem +32 -32
  10. isar/config/keyvault/keyvault_service.py +1 -2
  11. isar/config/log.py +47 -39
  12. isar/config/logging.conf +16 -31
  13. isar/config/open_telemetry.py +102 -0
  14. isar/config/predefined_mission_definition/default_exr.json +49 -0
  15. isar/config/predefined_mission_definition/default_mission.json +1 -5
  16. isar/config/predefined_mission_definition/default_turtlebot.json +4 -11
  17. isar/config/predefined_missions/default.json +67 -87
  18. isar/config/predefined_missions/default_extra_capabilities.json +107 -0
  19. isar/config/settings.py +119 -142
  20. isar/eventhandlers/eventhandler.py +123 -0
  21. isar/mission_planner/local_planner.py +6 -20
  22. isar/mission_planner/mission_planner_interface.py +1 -1
  23. isar/models/events.py +184 -0
  24. isar/models/status.py +18 -0
  25. isar/modules.py +118 -205
  26. isar/robot/robot.py +377 -0
  27. isar/robot/robot_battery.py +60 -0
  28. isar/robot/robot_monitor_mission.py +357 -0
  29. isar/robot/robot_pause_mission.py +74 -0
  30. isar/robot/robot_resume_mission.py +67 -0
  31. isar/robot/robot_start_mission.py +66 -0
  32. isar/robot/robot_status.py +61 -0
  33. isar/robot/robot_stop_mission.py +68 -0
  34. isar/robot/robot_upload_inspection.py +75 -0
  35. isar/script.py +171 -0
  36. isar/services/service_connections/mqtt/mqtt_client.py +47 -11
  37. isar/services/service_connections/mqtt/robot_heartbeat_publisher.py +32 -0
  38. isar/services/service_connections/mqtt/robot_info_publisher.py +4 -3
  39. isar/services/service_connections/persistent_memory.py +69 -0
  40. isar/services/utilities/mqtt_utilities.py +93 -0
  41. isar/services/utilities/robot_utilities.py +20 -0
  42. isar/services/utilities/scheduling_utilities.py +393 -65
  43. isar/state_machine/state_machine.py +227 -486
  44. isar/state_machine/states/__init__.py +0 -7
  45. isar/state_machine/states/await_next_mission.py +114 -0
  46. isar/state_machine/states/blocked_protective_stop.py +60 -0
  47. isar/state_machine/states/going_to_lockdown.py +95 -0
  48. isar/state_machine/states/going_to_recharging.py +92 -0
  49. isar/state_machine/states/home.py +115 -0
  50. isar/state_machine/states/intervention_needed.py +77 -0
  51. isar/state_machine/states/lockdown.py +38 -0
  52. isar/state_machine/states/maintenance.py +36 -0
  53. isar/state_machine/states/monitor.py +137 -166
  54. isar/state_machine/states/offline.py +60 -0
  55. isar/state_machine/states/paused.py +92 -23
  56. isar/state_machine/states/pausing.py +48 -0
  57. isar/state_machine/states/pausing_return_home.py +48 -0
  58. isar/state_machine/states/recharging.py +80 -0
  59. isar/state_machine/states/resuming.py +57 -0
  60. isar/state_machine/states/resuming_return_home.py +64 -0
  61. isar/state_machine/states/return_home_paused.py +109 -0
  62. isar/state_machine/states/returning_home.py +217 -0
  63. isar/state_machine/states/stopping.py +61 -0
  64. isar/state_machine/states/stopping_due_to_maintenance.py +61 -0
  65. isar/state_machine/states/stopping_go_to_lockdown.py +60 -0
  66. isar/state_machine/states/stopping_go_to_recharge.py +51 -0
  67. isar/state_machine/states/stopping_return_home.py +77 -0
  68. isar/state_machine/states/unknown_status.py +72 -0
  69. isar/state_machine/states_enum.py +22 -5
  70. isar/state_machine/transitions/mission.py +192 -0
  71. isar/state_machine/transitions/return_home.py +106 -0
  72. isar/state_machine/transitions/robot_status.py +80 -0
  73. isar/state_machine/utils/common_event_handlers.py +73 -0
  74. isar/storage/blob_storage.py +71 -45
  75. isar/storage/local_storage.py +28 -14
  76. isar/storage/storage_interface.py +28 -6
  77. isar/storage/uploader.py +184 -55
  78. isar/storage/utilities.py +35 -27
  79. isar-1.34.9.dist-info/METADATA +496 -0
  80. isar-1.34.9.dist-info/RECORD +135 -0
  81. {isar-1.15.0.dist-info → isar-1.34.9.dist-info}/WHEEL +1 -1
  82. isar-1.34.9.dist-info/entry_points.txt +3 -0
  83. robot_interface/models/exceptions/__init__.py +0 -7
  84. robot_interface/models/exceptions/robot_exceptions.py +274 -4
  85. robot_interface/models/initialize/__init__.py +0 -1
  86. robot_interface/models/inspection/__init__.py +0 -13
  87. robot_interface/models/inspection/inspection.py +43 -34
  88. robot_interface/models/mission/mission.py +18 -14
  89. robot_interface/models/mission/status.py +20 -25
  90. robot_interface/models/mission/task.py +156 -92
  91. robot_interface/models/robots/battery_state.py +6 -0
  92. robot_interface/models/robots/media.py +13 -0
  93. robot_interface/models/robots/robot_model.py +7 -7
  94. robot_interface/robot_interface.py +135 -66
  95. robot_interface/telemetry/mqtt_client.py +84 -12
  96. robot_interface/telemetry/payloads.py +111 -12
  97. robot_interface/utilities/json_service.py +7 -1
  98. isar/config/predefined_missions/default_turtlebot.json +0 -110
  99. isar/config/predefined_poses/__init__.py +0 -0
  100. isar/config/predefined_poses/predefined_poses.py +0 -616
  101. isar/config/settings.env +0 -26
  102. isar/mission_planner/sequential_task_selector.py +0 -23
  103. isar/mission_planner/task_selector_interface.py +0 -31
  104. isar/models/communication/__init__.py +0 -0
  105. isar/models/communication/message.py +0 -12
  106. isar/models/communication/queues/__init__.py +0 -4
  107. isar/models/communication/queues/queue_io.py +0 -12
  108. isar/models/communication/queues/queue_timeout_error.py +0 -2
  109. isar/models/communication/queues/queues.py +0 -19
  110. isar/models/communication/queues/status_queue.py +0 -20
  111. isar/models/mission_metadata/__init__.py +0 -0
  112. isar/services/readers/__init__.py +0 -0
  113. isar/services/readers/base_reader.py +0 -37
  114. isar/services/service_connections/mqtt/robot_status_publisher.py +0 -93
  115. isar/services/service_connections/stid/__init__.py +0 -0
  116. isar/services/service_connections/stid/stid_service.py +0 -45
  117. isar/services/utilities/queue_utilities.py +0 -39
  118. isar/state_machine/states/idle.py +0 -40
  119. isar/state_machine/states/initialize.py +0 -60
  120. isar/state_machine/states/initiate.py +0 -129
  121. isar/state_machine/states/off.py +0 -18
  122. isar/state_machine/states/stop.py +0 -78
  123. isar/storage/slimm_storage.py +0 -181
  124. isar-1.15.0.dist-info/METADATA +0 -417
  125. isar-1.15.0.dist-info/RECORD +0 -113
  126. robot_interface/models/initialize/initialize_params.py +0 -9
  127. robot_interface/models/mission/step.py +0 -211
  128. {isar-1.15.0.dist-info → isar-1.34.9.dist-info/licenses}/LICENSE +0 -0
  129. {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 typing import List
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 BaseSettings, Field, validator
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 VideoStream
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 initiate a step or mission before cancelling
48
- INITIATE_FAILURE_COUNTER_LIMIT: int = Field(default=10)
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=10)
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
- ROBOT_STATUS_PUBLISH_INTERVAL: int = Field(default=1)
61
- ROBOT_INFO_PUBLISH_INTERVAL: int = Field(default=5)
62
- ROBOT_API_STATUS_POLL_INTERVAL: int = Field(default=5)
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=False)
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
- BLOB_STORAGE_ACCOUNT_URL: str = Field(
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
- # Client ID for STID App Registration
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="1320")
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="Kårstø")
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="KAA")
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
- # Endpoints to reach video streams for the robot
192
- VIDEO_STREAMS: List[VideoStream] = Field(
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
- TOPIC_ISAR_STATE: str = Field(default="state")
232
- TOPIC_ISAR_MISSION: str = Field(default="mission")
233
- TOPIC_ISAR_TASK: str = Field(default="task")
234
- TOPIC_ISAR_STEP: str = Field(default="step")
235
- TOPIC_ISAR_INSPECTION_RESULT = Field(default="inspection_result")
236
- TOPIC_ISAR_ROBOT_STATUS: str = Field(default="robot_status")
237
- TOPIC_ISAR_ROBOT_INFO: str = Field(default="robot_info")
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
- # Log levels
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
- LOG_LEVELS: dict = Field(default={})
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
- @validator("LOG_LEVELS", pre=True, always=True)
266
- def set_log_levels(cls, v, values) -> dict:
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
- pre=True,
288
- always=True,
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
- load_dotenv()
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
- with pkg_resources.path(
311
- f"{settings.ROBOT_PACKAGE}.config", "settings.env"
312
- ) as path:
313
- env_file_path = path
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
- env_file_path = None
316
- super().__init__(_env_file=env_file_path)
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=["drive_to_pose", "take_image"])
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
- class Config:
327
- env_file_encoding = "utf-8"
328
- case_sensitive = True
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
- mission_dict: dict = BaseReader.read_json(location=mission_path)
41
- mission: Mission = BaseReader.dict_to_dataclass(
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 mission
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
- try:
58
- mission: Mission = self.read_mission_from_file(path_to_file)
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()}"
@@ -9,7 +9,7 @@ class MissionPlannerInterface(metaclass=ABCMeta):
9
9
  """
10
10
  Parameters
11
11
  ----------
12
- mission_id : int
12
+ mission_id : str
13
13
 
14
14
  Returns
15
15
  -------