bosdyn-client 3.3.2__py3-none-any.whl → 4.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 (56) hide show
  1. bosdyn/client/__init__.py +5 -6
  2. bosdyn/client/area_callback_region_handler_base.py +19 -4
  3. bosdyn/client/area_callback_service_servicer.py +29 -1
  4. bosdyn/client/area_callback_service_utils.py +45 -51
  5. bosdyn/client/auth.py +13 -28
  6. bosdyn/client/autowalk.py +3 -0
  7. bosdyn/client/channel.py +23 -26
  8. bosdyn/client/command_line.py +64 -13
  9. bosdyn/client/common.py +4 -4
  10. bosdyn/client/data_acquisition.py +47 -6
  11. bosdyn/client/data_acquisition_plugin.py +12 -2
  12. bosdyn/client/data_acquisition_plugin_service.py +33 -2
  13. bosdyn/client/data_acquisition_store.py +38 -0
  14. bosdyn/client/data_buffer.py +22 -8
  15. bosdyn/client/data_chunk.py +1 -0
  16. bosdyn/client/directory_registration.py +1 -14
  17. bosdyn/client/exceptions.py +0 -4
  18. bosdyn/client/frame_helpers.py +3 -1
  19. bosdyn/client/gps/NMEAParser.py +189 -0
  20. bosdyn/client/gps/__init__.py +6 -0
  21. bosdyn/client/gps/aggregator_client.py +56 -0
  22. bosdyn/client/gps/gps_listener.py +153 -0
  23. bosdyn/client/gps/registration_client.py +48 -0
  24. bosdyn/client/graph_nav.py +50 -20
  25. bosdyn/client/image.py +20 -7
  26. bosdyn/client/image_service_helpers.py +14 -14
  27. bosdyn/client/lease.py +27 -22
  28. bosdyn/client/lease_validator.py +5 -5
  29. bosdyn/client/manipulation_api_client.py +1 -1
  30. bosdyn/client/map_processing.py +10 -5
  31. bosdyn/client/math_helpers.py +21 -11
  32. bosdyn/client/metrics_logging.py +147 -0
  33. bosdyn/client/network_compute_bridge_client.py +6 -0
  34. bosdyn/client/power.py +40 -0
  35. bosdyn/client/recording.py +3 -3
  36. bosdyn/client/robot.py +15 -16
  37. bosdyn/client/robot_command.py +341 -203
  38. bosdyn/client/robot_id.py +6 -5
  39. bosdyn/client/robot_state.py +6 -0
  40. bosdyn/client/sdk.py +5 -11
  41. bosdyn/client/server_util.py +11 -11
  42. bosdyn/client/service_customization_helpers.py +776 -64
  43. bosdyn/client/signals_helpers.py +105 -0
  44. bosdyn/client/spot_cam/compositor.py +6 -2
  45. bosdyn/client/spot_cam/ptz.py +24 -14
  46. bosdyn/client/spot_check.py +160 -0
  47. bosdyn/client/time_sync.py +5 -5
  48. bosdyn/client/units_helpers.py +39 -0
  49. bosdyn/client/util.py +100 -64
  50. bosdyn/client/world_object.py +5 -5
  51. {bosdyn_client-3.3.2.dist-info → bosdyn_client-4.0.1.dist-info}/METADATA +4 -3
  52. bosdyn_client-4.0.1.dist-info/RECORD +97 -0
  53. {bosdyn_client-3.3.2.dist-info → bosdyn_client-4.0.1.dist-info}/WHEEL +1 -1
  54. bosdyn/client/log_annotation.py +0 -359
  55. bosdyn_client-3.3.2.dist-info/RECORD +0 -90
  56. {bosdyn_client-3.3.2.dist-info → bosdyn_client-4.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,105 @@
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
+ """Helpers for working with DAQ plugins and signals.proto."""
8
+ from bosdyn.api.alerts_pb2 import AlertData
9
+ from bosdyn.api.data_acquisition_pb2 import LiveDataResponse
10
+ from bosdyn.api.signals_pb2 import AlertConditionSpec, Signal, SignalData, SignalSpec
11
+ from bosdyn.client.units_helpers import units_to_string
12
+
13
+
14
+ def build_max_alert_spec(value: float, severity: AlertData.SeverityLevel) -> AlertConditionSpec:
15
+ """Builds a max AlertConditionSpec.
16
+
17
+ Args:
18
+ value(float): Max threshold.
19
+ severity(str): Severity of alert.
20
+ Returns:
21
+ AlertConditionSpec
22
+ """
23
+ alert = AlertConditionSpec()
24
+ alert.max = value
25
+ alert.alert_data.severity = severity
26
+ return alert
27
+
28
+
29
+ def build_simple_signal(name: str, value: float, units: str, max_warning: float = None,
30
+ max_critical: float = None) -> Signal:
31
+ """Builds a simple signal with a float value, string units, and optional max alerts.
32
+
33
+ Args:
34
+ name(str): Name of the signal.
35
+ value(float): Signal data value.
36
+ units(str): Simple units.
37
+ max_warning(float): Max warning threshold.
38
+ max_critical(float): Max critical threshold.
39
+ Returns:
40
+ Signal
41
+ """
42
+ # Bundle spec/data together as a signal.
43
+ signal = Signal()
44
+
45
+ # Setup signal specification.
46
+ signal_spec = signal.signal_spec
47
+ signal_spec.info.name = name
48
+ signal_spec.sensor.units.name = units
49
+ if max_warning:
50
+ alert_spec_1 = build_max_alert_spec(max_warning,
51
+ AlertData.SeverityLevel.SEVERITY_LEVEL_WARN)
52
+ signal_spec.alerts.append(alert_spec_1)
53
+ if max_critical:
54
+ alert_spec_2 = build_max_alert_spec(max_critical,
55
+ AlertData.SeverityLevel.SEVERITY_LEVEL_CRITICAL)
56
+ signal_spec.alerts.append(alert_spec_2)
57
+
58
+ # Add value to signal data.
59
+ signal.signal_data.data.double = value
60
+
61
+ return signal
62
+
63
+
64
+ def build_capability_live_data(signals: dict,
65
+ capability_name: str) -> LiveDataResponse.CapabilityLiveData:
66
+ """Takes a dictionary of signals and copies them into a CapabilityLiveData message.
67
+
68
+ Args:
69
+ signals(dict[str, Signal]): A dictionary of signal id to Signal.
70
+ capability_name(str): The capability name.
71
+ Returns:
72
+ CapabilityLiveData
73
+ """
74
+ capability_live_data = LiveDataResponse.CapabilityLiveData()
75
+ capability_live_data.name = capability_name
76
+ capability_live_data.status = LiveDataResponse.CapabilityLiveData.Status.STATUS_OK
77
+ for signal_id, signal in signals.items():
78
+ capability_live_data.signals[signal_id].CopyFrom(signal)
79
+ return capability_live_data
80
+
81
+
82
+ def build_live_data_response(live_data_capabilities: list) -> LiveDataResponse:
83
+ """Takes a list of CapabilityLiveData and adds them to a LiveDataResponse.
84
+
85
+ Args:
86
+ live_data_capabilities(list[LiveDataResponse.CapabilityLiveData]): A list of CapabilityLiveData.
87
+ Returns:
88
+ LiveDataResponse
89
+ """
90
+ response = LiveDataResponse()
91
+ response.live_data.extend(live_data_capabilities)
92
+ return response
93
+
94
+
95
+ def get_data(signal_data: SignalData):
96
+ """Checks type of SignalData and returns the value.
97
+
98
+ Args:
99
+ signal_data(SignalData): Signal data.
100
+ Returns:
101
+ The data value.
102
+ """
103
+ return getattr(signal_data.data, signal_data.data.WhichOneof('value'), None)
104
+
105
+
@@ -119,7 +119,9 @@ class CompositorClient(BaseClient):
119
119
  kwargs: extra arguments for controlling RPC details.
120
120
  """
121
121
  coords = compositor_pb2.IrMeterOverlay.NormalizedCoordinates(x=x, y=y)
122
- overlay = compositor_pb2.IrMeterOverlay(enable=enable, meter=[coords], unit=unit)
122
+ # setting both coords and meter fields for backwards compatibility
123
+ overlay = compositor_pb2.IrMeterOverlay(enable=enable, coords=coords, meter=[coords],
124
+ unit=unit)
123
125
  request = compositor_pb2.SetIrMeterOverlayRequest(overlay=overlay)
124
126
  return self.call(self._stub.SetIrMeterOverlay, request, self._return_response,
125
127
  self._compositor_error_from_response, copy_request=False, **kwargs)
@@ -127,7 +129,9 @@ class CompositorClient(BaseClient):
127
129
  def set_ir_meter_overlay_async(self, x, y, enable, unit, **kwargs):
128
130
  """Async version of set_ir_meter_overlay()"""
129
131
  coords = compositor_pb2.IrMeterOverlay.NormalizedCoordinates(x=x, y=y)
130
- overlay = compositor_pb2.IrMeterOverlay(enable=enable, meter=[coords], unit=unit)
132
+ # setting both coords and meter fields for backwards compatibility
133
+ overlay = compositor_pb2.IrMeterOverlay(enable=enable, coords=coords, meter=[coords],
134
+ unit=unit)
131
135
  request = compositor_pb2.SetIrMeterOverlayRequest(overlay=overlay)
132
136
  return self.call_async(self._stub.SetIrMeterOverlay, request, self._return_response,
133
137
  self._compositor_error_from_response, copy_request=False, **kwargs)
@@ -160,8 +160,18 @@ class PtzClient(BaseClient):
160
160
  SetPtzFocusStateResponse indicating whether the call was successful
161
161
  """
162
162
 
163
- request = self._make_set_ptz_focus_state_request(focus_mode, distance, focus_position)
164
-
163
+ if focus_position is not None:
164
+ focus_position_val = Int32Value(value=focus_position)
165
+ approx_distance = None
166
+ elif distance is not None:
167
+ approx_distance = FloatValue(value=distance)
168
+ focus_position_val = None
169
+ else:
170
+ raise ValueError("One of distance or focus_position must be specified.")
171
+
172
+ ptz_focus_state = ptz_pb2.PtzFocusState(mode=focus_mode, approx_distance=approx_distance,
173
+ focus_position=focus_position_val)
174
+ request = ptz_pb2.SetPtzFocusStateRequest(focus_state=ptz_focus_state)
165
175
  return self.call(self._stub.SetPtzFocusState, request,
166
176
  self._set_ptz_focus_state_from_response, common_header_errors,
167
177
  copy_request=False, **kwargs)
@@ -169,22 +179,22 @@ class PtzClient(BaseClient):
169
179
  def set_ptz_focus_state_async(self, focus_mode, distance=None, focus_position=None, **kwargs):
170
180
  """Async version of set_ptz_focus_state()"""
171
181
 
172
- request = self._make_set_ptz_focus_state_request(focus_mode, distance, focus_position)
173
-
182
+ if focus_position is not None:
183
+ focus_position_val = Int32Value(value=focus_position)
184
+ approx_distance = None
185
+ elif distance is not None:
186
+ approx_distance = FloatValue(value=distance)
187
+ focus_position_val = None
188
+ else:
189
+ raise ValueError("One of distance or focus_position must be specified.")
190
+
191
+ ptz_focus_state = ptz_pb2.PtzFocusState(mode=focus_mode, approx_distance=approx_distance,
192
+ focus_position=focus_position_val)
193
+ request = ptz_pb2.SetPtzFocusStateRequest(focus_state=ptz_focus_state)
174
194
  return self.call_async(self._stub.SetPtzFocusState, request,
175
195
  self._set_ptz_focus_state_from_response, common_header_errors,
176
196
  copy_request=False, **kwargs)
177
197
 
178
- @staticmethod
179
- def _make_set_ptz_focus_state_request(focus_mode, distance, focus_position):
180
- request = ptz_pb2.SetPtzFocusStateRequest()
181
- request.focus_state.mode = focus_mode
182
- if focus_position is not None:
183
- request.focus_state.focus_position.value = focus_position
184
- elif distance is not None:
185
- request.focus_state.approx_distance.value = distance
186
- return request
187
-
188
198
  @staticmethod
189
199
  def _list_ptz_from_response(response):
190
200
  return response.ptzs
@@ -103,6 +103,46 @@ class CameraCalibrationTimedOutError(Exception):
103
103
  """Timed out waiting for SUCCESS response from calibration."""
104
104
 
105
105
 
106
+ class GripperCameraCalibrationResponseError(SpotCheckError):
107
+ """General class of errors for gripper camera calibration routines."""
108
+
109
+
110
+ class GripperCameraCalibrationUserCanceledError(GripperCameraCalibrationResponseError):
111
+ """API client canceled calibration."""
112
+
113
+
114
+ class GripperCameraCalibrationPowerError(GripperCameraCalibrationResponseError):
115
+ """The robot is not powered on."""
116
+
117
+
118
+ class GripperCameraCalibrationLeaseError(GripperCameraCalibrationResponseError):
119
+ """The Lease is invalid."""
120
+
121
+
122
+ class GripperCameraCalibrationTargetNotCenteredError(GripperCameraCalibrationResponseError):
123
+ """Invalid starting configuration of robot."""
124
+
125
+
126
+ class GripperCameraCalibrationTargetUpsideDownError(GripperCameraCalibrationResponseError):
127
+ """The target is incorrectly oriented."""
128
+
129
+
130
+ class GripperCameraCalibrationCalibrationError(GripperCameraCalibrationResponseError):
131
+ """Calibration algorithm failure occurred."""
132
+
133
+
134
+ class GripperCameraCalibrationInitializationError(GripperCameraCalibrationResponseError):
135
+ """Initialization error occurred ."""
136
+
137
+
138
+ class GripperCameraCalibrationInternalError(GripperCameraCalibrationResponseError):
139
+ """Internal error occurred ."""
140
+
141
+
142
+ class GripperCameraCalibrationStuckError(GripperCameraCalibrationResponseError):
143
+ """Timed out waiting for robot to reach goal pose."""
144
+
145
+
106
146
  class SpotCheckClient(BaseClient):
107
147
  """A client for verifying robot health and running calibration routines."""
108
148
  default_service_name = 'spot-check'
@@ -170,6 +210,42 @@ class SpotCheckClient(BaseClient):
170
210
  return self.call_async(self._stub.CameraCalibrationFeedback, request, None,
171
211
  _calibration_feedback_error_from_response, **kwargs)
172
212
 
213
+ def gripper_camera_calibration_command(self, request, **kwargs):
214
+ """Issue a gripper camera calibration command to the robot.
215
+
216
+ Raises:
217
+ Error on header error or lease use result error.
218
+ """
219
+ return self.call(self._stub.GripperCameraCalibrationCommand, request, None,
220
+ _gripper_calibration_command_error_from_response, **kwargs)
221
+
222
+ def gripper_camera_calibration_command_async(self, request, **kwargs):
223
+ """Async version of gripper_camera_calibration_command()
224
+
225
+ Raises:
226
+ Error on header error or lease use result error.
227
+ """
228
+ return self.call_async(self._stub.GripperCameraCalibrationCommand, request, None,
229
+ _gripper_calibration_command_error_from_response, **kwargs)
230
+
231
+ def gripper_camera_calibration_feedback(self, request, **kwargs):
232
+ """Check the current status of gripper camera calibration.
233
+
234
+ Raises:
235
+ GripperCameraCalibrationResponseError on any feedback error.
236
+ """
237
+ return self.call(self._stub.GripperCameraCalibrationFeedback, request, None,
238
+ _gripper_calibration_feedback_error_from_response, **kwargs)
239
+
240
+ def gripper_camera_calibration_feedback_async(self, request, **kwargs):
241
+ """Async version of gripper_camera_calibration_feedback()
242
+
243
+ Raises:
244
+ GripperCameraCalibrationResponseError on any feedback error.
245
+ """
246
+ return self.call_async(self._stub.GripperCameraCalibrationFeedback, request, None,
247
+ _gripper_calibration_feedback_error_from_response, **kwargs)
248
+
173
249
 
174
250
  def run_spot_check(spot_check_client, lease, timeout_sec=212, update_frequency=0.25, verbose=False):
175
251
  """Run full spot check routine. The robot should be sitting on flat ground when this routine is
@@ -364,3 +440,87 @@ def _cal_status_error_from_response(response):
364
440
  status_to_error=_CAL_STATUS_TO_ERROR)
365
441
 
366
442
 
443
+
444
+
445
+ # Gripper Camera calibration error handlers.
446
+ @handle_common_header_errors
447
+ @handle_lease_use_result_errors
448
+ def _gripper_calibration_command_error_from_response(response):
449
+ return None
450
+
451
+
452
+ @handle_common_header_errors
453
+ def _gripper_calibration_feedback_error_from_response(response):
454
+ # Special handling of lease case.
455
+ if response.status == spot_check_pb2.GripperCameraCalibrationFeedbackResponse.STATUS_LEASE_ERROR:
456
+ return LeaseUseError(response, None)
457
+ return _gcal_status_error_from_response(response)
458
+
459
+
460
+ _GCAL_STATUS_TO_ERROR = collections.defaultdict(lambda:
461
+ (GripperCameraCalibrationResponseError, None))
462
+ _GCAL_STATUS_TO_ERROR.update({ # noqa
463
+ spot_check_pb2.GripperCameraCalibrationFeedbackResponse.STATUS_SUCCESS: (
464
+ None,
465
+ None,
466
+ ),
467
+ spot_check_pb2.GripperCameraCalibrationFeedbackResponse.STATUS_PROCESSING: (
468
+ None,
469
+ None,
470
+ ),
471
+ spot_check_pb2.GripperCameraCalibrationFeedbackResponse.STATUS_USER_CANCELED: (
472
+ GripperCameraCalibrationUserCanceledError,
473
+ GripperCameraCalibrationUserCanceledError.__doc__,
474
+ ),
475
+ spot_check_pb2.GripperCameraCalibrationFeedbackResponse.STATUS_NEVER_RUN: (
476
+ None,
477
+ None,
478
+ ),
479
+ spot_check_pb2.GripperCameraCalibrationFeedbackResponse.STATUS_POWER_ERROR: (
480
+ GripperCameraCalibrationPowerError,
481
+ GripperCameraCalibrationPowerError.__doc__,
482
+ ),
483
+ spot_check_pb2.GripperCameraCalibrationFeedbackResponse.STATUS_LEASE_ERROR: (
484
+ GripperCameraCalibrationLeaseError,
485
+ GripperCameraCalibrationLeaseError.__doc__,
486
+ ),
487
+
488
+ spot_check_pb2.GripperCameraCalibrationFeedbackResponse.STATUS_TARGET_NOT_CENTERED: (
489
+ GripperCameraCalibrationTargetNotCenteredError,
490
+ GripperCameraCalibrationTargetNotCenteredError.__doc__,
491
+ ),
492
+ spot_check_pb2.GripperCameraCalibrationFeedbackResponse.STATUS_TARGET_NOT_IN_VIEW: (
493
+ GripperCameraCalibrationTargetNotCenteredError,
494
+ GripperCameraCalibrationTargetNotCenteredError.__doc__,
495
+ ),
496
+ spot_check_pb2.GripperCameraCalibrationFeedbackResponse.STATUS_TARGET_UPSIDE_DOWN: (
497
+ GripperCameraCalibrationTargetUpsideDownError,
498
+ GripperCameraCalibrationTargetUpsideDownError.__doc__,
499
+ ),
500
+ spot_check_pb2.GripperCameraCalibrationFeedbackResponse.STATUS_INITIALIZATION_ERROR: (
501
+ GripperCameraCalibrationInitializationError,
502
+ GripperCameraCalibrationInitializationError.__doc__,
503
+ ),
504
+ spot_check_pb2.GripperCameraCalibrationFeedbackResponse.STATUS_INTERNAL_ERROR: (
505
+ GripperCameraCalibrationInternalError,
506
+ GripperCameraCalibrationInternalError.__doc__,
507
+ ),
508
+ spot_check_pb2.GripperCameraCalibrationFeedbackResponse.STATUS_STUCK: (
509
+ GripperCameraCalibrationStuckError,
510
+ GripperCameraCalibrationStuckError.__doc__,
511
+ ),
512
+ })
513
+
514
+
515
+ @handle_unset_status_error(
516
+ unset="STATUS_UNKNOWN",
517
+ statustype=spot_check_pb2.GripperCameraCalibrationFeedbackResponse,
518
+ )
519
+ def _gcal_status_error_from_response(response):
520
+ """Return a custom exception based on response, None if no error."""
521
+ return error_factory(
522
+ response,
523
+ response.status,
524
+ status_to_string=spot_check_pb2.GripperCameraCalibrationFeedbackResponse.Status.Name,
525
+ status_to_error=_GCAL_STATUS_TO_ERROR,
526
+ )
@@ -18,7 +18,7 @@ from google.protobuf import duration_pb2
18
18
 
19
19
  from bosdyn.api import time_sync_pb2, time_sync_service_pb2_grpc
20
20
  from bosdyn.api.time_range_pb2 import TimeRange
21
- from bosdyn.util import (RobotTimeConverter, now_nsec, nsec_to_timestamp, parse_timespan,
21
+ from bosdyn.util import (RobotTimeConverter, now_nsec, now_sec, nsec_to_timestamp, parse_timespan,
22
22
  set_timestamp_from_nsec, timestamp_to_nsec)
23
23
 
24
24
  from .common import BaseClient, common_header_errors
@@ -342,8 +342,8 @@ class TimeSyncThread:
342
342
  # When time-sync service is not yet ready, poll it at this interval
343
343
  TIME_SYNC_SERVICE_NOT_READY_INTERVAL_SEC = 5
344
344
 
345
- def __init__(self, time_sync_client):
346
- self._time_sync_endpoint = TimeSyncEndpoint(time_sync_client)
345
+ def __init__(self, time_sync_client, time_sync_endpoint=None):
346
+ self._time_sync_endpoint = time_sync_endpoint or TimeSyncEndpoint(time_sync_client)
347
347
  self._lock = Lock()
348
348
  self._locked_time_sync_interval_sec = self.DEFAULT_TIME_SYNC_INTERVAL_SEC
349
349
  self._locked_should_exit = False # Used to tell the thread to stop running.
@@ -412,11 +412,11 @@ class TimeSyncThread:
412
412
  """
413
413
  if self.has_established_time_sync:
414
414
  return
415
- end_time_sec = time.time() + timeout_sec
415
+ end_time_sec = now_sec() + timeout_sec
416
416
  while not self.stopped:
417
417
  if self.endpoint.has_established_time_sync:
418
418
  return
419
- if time.time() > end_time_sec:
419
+ if now_sec() > end_time_sec:
420
420
  raise TimedOutError
421
421
  time.sleep(0.1)
422
422
  thread_exc = self.thread_exception
@@ -0,0 +1,39 @@
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
+ """Helpers for working with units.proto."""
8
+
9
+ from bosdyn.api import units_pb2
10
+
11
+ TEMPERATURE_NAMES = {
12
+ units_pb2.TEMPERATURE_KELVIN: "K",
13
+ units_pb2.TEMPERATURE_CELSIUS: "°C",
14
+ units_pb2.TEMPERATURE_FAHRENHEIT: "°F"
15
+ }
16
+
17
+ PRESSURE_NAMES = {
18
+ units_pb2.PRESSURE_PSI: "psi",
19
+ units_pb2.PRESSURE_KPA: "kPa",
20
+ units_pb2.PRESSURE_BAR: "bar"
21
+ }
22
+
23
+
24
+ def units_to_string(units: units_pb2.Units):
25
+ """Gets the units in string form to use for display. Ex: TEMPERATURE_KELVIN = "K"
26
+
27
+ Args:
28
+ units(Units): Populate units message.
29
+
30
+ Returns:
31
+ String
32
+ """
33
+ if units.HasField("temp"):
34
+ return TEMPERATURE_NAMES.get(units.temp, "")
35
+ if units.HasField("press"):
36
+ return PRESSURE_NAMES.get(units.press, "")
37
+ if units.HasField("name"):
38
+ return units.name
39
+ return ""
bosdyn/client/util.py CHANGED
@@ -11,11 +11,14 @@ import getpass
11
11
  import glob
12
12
  import logging
13
13
  import os
14
+ import pathlib
14
15
  import signal
15
16
  import sys
16
17
  import threading
17
18
  import time
18
19
  from concurrent import futures
20
+ from secrets import token_urlsafe
21
+ from uuid import uuid4
19
22
 
20
23
  import google.protobuf.descriptor
21
24
  import grpc
@@ -25,6 +28,7 @@ import bosdyn.client.server_util
25
28
  from bosdyn.client.auth import InvalidLoginError, InvalidTokenError
26
29
  from bosdyn.client.channel import generate_channel_options
27
30
  from bosdyn.client.exceptions import Error
31
+ from bosdyn.deprecated import moved_to
28
32
 
29
33
  _LOGGER = logging.getLogger(__name__)
30
34
 
@@ -254,7 +258,7 @@ def add_common_arguments(parser, credentials_no_warn=False):
254
258
 
255
259
 
256
260
  def read_payload_credentials(filename):
257
- """Read the guid and secret from a file. The file should have the guid and secret
261
+ """Read the guid and secret from a file that already exists. The file should have the guid and secret
258
262
  as the first and second lines in the file.
259
263
 
260
264
  Args:
@@ -275,6 +279,40 @@ def read_payload_credentials(filename):
275
279
  return guid, secret
276
280
 
277
281
 
282
+ def read_or_create_payload_credentials(filename):
283
+ """
284
+ Only for use when attempting to register a payload. If simply trying to authenticate,
285
+ use get_guid_or_secret or read_payload_credentials instead.
286
+
287
+ When registering, attempt to read the payload's guid and secret from the specified file.
288
+ If this file exists, it should have the guid and secret as the first and second lines in the
289
+ file. If the file does not exist, this function creates a valid credentials file at filename.
290
+
291
+ Args:
292
+ filename: Name of the file to read. Its parent directories should already exist and
293
+ it should have the right permissions to be read by the payload registration
294
+ service. If running on a CORE I/O, also ensure that this location is mounted
295
+ as a volume to the CORE I/O's /data or /persist locations.
296
+
297
+ Returns:
298
+ Tuple of (guid, secret)
299
+
300
+ Raises:
301
+ OSError if the credential file cannot be read.
302
+ ValueError if the guid or secret are missing from the file.
303
+ """
304
+ try:
305
+ return read_payload_credentials(filename)
306
+ except (OSError, ValueError):
307
+ # If we can't read the credentials, write them assuming that parent directories exist.
308
+ guid = str(uuid4())
309
+ secret = token_urlsafe(16)
310
+ with open(filename, 'w') as credentials_file:
311
+ credentials_file.write(guid + "\n")
312
+ credentials_file.write(secret)
313
+ return guid, secret
314
+
315
+
278
316
  def get_guid_and_secret(parsed_options):
279
317
  """Get the guid and secret for a payload, based on the options that were added
280
318
  via add_payload_credentials_arguments().
@@ -289,12 +327,30 @@ def get_guid_and_secret(parsed_options):
289
327
  OSError if the credential file cannot be read.
290
328
  Exception if no applicable arguments are given.
291
329
  """
292
- if parsed_options.guid or parsed_options.secret:
330
+ if (hasattr(parsed_options, "guid") and
331
+ parsed_options.guid) or (hasattr(parsed_options, "secret") and parsed_options.secret):
293
332
  return parsed_options.guid, parsed_options.secret
294
333
  if parsed_options.payload_credentials_file:
295
334
  return read_payload_credentials(parsed_options.payload_credentials_file)
296
335
  raise Exception('No payload credentials provided. Use --guid and --secret'
297
- ' or --payload-credentials-file.')
336
+ ' or --payload-credentials-file. The latter in conjunction with'
337
+ ' read_or_create_payload_credentials is recommended for easy'
338
+ ' management of unique per-robot credentials')
339
+
340
+
341
+ def add_payload_credentials_file_argument(parser):
342
+ """Add argument for payload_credentials_file to an ArgumentParser or argument group.
343
+ This file is where the payload's GUID and secret are stored. The GUID and secret can
344
+ be securely generated on a per-robot basis and written to this file with
345
+ read_or_create_payload_credentials(filename).
346
+
347
+ Args:
348
+ parser: Argument parser object
349
+ """
350
+ parser.add_argument(
351
+ '--payload-credentials-file', help=
352
+ 'File from which to read payload guid and secret. Preferred in conjunction with read_or_create_payload_credentials for easy management of unique per-robot credentials'
353
+ )
298
354
 
299
355
 
300
356
  def add_payload_credentials_arguments(parser, required=True):
@@ -308,8 +364,7 @@ def add_payload_credentials_arguments(parser, required=True):
308
364
  group = parser.add_mutually_exclusive_group(required=required)
309
365
  group.add_argument('--guid', help='Unique GUID of the payload.')
310
366
  parser.add_argument('--secret', help='Secret of the payload.')
311
- group.add_argument('--payload-credentials-file',
312
- help='File from which to read payload guid and secret')
367
+ add_payload_credentials_file_argument(group)
313
368
 
314
369
 
315
370
  def add_service_hosting_arguments(parser):
@@ -336,11 +391,19 @@ def add_service_endpoint_arguments(parser):
336
391
  ' e.g. "192.168.50.5"')
337
392
 
338
393
 
339
- @deprecated(reason='App tokens are no longer in use. Authorization is now handled via licenses.',
340
- version='2.0.1', action="always")
341
- def default_app_token_path():
342
- """Do nothing, this method is kept only to maintain backwards compatibility."""
343
- return
394
+ def safe_pb_enum_to_string(value, pb_enum_obj):
395
+ """Safe wrapper to convert a protobuf enum object to its string representation.
396
+ Avoids throughing an exception if the status is unknown by the enum object.
397
+
398
+ Args:
399
+ value: The enum value to convert
400
+ pb_enum_obj: The protobuf enum object to decode the value
401
+ """
402
+ try:
403
+ return pb_enum_obj.Name(value)
404
+ except ValueError:
405
+ pass
406
+ return '<unknown> (value: {})'.format(value)
344
407
 
345
408
 
346
409
  @deprecated(reason='The GrpcServiceRunner class helper has moved to server_util.py. Please use '
@@ -408,57 +471,30 @@ class GrpcServiceRunner(object):
408
471
 
409
472
 
410
473
 
411
- populate_response_header = deprecated(
412
- reason='The populate_response_header helper has moved to '
413
- 'server_util.py. Please use bosdyn.client.server_util.populate_response_header.',
414
- version='3.0.0', action="always")(bosdyn.client.server_util.populate_response_header)
415
-
416
- strip_large_bytes_fields = deprecated(
417
- reason='The strip_large_bytes_fields helper has moved to '
418
- 'server_util.py. Please use bosdyn.client.server_util.strip_large_bytes_fields.',
419
- version='3.0.0', action="always")(bosdyn.client.server_util.strip_large_bytes_fields)
420
-
421
- get_bytes_field_whitelist = deprecated(
422
- reason='The get_bytes_field_whitelist helper has moved to '
423
- 'server_util.py. Please use bosdyn.client.server_util.get_bytes_field_allowlist.',
424
- version='3.0.0', action="always")(bosdyn.client.server_util.get_bytes_field_allowlist)
425
-
426
- strip_image_response = deprecated(
427
- reason='The strip_image_response helper has moved to '
428
- 'server_util.py. Please use bosdyn.client.server_util.strip_image_response.', version='3.0.0',
429
- action="always")(bosdyn.client.server_util.strip_image_response)
430
-
431
- strip_get_image_response = deprecated(
432
- reason='The strip_get_image_response helper has moved to '
433
- 'server_util.py. Please use bosdyn.client.server_util.strip_get_image_response.',
434
- version='3.0.0', action="always")(bosdyn.client.server_util.strip_get_image_response)
435
-
436
- strip_local_grid_responses = deprecated(
437
- reason='The strip_local_grid_responses helper has moved to '
438
- 'server_util.py. Please use bosdyn.client.server_util.strip_local_grid_responses.',
439
- version='3.0.0', action="always")(bosdyn.client.server_util.strip_local_grid_responses)
440
-
441
- strip_store_image_request = deprecated(
442
- reason='The strip_store_image_request helper has moved to '
443
- 'server_util.py. Please use bosdyn.client.server_util.strip_store_image_request.',
444
- version='3.0.0', action="always")(bosdyn.client.server_util.strip_store_image_request)
445
-
446
- strip_store_data_request = deprecated(
447
- reason='The strip_store_data_request helper has moved to '
448
- 'server_util.py. Please use bosdyn.client.server_util.strip_store_data_request.',
449
- version='3.0.0', action="always")(bosdyn.client.server_util.strip_store_data_request)
450
-
451
- strip_record_signal_tick = deprecated(
452
- reason='The strip_record_signal_tick helper has moved to '
453
- 'server_util.py. Please use bosdyn.client.server_util.strip_record_signal_tick.',
454
- version='3.0.0', action="always")(bosdyn.client.server_util.strip_record_signal_tick)
455
-
456
- strip_record_data_blob = deprecated(
457
- reason='The strip_record_data_blob helper has moved to '
458
- 'server_util.py. Please use bosdyn.client.server_util.strip_record_data_blob.', version='3.0.0',
459
- action="always")(bosdyn.client.server_util.strip_record_data_blob)
460
-
461
- strip_log_annotation = deprecated(
462
- reason='The strip_log_annotation helper has moved to '
463
- 'server_util.py. Please use bosdyn.client.server_util.strip_log_annotation.', version='3.0.0',
464
- action="always")(bosdyn.client.server_util.strip_log_annotation)
474
+ populate_response_header = moved_to(bosdyn.client.server_util.populate_response_header,
475
+ version='3.0.0')
476
+
477
+ strip_large_bytes_fields = moved_to(bosdyn.client.server_util.strip_large_bytes_fields,
478
+ version='3.0.0')
479
+
480
+ get_bytes_field_whitelist = moved_to(bosdyn.client.server_util.get_bytes_field_allowlist,
481
+ version='3.0.0')
482
+
483
+ strip_image_response = moved_to(bosdyn.client.server_util.strip_image_response, version='3.0.0')
484
+
485
+ strip_get_image_response = moved_to(bosdyn.client.server_util.strip_get_image_response,
486
+ version='3.0.0')
487
+
488
+ strip_local_grid_responses = moved_to(bosdyn.client.server_util.strip_local_grid_responses,
489
+ version='3.0.0')
490
+
491
+ strip_store_image_request = moved_to(bosdyn.client.server_util.strip_store_image_request,
492
+ version='3.0.0')
493
+
494
+ strip_store_data_request = moved_to(bosdyn.client.server_util.strip_store_data_request,
495
+ version='3.0.0')
496
+
497
+ strip_record_signal_tick = moved_to(bosdyn.client.server_util.strip_record_signal_tick,
498
+ version='3.0.0')
499
+
500
+ strip_record_data_blob = moved_to(bosdyn.client.server_util.strip_record_data_blob, version='3.0.0')