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.
Files changed (33) hide show
  1. bosdyn/client/__init__.py +1 -0
  2. bosdyn/client/audio_visual.py +339 -0
  3. bosdyn/client/audio_visual_helpers.py +112 -0
  4. bosdyn/client/command_line.py +15 -7
  5. bosdyn/client/data_acquisition_helpers.py +1 -1
  6. bosdyn/client/data_acquisition_store.py +2 -2
  7. bosdyn/client/directory_registration.py +34 -5
  8. bosdyn/client/error_callback_result.py +29 -0
  9. bosdyn/client/gps/NMEAParser.py +16 -6
  10. bosdyn/client/gps/gps_listener.py +31 -1
  11. bosdyn/client/gps/ntrip_client.py +240 -0
  12. bosdyn/client/graph_nav.py +16 -1
  13. bosdyn/client/gripper_camera_param.py +40 -0
  14. bosdyn/client/image.py +16 -0
  15. bosdyn/client/image_service_helpers.py +88 -68
  16. bosdyn/client/keepalive.py +37 -8
  17. bosdyn/client/log_status.py +6 -0
  18. bosdyn/client/math_helpers.py +18 -0
  19. bosdyn/client/payload_registration.py +40 -6
  20. bosdyn/client/payload_software_update.py +185 -0
  21. bosdyn/client/payload_software_update_initiation.py +79 -0
  22. bosdyn/client/point_cloud.py +9 -0
  23. bosdyn/client/robot.py +9 -4
  24. bosdyn/client/sdk.py +4 -2
  25. bosdyn/client/service_customization_helpers.py +19 -6
  26. bosdyn/client/spot_cam/__init__.py +2 -0
  27. bosdyn/client/spot_cam/ptz.py +20 -24
  28. bosdyn/client/token_manager.py +56 -27
  29. bosdyn/client/util.py +1 -1
  30. {bosdyn_client-4.1.1.dist-info → bosdyn_client-5.0.1.dist-info}/METADATA +4 -4
  31. {bosdyn_client-4.1.1.dist-info → bosdyn_client-5.0.1.dist-info}/RECORD +33 -27
  32. {bosdyn_client-4.1.1.dist-info → bosdyn_client-5.0.1.dist-info}/WHEEL +0 -0
  33. {bosdyn_client-4.1.1.dist-info → bosdyn_client-5.0.1.dist-info}/top_level.txt +0 -0
bosdyn/client/__init__.py CHANGED
@@ -22,3 +22,4 @@ from .exceptions import (ClientCancelledOperationError, CustomParamError, Error,
22
22
  from .robot import Robot
23
23
  from .sdk import BOSDYN_RESOURCE_ROOT, Sdk, create_standard_sdk
24
24
 
25
+
@@ -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.')
@@ -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__(subparsers, command_dict, [
709
- GetLogCommand,
710
- GetActiveLogStatusesCommand,
711
- ExperimentLogCommand,
712
- StartRetroLogCommand,
713
- TerminateLogCommand,
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 skiping DAQ synchronization
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 .exceptions import ResponseError, RetryableUnavailableError, TimedOutError
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 True:
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
- exec_sec = time.time() - exec_start
461
- if self._end_reregister_signal.wait(self._reregister_period - exec_sec):
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