isar 1.21.0__py3-none-any.whl → 1.22.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of isar might be problematic. Click here for more details.
- isar/apis/models/start_mission_definition.py +21 -15
- isar/config/settings.py +4 -1
- isar/services/service_connections/mqtt/robot_heartbeat_publisher.py +2 -2
- isar/services/service_connections/mqtt/robot_info_publisher.py +3 -2
- isar/state_machine/state_machine.py +17 -15
- isar/state_machine/states/monitor.py +75 -80
- isar/storage/uploader.py +8 -6
- isar/storage/utilities.py +3 -3
- {isar-1.21.0.dist-info → isar-1.22.1.dist-info}/METADATA +4 -2
- {isar-1.21.0.dist-info → isar-1.22.1.dist-info}/RECORD +19 -19
- {isar-1.21.0.dist-info → isar-1.22.1.dist-info}/WHEEL +1 -1
- robot_interface/models/exceptions/robot_exceptions.py +14 -0
- robot_interface/models/mission/mission.py +2 -0
- robot_interface/models/mission/task.py +8 -5
- robot_interface/telemetry/mqtt_client.py +2 -2
- robot_interface/telemetry/payloads.py +7 -0
- {isar-1.21.0.dist-info → isar-1.22.1.dist-info}/LICENSE +0 -0
- {isar-1.21.0.dist-info → isar-1.22.1.dist-info}/entry_points.txt +0 -0
- {isar-1.21.0.dist-info → isar-1.22.1.dist-info}/top_level.txt +0 -0
|
@@ -2,7 +2,7 @@ import time
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from typing import Any, Dict, List, Optional, Union
|
|
4
4
|
|
|
5
|
-
from alitra import
|
|
5
|
+
from alitra import Frame, Orientation, Pose, Position
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
8
|
from isar.apis.models.models import InputPose, InputPosition
|
|
@@ -62,6 +62,8 @@ class StartMissionDefinition(BaseModel):
|
|
|
62
62
|
id: Optional[str] = None
|
|
63
63
|
name: Optional[str] = None
|
|
64
64
|
start_pose: Optional[InputPose] = None
|
|
65
|
+
dock: Optional[bool] = None
|
|
66
|
+
undock: Optional[bool] = None
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
def to_isar_mission(mission_definition: StartMissionDefinition) -> Mission:
|
|
@@ -85,6 +87,9 @@ def to_isar_mission(mission_definition: StartMissionDefinition) -> Mission:
|
|
|
85
87
|
|
|
86
88
|
isar_mission: Mission = Mission(tasks=isar_tasks)
|
|
87
89
|
|
|
90
|
+
isar_mission.dock = mission_definition.dock
|
|
91
|
+
isar_mission.undock = mission_definition.undock
|
|
92
|
+
|
|
88
93
|
if mission_definition.name:
|
|
89
94
|
isar_mission.name = mission_definition.name
|
|
90
95
|
else:
|
|
@@ -127,20 +132,21 @@ def check_for_duplicate_ids(items: Union[List[Task], List[STEPS]]):
|
|
|
127
132
|
|
|
128
133
|
def generate_steps(task) -> List[STEPS]:
|
|
129
134
|
steps: List[STEPS] = []
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
135
|
+
|
|
136
|
+
if task.type == TaskType.Inspection:
|
|
137
|
+
steps.extend(generate_steps_for_inspection_task(task=task))
|
|
138
|
+
elif task.type == TaskType.DriveTo:
|
|
139
|
+
steps.append(generate_steps_for_drive_to_task(task=task))
|
|
140
|
+
elif task.type == TaskType.Localization:
|
|
141
|
+
steps.append(generate_steps_for_localization_task(task=task))
|
|
142
|
+
elif task.type == TaskType.ReturnToHome:
|
|
143
|
+
steps.append(generate_steps_for_return_to_home_task(task=task))
|
|
144
|
+
elif task.type == TaskType.Dock:
|
|
145
|
+
steps.append(generate_steps_for_dock_task())
|
|
146
|
+
else:
|
|
147
|
+
raise MissionPlannerError(
|
|
148
|
+
f"Failed to create task: '{task.type}' is not a valid"
|
|
149
|
+
)
|
|
144
150
|
|
|
145
151
|
return steps
|
|
146
152
|
|
isar/config/settings.py
CHANGED
|
@@ -8,7 +8,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
8
8
|
|
|
9
9
|
from isar.config import predefined_missions
|
|
10
10
|
from robot_interface.models.robots.robot_model import RobotModel
|
|
11
|
-
from robot_interface.telemetry.payloads import VideoStream
|
|
11
|
+
from robot_interface.telemetry.payloads import VideoStream, DocumentInfo
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Settings(BaseSettings):
|
|
@@ -190,6 +190,9 @@ class Settings(BaseSettings):
|
|
|
190
190
|
# Serial number of the robot ISAR is connected to
|
|
191
191
|
SERIAL_NUMBER: str = Field(default="0001")
|
|
192
192
|
|
|
193
|
+
# Info about robot documentation
|
|
194
|
+
DOCUMENTATION: List[DocumentInfo] = Field(default=[])
|
|
195
|
+
|
|
193
196
|
# Endpoints to reach video streams for the robot
|
|
194
197
|
VIDEO_STREAMS: List[VideoStream] = Field(
|
|
195
198
|
default=[
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import time
|
|
3
|
-
from datetime import
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
4
|
from queue import Queue
|
|
5
5
|
|
|
6
6
|
from isar.config.settings import settings
|
|
@@ -18,7 +18,7 @@ class RobotHeartbeatPublisher:
|
|
|
18
18
|
payload: RobotHeartbeatPayload = RobotHeartbeatPayload(
|
|
19
19
|
isar_id=settings.ISAR_ID,
|
|
20
20
|
robot_name=settings.ROBOT_NAME,
|
|
21
|
-
timestamp=datetime.now(
|
|
21
|
+
timestamp=datetime.now(timezone.utc),
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
self.mqtt_publisher.publish(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import time
|
|
3
|
-
from datetime import
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
4
|
from queue import Queue
|
|
5
5
|
|
|
6
6
|
from isar.config.settings import robot_settings, settings
|
|
@@ -21,11 +21,12 @@ class RobotInfoPublisher:
|
|
|
21
21
|
robot_model=robot_settings.ROBOT_MODEL, # type: ignore
|
|
22
22
|
robot_serial_number=settings.SERIAL_NUMBER,
|
|
23
23
|
robot_asset=settings.PLANT_SHORT_NAME,
|
|
24
|
+
documentation=settings.DOCUMENTATION,
|
|
24
25
|
video_streams=settings.VIDEO_STREAMS,
|
|
25
26
|
host=settings.API_HOST_VIEWED_EXTERNALLY,
|
|
26
27
|
port=settings.API_PORT,
|
|
27
28
|
capabilities=robot_settings.CAPABILITIES,
|
|
28
|
-
timestamp=datetime.now(
|
|
29
|
+
timestamp=datetime.now(timezone.utc),
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
self.mqtt_publisher.publish(
|
|
@@ -2,7 +2,7 @@ import json
|
|
|
2
2
|
import logging
|
|
3
3
|
import queue
|
|
4
4
|
from collections import deque
|
|
5
|
-
from datetime import
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
6
|
from typing import Deque, List, Optional
|
|
7
7
|
|
|
8
8
|
from alitra import Pose
|
|
@@ -292,7 +292,7 @@ class StateMachine(object):
|
|
|
292
292
|
self.queues.resume_mission.output.put(resume_mission_response)
|
|
293
293
|
|
|
294
294
|
self.current_task.reset_task()
|
|
295
|
-
self.
|
|
295
|
+
self.iterate_current_step()
|
|
296
296
|
|
|
297
297
|
self.robot.resume()
|
|
298
298
|
|
|
@@ -337,12 +337,14 @@ class StateMachine(object):
|
|
|
337
337
|
else:
|
|
338
338
|
self.current_task.status = TaskStatus.InProgress
|
|
339
339
|
self.publish_task_status(task=self.current_task)
|
|
340
|
-
self.
|
|
340
|
+
self.iterate_current_step()
|
|
341
341
|
|
|
342
342
|
def _step_finished(self) -> None:
|
|
343
343
|
self.publish_step_status(step=self.current_step)
|
|
344
|
-
self.
|
|
345
|
-
self.
|
|
344
|
+
self.current_task.update_task_status()
|
|
345
|
+
self.publish_task_status(task=self.current_task)
|
|
346
|
+
self.iterate_current_task()
|
|
347
|
+
self.iterate_current_step()
|
|
346
348
|
|
|
347
349
|
def _full_mission_finished(self) -> None:
|
|
348
350
|
self.current_task = None
|
|
@@ -379,8 +381,10 @@ class StateMachine(object):
|
|
|
379
381
|
if self.stepwise_mission:
|
|
380
382
|
self.current_step.status = StepStatus.Failed
|
|
381
383
|
self.publish_step_status(step=self.current_step)
|
|
382
|
-
self.
|
|
383
|
-
self.
|
|
384
|
+
self.current_task.update_task_status()
|
|
385
|
+
self.publish_task_status(task=self.current_task)
|
|
386
|
+
self.iterate_current_task()
|
|
387
|
+
self.iterate_current_step()
|
|
384
388
|
|
|
385
389
|
def _mission_stopped(self) -> None:
|
|
386
390
|
self.current_mission.status = MissionStatus.Cancelled
|
|
@@ -427,10 +431,8 @@ class StateMachine(object):
|
|
|
427
431
|
"""
|
|
428
432
|
self.to_idle()
|
|
429
433
|
|
|
430
|
-
def
|
|
434
|
+
def iterate_current_task(self):
|
|
431
435
|
if self.current_task.is_finished():
|
|
432
|
-
self.current_task.update_task_status()
|
|
433
|
-
self.publish_task_status(task=self.current_task)
|
|
434
436
|
try:
|
|
435
437
|
self.current_task = self.task_selector.next_task()
|
|
436
438
|
self.current_task.status = TaskStatus.InProgress
|
|
@@ -439,7 +441,7 @@ class StateMachine(object):
|
|
|
439
441
|
# Indicates that all tasks are finished
|
|
440
442
|
self.current_task = None
|
|
441
443
|
|
|
442
|
-
def
|
|
444
|
+
def iterate_current_step(self):
|
|
443
445
|
if self.current_task != None:
|
|
444
446
|
self.current_step = self.current_task.next_step()
|
|
445
447
|
|
|
@@ -524,7 +526,7 @@ class StateMachine(object):
|
|
|
524
526
|
"error_description": (
|
|
525
527
|
error_message.error_description if error_message else None
|
|
526
528
|
),
|
|
527
|
-
"timestamp": datetime.now(
|
|
529
|
+
"timestamp": datetime.now(timezone.utc),
|
|
528
530
|
},
|
|
529
531
|
cls=EnhancedJSONEncoder,
|
|
530
532
|
)
|
|
@@ -557,7 +559,7 @@ class StateMachine(object):
|
|
|
557
559
|
"error_description": (
|
|
558
560
|
error_message.error_description if error_message else None
|
|
559
561
|
),
|
|
560
|
-
"timestamp": datetime.now(
|
|
562
|
+
"timestamp": datetime.now(timezone.utc),
|
|
561
563
|
},
|
|
562
564
|
cls=EnhancedJSONEncoder,
|
|
563
565
|
)
|
|
@@ -592,7 +594,7 @@ class StateMachine(object):
|
|
|
592
594
|
"error_description": (
|
|
593
595
|
error_message.error_description if error_message else None
|
|
594
596
|
),
|
|
595
|
-
"timestamp": datetime.now(
|
|
597
|
+
"timestamp": datetime.now(timezone.utc),
|
|
596
598
|
},
|
|
597
599
|
cls=EnhancedJSONEncoder,
|
|
598
600
|
)
|
|
@@ -612,7 +614,7 @@ class StateMachine(object):
|
|
|
612
614
|
"isar_id": settings.ISAR_ID,
|
|
613
615
|
"robot_name": settings.ROBOT_NAME,
|
|
614
616
|
"status": self._current_status(),
|
|
615
|
-
"timestamp": datetime.now(
|
|
617
|
+
"timestamp": datetime.now(timezone.utc),
|
|
616
618
|
},
|
|
617
619
|
cls=EnhancedJSONEncoder,
|
|
618
620
|
)
|
|
@@ -71,37 +71,14 @@ class Monitor(State):
|
|
|
71
71
|
thread_name="State Machine Monitor Get Step Status",
|
|
72
72
|
)
|
|
73
73
|
try:
|
|
74
|
-
status:
|
|
75
|
-
self.step_status_thread.get_output()
|
|
76
|
-
)
|
|
74
|
+
status: StepStatus = self.step_status_thread.get_output()
|
|
77
75
|
except ThreadedRequestNotFinishedError:
|
|
78
76
|
time.sleep(self.state_machine.sleep_time)
|
|
79
77
|
continue
|
|
80
78
|
|
|
81
79
|
except RobotCommunicationTimeoutException as e:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
self.step_status_thread = None
|
|
86
|
-
self.request_status_failure_counter += 1
|
|
87
|
-
self.logger.warning(
|
|
88
|
-
f"Monitoring step {self.state_machine.current_step.id} failed #: "
|
|
89
|
-
f"{self.request_status_failure_counter} failed because: {e.error_description}"
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
self.request_status_failure_counter
|
|
94
|
-
>= self.request_status_failure_counter_limit
|
|
95
|
-
):
|
|
96
|
-
self.state_machine.current_step.error_message = ErrorMessage(
|
|
97
|
-
error_reason=e.error_reason,
|
|
98
|
-
error_description=e.error_description,
|
|
99
|
-
)
|
|
100
|
-
self.logger.error(
|
|
101
|
-
f"Step will be cancelled after failing to get step status "
|
|
102
|
-
f"{self.request_status_failure_counter} times because: "
|
|
103
|
-
f"{e.error_description}"
|
|
104
|
-
)
|
|
80
|
+
step_failed: bool = self._handle_communication_timeout(e)
|
|
81
|
+
if step_failed:
|
|
105
82
|
status = StepStatus.Failed
|
|
106
83
|
else:
|
|
107
84
|
continue
|
|
@@ -116,16 +93,6 @@ class Monitor(State):
|
|
|
116
93
|
)
|
|
117
94
|
status = StepStatus.Failed
|
|
118
95
|
|
|
119
|
-
except RobotMissionStatusException as e:
|
|
120
|
-
self.state_machine.current_mission.error_message = ErrorMessage(
|
|
121
|
-
error_reason=e.error_reason, error_description=e.error_description
|
|
122
|
-
)
|
|
123
|
-
self.logger.error(
|
|
124
|
-
f"Monitoring mission {self.state_machine.current_mission.id} "
|
|
125
|
-
f"failed because: {e.error_description}"
|
|
126
|
-
)
|
|
127
|
-
status = MissionStatus.Failed
|
|
128
|
-
|
|
129
96
|
except RobotException as e:
|
|
130
97
|
self._set_error_message(e)
|
|
131
98
|
status = StepStatus.Failed
|
|
@@ -134,13 +101,18 @@ class Monitor(State):
|
|
|
134
101
|
f"Retrieving the status failed because: {e.error_description}"
|
|
135
102
|
)
|
|
136
103
|
|
|
137
|
-
if isinstance(status, StepStatus):
|
|
138
|
-
self.state_machine.current_step.status = status
|
|
139
|
-
elif isinstance(status, MissionStatus):
|
|
140
|
-
self.state_machine.current_mission.status = status
|
|
104
|
+
if not isinstance(status, StepStatus):
|
|
141
105
|
self.logger.error(
|
|
142
106
|
f"Received an invalid status update when monitoring mission. Only StepStatus is expected."
|
|
143
107
|
)
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
if self.state_machine.current_task == None:
|
|
111
|
+
self.state_machine.iterate_current_task()
|
|
112
|
+
if self.state_machine.current_step == None:
|
|
113
|
+
self.state_machine.iterate_current_step()
|
|
114
|
+
|
|
115
|
+
self.state_machine.current_step.status = status
|
|
144
116
|
|
|
145
117
|
if self._should_upload_inspections():
|
|
146
118
|
get_inspections_thread = ThreadedRequest(
|
|
@@ -153,38 +125,37 @@ class Monitor(State):
|
|
|
153
125
|
)
|
|
154
126
|
|
|
155
127
|
if self.state_machine.stepwise_mission:
|
|
156
|
-
if self.
|
|
128
|
+
if self._is_step_finished(self.state_machine.current_step):
|
|
129
|
+
self._report_step_status(self.state_machine.current_step)
|
|
157
130
|
transition = self.state_machine.step_finished # type: ignore
|
|
158
131
|
break
|
|
159
132
|
else:
|
|
160
|
-
if
|
|
161
|
-
|
|
162
|
-
|
|
133
|
+
if self._is_step_finished(self.state_machine.current_step):
|
|
134
|
+
self._report_step_status(self.state_machine.current_step)
|
|
135
|
+
|
|
136
|
+
if self.state_machine.current_task.is_finished():
|
|
137
|
+
# Report and update finished task
|
|
138
|
+
self.state_machine.current_task.update_task_status() # Uses the updated step status to set the task status
|
|
139
|
+
self.state_machine.publish_task_status(
|
|
140
|
+
task=self.state_machine.current_task
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
self.state_machine.iterate_current_task()
|
|
144
|
+
|
|
163
145
|
if self.state_machine.current_task == None:
|
|
164
146
|
transition = self.state_machine.full_mission_finished # type: ignore
|
|
165
147
|
break
|
|
166
|
-
|
|
148
|
+
|
|
149
|
+
# Report and update next task
|
|
167
150
|
self.state_machine.current_task.update_task_status()
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
self.state_machine.current_task = (
|
|
177
|
-
self.state_machine.task_selector.next_task()
|
|
178
|
-
)
|
|
179
|
-
except TaskSelectorStop:
|
|
180
|
-
# Indicates that all tasks are finished
|
|
181
|
-
self.state_machine.current_task = None
|
|
182
|
-
transition = self.state_machine.full_mission_finished # type: ignore
|
|
183
|
-
break
|
|
184
|
-
self.state_machine.update_current_step()
|
|
185
|
-
elif self._mission_finished(self.state_machine.current_mission):
|
|
186
|
-
transition = self.state_machine.full_mission_finished # type: ignore
|
|
187
|
-
break
|
|
151
|
+
self.state_machine.publish_task_status(
|
|
152
|
+
task=self.state_machine.current_task
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
self.state_machine.iterate_current_step()
|
|
156
|
+
|
|
157
|
+
else: # If not all steps are done
|
|
158
|
+
self.state_machine.current_task.status = TaskStatus.InProgress
|
|
188
159
|
|
|
189
160
|
self.step_status_thread = None
|
|
190
161
|
time.sleep(self.state_machine.sleep_time)
|
|
@@ -227,34 +198,28 @@ class Monitor(State):
|
|
|
227
198
|
self.state_machine.queues.upload_queue.put(message)
|
|
228
199
|
self.logger.info(f"Inspection: {str(inspection.id)[:8]} queued for upload")
|
|
229
200
|
|
|
230
|
-
def
|
|
201
|
+
def _is_step_finished(self, step: Step) -> bool:
|
|
231
202
|
finished: bool = False
|
|
203
|
+
if step.status == StepStatus.Failed:
|
|
204
|
+
finished = True
|
|
205
|
+
elif step.status == StepStatus.Successful:
|
|
206
|
+
finished = True
|
|
207
|
+
return finished
|
|
208
|
+
|
|
209
|
+
def _report_step_status(self, step: Step) -> None:
|
|
232
210
|
if step.status == StepStatus.Failed:
|
|
233
211
|
self.logger.warning(
|
|
234
212
|
f"Step: {str(step.id)[:8]} was reported as failed by the robot"
|
|
235
213
|
)
|
|
236
|
-
finished = True
|
|
237
214
|
elif step.status == StepStatus.Successful:
|
|
238
215
|
self.logger.info(
|
|
239
216
|
f"{type(step).__name__} step: {str(step.id)[:8]} completed"
|
|
240
217
|
)
|
|
241
|
-
finished = True
|
|
242
|
-
return finished
|
|
243
|
-
|
|
244
|
-
@staticmethod
|
|
245
|
-
def _mission_finished(mission: Mission) -> bool:
|
|
246
|
-
if (
|
|
247
|
-
mission.status == MissionStatus.Successful
|
|
248
|
-
or mission.status == MissionStatus.PartiallySuccessful
|
|
249
|
-
or mission.status == MissionStatus.Failed
|
|
250
|
-
):
|
|
251
|
-
return True
|
|
252
|
-
return False
|
|
253
218
|
|
|
254
219
|
def _should_upload_inspections(self) -> bool:
|
|
255
220
|
step: Step = self.state_machine.current_step
|
|
256
221
|
return (
|
|
257
|
-
self.
|
|
222
|
+
self._is_step_finished(step)
|
|
258
223
|
and step.status == StepStatus.Successful
|
|
259
224
|
and isinstance(step, InspectionStep)
|
|
260
225
|
)
|
|
@@ -264,3 +229,33 @@ class Monitor(State):
|
|
|
264
229
|
error_reason=e.error_reason, error_description=e.error_description
|
|
265
230
|
)
|
|
266
231
|
self.state_machine.current_step.error_message = error_message
|
|
232
|
+
|
|
233
|
+
def _handle_communication_timeout(
|
|
234
|
+
self, e: RobotCommunicationTimeoutException
|
|
235
|
+
) -> bool:
|
|
236
|
+
self.state_machine.current_mission.error_message = ErrorMessage(
|
|
237
|
+
error_reason=e.error_reason, error_description=e.error_description
|
|
238
|
+
)
|
|
239
|
+
self.step_status_thread = None
|
|
240
|
+
self.request_status_failure_counter += 1
|
|
241
|
+
self.logger.warning(
|
|
242
|
+
f"Monitoring step {self.state_machine.current_step.id} failed #: "
|
|
243
|
+
f"{self.request_status_failure_counter} failed because: {e.error_description}"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
self.request_status_failure_counter
|
|
248
|
+
>= self.request_status_failure_counter_limit
|
|
249
|
+
):
|
|
250
|
+
self.state_machine.current_step.error_message = ErrorMessage(
|
|
251
|
+
error_reason=e.error_reason,
|
|
252
|
+
error_description=e.error_description,
|
|
253
|
+
)
|
|
254
|
+
self.logger.error(
|
|
255
|
+
f"Step will be cancelled after failing to get step status "
|
|
256
|
+
f"{self.request_status_failure_counter} times because: "
|
|
257
|
+
f"{e.error_description}"
|
|
258
|
+
)
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
return False
|
isar/storage/uploader.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from datetime import
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
5
|
from queue import Empty, Queue
|
|
6
6
|
from typing import List, Union
|
|
7
7
|
|
|
@@ -22,12 +22,12 @@ class UploaderQueueItem:
|
|
|
22
22
|
mission: Mission
|
|
23
23
|
storage_handler: StorageInterface
|
|
24
24
|
_retry_count: int
|
|
25
|
-
_next_retry_time: datetime = datetime.now(
|
|
25
|
+
_next_retry_time: datetime = datetime.now(timezone.utc)
|
|
26
26
|
|
|
27
27
|
def increment_retry(self, max_wait_time: int) -> None:
|
|
28
28
|
self._retry_count += 1
|
|
29
29
|
seconds_until_retry: int = min(2**self._retry_count, max_wait_time)
|
|
30
|
-
self._next_retry_time = datetime.now(
|
|
30
|
+
self._next_retry_time = datetime.now(timezone.utc) + timedelta(
|
|
31
31
|
seconds=seconds_until_retry
|
|
32
32
|
)
|
|
33
33
|
|
|
@@ -35,10 +35,12 @@ class UploaderQueueItem:
|
|
|
35
35
|
return self._retry_count
|
|
36
36
|
|
|
37
37
|
def is_ready_for_upload(self) -> bool:
|
|
38
|
-
return datetime.now(
|
|
38
|
+
return datetime.now(timezone.utc) >= self._next_retry_time
|
|
39
39
|
|
|
40
40
|
def seconds_until_retry(self) -> int:
|
|
41
|
-
return max(
|
|
41
|
+
return max(
|
|
42
|
+
0, int((self._next_retry_time - datetime.now(timezone.utc)).total_seconds())
|
|
43
|
+
)
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
class Uploader:
|
|
@@ -154,7 +156,7 @@ class Uploader:
|
|
|
154
156
|
"inspection_id": inspection.id,
|
|
155
157
|
"inspection_path": inspection_path,
|
|
156
158
|
"analysis_type": inspection.metadata.analysis_type,
|
|
157
|
-
"timestamp": datetime.now(
|
|
159
|
+
"timestamp": datetime.now(timezone.utc),
|
|
158
160
|
},
|
|
159
161
|
cls=EnhancedJSONEncoder,
|
|
160
162
|
)
|
isar/storage/utilities.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import time
|
|
3
|
-
from datetime import
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Tuple
|
|
6
6
|
|
|
@@ -37,7 +37,7 @@ def construct_metadata_file(
|
|
|
37
37
|
"mission_id": mission.id,
|
|
38
38
|
"mission_name": mission.name,
|
|
39
39
|
"plant_name": settings.PLANT_NAME,
|
|
40
|
-
"mission_date": datetime.now(
|
|
40
|
+
"mission_date": datetime.now(timezone.utc).date(),
|
|
41
41
|
"isar_id": settings.ISAR_ID,
|
|
42
42
|
"robot_name": settings.ROBOT_NAME,
|
|
43
43
|
"analysis_type": (
|
|
@@ -80,4 +80,4 @@ def get_filename(
|
|
|
80
80
|
|
|
81
81
|
def get_foldername(mission: Mission) -> str:
|
|
82
82
|
mission_name: str = mission.name.replace(" ", "-")
|
|
83
|
-
return f"{datetime.now(
|
|
83
|
+
return f"{datetime.now(timezone.utc).date()}__{settings.PLANT_SHORT_NAME}__{mission_name}__{mission.id}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: isar
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.22.1
|
|
4
4
|
Summary: Integration and Supervisory control of Autonomous Robots
|
|
5
5
|
Author-email: Equinor ASA <fg_robots_dev@equinor.com>
|
|
6
6
|
License: Eclipse Public License version 2.0
|
|
@@ -91,12 +91,14 @@ Classifier: Intended Audience :: Science/Research
|
|
|
91
91
|
Classifier: License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)
|
|
92
92
|
Classifier: Natural Language :: English
|
|
93
93
|
Classifier: Programming Language :: Python :: 3
|
|
94
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
95
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
94
96
|
Classifier: Programming Language :: Python :: 3.11
|
|
95
97
|
Classifier: Programming Language :: Python :: 3.12
|
|
96
98
|
Classifier: Topic :: Scientific/Engineering
|
|
97
99
|
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
98
100
|
Classifier: Topic :: Software Development :: Libraries
|
|
99
|
-
Requires-Python: >=3.
|
|
101
|
+
Requires-Python: >=3.9
|
|
100
102
|
Description-Content-Type: text/markdown
|
|
101
103
|
License-File: LICENSE
|
|
102
104
|
Requires-Dist: alitra >=1.1.3
|
|
@@ -5,7 +5,7 @@ isar/apis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
5
5
|
isar/apis/api.py,sha256=vSXfkWR7iITWbh4BsDzjEDtQrz6KS-vi2S8AeaeDc3Q,13112
|
|
6
6
|
isar/apis/models/__init__.py,sha256=NI1BYyN__Ogr00Qqe0XJ-9gEVPva2brXo2RJsbrS4tM,52
|
|
7
7
|
isar/apis/models/models.py,sha256=BRm3Wl6TJHdHEKLRQ2SGDx6Y54qq8IesMbBuVO7RuxE,1757
|
|
8
|
-
isar/apis/models/start_mission_definition.py,sha256=
|
|
8
|
+
isar/apis/models/start_mission_definition.py,sha256=Ch8625e-ZDWQv5L-cVlpotTHm1b42ZrDBx2recvTIjM,7394
|
|
9
9
|
isar/apis/schedule/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
isar/apis/schedule/scheduling_controller.py,sha256=w2F-6IKBgVN9HVopAOWBb3KUgPjhsnuPLQ3eqjDNfOg,11888
|
|
11
11
|
isar/apis/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -15,7 +15,7 @@ isar/config/configuration_error.py,sha256=rO6WOhafX6xvVib8WxV-eY483Z0PpN-9PxGsq5
|
|
|
15
15
|
isar/config/log.py,sha256=zHFLmGWQRn8TrcsxUS6KHpJt2JE86kYazU7b-bkcN9o,2285
|
|
16
16
|
isar/config/logging.conf,sha256=mYO1xf27gAopEMHhGzY7-mwyfN16rwRLkPNMvy3zn2g,1127
|
|
17
17
|
isar/config/settings.env,sha256=-kivj0osAAKlInnY81ugySTlcImhVABbnj9kUoBDLu8,535
|
|
18
|
-
isar/config/settings.py,sha256=
|
|
18
|
+
isar/config/settings.py,sha256=RUsQr4ZFhsMGQRmBQFYxYZkowsP82UG2EDisenLQXes,13478
|
|
19
19
|
isar/config/certs/ca-cert.pem,sha256=gSBTyY0tKSFnssyvrvbFvHpQwii0kEkBryklVmevdtc,2029
|
|
20
20
|
isar/config/keyvault/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
isar/config/keyvault/keyvault_error.py,sha256=zvPCsZLjboxsxthYkxpRERCTFxYV8R5WmACewAUQLwk,41
|
|
@@ -59,21 +59,21 @@ isar/services/service_connections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeR
|
|
|
59
59
|
isar/services/service_connections/request_handler.py,sha256=0LxC0lu_HXeEf_xmJWjfEsh14oAUI97cpG1IWtBlcs4,4278
|
|
60
60
|
isar/services/service_connections/mqtt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
61
61
|
isar/services/service_connections/mqtt/mqtt_client.py,sha256=Aib0lqaddxW9aVXXYD7wGL9jIpr2USCCH91SQgFdIG4,3548
|
|
62
|
-
isar/services/service_connections/mqtt/robot_heartbeat_publisher.py,sha256=
|
|
63
|
-
isar/services/service_connections/mqtt/robot_info_publisher.py,sha256=
|
|
62
|
+
isar/services/service_connections/mqtt/robot_heartbeat_publisher.py,sha256=_bUOG7CfqBlCRvG4vh2XGoMXucBxsJarFIeXIKOH1aw,1019
|
|
63
|
+
isar/services/service_connections/mqtt/robot_info_publisher.py,sha256=5G6ahslydhO2Z4Ug3abf5KVHeOiWdWBMxwraRbJZS_I,1456
|
|
64
64
|
isar/services/service_connections/stid/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
65
65
|
isar/services/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
66
66
|
isar/services/utilities/queue_utilities.py,sha256=Pw3hehSwkXJNeDv-bDVDfs58VOwtt3i5hpiJ2ZpphuQ,1225
|
|
67
67
|
isar/services/utilities/scheduling_utilities.py,sha256=LFimEmacML3J9q-FNLfKPhcAr-R3f2rkYkbsoro0Gyo,8434
|
|
68
68
|
isar/services/utilities/threaded_request.py,sha256=py4G-_RjnIdHljmKFAcQ6ddqMmp-ZYV39Ece-dqRqjs,1874
|
|
69
69
|
isar/state_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
70
|
-
isar/state_machine/state_machine.py,sha256=
|
|
70
|
+
isar/state_machine/state_machine.py,sha256=JrVwo3tU5lzWKlUE5oMpoE8aEte3HcVOHbjCGMIdv_c,24357
|
|
71
71
|
isar/state_machine/states_enum.py,sha256=BlrUcBWkM5K6D_UZXRwTaUgGpAagWmVZH6HhDBGzVU4,278
|
|
72
72
|
isar/state_machine/states/__init__.py,sha256=kErbKPDTwNfCLijvdyN6_AuOqDwR23nu9F0Qovsnir4,218
|
|
73
73
|
isar/state_machine/states/idle.py,sha256=_nrM17s4artaHezanl28_WcNyJod1_hkCyzAqZlPQiE,3034
|
|
74
74
|
isar/state_machine/states/initialize.py,sha256=KUuyXVwzWK5bJNspA1JnYO_Xwu8fPPK6bnHK4mtwf5A,2359
|
|
75
75
|
isar/state_machine/states/initiate.py,sha256=WqBROOGAh0DVB0f39RFkpqzkr0qrHMyrBGkh2svBbKw,5652
|
|
76
|
-
isar/state_machine/states/monitor.py,sha256=
|
|
76
|
+
isar/state_machine/states/monitor.py,sha256=Jc1ZZ4mOJQU-VI5-UQJLbKNQGqHMBruvurHeJOlGnD0,10277
|
|
77
77
|
isar/state_machine/states/off.py,sha256=jjqN_oJMpBtWuY7hP-c9f0w3p2CYCfe-NpmYHHPnmyI,544
|
|
78
78
|
isar/state_machine/states/offline.py,sha256=wEMMIwM4JWfmDjI7pe9yKce_Mfz9aXqs6WEkxn8cx5I,2125
|
|
79
79
|
isar/state_machine/states/paused.py,sha256=qFSauRwalyws8K5bDZ5wkcRDVYyevTDVRtbXkiF9rZc,1174
|
|
@@ -83,34 +83,34 @@ isar/storage/blob_storage.py,sha256=oKdml1VVN8iTr-d_5H4Lz5E7zrhJRknCzOxHD-tO7m8,
|
|
|
83
83
|
isar/storage/local_storage.py,sha256=Bnmoi5gyN8r-oRh0aHrOdGqaH3JqRScFKMRXYojW5kY,1855
|
|
84
84
|
isar/storage/slimm_storage.py,sha256=Hp7ZIgZgIR4KAFjzxDKfgMZjPZwP2kmdc1gG8zVcsMk,8966
|
|
85
85
|
isar/storage/storage_interface.py,sha256=DYDry4I7aZpDHJhsBF6s8zrgokFAc7fdKJKfA8AvL7o,828
|
|
86
|
-
isar/storage/uploader.py,sha256=
|
|
87
|
-
isar/storage/utilities.py,sha256=
|
|
86
|
+
isar/storage/uploader.py,sha256=_f-uIiL5ye6GLc7nYQxHrqDXrIquoL_t5tt9SSaYoDA,6440
|
|
87
|
+
isar/storage/utilities.py,sha256=AGqOzhnyPXSStpJjBstqQ4QgUoHJioQB2DJ1NqeWn_w,3136
|
|
88
88
|
robot_interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
89
89
|
robot_interface/robot_interface.py,sha256=pY1Wuka0fTP-kCmkEndAcFytkS73cEE2zIHv-v5Fm1E,9466
|
|
90
90
|
robot_interface/test_robot_interface.py,sha256=FV1urn7SbsMyWBIcTKjsBwAG4IsXeZ6pLHE0mA9EGGs,692
|
|
91
91
|
robot_interface/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
92
92
|
robot_interface/models/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
93
|
-
robot_interface/models/exceptions/robot_exceptions.py,sha256=
|
|
93
|
+
robot_interface/models/exceptions/robot_exceptions.py,sha256=DNlecQm2m4ld5mLLFQl1EkQ4b9XgX9H7g-b5c1EuUf4,9549
|
|
94
94
|
robot_interface/models/initialize/__init__.py,sha256=rz5neEDr59GDbzzI_FF0DId-C-I-50l113P-h-C_QBY,48
|
|
95
95
|
robot_interface/models/initialize/initialize_params.py,sha256=2eG5Aq5bDKU6tVkaUMAoc46GERBgyaKkqv6yLupdRLc,164
|
|
96
96
|
robot_interface/models/inspection/__init__.py,sha256=14wfuj4XZazrigKD7fL98khFKz-eckIpEgPcYRj40Kg,227
|
|
97
97
|
robot_interface/models/inspection/inspection.py,sha256=TVqUl5o3d3fp8IravOMwJIuRoEU8y4BltFrF1IkwvTA,2176
|
|
98
98
|
robot_interface/models/mission/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
99
|
-
robot_interface/models/mission/mission.py,sha256=
|
|
99
|
+
robot_interface/models/mission/mission.py,sha256=r_tYqhZeKASdJ_YIN0ZhCl6YdokuuDySI0o9IT2Qe7M,905
|
|
100
100
|
robot_interface/models/mission/status.py,sha256=R5jLmmn6M7oNX907QvbrhoAqAo4C1zB653Ed1PcxAtg,922
|
|
101
101
|
robot_interface/models/mission/step.py,sha256=DEzU-LD-i3RTAaXBy5KwJZ6OnN5S0F3wwDaqX2DZ72M,5587
|
|
102
|
-
robot_interface/models/mission/task.py,sha256=
|
|
102
|
+
robot_interface/models/mission/task.py,sha256=Nup8e_M_KYhtEtGNeV4FzVzaRH4YpDWiKfMMkLJu0ms,4788
|
|
103
103
|
robot_interface/models/robots/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
104
104
|
robot_interface/models/robots/robot_model.py,sha256=pZQsqhn9hh6XE3EjMZhWMzYqg5oJ4CJ4CXeOASKvEf8,452
|
|
105
105
|
robot_interface/telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
106
|
-
robot_interface/telemetry/mqtt_client.py,sha256=
|
|
107
|
-
robot_interface/telemetry/payloads.py,sha256=
|
|
106
|
+
robot_interface/telemetry/mqtt_client.py,sha256=um7j7XDSAlY6-wWLpBl6ZYjmQ-G0lY7f2_NaZ3ciZ7U,2757
|
|
107
|
+
robot_interface/telemetry/payloads.py,sha256=JM5E_IHkZpim_zdwc-w52D7dYFBeP4iO1-xupOkHcFQ,1562
|
|
108
108
|
robot_interface/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
109
109
|
robot_interface/utilities/json_service.py,sha256=nU2Q_3P9Fq9hs6F_wtUjWtHfl_g1Siy-yDhXXSKwHwg,1018
|
|
110
110
|
robot_interface/utilities/uuid_string_factory.py,sha256=_NQIbBQ56w0qqO0MUDP6aPpHbxW7ATRhK8HnQiBSLkc,76
|
|
111
|
-
isar-1.
|
|
112
|
-
isar-1.
|
|
113
|
-
isar-1.
|
|
114
|
-
isar-1.
|
|
115
|
-
isar-1.
|
|
116
|
-
isar-1.
|
|
111
|
+
isar-1.22.1.dist-info/LICENSE,sha256=3fc2-ebLwHWwzfQbulGNRdcNob3SBQeCfEVUDYxsuqw,14058
|
|
112
|
+
isar-1.22.1.dist-info/METADATA,sha256=XlWkAOiQDpPAUiShyCpojSATloy3CHCj1xT78hP6dAk,30674
|
|
113
|
+
isar-1.22.1.dist-info/WHEEL,sha256=5Mi1sN9lKoFv_gxcPtisEVrJZihrm_beibeg5R6xb4I,91
|
|
114
|
+
isar-1.22.1.dist-info/entry_points.txt,sha256=TFam7uNNw7J0iiDYzsH2gfG0u1eV1wh3JTw_HkhgKLk,49
|
|
115
|
+
isar-1.22.1.dist-info/top_level.txt,sha256=UwIML2RtuQKCyJJkatcSnyp6-ldDjboB9k9JgKipO-U,21
|
|
116
|
+
isar-1.22.1.dist-info/RECORD,,
|
|
@@ -21,6 +21,9 @@ class ErrorReason(str, Enum):
|
|
|
21
21
|
RobotUnknownErrorException: str = "robot_unknown_error_exception"
|
|
22
22
|
RobotDisconnectedException: str = "robot_disconnected_exception"
|
|
23
23
|
RobotMissionNotSupportedException: str = "robot_mission_not_supported_exception"
|
|
24
|
+
RobotMissionMissingStartPoseException: str = (
|
|
25
|
+
"robot_mission_missing_start_pose_exception"
|
|
26
|
+
)
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
@dataclass
|
|
@@ -244,3 +247,14 @@ class RobotMissionNotSupportedException(RobotException):
|
|
|
244
247
|
)
|
|
245
248
|
|
|
246
249
|
pass
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# An exception which should be thrown by the robot package if the mission is missing start pose and it needed it
|
|
253
|
+
class RobotMissionMissingStartPoseException(RobotException):
|
|
254
|
+
def __init__(self, error_description: str) -> None:
|
|
255
|
+
super().__init__(
|
|
256
|
+
error_reason=ErrorReason.RobotMissionMissingStartPoseException,
|
|
257
|
+
error_description=error_description,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
pass
|
|
@@ -15,6 +15,8 @@ class Mission:
|
|
|
15
15
|
id: str = field(default_factory=uuid4_string, init=True)
|
|
16
16
|
name: str = ""
|
|
17
17
|
start_pose: Optional[Pose] = None
|
|
18
|
+
dock: Optional[bool] = None
|
|
19
|
+
undock: Optional[bool] = None
|
|
18
20
|
status: MissionStatus = MissionStatus.NotStarted
|
|
19
21
|
error_message: Optional[ErrorMessage] = field(default=None, init=False)
|
|
20
22
|
|
|
@@ -22,11 +22,14 @@ class Task:
|
|
|
22
22
|
id: str = field(default_factory=uuid4_string, init=True)
|
|
23
23
|
_iterator: Iterator = None
|
|
24
24
|
|
|
25
|
-
def next_step(self) -> Step:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
step
|
|
29
|
-
|
|
25
|
+
def next_step(self) -> Optional[Step]:
|
|
26
|
+
try:
|
|
27
|
+
step: Step = next(self._iterator)
|
|
28
|
+
while step.status != StepStatus.NotStarted:
|
|
29
|
+
step = next(self._iterator)
|
|
30
|
+
return step
|
|
31
|
+
except StopIteration:
|
|
32
|
+
return None
|
|
30
33
|
|
|
31
34
|
def is_finished(self) -> bool:
|
|
32
35
|
for step in self.steps:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import time
|
|
3
3
|
from abc import ABCMeta, abstractmethod
|
|
4
|
-
from datetime import
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
5
|
from queue import Queue
|
|
6
6
|
from typing import Callable, Tuple
|
|
7
7
|
|
|
@@ -73,7 +73,7 @@ class MqttTelemetryPublisher(MqttClientInterface):
|
|
|
73
73
|
topic = self.topic
|
|
74
74
|
except RobotTelemetryException:
|
|
75
75
|
payload = json.dumps(
|
|
76
|
-
CloudHealthPayload(isar_id, robot_name, datetime.now(
|
|
76
|
+
CloudHealthPayload(isar_id, robot_name, datetime.now(timezone.utc)),
|
|
77
77
|
cls=EnhancedJSONEncoder,
|
|
78
78
|
)
|
|
79
79
|
topic = self.cloud_healt_topic
|
|
@@ -42,6 +42,12 @@ class TelemetryPressurePayload(TelemetryPayload):
|
|
|
42
42
|
pressure_level: float
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
@dataclass
|
|
46
|
+
class DocumentInfo:
|
|
47
|
+
name: str
|
|
48
|
+
url: str
|
|
49
|
+
|
|
50
|
+
|
|
45
51
|
@dataclass
|
|
46
52
|
class VideoStream:
|
|
47
53
|
name: str
|
|
@@ -69,6 +75,7 @@ class RobotInfoPayload:
|
|
|
69
75
|
robot_model: str
|
|
70
76
|
robot_serial_number: str
|
|
71
77
|
robot_asset: str
|
|
78
|
+
documentation: List[DocumentInfo]
|
|
72
79
|
video_streams: List[VideoStream]
|
|
73
80
|
host: str
|
|
74
81
|
port: int
|
|
File without changes
|
|
File without changes
|
|
File without changes
|