bosdyn-client 5.0.0__py3-none-any.whl → 5.0.1.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.
@@ -0,0 +1,517 @@
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
+ def add_or_modify_behavior(self, name, behavior, **kwargs):
149
+ """Add or modify an AudioVisualBehavior.
150
+
151
+ Args:
152
+ name: The name of the behavior to add.
153
+ behavior: The AudioVisualBehavior proto to add.
154
+
155
+ Returns:
156
+ The LiveAudioVisualBehavior proto that was just added or modified.
157
+
158
+ Raises:
159
+ RpcError: Problem communicating with the robot.
160
+ PermanentBehaviorError: The behavior specified is permanent and cannot be modified.
161
+ InvalidBehaviorError: The request contained a behavior with invalid fields.
162
+ """
163
+ # Clamp and normalize colors in the behavior before sending the request.
164
+ led_sequence_group = getattr(behavior, "led_sequence_group", None)
165
+ if led_sequence_group is not None:
166
+ behavior.led_sequence_group.CopyFrom(check_color(led_sequence_group))
167
+
168
+ req = audio_visual_pb2.AddOrModifyBehaviorRequest(name=name, behavior=behavior)
169
+ return self.call(self._stub.AddOrModifyBehavior, req,
170
+ value_from_response=_get_live_behavior,
171
+ error_from_response=_add_or_modify_behavior_error, copy_request=False,
172
+ **kwargs)
173
+
174
+ def add_or_modify_behavior_async(self, name, behavior, **kwargs):
175
+ """Add or modify an AudioVisualBehavior.
176
+
177
+ Args:
178
+ name: The name of the behavior to add.
179
+ behavior: The AudioVisualBehavior proto to add.
180
+
181
+ Returns:
182
+ The LiveAudioVisualBehavior proto that was just added or modified.
183
+
184
+ Raises:
185
+ RpcError: Problem communicating with the robot.
186
+ PermanentBehaviorError: The behavior specified is permanent and cannot be modified.
187
+ InvalidBehaviorError: The request contained a behavior with invalid fields.
188
+ """
189
+ # Clamp and normalize colors in the behavior before sending the request.
190
+ led_sequence_group = getattr(behavior, "led_sequence_group", None)
191
+ if led_sequence_group is not None:
192
+ behavior.led_sequence_group.CopyFrom(check_color(led_sequence_group))
193
+
194
+ req = audio_visual_pb2.AddOrModifyBehaviorRequest(name=name, behavior=behavior)
195
+ return self.call_async(self._stub.AddOrModifyBehavior, req,
196
+ value_from_response=_get_live_behavior,
197
+ error_from_response=_add_or_modify_behavior_error,
198
+ copy_request=False, **kwargs)
199
+
200
+ def delete_behaviors(self, behavior_names, **kwargs):
201
+ """Delete an AudioVisualBehavior.
202
+
203
+ Args:
204
+ behavior_names: A list of behavior names to delete.
205
+
206
+ Returns:
207
+ A list of LiveAudioVisualBehavior protos that were deleted.
208
+
209
+ Raises:
210
+ RpcError: Problem communicating with the robot.
211
+ DoesNotExistError: A specified behavior name has not been added to the system.
212
+ PermanentBehaviorError: A specified behavior is permanent and cannot be deleted.
213
+ """
214
+
215
+ req = audio_visual_pb2.DeleteBehaviorsRequest(behavior_names=behavior_names)
216
+ return self.call(self._stub.DeleteBehaviors, req,
217
+ value_from_response=_get_deleted_behaviors,
218
+ error_from_response=_delete_behaviors_error, copy_request=False, **kwargs)
219
+
220
+ def delete_behaviors_async(self, behavior_names, **kwargs):
221
+ """Async version of delete_behaviors().
222
+
223
+ Args:
224
+ behavior_names: A list of behavior names to delete.
225
+
226
+ Returns:
227
+ A list of LiveAudioVisualBehavior protos that were deleted.
228
+
229
+ Raises:
230
+ RpcError: Problem communicating with the robot.
231
+ DoesNotExistError: A specified behavior name has not been added to the system.
232
+ PermanentBehaviorError: A specified behavior is permanent and cannot be deleted.
233
+ """
234
+
235
+ req = audio_visual_pb2.DeleteBehaviorsRequest(behavior_names=behavior_names)
236
+ return self.call_async(self._stub.DeleteBehaviors, req,
237
+ value_from_response=_get_deleted_behaviors,
238
+ error_from_response=_delete_behaviors_error, copy_request=False,
239
+ **kwargs)
240
+
241
+ def list_behaviors(self, **kwargs):
242
+ """List all currently added AudioVisualBehaviors.
243
+
244
+ Returns:
245
+ A list of all LiveAudioVisualBehavior protos.
246
+
247
+ Raises:
248
+ RpcError: Problem communicating with the robot.
249
+ """
250
+
251
+ req = audio_visual_pb2.ListBehaviorsRequest()
252
+ return self.call(self._stub.ListBehaviors, req, value_from_response=_get_behavior_list,
253
+ error_from_response=common_header_errors, copy_request=False, **kwargs)
254
+
255
+ def list_behaviors_async(self, **kwargs):
256
+ """Async version of list_behaviors().
257
+
258
+ Returns:
259
+ A list of all LiveAudioVisualBehavior protos.
260
+
261
+ Raises:
262
+ RpcError: Problem communicating with the robot.
263
+ """
264
+
265
+ req = audio_visual_pb2.ListBehaviorsRequest()
266
+ return self.call_async(self._stub.ListBehaviors, req,
267
+ value_from_response=_get_behavior_list,
268
+ error_from_response=common_header_errors, copy_request=False,
269
+ **kwargs)
270
+
271
+ def get_system_params(self, **kwargs):
272
+ """Get the current system params.
273
+
274
+ Returns:
275
+ An AudioVisualSystemParams proto containing the current system param values.
276
+
277
+ Raises:
278
+ RpcError: Problem communicating with the robot.
279
+ """
280
+
281
+ req = audio_visual_pb2.GetSystemParamsRequest()
282
+ return self.call(self._stub.GetSystemParams, req, error_from_response=common_header_errors,
283
+ copy_request=False, **kwargs)
284
+
285
+ def get_system_params_async(self, **kwargs):
286
+ """Async version of get_system_params().
287
+
288
+ Returns:
289
+ An AudioVisualSystemParams proto containing the current system param values.
290
+
291
+ Raises:
292
+ RpcError: Problem communicating with the robot.
293
+ """
294
+
295
+ req = audio_visual_pb2.GetSystemParamsRequest()
296
+ return self.call_async(self._stub.GetSystemParams, req,
297
+ error_from_response=common_header_errors, copy_request=False,
298
+ **kwargs)
299
+
300
+ def set_system_params(self, enabled=None, max_brightness=None, buzzer_max_volume=None,
301
+ speaker_max_volume=None, normal_color_association=None,
302
+ warning_color_association=None, danger_color_association=None, **kwargs):
303
+ """Set the system params.
304
+
305
+ Args:
306
+ enabled: [optional] System is enabled or disabled (boolean).
307
+ max_brightness: [optional] New max_brightness value [0, 1].
308
+ buzzer_max_volume: [optional] New buzzer_max_volume value [0, 1].
309
+ speaker_max_volume: [optional] New speaker_max_volume value [0, 1].
310
+ normal_color_association: [optional] The color to associate with the normal color preset.
311
+ warning_color_association: [optional] The color to associate with the warning color preset.
312
+ danger_color_association: [optional] The color to associate with the danger color preset.
313
+
314
+
315
+ Raises:
316
+ RpcError: Problem communicating with the robot.
317
+ """
318
+
319
+ req = audio_visual_pb2.SetSystemParamsRequest()
320
+ if (enabled is not None):
321
+ req.enabled.value = enabled
322
+ if (max_brightness is not None):
323
+ req.max_brightness.value = max_brightness
324
+ if (buzzer_max_volume is not None):
325
+ req.buzzer_max_volume.value = buzzer_max_volume
326
+ if (speaker_max_volume is not None):
327
+ req.speaker_max_volume.value = speaker_max_volume
328
+ if (normal_color_association is not None):
329
+ req.normal_color_association.CopyFrom(normal_color_association)
330
+ if (warning_color_association is not None):
331
+ req.warning_color_association.CopyFrom(warning_color_association)
332
+ if (danger_color_association is not None):
333
+ req.danger_color_association.CopyFrom(danger_color_association)
334
+ return self.call(self._stub.SetSystemParams, req, error_from_response=common_header_errors,
335
+ copy_request=False, **kwargs)
336
+
337
+ def set_system_params_async(self, enabled=None, max_brightness=None, buzzer_max_volume=None,
338
+ speaker_max_volume=None, normal_color_association=None,
339
+ warning_color_association=None, danger_color_association=None,
340
+ **kwargs):
341
+ """Async version of set_system_params().
342
+
343
+ Args:
344
+ enabled: [optional] System is enabled or disabled (boolean).
345
+ max_brightness: [optional] New max_brightness value [0, 1].
346
+ buzzer_max_volume: [optional] New buzzer_max_volume value [0, 1].
347
+ speaker_max_volume: [optional] New speaker_max_volume value [0, 1].
348
+ normal_color_association: [optional] The color to associate with the normal color preset.
349
+ warning_color_association: [optional] The color to associate with the warning color preset.
350
+ danger_color_association: [optional] The color to associate with the danger color preset.
351
+
352
+ Raises:
353
+ RpcError: Problem communicating with the robot.
354
+ """
355
+
356
+ req = audio_visual_pb2.SetSystemParamsRequest()
357
+ if (enabled is not None):
358
+ req.enabled.value = enabled
359
+ if (max_brightness is not None):
360
+ req.max_brightness.value = max_brightness
361
+ if (buzzer_max_volume is not None):
362
+ req.buzzer_max_volume.value = buzzer_max_volume
363
+ if (speaker_max_volume is not None):
364
+ req.speaker_max_volume.value = speaker_max_volume
365
+ if (normal_color_association is not None):
366
+ req.normal_color_association.CopyFrom(normal_color_association)
367
+ if (warning_color_association is not None):
368
+ req.warning_color_association.CopyFrom(warning_color_association)
369
+ if (danger_color_association is not None):
370
+ req.danger_color_association.CopyFrom(danger_color_association)
371
+ return self.call_async(self._stub.SetSystemParams, req,
372
+ error_from_response=common_header_errors, copy_request=False,
373
+ **kwargs)
374
+
375
+ def _timestamp_to_robot_time(self, timestamp, timesync_endpoint=None):
376
+ # Create a time converter to convert timestamp to robot time
377
+ time_converter = None
378
+ if (timesync_endpoint):
379
+ time_converter = timesync_endpoint.get_robot_time_converter()
380
+ elif (self.timesync_endpoint):
381
+ time_converter = self.timesync_endpoint.get_robot_time_converter()
382
+ else:
383
+ raise NoTimeSyncError("No timesync endpoint was passed to audio visual client.")
384
+
385
+ return time_converter.robot_timestamp_from_local_secs(timestamp)
386
+
387
+
388
+ def _get_behavior_list(response):
389
+ return response.behaviors
390
+
391
+
392
+ def _get_live_behavior(response):
393
+ return response.live_behavior
394
+
395
+
396
+ def _get_deleted_behaviors(response):
397
+ return response.deleted_behaviors
398
+
399
+
400
+ _AUDIO_VISUAL_RUN_BEHAVIOR_STATUS_TO_ERROR = collections.defaultdict(
401
+ lambda: (AudioVisualResponseError, None))
402
+ _AUDIO_VISUAL_RUN_BEHAVIOR_STATUS_TO_ERROR.update({
403
+ audio_visual_pb2.RunBehaviorResponse.STATUS_SUCCESS: (None, None),
404
+ audio_visual_pb2.RunBehaviorResponse.STATUS_DOES_NOT_EXIST: error_pair(DoesNotExistError),
405
+ audio_visual_pb2.RunBehaviorResponse.STATUS_EXPIRED: error_pair(BehaviorExpiredError),
406
+ })
407
+
408
+ _AUDIO_VISUAL_STOP_BEHAVIOR_STATUS_TO_ERROR = collections.defaultdict(
409
+ lambda: (AudioVisualResponseError, None))
410
+ _AUDIO_VISUAL_STOP_BEHAVIOR_STATUS_TO_ERROR.update({
411
+ audio_visual_pb2.StopBehaviorResponse.STATUS_SUCCESS: (None, None),
412
+ audio_visual_pb2.StopBehaviorResponse.STATUS_INVALID_CLIENT: error_pair(InvalidClientError)
413
+ })
414
+
415
+ _AUDIO_VISUAL_ADD_OR_MODIFY_BEHAVIOR_STATUS_TO_ERROR = collections.defaultdict(
416
+ lambda: (AudioVisualResponseError, None))
417
+ _AUDIO_VISUAL_ADD_OR_MODIFY_BEHAVIOR_STATUS_TO_ERROR.update({
418
+ audio_visual_pb2.AddOrModifyBehaviorResponse.STATUS_SUCCESS: (None, None),
419
+ audio_visual_pb2.AddOrModifyBehaviorResponse.STATUS_INVALID:
420
+ error_pair(InvalidBehaviorError),
421
+ audio_visual_pb2.AddOrModifyBehaviorResponse.STATUS_MODIFY_PERMANENT:
422
+ error_pair(PermanentBehaviorError),
423
+ })
424
+
425
+ _AUDIO_VISUAL_DELETE_BEHAVIORS_STATUS_TO_ERROR = collections.defaultdict(
426
+ lambda: (AudioVisualResponseError, None))
427
+ _AUDIO_VISUAL_DELETE_BEHAVIORS_STATUS_TO_ERROR.update({
428
+ audio_visual_pb2.DeleteBehaviorsResponse.STATUS_SUCCESS: (None, None),
429
+ audio_visual_pb2.DeleteBehaviorsResponse.STATUS_DOES_NOT_EXIST:
430
+ error_pair(DoesNotExistError),
431
+ audio_visual_pb2.DeleteBehaviorsResponse.STATUS_DELETE_PERMANENT:
432
+ error_pair(PermanentBehaviorError),
433
+ })
434
+
435
+
436
+ @handle_common_header_errors
437
+ @handle_unset_status_error(unset='STATUS_UNKNOWN')
438
+ def _run_behavior_error(response):
439
+ """RunBehaviorResponse response to exception."""
440
+ return error_factory(response, response.status,
441
+ status_to_string=audio_visual_pb2.RunBehaviorResponse.Status.Name,
442
+ status_to_error=_AUDIO_VISUAL_RUN_BEHAVIOR_STATUS_TO_ERROR)
443
+
444
+
445
+ @handle_common_header_errors
446
+ @handle_unset_status_error(unset='STATUS_UNKNOWN')
447
+ def _stop_behavior_error(response):
448
+ """StopBehaviorResponse response to exception."""
449
+ return error_factory(response, response.status,
450
+ status_to_string=audio_visual_pb2.StopBehaviorResponse.Status.Name,
451
+ status_to_error=_AUDIO_VISUAL_STOP_BEHAVIOR_STATUS_TO_ERROR)
452
+
453
+
454
+ @handle_common_header_errors
455
+ @handle_unset_status_error(unset='STATUS_UNKNOWN')
456
+ def _add_or_modify_behavior_error(response):
457
+ """AddOrModifyBehaviorResponse response to exception."""
458
+ return error_factory(response, response.status,
459
+ status_to_string=audio_visual_pb2.AddOrModifyBehaviorResponse.Status.Name,
460
+ status_to_error=_AUDIO_VISUAL_ADD_OR_MODIFY_BEHAVIOR_STATUS_TO_ERROR)
461
+
462
+
463
+ @handle_common_header_errors
464
+ @handle_unset_status_error(unset='STATUS_UNKNOWN')
465
+ def _delete_behaviors_error(response):
466
+ """DeleteBehaviorResponse response to exception."""
467
+ return error_factory(response, response.status,
468
+ status_to_string=audio_visual_pb2.DeleteBehaviorsResponse.Status.Name,
469
+ status_to_error=_AUDIO_VISUAL_DELETE_BEHAVIORS_STATUS_TO_ERROR)
470
+
471
+
472
+ def check_color(led_sequence_group):
473
+ # Check every LED
474
+ leds = ["center", "front_left", "front_right", "hind_left", "hind_right"]
475
+ for led in leds:
476
+ # Get the LED sequence by location
477
+ led_sequence = getattr(led_sequence_group, led, None)
478
+ if led_sequence is not None:
479
+ # Now, normalize the color in the LED sequence by location
480
+ if led_sequence.HasField("animation_sequence"):
481
+ for frame, idx in enumerate(led_sequence.animation_sequence.frames):
482
+ if frame.HasField("color"):
483
+ color = clamp_and_normalize_color(frame.color)
484
+ led_sequence.animation_sequence.frames[idx] = color
485
+ elif led_sequence.HasField("blink_sequence"):
486
+ if led_sequence.blink_sequence.HasField("color"):
487
+ led_sequence.blink_sequence.color.CopyFrom(
488
+ clamp_and_normalize_color(led_sequence.blink_sequence.color))
489
+ elif led_sequence.HasField("pulse_sequence"):
490
+ if led_sequence.pulse_sequence.HasField("color"):
491
+ led_sequence.pulse_sequence.color.CopyFrom(
492
+ clamp_and_normalize_color(led_sequence.pulse_sequence.color))
493
+ elif led_sequence.HasField("synced_blink_sequence"):
494
+ for frame, idx in enumerate(led_sequence.synced_blink_sequence.frames):
495
+ if frame.HasField("color"):
496
+ color = clamp_and_normalize_color(frame.color)
497
+ led_sequence.synced_blink_sequence.frames[idx] = color
498
+ elif led_sequence.HasField("solid_color_sequence"):
499
+ if led_sequence.solid_color_sequence.HasField("color"):
500
+ led_sequence.solid_color_sequence.color.CopyFrom(
501
+ clamp_and_normalize_color(led_sequence.solid_color_sequence.color))
502
+ return led_sequence_group
503
+
504
+
505
+ # Scale color so that their Euclidean norm does not exceed max_color_magnitude.
506
+ # NOTE: max_color_magnitude of 255 (roughly 50% of sqrt(3*255^2)=441.67) is a heuristic chosen to prevent damage to the robot's LEDs.
507
+ # Exceeding this value may result in damage to the robot's LEDs that will NOT be covered under warranty.
508
+ def clamp_and_normalize_color(color, max_color_magnitude=255):
509
+ r, g, b = color.rgb.r, color.rgb.g, color.rgb.b
510
+ norm = math.sqrt(r**2 + g**2 + b**2)
511
+ if norm > max_color_magnitude and norm > 0:
512
+ scale = max_color_magnitude / norm
513
+ scaled_color = audio_visual_pb2.Color(
514
+ rgb=audio_visual_pb2.Color.RGB(r=int(r * scale), g=int(g * scale), b=int(b * scale)))
515
+ print(f"Input color {color} scaled by {scale:.2f}. Clamped color: {scaled_color}.")
516
+ color = scaled_color
517
+ return color
@@ -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.')
@@ -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
@@ -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
+ from deprecated.sphinx import deprecated
8
+
7
9
  from bosdyn.api import gripper_camera_param_service_pb2_grpc
8
10
  from bosdyn.client.common import BaseClient, common_header_errors
9
11
 
@@ -77,12 +79,16 @@ class GripperCameraParamClient(BaseClient):
77
79
  get_gripper_camera_calib_request (gripper_camera_params_pb2.GripperCameraGetCalibrationRequest) : The command reqeust to get gripper camera calibration
78
80
 
79
81
  Returns:
80
- The GripperCameraGetCalibrationResponse message, which contains the GripperCameraCalibrationProto
82
+ The GripperCameraGetCalibrationResponse message, which contains the GripperCameraCalibrationProto
81
83
  """
82
84
  return self.call(self._stub.GetCamCalib, get_gripper_camera_calib_request,
83
85
  error_from_response=common_header_errors, **kwargs)
84
86
 
85
- def get_camera_calib_asnyc(self, get_gripper_camera_calib_request, **kwargs):
86
- """Asnyc version of get_camera_calib()."""
87
+ def get_camera_calib_async(self, get_gripper_camera_calib_request, **kwargs):
88
+ """Async version of get_camera_calib()."""
87
89
  return self.call_async(self._stub.GetCamCalib, get_gripper_camera_calib_request,
88
90
  error_from_response=common_header_errors, **kwargs)
91
+
92
+ @deprecated(version='5.0', reason='Use get_camera_calib_async() instead.')
93
+ def get_camera_calib_asnyc(self, get_gripper_camera_calib_request, **kwargs):
94
+ return self.get_camera_calib_async(get_gripper_camera_calib_request, **kwargs)
@@ -19,6 +19,7 @@ import bosdyn.util
19
19
  from bosdyn.api.keepalive import keepalive_pb2, keepalive_service_pb2_grpc
20
20
  from bosdyn.client.common import (BaseClient, common_header_errors, error_factory, error_pair,
21
21
  handle_common_header_errors, handle_unset_status_error)
22
+ from bosdyn.client.error_callback_result import ErrorCallbackResult
22
23
  from bosdyn.client.exceptions import ResponseError, RetryableRpcError
23
24
 
24
25
 
@@ -225,7 +226,7 @@ class PolicyKeepalive():
225
226
  #pylint: disable=too-many-arguments
226
227
  def __init__(self, client: KeepaliveClient, policy: Policy, rpc_timeout_seconds: float = None,
227
228
  rpc_interval_seconds: float = None, logger: 'logging.Logger' = None,
228
- remove_policy_on_exit: bool = False):
229
+ remove_policy_on_exit: bool = False, initial_retry_seconds: float = 1.0):
229
230
 
230
231
  self.logger = logger or logging.getLogger()
231
232
  self.remove_policy_on_exit = remove_policy_on_exit
@@ -238,6 +239,11 @@ class PolicyKeepalive():
238
239
  # This will raise an exception if there's no action at all.
239
240
  self._rpc_interval_seconds = rpc_interval_seconds or policy.shortest_action_delay() / 3
240
241
  self._rpc_timeout_seconds = rpc_timeout_seconds
242
+ self._initial_retry_seconds = initial_retry_seconds
243
+
244
+ #: Callable[[Exception], ErrorCallbackResult] | None: Optional callback to be called when
245
+ #: an error occurs in the keepalive thread.
246
+ self.keepalive_error_callback = None
241
247
 
242
248
  self._end_check_in_signal = threading.Event()
243
249
  self._thread = threading.Thread(target=self._periodic_check_in)
@@ -277,24 +283,47 @@ class PolicyKeepalive():
277
283
  self._end_check_in_signal.set()
278
284
 
279
285
  def _periodic_check_in(self):
280
- while True:
286
+ retry_interval = self._initial_retry_seconds
287
+ wait_time = self._rpc_interval_seconds
288
+
289
+ # Block and wait for the stop signal. If we receive it within the check-in period,
290
+ # leave the loop. Under normal conditions, wait up to self._check_in_period seconds, minus
291
+ # the RPC processing time. (values < 0 are OK and unblock immediately)
292
+ while not self._end_check_in_signal.wait(wait_time):
281
293
  exec_start = time.time()
294
+ action = ErrorCallbackResult.RESUME_NORMAL_OPERATION
282
295
 
283
296
  try:
284
297
  self._check_in()
285
298
  except RetryableRpcError as exc:
286
299
  self.logger.warning('exception during check-in:\n%s\n', exc)
287
300
  self.logger.info('continuing check-in')
288
-
301
+ except Exception as exc: # pylint: disable=broad-except
302
+ if self.keepalive_error_callback is not None:
303
+ action = ErrorCallbackResult.DEFAULT_ACTION
304
+ try:
305
+ action = self.keepalive_error_callback(exc)
306
+ except Exception: # pylint: disable=broad-except
307
+ self.logger.exception(
308
+ 'Exception thrown in the provided keepalive error callback')
309
+ else:
310
+ raise
289
311
  # How long did the RPC and processing of said RPC take?
290
312
  exec_seconds = time.time() - exec_start
291
313
 
292
- # Block and wait for the stop signal. If we receive it within the check-in period,
293
- # leave the loop. This check must be at the end of the loop!
294
- # Wait up to self._check_in_period seconds, minus the RPC processing time.
295
- # (values < 0 are OK and will return immediately)
296
- if self._end_check_in_signal.wait(self._rpc_interval_seconds - exec_seconds):
314
+ if action == ErrorCallbackResult.ABORT:
315
+ self.logger.warning('Callback directed the keepalive thread to exit.')
297
316
  break
317
+ elif action == ErrorCallbackResult.RETRY_IMMEDIATELY:
318
+ wait_time = 0
319
+ continue
320
+ elif action == ErrorCallbackResult.RETRY_WITH_EXPONENTIAL_BACK_OFF:
321
+ wait_time = retry_interval - exec_seconds
322
+ retry_interval = min(2 * retry_interval, self._rpc_interval_seconds)
323
+ else:
324
+ # Success path, or default action (resume normal operation)
325
+ wait_time = self._rpc_interval_seconds - exec_seconds
326
+ retry_interval = self._initial_retry_seconds
298
327
  self.logger.info('Policy check-in stopped')
299
328
 
300
329
 
@@ -660,6 +660,10 @@ class SE3Pose(object):
660
660
  ret[0:3, 3] = [self.x, self.y, self.z]
661
661
  return ret
662
662
 
663
+ def translation_norm(self):
664
+ """Calculates the Euclidean norm (magnitude) of the translation component pose."""
665
+ return math.sqrt(self.x**2 + self.y**2 + self.z**2)
666
+
663
667
  def mult(self, se3pose):
664
668
  """
665
669
  Computes the multiplication between the current math_helpers.SE3Pose and the input se3pose.
@@ -1176,4 +1180,4 @@ def quat_to_eulerZYX(q):
1176
1180
  yaw = math.atan2(2 * (q.x * q.y + q.w * q.z), q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z)
1177
1181
  roll = math.atan2(2 * (q.y * q.z + q.w * q.x),
1178
1182
  q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z)
1179
- return yaw, pitch, roll
1183
+ return yaw, pitch, roll
@@ -20,6 +20,7 @@ from bosdyn.client import (ResponseError, RetryableUnavailableError, TimedOutErr
20
20
  TooManyRequestsError)
21
21
  from bosdyn.client.common import (BaseClient, error_factory, handle_common_header_errors,
22
22
  handle_lease_use_result_errors, handle_unset_status_error)
23
+ from bosdyn.client.error_callback_result import ErrorCallbackResult
23
24
 
24
25
  LOGGER = logging.getLogger('payload_registration_client')
25
26
 
@@ -381,16 +382,23 @@ class PayloadRegistrationKeepAlive(object):
381
382
  class name is acquired.
382
383
  rpc_timeout_secs: Number of seconds to wait for a pay_reg_client RPC. Defaults to None,
383
384
  for no timeout.
385
+ initial_retry_seconds: Number of seconds to wait before retrying registration that failed
386
+ due to unhandled errors including RPC transport issues.
384
387
  """
385
388
 
386
389
  def __init__(self, pay_reg_client, payload, secret, registration_interval_secs=30, logger=None,
387
- rpc_timeout_secs=None):
390
+ rpc_timeout_secs=None, initial_retry_seconds=1.0):
388
391
  self.pay_reg_client = pay_reg_client
389
392
  self.payload = payload
390
393
  self.secret = secret
391
394
  self._registration_interval_secs = registration_interval_secs
392
395
  self.logger = logger or logging.getLogger(self.__class__.__name__)
393
396
  self._rpc_timeout_secs = rpc_timeout_secs
397
+ self._initial_retry_seconds = initial_retry_seconds
398
+
399
+ #: Callable[[Exception], ErrorCallbackResult] | None: Optional callback to be called when
400
+ #: an error occurs in the re-registration thread.
401
+ self.reregistration_error_callback = None
394
402
 
395
403
  # Configure the thread to do re-registration.
396
404
  self._end_reregister_signal = threading.Event()
@@ -423,6 +431,7 @@ class PayloadRegistrationKeepAlive(object):
423
431
 
424
432
  # This will raise an exception if the thread has already started.
425
433
  self._thread.start()
434
+ return self
426
435
 
427
436
  def is_alive(self):
428
437
  """Are we still periodically re-registering?
@@ -445,8 +454,12 @@ class PayloadRegistrationKeepAlive(object):
445
454
  RpcError: Problem communicating with the robot.
446
455
  """
447
456
  self.logger.info('Starting registration loop')
448
- while True:
457
+ retry_interval = self._initial_retry_seconds
458
+ wait_time = self._registration_interval_secs
459
+
460
+ while not self._end_reregister_signal.wait(wait_time):
449
461
  exec_start = time.time()
462
+ action = ErrorCallbackResult.RESUME_NORMAL_OPERATION
450
463
  try:
451
464
  self.pay_reg_client.register_payload(self.payload, self.secret)
452
465
  except PayloadAlreadyExistsError:
@@ -459,10 +472,31 @@ class PayloadRegistrationKeepAlive(object):
459
472
  self.logger.warning('Timed out, timeout set to "{}"'.format(self._rpc_timeout_secs))
460
473
  except TooManyRequestsError:
461
474
  self.logger.warning("Too many requests error")
462
- except Exception as exc:
463
- # Log all other exceptions, but continue looping in hopes that it resolves itself
464
- self.logger.exception('Caught general exception.')
475
+ except Exception as exc: # pylint: disable=broad-except
476
+ # If the application provided an error handler, give it an opportunity to resolve
477
+ # the issue.
478
+ if self.reregistration_error_callback is not None:
479
+ action = ErrorCallbackResult.DEFAULT_ACTION
480
+ try:
481
+ action = self.reregistration_error_callback(exc)
482
+ except Exception: # pylint: disable=broad-except
483
+ self.logger.exception(
484
+ 'Exception thrown in the provided re-registration error callback ')
485
+ else:
486
+ # Log all other exceptions, but continue looping in hopes that it resolves itself
487
+ self.logger.exception('Caught general exception.')
488
+
465
489
  exec_sec = time.time() - exec_start
466
- if self._end_reregister_signal.wait(self._registration_interval_secs - exec_sec):
490
+ if action == ErrorCallbackResult.ABORT:
491
+ self.logger.warning('Callback directed the re-registration loop to exit.')
467
492
  break
493
+ elif action == ErrorCallbackResult.RETRY_IMMEDIATELY:
494
+ wait_time = 0.0
495
+ elif action == ErrorCallbackResult.RETRY_WITH_EXPONENTIAL_BACK_OFF:
496
+ wait_time = retry_interval - exec_sec
497
+ retry_interval = min(retry_interval * 2, self._registration_interval_secs)
498
+ else:
499
+ # Success path, or default action (resume normal operation)
500
+ wait_time = self._registration_interval_secs - exec_sec
501
+ retry_interval = self._initial_retry_seconds
468
502
  self.logger.info('Re-registration stopped')
bosdyn/client/robot.py CHANGED
@@ -131,6 +131,10 @@ class Robot(object):
131
131
  self._time_sync_thread = None
132
132
  self.executor = None
133
133
 
134
+ #: Callable[[Exception], ErrorCallbackResult] | None: Optional callback to be invoked when
135
+ #: an error occurs in the token refresh thread.
136
+ self.token_refresh_error_callback = None
137
+
134
138
  # Set default max message length for sending and receiving. These values are used when
135
139
  # creating channels.
136
140
  self.max_send_message_length = DEFAULT_MAX_MESSAGE_LENGTH
bosdyn/client/sdk.py CHANGED
@@ -64,6 +64,7 @@ from .spot_check import SpotCheckClient
64
64
  from .time_sync import TimeSyncClient
65
65
  from .world_object import WorldObjectClient
66
66
 
67
+ from .audio_visual import AudioVisualClient # isort:skip
67
68
 
68
69
 
69
70
  class SdkError(Error):
@@ -111,6 +112,7 @@ _DEFAULT_SERVICE_CLIENTS = [
111
112
  AuthClient,
112
113
  AutoReturnClient,
113
114
  AutowalkClient,
115
+ AudioVisualClient,
114
116
  DataAcquisitionClient,
115
117
  DataAcquisitionStoreClient,
116
118
  DataBufferClient,
@@ -356,7 +356,7 @@ class _StringParamValidator(_ParamValidatorInterface):
356
356
  if err:
357
357
  return err
358
358
 
359
- if len(self.param_spec.options) > 0:
359
+ if not self.param_spec.editable and len(self.param_spec.options) > 0:
360
360
  if param_value.value not in self.param_spec.options:
361
361
  return CustomParamError(
362
362
  status=CustomParamError.STATUS_INVALID_VALUE, error_messages=[
@@ -14,11 +14,15 @@ import logging
14
14
  import threading
15
15
 
16
16
  from .auth import InvalidTokenError
17
- from .exceptions import ResponseError, RpcError
17
+ from .error_callback_result import ErrorCallbackResult
18
+ from .exceptions import ResponseError, RpcError, TimedOutError
18
19
  from .token_cache import WriteFailedError
19
20
 
20
21
  _LOGGER = logging.getLogger(__name__)
21
22
 
23
+ USER_TOKEN_REFRESH_TIME_DELTA = datetime.timedelta(hours=1)
24
+ USER_TOKEN_RETRY_INTERVAL_START = datetime.timedelta(seconds=1)
25
+
22
26
 
23
27
  class TokenManager:
24
28
  """Refreshes the user token in the robot object.
@@ -26,10 +30,13 @@ class TokenManager:
26
30
  The refresh policy assumes the token is minted and then the manager is
27
31
  launched."""
28
32
 
29
- def __init__(self, robot, timestamp=None):
33
+ def __init__(self, robot, timestamp=None, refresh_interval=USER_TOKEN_REFRESH_TIME_DELTA,
34
+ initial_retry_interval=USER_TOKEN_RETRY_INTERVAL_START):
30
35
  self.robot = robot
31
36
 
32
37
  self._last_timestamp = timestamp or datetime.datetime.now()
38
+ self._refresh_interval = refresh_interval
39
+ self._initial_retry_seconds = initial_retry_interval
33
40
 
34
41
  # Daemon threads can still run during shutdown after python has
35
42
  # started to clear out things in globals().
@@ -51,33 +58,55 @@ class TokenManager:
51
58
 
52
59
  def update(self):
53
60
  """Refresh the user token as needed."""
54
- USER_TOKEN_REFRESH_TIME_DELTA = datetime.timedelta(hours=1)
55
- USER_TOKEN_RETRY_INTERVAL_START = datetime.timedelta(seconds=1)
56
-
57
- retry_interval = USER_TOKEN_RETRY_INTERVAL_START
58
- while not self._exit_thread.is_set():
59
- elapsed_time = datetime.datetime.now() - self._last_timestamp
60
- if elapsed_time >= USER_TOKEN_REFRESH_TIME_DELTA:
61
- try:
62
- self.robot.authenticate_with_token(self.robot.user_token)
63
- except WriteFailedError:
64
- _LOGGER.exception(
65
- "Failed to save the token to the cache. Continuing without caching.")
66
- except (InvalidTokenError, ResponseError, RpcError):
67
- _LOGGER.exception("Error refreshing the token. Retry in %s", retry_interval)
68
-
69
- # Exponential back-off on retrying
70
- self._exit_thread.wait(retry_interval.seconds)
71
- retry_interval = min(2 * retry_interval, USER_TOKEN_REFRESH_TIME_DELTA)
72
- continue
73
-
74
- retry_interval = USER_TOKEN_RETRY_INTERVAL_START
75
-
76
- # Wait until the specified time or get interrupted by user.
61
+ retry_interval = self._initial_retry_seconds
62
+ wait_time = min(self._refresh_interval - (datetime.datetime.now() - self._last_timestamp),
63
+ self._refresh_interval)
64
+
65
+ while not self._exit_thread.wait(wait_time.total_seconds()):
66
+ start_time = datetime.datetime.now()
67
+ action = ErrorCallbackResult.RESUME_NORMAL_OPERATION
68
+ try:
69
+ self.robot.authenticate_with_token(self.robot.user_token)
77
70
  self._last_timestamp = datetime.datetime.now()
78
- elapsed_time = USER_TOKEN_REFRESH_TIME_DELTA
71
+ except WriteFailedError:
72
+ _LOGGER.exception(
73
+ "Failed to save the token to the cache. Continuing without caching.")
74
+ except (InvalidTokenError, ResponseError, RpcError) as exc:
75
+ _LOGGER.exception("Error refreshing the token.")
76
+ # Default course of action is to retry with a back-off, unless the application
77
+ # supplied callback directs us to do otherwise.
78
+ action = ErrorCallbackResult.RETRY_WITH_EXPONENTIAL_BACK_OFF
79
+ # If the application provided a callback and the error was encountered while
80
+ # refreshing the token, invoke the callback so that the application can take
81
+ # appropriate action.
82
+ if self.robot.token_refresh_error_callback is not None and not isinstance(
83
+ exc, TimedOutError):
84
+ try:
85
+ action = self.robot.token_refresh_error_callback(exc)
86
+ except Exception: #pylint: disable=broad-except
87
+ _LOGGER.exception(
88
+ "Exception thrown in the provided token refresh error callback")
89
+ if action == ErrorCallbackResult.RESUME_NORMAL_OPERATION:
90
+ _LOGGER.warning("Refreshing token in %s", self._refresh_interval)
91
+
92
+ elapsed = datetime.datetime.now() - start_time
93
+ if action == ErrorCallbackResult.ABORT:
94
+ _LOGGER.warning(
95
+ "Application-supplied callback directed the token refresh loop to exit.")
96
+ break
97
+ elif action == ErrorCallbackResult.RETRY_IMMEDIATELY:
98
+ _LOGGER.warning("Retrying to refresh token immediately.")
99
+ wait_time = datetime.timedelta(seconds=0)
100
+ elif action == ErrorCallbackResult.RESUME_NORMAL_OPERATION:
101
+ wait_time = self._refresh_interval - elapsed
102
+ retry_interval = self._initial_retry_seconds
103
+ else:
104
+ # action doesn't match one of the enum values or is one of
105
+ # RETRY_WITH_EXPONENTIAL_BACK_OFF or DEFAULT_ACTION
106
+ _LOGGER.warning("Retrying token refresh in %s", retry_interval)
107
+ wait_time = retry_interval - elapsed
108
+ retry_interval = min(2 * retry_interval, self._refresh_interval)
79
109
 
80
- self._exit_thread.wait(elapsed_time.seconds)
81
110
  message = 'Shutting down monitoring of token belonging to robot {}'.format(
82
111
  self.robot.address)
83
112
  _LOGGER.debug(message)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bosdyn-client
3
- Version: 5.0.0
3
+ Version: 5.0.1.1
4
4
  Summary: Boston Dynamics API client code and interfaces
5
5
  Home-page: https://dev.bostondynamics.com/
6
6
  Author: Boston Dynamics
@@ -15,8 +15,8 @@ Classifier: License :: Other/Proprietary License
15
15
  Classifier: Operating System :: OS Independent
16
16
  Requires-Python: >=3.7
17
17
  Description-Content-Type: text/markdown
18
- Requires-Dist: bosdyn-api (==5.0.0)
19
- Requires-Dist: bosdyn-core (==5.0.0)
18
+ Requires-Dist: bosdyn-api (==5.0.1.1)
19
+ Requires-Dist: bosdyn-core (==5.0.1.1)
20
20
  Requires-Dist: grpcio
21
21
  Requires-Dist: pyjwt
22
22
  Requires-Dist: numpy
@@ -8,6 +8,8 @@ bosdyn/client/area_callback_service_servicer.py,sha256=o1kYKV83Q-ud-_rmT17XTSqBd
8
8
  bosdyn/client/area_callback_service_utils.py,sha256=R8ljJe8fPszMI6RyuGRyv_QGu63kw1yZAveZydlpERI,5858
9
9
  bosdyn/client/arm_surface_contact.py,sha256=DRfPfsFEzfk6ufe080ViqasUefl2ZUtcvcNENgcf55k,3710
10
10
  bosdyn/client/async_tasks.py,sha256=gEPev6_jaUCe-G5PqktMiMGb7ohDy0daunxzQD5jafg,5594
11
+ bosdyn/client/audio_visual.py,sha256=_o2kvOuTgYfXfOgCPotBGdg3xYRCz55wv2tZt4VhvgE,23194
12
+ bosdyn/client/audio_visual_helpers.py,sha256=9qksn7epH5jBdbwa6VVMcm2mn8HCVtJuSW1C847Jo6w,4499
11
13
  bosdyn/client/auth.py,sha256=YLo6jP0Ssl_Z6rHtTiPiKUNIweDRYub9w3iHdUe6n40,5302
12
14
  bosdyn/client/auto_return.py,sha256=kqT1gaaneLYIPFVAFzpvTHRwa8NYYQ2OBf7wViBQudE,5598
13
15
  bosdyn/client/autowalk.py,sha256=e57FcAC4fu2BPD57Yy4j1F8DPIPPic0Qt1E0UM-oZWo,5734
@@ -25,20 +27,21 @@ bosdyn/client/data_buffer.py,sha256=Ta-7CCoy_-StzXDm8cLIjvOnMDgKEzKFc9mcDmVkubc,
25
27
  bosdyn/client/data_chunk.py,sha256=6xq9zqLD-JH72ZxzEQEKYqhXvb34TlcZoeqAYaXRxCw,1959
26
28
  bosdyn/client/data_service.py,sha256=aKfXJCciJ2M2WZRKfdWiLS2n-UEKWSRkOgcTnArbc5o,5201
27
29
  bosdyn/client/directory.py,sha256=jWBxnPzBJSrfr4fuLf2VuZGsxOo8Q5iX5tEw6Zx2VY0,4906
28
- bosdyn/client/directory_registration.py,sha256=tO-OMVYrFr1tU3oVifoFcbjW_EIXWo6bdsdvS_Gr7RA,19101
30
+ bosdyn/client/directory_registration.py,sha256=SV2xWly0Zu1XaWhyJ9eqXYeWSjFbwfyFmup_RjR3Cdw,20806
29
31
  bosdyn/client/docking.py,sha256=hYwZNIir3_pt-WvMFiXzMqDCwW8nA5H1BYOUD6_3-00,17596
30
32
  bosdyn/client/door.py,sha256=dbBuGcuoZL3gRhxTQoQHKDlnzcML8ELBtQxCQqomYpo,3110
33
+ bosdyn/client/error_callback_result.py,sha256=B3O9D8ceV1ZsS19UvguISlbvJSjtQ1aZfktVuekZXLU,1317
31
34
  bosdyn/client/estop.py,sha256=-dy3SP6lcOryyTB_VXe0C_cFk-qK1TNJdSm9DKSz-MM,32073
32
35
  bosdyn/client/exceptions.py,sha256=VQg5cSrzB9_pmFcUCbBaRF2QtBUKfdzpR94N3q2rNx0,5323
33
36
  bosdyn/client/fault.py,sha256=ymo4M1Vy-bUasD8uOm1pxE1HnjheCds6qA9hZiJbAzY,6050
34
37
  bosdyn/client/frame_helpers.py,sha256=Rrm0Zx10LoaVsJv-FPCX02LVQwNkyKVO28lZz7Bmlvg,12572
35
38
  bosdyn/client/graph_nav.py,sha256=8sULbBAFaFxJPF3ZdI_kqLYpzA0XbhGO-zECRyVH1J4,70681
36
- bosdyn/client/gripper_camera_param.py,sha256=Zrd_YQfuaKaNRXSUQpcgsAkQjDVl39XtTIGAy9fKvZI,4393
39
+ bosdyn/client/gripper_camera_param.py,sha256=kxhBffjbq5R-t1OxWEAFguo_tKW5xGIuasdflSmfAwY,4683
37
40
  bosdyn/client/image.py,sha256=fn41QOvaAOH7UT7m2ENXayI4fLXg5NQBZrmpLFZVBBE,21980
38
41
  bosdyn/client/image_service_helpers.py,sha256=l2hZKqcG7PI05-_bhh8BLkpN4METmG53JCR0YF3q-8Y,40421
39
42
  bosdyn/client/inverse_kinematics.py,sha256=KBgvGdnt-JZJvDzbdJ72Kqw8RHoOsqKq_p9rQtvwuKo,1708
40
43
  bosdyn/client/ir_enable_disable.py,sha256=6c5MAuO1Fj0RlVcjkP1vhs-qTLiOpM3RnD8F7_ik1Gw,2153
41
- bosdyn/client/keepalive.py,sha256=C-Vf17-sSfAHq4cbuuVIh7JQM7NLzv2pRekjHJywfdU,13214
44
+ bosdyn/client/keepalive.py,sha256=Sz1sobzkyDfamKfaam7-dP3jIVlVOBs4l6JE5qqTjEA,14892
42
45
  bosdyn/client/lease.py,sha256=-B698QATgRuddH_pc5YN2LhMkZDacZP1kkf1MeYjqec,45412
43
46
  bosdyn/client/lease_resource_hierarchy.py,sha256=b_YpVCeiJwVQzAy1Xh5h-1HYjpbzZvmDZAcREGCOgBc,3070
44
47
  bosdyn/client/lease_validator.py,sha256=Vo5-2mtfCh50i7MDXLf4NlOBOgqd28ynPp2FF7NNGx0,14505
@@ -47,11 +50,11 @@ bosdyn/client/local_grid.py,sha256=YszM_pTmeGuGIwExgRwB76PXJciO6rZqfaL2RHLxAf0,3
47
50
  bosdyn/client/log_status.py,sha256=wcKZEFw8GHyWeYKysZ75i3PrLb1r2xVUEAyO3SDmxOI,12656
48
51
  bosdyn/client/manipulation_api_client.py,sha256=bdTTqZk73m7l9bty9CNC9Bs-hTTRFEA_wDweMb4mLu4,4204
49
52
  bosdyn/client/map_processing.py,sha256=xmwTkAfIIvRbaRnOCj-JTkrfS13n4z_DAqg-sc8hL2Y,10239
50
- bosdyn/client/math_helpers.py,sha256=Xb74-Yxd85BChnOt7j9uiLKPd3h5aqREeiys0QMJiL8,48608
53
+ bosdyn/client/math_helpers.py,sha256=HLJ2pmul--Ualfa2cWHIgdNAZScK8MC6kjwqNKSCX44,48793
51
54
  bosdyn/client/metrics_logging.py,sha256=9gjVK5Xu4JpOqMg4_tKMHd-XHOh--xaiU_OP9zcGOMs,6646
52
55
  bosdyn/client/network_compute_bridge_client.py,sha256=L7RmgCRKiPBn3SVwIxCqP0hkEkhRhF5k5xz2BpC-Omk,7966
53
56
  bosdyn/client/payload.py,sha256=12vZirEI4-gu9KPHsDg6MH0QCmnxbolWR1j81lvgfVw,2271
54
- bosdyn/client/payload_registration.py,sha256=ilT-2W1iuCZImLeDlPLovPYg5HJCk7nLTHxod0Rn8nY,22316
57
+ bosdyn/client/payload_registration.py,sha256=rDGMK3ZtI6nmvCFsjm1MjueYglDNuHHu-7Q5V901VM8,24306
55
58
  bosdyn/client/payload_software_update.py,sha256=nYrFOxuikVd-cChkx8aZ9gpVQSxjadgPcxyT6eklwT4,8716
56
59
  bosdyn/client/payload_software_update_initiation.py,sha256=LDAe_gDUCLDKOC54MbhC-hsywqNMmT_dtnYjmMaef1g,3216
57
60
  bosdyn/client/point_cloud.py,sha256=F_AJBYql8b6Ok_-IEmxpV4ajdXZ_GOahPe_QwWLf7xk,8627
@@ -59,18 +62,18 @@ bosdyn/client/power.py,sha256=teVPBPcwsUHvn996upF06Y5MJsfQrE-93ezMXAAYSmA,26955
59
62
  bosdyn/client/processors.py,sha256=Z-Djf_I_lhfokB-f_L0PewAY8J95LThdWVju1zJ2BaE,1275
60
63
  bosdyn/client/ray_cast.py,sha256=Ca1yJo0aY6OmVAazb19fy44L-9LzcKVxr_fHt_EoQtg,4465
61
64
  bosdyn/client/recording.py,sha256=sQ34G_ckrE-M42ER2GUbYI7UibvdrjHycaia58IpJ2s,25913
62
- bosdyn/client/robot.py,sha256=kOtUJq-cg7ovn7EvePe06l5nCUDnvFYhm0XCwSL2XXE,30909
65
+ bosdyn/client/robot.py,sha256=XQCp9NjcwS4Bhhohjup_AcLlYm67DV0tlPxuqZTwU1M,31114
63
66
  bosdyn/client/robot_command.py,sha256=LtoVKlJwwhTmADRMvJIWJ4B5rY_MpdhHnL72zKm1ECU,108248
64
67
  bosdyn/client/robot_id.py,sha256=0VZHG9hltwTLAm1_Bt26Xq1O6EROswqNwHvjY7kaplk,2482
65
68
  bosdyn/client/robot_state.py,sha256=h551ke5eHdAC7NgVuLphY8FZR899Ii8_lYwuoX1w1nk,7073
66
- bosdyn/client/sdk.py,sha256=UFG5sRbnzEiB7hlUBIPm9KVceBrEOLGDuLQJtoIdyzM,12864
69
+ bosdyn/client/sdk.py,sha256=XGW0DqlBfZv31LNYjjtU8p-86C5U-x2zJd7bAF3eDzg,12945
67
70
  bosdyn/client/server_util.py,sha256=uLT12vs5nAhdJ0ryaKuE82dxXnBOupebyDuzI8tbLRo,10560
68
- bosdyn/client/service_customization_helpers.py,sha256=KSRp585Ux87Ozj_pGDLbzG119MGnIGf8YDQ8WFs2qGc,49122
71
+ bosdyn/client/service_customization_helpers.py,sha256=GD23vhBfwCi1S4eBYIBoTbvHe9kwap1cbq0CHXlciGw,49155
69
72
  bosdyn/client/signals_helpers.py,sha256=Sp91IrMxVU-PeH6TK2njzFCKmFMyshRJqNa4DYRMqDU,3682
70
73
  bosdyn/client/spot_check.py,sha256=PKqN3kwL6oISkqwXEm_R4vz0uixIsfowWY9mC0mM8Cc,14619
71
74
  bosdyn/client/time_sync.py,sha256=mDkcR5RlAKfAOwEUoBjwxtJFDKuGFGmiDcrOeCO2P_g,23089
72
75
  bosdyn/client/token_cache.py,sha256=Vwf9YfsR7pTyu1fLRzXrvDo9hG5GBJcen8Azlo_5_iA,3507
73
- bosdyn/client/token_manager.py,sha256=FvDFCXKIiGXZNkagKZM41Ut8Q0ChlYHN3O61CzrqMF8,3144
76
+ bosdyn/client/token_manager.py,sha256=FkF2-xWXrKG1ttTH2omhjx7FtbIGnYkB1vAaKEhg3xM,5112
74
77
  bosdyn/client/units_helpers.py,sha256=5SAmL8vsnl06oGNjzb57fUkuUbGvtbeNdg4NgW0wYAY,1084
75
78
  bosdyn/client/util.py,sha256=Cr_IB1taOh6Hmu1EdTHa_NU08eXnTEG7kzGX_2xDfoE,19968
76
79
  bosdyn/client/world_object.py,sha256=CNfZJxwdTjd-Oh35liNdkZ27sAzdnBVFTkpVIICfRTo,17066
@@ -94,7 +97,7 @@ bosdyn/client/spot_cam/power.py,sha256=HS3nJF8hXq9m1JziOIwLHGLtlNMyLgewWBgs-mRZm
94
97
  bosdyn/client/spot_cam/ptz.py,sha256=O1m7zDZ92zRmvy9qhjojiphMQwAweTO0HVizQFdWFFE,10630
95
98
  bosdyn/client/spot_cam/streamquality.py,sha256=e-RjizZPwZSOS4Jlqb5Ds-mC6uKam252dpEHkb58Oc8,6364
96
99
  bosdyn/client/spot_cam/version.py,sha256=R82eyCAY9PfZqbN8D6hNzSeZatpgpsFr995dRt1Mbe0,2856
97
- bosdyn_client-5.0.0.dist-info/METADATA,sha256=2nuQY0XUTFHO8hKP4nWMxstk-raf3F3O3VCg6ebzh3s,3987
98
- bosdyn_client-5.0.0.dist-info/WHEEL,sha256=AtBG6SXL3KF_v0NxLf0ehyVOh0cold-JbJYXNGorC6Q,92
99
- bosdyn_client-5.0.0.dist-info/top_level.txt,sha256=an2OWgx1ej2jFjmBjPWNQ68ZglvUfKhmXWW-WhTtDmA,7
100
- bosdyn_client-5.0.0.dist-info/RECORD,,
100
+ bosdyn_client-5.0.1.1.dist-info/METADATA,sha256=4xSEgKQR1XVaeQsiKHhel8oRa_4wNT1QJDSMXRUV1-U,3993
101
+ bosdyn_client-5.0.1.1.dist-info/WHEEL,sha256=AtBG6SXL3KF_v0NxLf0ehyVOh0cold-JbJYXNGorC6Q,92
102
+ bosdyn_client-5.0.1.1.dist-info/top_level.txt,sha256=an2OWgx1ej2jFjmBjPWNQ68ZglvUfKhmXWW-WhTtDmA,7
103
+ bosdyn_client-5.0.1.1.dist-info/RECORD,,