bosdyn-client 4.1.1__py3-none-any.whl → 5.0.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.
- bosdyn/client/__init__.py +1 -0
- bosdyn/client/audio_visual.py +339 -0
- bosdyn/client/audio_visual_helpers.py +112 -0
- bosdyn/client/command_line.py +15 -7
- bosdyn/client/data_acquisition_helpers.py +1 -1
- bosdyn/client/data_acquisition_store.py +2 -2
- bosdyn/client/directory_registration.py +34 -5
- bosdyn/client/error_callback_result.py +29 -0
- bosdyn/client/gps/NMEAParser.py +16 -6
- bosdyn/client/gps/gps_listener.py +31 -1
- bosdyn/client/gps/ntrip_client.py +240 -0
- bosdyn/client/graph_nav.py +16 -1
- bosdyn/client/gripper_camera_param.py +40 -0
- bosdyn/client/image.py +16 -0
- bosdyn/client/image_service_helpers.py +88 -68
- bosdyn/client/keepalive.py +37 -8
- bosdyn/client/log_status.py +6 -0
- bosdyn/client/math_helpers.py +18 -0
- bosdyn/client/payload_registration.py +40 -6
- bosdyn/client/payload_software_update.py +185 -0
- bosdyn/client/payload_software_update_initiation.py +79 -0
- bosdyn/client/point_cloud.py +9 -0
- bosdyn/client/robot.py +9 -4
- bosdyn/client/sdk.py +4 -2
- bosdyn/client/service_customization_helpers.py +19 -6
- bosdyn/client/spot_cam/__init__.py +2 -0
- bosdyn/client/spot_cam/ptz.py +20 -24
- bosdyn/client/token_manager.py +56 -27
- bosdyn/client/util.py +1 -1
- {bosdyn_client-4.1.1.dist-info → bosdyn_client-5.0.1.dist-info}/METADATA +4 -4
- {bosdyn_client-4.1.1.dist-info → bosdyn_client-5.0.1.dist-info}/RECORD +33 -27
- {bosdyn_client-4.1.1.dist-info → bosdyn_client-5.0.1.dist-info}/WHEEL +0 -0
- {bosdyn_client-4.1.1.dist-info → bosdyn_client-5.0.1.dist-info}/top_level.txt +0 -0
bosdyn/client/__init__.py
CHANGED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# Copyright (c) 2023 Boston Dynamics, Inc. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Downloading, reproducing, distributing or otherwise using the SDK Software
|
|
4
|
+
# is subject to the terms and conditions of the Boston Dynamics Software
|
|
5
|
+
# Development Kit License (20191101-BDSDK-SL).
|
|
6
|
+
|
|
7
|
+
import collections
|
|
8
|
+
import math
|
|
9
|
+
|
|
10
|
+
from bosdyn.api import audio_visual_pb2, audio_visual_service_pb2_grpc
|
|
11
|
+
from bosdyn.client.common import (BaseClient, common_header_errors, error_factory, error_pair,
|
|
12
|
+
handle_common_header_errors, handle_unset_status_error)
|
|
13
|
+
|
|
14
|
+
from .exceptions import Error as BaseError
|
|
15
|
+
from .exceptions import ResponseError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AudioVisualResponseError(ResponseError):
|
|
19
|
+
"""General class of errors for AudioVisual service."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Error(BaseError):
|
|
23
|
+
"""Base class for non-response errors in this module."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NoTimeSyncError(BaseError):
|
|
27
|
+
"""Client has not done timesync with robot."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DoesNotExistError(AudioVisualResponseError):
|
|
31
|
+
"""The specified behavior does not exist."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PermanentBehaviorError(AudioVisualResponseError):
|
|
35
|
+
"""Permanent behaviors cannot be modified or deleted."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BehaviorExpiredError(AudioVisualResponseError):
|
|
39
|
+
"""The specified end_time has already expired."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class InvalidBehaviorError(AudioVisualResponseError):
|
|
43
|
+
"""The request contained a behavior with invalid fields."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InvalidClientError(AudioVisualResponseError):
|
|
47
|
+
"""The behavior cannot be stopped because a different client is running it."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AudioVisualClient(BaseClient):
|
|
51
|
+
"""Client for calling the Audio Visual Service."""
|
|
52
|
+
default_service_name = 'audio-visual'
|
|
53
|
+
service_type = 'bosdyn.api.AudioVisualService'
|
|
54
|
+
|
|
55
|
+
def __init__(self):
|
|
56
|
+
super(AudioVisualClient,
|
|
57
|
+
self).__init__(audio_visual_service_pb2_grpc.AudioVisualServiceStub)
|
|
58
|
+
self.timesync_endpoint = None
|
|
59
|
+
|
|
60
|
+
def update_from(self, other):
|
|
61
|
+
"""Update instance from another object.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
other: The object where to copy from.
|
|
65
|
+
"""
|
|
66
|
+
super(AudioVisualClient, self).update_from(other)
|
|
67
|
+
|
|
68
|
+
# Grab a timesync endpoint if it is available.
|
|
69
|
+
try:
|
|
70
|
+
self.timesync_endpoint = other.time_sync.endpoint
|
|
71
|
+
except AttributeError:
|
|
72
|
+
pass # other doesn't have a time_sync accessor
|
|
73
|
+
|
|
74
|
+
def run_behavior(self, name, end_time_secs, restart=False, timesync_endpoint=None, **kwargs):
|
|
75
|
+
"""Run a behavior on the robot.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
name: The name of the behavior to run.
|
|
79
|
+
end_time_secs: The time that this behavior should stop.
|
|
80
|
+
restart: If this behavior is already running, should we restart it from the beginning.
|
|
81
|
+
timesync_endpoint: Timesync endpoint.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
RpcError: Problem communicating with the robot.
|
|
85
|
+
DoesNotExistError: The behavior name specified has not been added to the system.
|
|
86
|
+
BehaviorExpiredError: The specified end_time has already expired.
|
|
87
|
+
NoTimeSyncError: Time sync has not been established with the robot yet.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
end_time = self._timestamp_to_robot_time(end_time_secs, timesync_endpoint)
|
|
91
|
+
req = audio_visual_pb2.RunBehaviorRequest(name=name, end_time=end_time, restart=restart)
|
|
92
|
+
return self.call(self._stub.RunBehavior, req, error_from_response=_run_behavior_error,
|
|
93
|
+
copy_request=False, **kwargs)
|
|
94
|
+
|
|
95
|
+
def run_behavior_async(self, name, end_time_secs, restart=False, timesync_endpoint=None,
|
|
96
|
+
**kwargs):
|
|
97
|
+
"""Async version of run_behavior().
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
name: The name of the behavior to run.
|
|
101
|
+
end_time_secs: The time that this behavior should stop.
|
|
102
|
+
restart: If this behavior is already running, should we restart it from the beginning.
|
|
103
|
+
timesync_endpoint: Timesync endpoint.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
RpcError: Problem communicating with the robot.
|
|
107
|
+
DoesNotExistError: The behavior name specified has not been added to the system.
|
|
108
|
+
BehaviorExpiredError: The specified end_time has already expired.
|
|
109
|
+
NoTimeSyncError: Time sync has not been established with the robot yet.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
end_time = self._timestamp_to_robot_time(end_time_secs, timesync_endpoint)
|
|
113
|
+
req = audio_visual_pb2.RunBehaviorRequest(name=name, end_time=end_time, restart=restart)
|
|
114
|
+
return self.call_async(self._stub.RunBehavior, req, error_from_response=_run_behavior_error,
|
|
115
|
+
copy_request=False, **kwargs)
|
|
116
|
+
|
|
117
|
+
def stop_behavior(self, name, **kwargs):
|
|
118
|
+
"""Stop a behavior that is currently running.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
name: The name of the behavior to stop.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
RpcError: Problem communicating with the robot.
|
|
125
|
+
InvalidClientError: A different client is running this behavior."""
|
|
126
|
+
|
|
127
|
+
req = audio_visual_pb2.StopBehaviorRequest(behavior_name=name)
|
|
128
|
+
|
|
129
|
+
return self.call(self._stub.StopBehavior, req, error_from_response=_stop_behavior_error,
|
|
130
|
+
copy_request=False, **kwargs)
|
|
131
|
+
|
|
132
|
+
def stop_behavior_async(self, name, **kwargs):
|
|
133
|
+
"""Async version of stop_behavior().
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
name: The name of the behavior to stop.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
RpcError: Problem communicating with the robot.
|
|
140
|
+
InvalidClientError: A different client is running this behavior."""
|
|
141
|
+
|
|
142
|
+
req = audio_visual_pb2.StopBehaviorRequest(behavior_name=name)
|
|
143
|
+
|
|
144
|
+
return self.call_async(self._stub.StopBehavior, req,
|
|
145
|
+
error_from_response=_stop_behavior_error, copy_request=False,
|
|
146
|
+
**kwargs)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def list_behaviors(self, **kwargs):
|
|
150
|
+
"""List all currently added AudioVisualBehaviors.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
A list of all LiveAudioVisualBehavior protos.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
RpcError: Problem communicating with the robot.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
req = audio_visual_pb2.ListBehaviorsRequest()
|
|
160
|
+
return self.call(self._stub.ListBehaviors, req, value_from_response=_get_behavior_list,
|
|
161
|
+
error_from_response=common_header_errors, copy_request=False, **kwargs)
|
|
162
|
+
|
|
163
|
+
def list_behaviors_async(self, **kwargs):
|
|
164
|
+
"""Async version of list_behaviors().
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
A list of all LiveAudioVisualBehavior protos.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
RpcError: Problem communicating with the robot.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
req = audio_visual_pb2.ListBehaviorsRequest()
|
|
174
|
+
return self.call_async(self._stub.ListBehaviors, req,
|
|
175
|
+
value_from_response=_get_behavior_list,
|
|
176
|
+
error_from_response=common_header_errors, copy_request=False,
|
|
177
|
+
**kwargs)
|
|
178
|
+
|
|
179
|
+
def get_system_params(self, **kwargs):
|
|
180
|
+
"""Get the current system params.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
An AudioVisualSystemParams proto containing the current system param values.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
RpcError: Problem communicating with the robot.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
req = audio_visual_pb2.GetSystemParamsRequest()
|
|
190
|
+
return self.call(self._stub.GetSystemParams, req, error_from_response=common_header_errors,
|
|
191
|
+
copy_request=False, **kwargs)
|
|
192
|
+
|
|
193
|
+
def get_system_params_async(self, **kwargs):
|
|
194
|
+
"""Async version of get_system_params().
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
An AudioVisualSystemParams proto containing the current system param values.
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
RpcError: Problem communicating with the robot.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
req = audio_visual_pb2.GetSystemParamsRequest()
|
|
204
|
+
return self.call_async(self._stub.GetSystemParams, req,
|
|
205
|
+
error_from_response=common_header_errors, copy_request=False,
|
|
206
|
+
**kwargs)
|
|
207
|
+
|
|
208
|
+
def set_system_params(self, enabled=None, max_brightness=None, buzzer_max_volume=None,
|
|
209
|
+
speaker_max_volume=None, normal_color_association=None,
|
|
210
|
+
warning_color_association=None, danger_color_association=None, **kwargs):
|
|
211
|
+
"""Set the system params.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
enabled: [optional] System is enabled or disabled (boolean).
|
|
215
|
+
max_brightness: [optional] New max_brightness value [0, 1].
|
|
216
|
+
buzzer_max_volume: [optional] New buzzer_max_volume value [0, 1].
|
|
217
|
+
speaker_max_volume: [optional] New speaker_max_volume value [0, 1].
|
|
218
|
+
normal_color_association: [optional] The color to associate with the normal color preset.
|
|
219
|
+
warning_color_association: [optional] The color to associate with the warning color preset.
|
|
220
|
+
danger_color_association: [optional] The color to associate with the danger color preset.
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
RpcError: Problem communicating with the robot.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
req = audio_visual_pb2.SetSystemParamsRequest()
|
|
228
|
+
if (enabled is not None):
|
|
229
|
+
req.enabled.value = enabled
|
|
230
|
+
if (max_brightness is not None):
|
|
231
|
+
req.max_brightness.value = max_brightness
|
|
232
|
+
if (buzzer_max_volume is not None):
|
|
233
|
+
req.buzzer_max_volume.value = buzzer_max_volume
|
|
234
|
+
if (speaker_max_volume is not None):
|
|
235
|
+
req.speaker_max_volume.value = speaker_max_volume
|
|
236
|
+
if (normal_color_association is not None):
|
|
237
|
+
req.normal_color_association.CopyFrom(normal_color_association)
|
|
238
|
+
if (warning_color_association is not None):
|
|
239
|
+
req.warning_color_association.CopyFrom(warning_color_association)
|
|
240
|
+
if (danger_color_association is not None):
|
|
241
|
+
req.danger_color_association.CopyFrom(danger_color_association)
|
|
242
|
+
return self.call(self._stub.SetSystemParams, req, error_from_response=common_header_errors,
|
|
243
|
+
copy_request=False, **kwargs)
|
|
244
|
+
|
|
245
|
+
def set_system_params_async(self, enabled=None, max_brightness=None, buzzer_max_volume=None,
|
|
246
|
+
speaker_max_volume=None, normal_color_association=None,
|
|
247
|
+
warning_color_association=None, danger_color_association=None,
|
|
248
|
+
**kwargs):
|
|
249
|
+
"""Async version of set_system_params().
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
enabled: [optional] System is enabled or disabled (boolean).
|
|
253
|
+
max_brightness: [optional] New max_brightness value [0, 1].
|
|
254
|
+
buzzer_max_volume: [optional] New buzzer_max_volume value [0, 1].
|
|
255
|
+
speaker_max_volume: [optional] New speaker_max_volume value [0, 1].
|
|
256
|
+
normal_color_association: [optional] The color to associate with the normal color preset.
|
|
257
|
+
warning_color_association: [optional] The color to associate with the warning color preset.
|
|
258
|
+
danger_color_association: [optional] The color to associate with the danger color preset.
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
RpcError: Problem communicating with the robot.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
req = audio_visual_pb2.SetSystemParamsRequest()
|
|
265
|
+
if (enabled is not None):
|
|
266
|
+
req.enabled.value = enabled
|
|
267
|
+
if (max_brightness is not None):
|
|
268
|
+
req.max_brightness.value = max_brightness
|
|
269
|
+
if (buzzer_max_volume is not None):
|
|
270
|
+
req.buzzer_max_volume.value = buzzer_max_volume
|
|
271
|
+
if (speaker_max_volume is not None):
|
|
272
|
+
req.speaker_max_volume.value = speaker_max_volume
|
|
273
|
+
if (normal_color_association is not None):
|
|
274
|
+
req.normal_color_association.CopyFrom(normal_color_association)
|
|
275
|
+
if (warning_color_association is not None):
|
|
276
|
+
req.warning_color_association.CopyFrom(warning_color_association)
|
|
277
|
+
if (danger_color_association is not None):
|
|
278
|
+
req.danger_color_association.CopyFrom(danger_color_association)
|
|
279
|
+
return self.call_async(self._stub.SetSystemParams, req,
|
|
280
|
+
error_from_response=common_header_errors, copy_request=False,
|
|
281
|
+
**kwargs)
|
|
282
|
+
|
|
283
|
+
def _timestamp_to_robot_time(self, timestamp, timesync_endpoint=None):
|
|
284
|
+
# Create a time converter to convert timestamp to robot time
|
|
285
|
+
time_converter = None
|
|
286
|
+
if (timesync_endpoint):
|
|
287
|
+
time_converter = timesync_endpoint.get_robot_time_converter()
|
|
288
|
+
elif (self.timesync_endpoint):
|
|
289
|
+
time_converter = self.timesync_endpoint.get_robot_time_converter()
|
|
290
|
+
else:
|
|
291
|
+
raise NoTimeSyncError("No timesync endpoint was passed to audio visual client.")
|
|
292
|
+
|
|
293
|
+
return time_converter.robot_timestamp_from_local_secs(timestamp)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _get_behavior_list(response):
|
|
297
|
+
return response.behaviors
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _get_live_behavior(response):
|
|
301
|
+
return response.live_behavior
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
_AUDIO_VISUAL_RUN_BEHAVIOR_STATUS_TO_ERROR = collections.defaultdict(
|
|
306
|
+
lambda: (AudioVisualResponseError, None))
|
|
307
|
+
_AUDIO_VISUAL_RUN_BEHAVIOR_STATUS_TO_ERROR.update({
|
|
308
|
+
audio_visual_pb2.RunBehaviorResponse.STATUS_SUCCESS: (None, None),
|
|
309
|
+
audio_visual_pb2.RunBehaviorResponse.STATUS_DOES_NOT_EXIST: error_pair(DoesNotExistError),
|
|
310
|
+
audio_visual_pb2.RunBehaviorResponse.STATUS_EXPIRED: error_pair(BehaviorExpiredError),
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
_AUDIO_VISUAL_STOP_BEHAVIOR_STATUS_TO_ERROR = collections.defaultdict(
|
|
314
|
+
lambda: (AudioVisualResponseError, None))
|
|
315
|
+
_AUDIO_VISUAL_STOP_BEHAVIOR_STATUS_TO_ERROR.update({
|
|
316
|
+
audio_visual_pb2.StopBehaviorResponse.STATUS_SUCCESS: (None, None),
|
|
317
|
+
audio_visual_pb2.StopBehaviorResponse.STATUS_INVALID_CLIENT: error_pair(InvalidClientError)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@handle_common_header_errors
|
|
323
|
+
@handle_unset_status_error(unset='STATUS_UNKNOWN')
|
|
324
|
+
def _run_behavior_error(response):
|
|
325
|
+
"""RunBehaviorResponse response to exception."""
|
|
326
|
+
return error_factory(response, response.status,
|
|
327
|
+
status_to_string=audio_visual_pb2.RunBehaviorResponse.Status.Name,
|
|
328
|
+
status_to_error=_AUDIO_VISUAL_RUN_BEHAVIOR_STATUS_TO_ERROR)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@handle_common_header_errors
|
|
332
|
+
@handle_unset_status_error(unset='STATUS_UNKNOWN')
|
|
333
|
+
def _stop_behavior_error(response):
|
|
334
|
+
"""StopBehaviorResponse response to exception."""
|
|
335
|
+
return error_factory(response, response.status,
|
|
336
|
+
status_to_string=audio_visual_pb2.StopBehaviorResponse.Status.Name,
|
|
337
|
+
status_to_error=_AUDIO_VISUAL_STOP_BEHAVIOR_STATUS_TO_ERROR)
|
|
338
|
+
|
|
339
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Copyright (c) 2023 Boston Dynamics, Inc. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Downloading, reproducing, distributing or otherwise using the SDK Software
|
|
4
|
+
# is subject to the terms and conditions of the Boston Dynamics Software
|
|
5
|
+
# Development Kit License (20191101-BDSDK-SL).
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from concurrent.futures import Future
|
|
11
|
+
|
|
12
|
+
import bosdyn.client
|
|
13
|
+
from bosdyn.api import audio_visual_pb2
|
|
14
|
+
from bosdyn.client.audio_visual import (AudioVisualClient, BehaviorExpiredError, DoesNotExistError,
|
|
15
|
+
InvalidClientError)
|
|
16
|
+
|
|
17
|
+
_LOGGER = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AudioVisualHelper:
|
|
21
|
+
"""Context manager that runs an AV behavior for the duration of the context.
|
|
22
|
+
|
|
23
|
+
Use as follows:
|
|
24
|
+
|
|
25
|
+
.. code-block:: python
|
|
26
|
+
|
|
27
|
+
with AudioVisualHelper(robot, behavior_name, refresh_rate):
|
|
28
|
+
# Lights and sounds will play here
|
|
29
|
+
# Lights and sounds will stop here.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
robot: Robot object for creating clients
|
|
33
|
+
behavior_name: Name of the desired behavior to run
|
|
34
|
+
refresh_rate: What rate to refresh the behavior (seconds)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, robot, behavior_name, refresh_rate, logger=None):
|
|
38
|
+
self.robot = robot
|
|
39
|
+
self.logger = logger
|
|
40
|
+
self.behavior_name = behavior_name
|
|
41
|
+
self.refresh_rate = refresh_rate
|
|
42
|
+
self.av_client = None
|
|
43
|
+
self._behavior_running_fut = None
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
self.av_client = robot.ensure_client(AudioVisualClient.default_service_name)
|
|
47
|
+
except:
|
|
48
|
+
_LOGGER.warning("Could not initialize AV client, skipping AudioVisualHelper.")
|
|
49
|
+
|
|
50
|
+
self.av_thread = None
|
|
51
|
+
self.stop_event = threading.Event()
|
|
52
|
+
|
|
53
|
+
def __enter__(self):
|
|
54
|
+
self._behavior_running_fut = Future()
|
|
55
|
+
self._behavior_running_fut.set_running_or_notify_cancel()
|
|
56
|
+
|
|
57
|
+
if self.av_client:
|
|
58
|
+
self.av_thread = threading.Thread(target=self._run_behavior_thread, args=())
|
|
59
|
+
self.av_thread.start()
|
|
60
|
+
else:
|
|
61
|
+
self._behavior_running_fut.set_result(False)
|
|
62
|
+
|
|
63
|
+
return self._behavior_running_fut
|
|
64
|
+
|
|
65
|
+
def __exit__(self, exc_type, exc_value, tb):
|
|
66
|
+
if self.av_thread:
|
|
67
|
+
self.stop_event.set()
|
|
68
|
+
self.av_thread.join()
|
|
69
|
+
|
|
70
|
+
def _run_behavior_thread(self):
|
|
71
|
+
# Check if the robot has AV hardware
|
|
72
|
+
if not self.robot.get_cached_hardware_hardware_configuration().has_audio_visual_system:
|
|
73
|
+
self._behavior_running_fut.set_result(False)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
def set_future_result(result):
|
|
77
|
+
if not self._behavior_running_fut.done():
|
|
78
|
+
self._behavior_running_fut.set_result(result)
|
|
79
|
+
|
|
80
|
+
def set_future_exception(exc):
|
|
81
|
+
if not self._behavior_running_fut.done():
|
|
82
|
+
self._behavior_running_fut.set_exception(exc)
|
|
83
|
+
|
|
84
|
+
# Run the AV behavior until the stop_event is triggered
|
|
85
|
+
while not self.stop_event.wait(self.refresh_rate):
|
|
86
|
+
try:
|
|
87
|
+
end_time_secs = time.time() + self.refresh_rate + 0.10 # add 100ms margin
|
|
88
|
+
result = self.av_client.run_behavior(self.behavior_name, end_time_secs)
|
|
89
|
+
set_future_result(
|
|
90
|
+
result.run_result == audio_visual_pb2.RunBehaviorResponse.RESULT_BEHAVIOR_RUN)
|
|
91
|
+
except DoesNotExistError as exc:
|
|
92
|
+
set_future_exception(exc)
|
|
93
|
+
_LOGGER.exception(f'Audio Visual Behavior {self.behavior_name} does not exist.')
|
|
94
|
+
return # Since the behavior doesn't exist, we can stop trying to run it
|
|
95
|
+
except BehaviorExpiredError as exc:
|
|
96
|
+
set_future_exception(exc)
|
|
97
|
+
_LOGGER.warning('Behavior was expired when received by client.')
|
|
98
|
+
except bosdyn.client.PersistentRpcError as exc:
|
|
99
|
+
set_future_exception(exc)
|
|
100
|
+
_LOGGER.exception('Failed to run behavior. Quitting AudioVisualHelper.')
|
|
101
|
+
return # A persistent error means we can't talk to the AV service, we can stop.
|
|
102
|
+
except bosdyn.client.RpcError:
|
|
103
|
+
_LOGGER.exception('Failed to run behavior. Retrying.')
|
|
104
|
+
except bosdyn.client.Error as exc:
|
|
105
|
+
set_future_exception(exc)
|
|
106
|
+
_LOGGER.exception('Unknown exception caught, quitting AudioVisualHelper.')
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
self.av_client.stop_behavior(self.behavior_name)
|
|
111
|
+
except InvalidClientError:
|
|
112
|
+
_LOGGER.warning('Failed to stop behavior, run by a different client.')
|
bosdyn/client/command_line.py
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
# is subject to the terms and conditions of the Boston Dynamics Software
|
|
5
5
|
# Development Kit License (20191101-BDSDK-SL).
|
|
6
6
|
|
|
7
|
+
# Boston Dynamics, Inc. Confidential Information.
|
|
8
|
+
# Copyright 2025. All Rights Reserved.
|
|
7
9
|
"""Command-line utility code for interacting with robot services."""
|
|
8
10
|
|
|
9
11
|
from __future__ import division
|
|
@@ -705,13 +707,16 @@ class LogStatusCommands(Subcommands):
|
|
|
705
707
|
subparsers: List of argument parsers.
|
|
706
708
|
command_dict: Dictionary of command names which take parsed options.
|
|
707
709
|
"""
|
|
708
|
-
super(LogStatusCommands, self).__init__(
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
710
|
+
super(LogStatusCommands, self).__init__(
|
|
711
|
+
subparsers,
|
|
712
|
+
command_dict,
|
|
713
|
+
[
|
|
714
|
+
GetLogCommand,
|
|
715
|
+
GetActiveLogStatusesCommand,
|
|
716
|
+
ExperimentLogCommand,
|
|
717
|
+
StartRetroLogCommand,
|
|
718
|
+
TerminateLogCommand,
|
|
719
|
+
])
|
|
715
720
|
|
|
716
721
|
|
|
717
722
|
class GetLogCommand(Command):
|
|
@@ -947,6 +952,8 @@ class TerminateLogCommand(Command):
|
|
|
947
952
|
return True
|
|
948
953
|
|
|
949
954
|
|
|
955
|
+
|
|
956
|
+
|
|
950
957
|
class RobotIdCommand(Command):
|
|
951
958
|
"""Show robot-id."""
|
|
952
959
|
|
|
@@ -2646,6 +2653,7 @@ def main(args=None):
|
|
|
2646
2653
|
parser = argparse.ArgumentParser(prog='bosdyn.client', description=main.__doc__)
|
|
2647
2654
|
add_common_arguments(parser, credentials_no_warn=True)
|
|
2648
2655
|
|
|
2656
|
+
|
|
2649
2657
|
command_dict = {} # command name to fn which takes parsed options
|
|
2650
2658
|
subparsers = parser.add_subparsers(title='commands', dest='command')
|
|
2651
2659
|
|
|
@@ -167,7 +167,7 @@ def clean_filename(filename):
|
|
|
167
167
|
filename(string): Original filename to clean.
|
|
168
168
|
|
|
169
169
|
Returns:
|
|
170
|
-
Valid filename with removed characters
|
|
170
|
+
Valid filename with removed characters :?<>|*
|
|
171
171
|
"""
|
|
172
172
|
|
|
173
173
|
return "".join(i for i in filename if i not in ":*?<>|")
|
|
@@ -338,8 +338,8 @@ class DataAcquisitionStoreClient(BaseClient):
|
|
|
338
338
|
def query_max_capture_id(self, **kwargs):
|
|
339
339
|
"""Query max capture id from the robot.
|
|
340
340
|
Returns:
|
|
341
|
-
QueryMaxCaptureIdResult, which has a max_capture_id uint64, corresponding to the
|
|
342
|
-
greatest capture id on the robot. Used for
|
|
341
|
+
QueryMaxCaptureIdResult, which has a max_capture_id uint64, corresponding to the
|
|
342
|
+
greatest capture id on the robot. Used for skipping DAQ synchronization
|
|
343
343
|
on connect.
|
|
344
344
|
"""
|
|
345
345
|
request = data_acquisition_store.QueryMaxCaptureIdRequest()
|
|
@@ -19,7 +19,8 @@ from bosdyn.api import (directory_pb2, directory_registration_pb2,
|
|
|
19
19
|
from bosdyn.client.common import (BaseClient, error_factory, error_pair,
|
|
20
20
|
handle_common_header_errors, handle_unset_status_error)
|
|
21
21
|
|
|
22
|
-
from .
|
|
22
|
+
from .error_callback_result import ErrorCallbackResult
|
|
23
|
+
from .exceptions import ResponseError, RetryableUnavailableError, RpcError, TimedOutError
|
|
23
24
|
|
|
24
25
|
_LOGGER = logging.getLogger(__name__)
|
|
25
26
|
|
|
@@ -299,10 +300,12 @@ class DirectoryRegistrationKeepAlive(object):
|
|
|
299
300
|
rpc_timeout_seconds: Number of seconds to wait for a dir_reg_client RPC. Defaults to None,
|
|
300
301
|
for no timeout.
|
|
301
302
|
rpc_interval_seconds: Interval at which to request service registrations.
|
|
303
|
+
initial_retry_seconds: Initial number of seconds to wait before retrying a failed
|
|
304
|
+
registration request. Defaults to 1 second.
|
|
302
305
|
"""
|
|
303
306
|
|
|
304
307
|
def __init__(self, dir_reg_client, logger=None, rpc_timeout_seconds=None,
|
|
305
|
-
rpc_interval_seconds=30):
|
|
308
|
+
rpc_interval_seconds=30, initial_retry_seconds=1):
|
|
306
309
|
self.authority = None
|
|
307
310
|
self.directory_name = None
|
|
308
311
|
self.host = None
|
|
@@ -310,11 +313,15 @@ class DirectoryRegistrationKeepAlive(object):
|
|
|
310
313
|
self.port = None
|
|
311
314
|
self.service_type = None
|
|
312
315
|
self.dir_reg_client = dir_reg_client
|
|
316
|
+
#: Callable[[Exception], ErrorCallbackResult] | None: Optional callback to be called when
|
|
317
|
+
#: an error occurs in the reregistration thread.
|
|
318
|
+
self.reregistration_error_callback = None
|
|
313
319
|
|
|
314
320
|
self._end_reregister_signal = threading.Event()
|
|
315
321
|
self._lock = threading.Lock()
|
|
316
322
|
self._rpc_timeout = rpc_timeout_seconds
|
|
317
323
|
self._reregister_period = rpc_interval_seconds
|
|
324
|
+
self._initial_retry_seconds = initial_retry_seconds
|
|
318
325
|
|
|
319
326
|
# Configure the thread to do re-registration.
|
|
320
327
|
self._thread = threading.Thread(target=self._periodic_reregister)
|
|
@@ -430,9 +437,13 @@ class DirectoryRegistrationKeepAlive(object):
|
|
|
430
437
|
Raises:
|
|
431
438
|
RpcError: Problem communicating with the robot.
|
|
432
439
|
"""
|
|
440
|
+
retry_interval = self._initial_retry_seconds
|
|
441
|
+
wait_time = self._reregister_period
|
|
442
|
+
|
|
433
443
|
self.logger.info('Starting directory registration loop for {}'.format(self.directory_name))
|
|
434
|
-
while
|
|
444
|
+
while not self._end_reregister_signal.wait(wait_time):
|
|
435
445
|
exec_start = time.time()
|
|
446
|
+
action = ErrorCallbackResult.RESUME_NORMAL_OPERATION
|
|
436
447
|
try:
|
|
437
448
|
self.dir_reg_client.register(
|
|
438
449
|
self.directory_name,
|
|
@@ -454,9 +465,27 @@ class DirectoryRegistrationKeepAlive(object):
|
|
|
454
465
|
pass
|
|
455
466
|
except TimedOutError:
|
|
456
467
|
self.logger.warning('Timed out, timeout set to "{}"'.format(self._rpc_timeout))
|
|
468
|
+
except RpcError as exc:
|
|
469
|
+
self.logger.exception('Reregistration failed with RpcError')
|
|
470
|
+
if self.reregistration_error_callback is not None:
|
|
471
|
+
try:
|
|
472
|
+
action = self.reregistration_error_callback(exc)
|
|
473
|
+
except Exception: #pylint: disable=broad-except
|
|
474
|
+
self.logger.exception('Exception thrown in the provided error callback')
|
|
457
475
|
except Exception:
|
|
458
476
|
# Log all other exceptions, but continue looping in hopes that it resolves itself
|
|
459
477
|
self.logger.exception('Caught general exception')
|
|
460
|
-
|
|
461
|
-
|
|
478
|
+
|
|
479
|
+
elapsed = time.time() - exec_start
|
|
480
|
+
if action == ErrorCallbackResult.RETRY_IMMEDIATELY:
|
|
481
|
+
wait_time = 0.0
|
|
482
|
+
elif action == ErrorCallbackResult.ABORT:
|
|
462
483
|
break
|
|
484
|
+
elif action == ErrorCallbackResult.RETRY_WITH_EXPONENTIAL_BACK_OFF:
|
|
485
|
+
wait_time = retry_interval - elapsed
|
|
486
|
+
retry_interval = min(2.0 * retry_interval, self._reregister_period)
|
|
487
|
+
else:
|
|
488
|
+
# action doesn't match one of the enum values or is one of
|
|
489
|
+
# RESUME_NORMAL_OPERATION or DEFAULT_ACTION
|
|
490
|
+
retry_interval = self._initial_retry_seconds
|
|
491
|
+
wait_time = self._reregister_period - elapsed
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Copyright (c) 2023 Boston Dynamics, Inc. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Downloading, reproducing, distributing or otherwise using the SDK Software
|
|
4
|
+
# is subject to the terms and conditions of the Boston Dynamics Software
|
|
5
|
+
# Development Kit License (20191101-BDSDK-SL).
|
|
6
|
+
|
|
7
|
+
import enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ErrorCallbackResult(enum.Enum):
|
|
11
|
+
"""Enum indicating error resolution for errors encountered on SDK background threads.
|
|
12
|
+
|
|
13
|
+
There are a few places in the SDK where errors can occur in background threads and it would
|
|
14
|
+
be useful to provide these errors to client code to resolve. Once the application's provided
|
|
15
|
+
callback performs its action, it returns one of these enum values to indicate what the
|
|
16
|
+
background thread should do next.
|
|
17
|
+
"""
|
|
18
|
+
#: Take the default action as if no error handler had been provided.
|
|
19
|
+
DEFAULT_ACTION = 1
|
|
20
|
+
#: Retry the operation immediately, presumably because the error has been resolved and the
|
|
21
|
+
#: operation can be retried.
|
|
22
|
+
RETRY_IMMEDIATELY = 2
|
|
23
|
+
#: Retry, with the period between successive retries increasing exponentially.
|
|
24
|
+
RETRY_WITH_EXPONENTIAL_BACK_OFF = 3
|
|
25
|
+
#: Continue normal operation, presuming the error has been resolved and no further action
|
|
26
|
+
#: is needed.
|
|
27
|
+
RESUME_NORMAL_OPERATION = 4
|
|
28
|
+
#: Abort the loop in the background thread.
|
|
29
|
+
ABORT = 5
|