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
bosdyn/client/image.py CHANGED
@@ -7,6 +7,7 @@
7
7
  """For clients to use the image service."""
8
8
  import collections
9
9
  import os
10
+ import warnings
10
11
 
11
12
  import numpy as np
12
13
 
@@ -336,7 +337,8 @@ def pixel_to_camera_space(image_proto, pixel_x, pixel_y, depth=1.0):
336
337
  the (u,v) pixel coordinates.
337
338
 
338
339
  Args:
339
- image_proto (image_pb2.Image): The image in which the pixel coordinates are from
340
+ image_proto (image_pb2.ImageSource): The image source proto which the pixel coordinates are from.
341
+ Use of image_pb2.ImageCaptureAndSource or image_pb2.ImageResponse types here have been deprecated.
340
342
  pixel_x (int): x-coordinate.
341
343
  pixel_y (int): y-coordinate.
342
344
  depth (double): The depth from the camera to the point of interest.
@@ -345,14 +347,25 @@ def pixel_to_camera_space(image_proto, pixel_x, pixel_y, depth=1.0):
345
347
  An (x,y,z) tuple representing the pixel point of interest now described as a point
346
348
  in the camera frame.
347
349
  """
348
- if not image_proto.source.HasField('pinhole'):
350
+ if 'source' in image_proto.ListFields() or isinstance(
351
+ image_proto, image_pb2.ImageResponse) or isinstance(image_proto,
352
+ image_pb2.ImageCaptureAndSource):
353
+ warnings.warn(
354
+ "Use of image_pb2.ImageCaptureAndSource or image_pb2.ImageResponse types for image_proto"
355
+ " argument have been deprecated, use image_pb2.ImageSource instead. version=4.0.0",
356
+ DeprecationWarning, stacklevel=2)
357
+ image_source = image_proto.source
358
+ else:
359
+ image_source = image_proto
360
+
361
+ if not image_source.HasField('pinhole'):
349
362
  raise ValueError('Requires a pinhole camera_model.')
350
363
 
351
- focal_x = image_proto.source.pinhole.intrinsics.focal_length.x
352
- principal_x = image_proto.source.pinhole.intrinsics.principal_point.x
364
+ focal_x = image_source.pinhole.intrinsics.focal_length.x
365
+ principal_x = image_source.pinhole.intrinsics.principal_point.x
353
366
 
354
- focal_y = image_proto.source.pinhole.intrinsics.focal_length.y
355
- principal_y = image_proto.source.pinhole.intrinsics.principal_point.y
367
+ focal_y = image_source.pinhole.intrinsics.focal_length.y
368
+ principal_y = image_source.pinhole.intrinsics.principal_point.y
356
369
 
357
370
  x_rt_camera = depth * (pixel_x - principal_x) / focal_x
358
371
  y_rt_camera = depth * (pixel_y - principal_y) / focal_y
@@ -403,7 +416,7 @@ def depth_image_to_pointcloud(image_response, min_dist=0, max_dist=1000):
403
416
  """Converts a depth image into a point cloud using the camera intrinsics. The point
404
417
  cloud is represented as a numpy array of (x,y,z) values. Requests can optionally filter
405
418
  the results based on the points distance to the image plane. A depth image is represented
406
- with an unsigned 16 bit integer and a scale factor to convert that distance to meters. In
419
+ with an unsigned 16-bit integer and a scale factor to convert that distance to meters. In
407
420
  addition, values of zero and 2^16 (uint 16 maximum) are used to represent invalid indices.
408
421
  A (min_dist * depth_scale) value that casts to an integer value <=0 will be assigned a
409
422
  value of 1 (the minimum representational distance). Similarly, a (max_dist * depth_scale)
@@ -65,9 +65,9 @@ class CameraInterface(ABC):
65
65
  threads/requests may try to call blocking_capture at the same time. A Lock (self.capture_lock) is provided
66
66
  as a convenience to help with this.
67
67
 
68
- Keyword Args:
68
+ Args:
69
69
  custom_params (service_customization_pb2.DictParam): Custom parameters defined by the image source
70
- affecting the resulting capture
70
+ affecting the resulting capture
71
71
  **kwargs: Other keyword arguments including future additions to the request that affect the capture.
72
72
  If an implementer is looking to use kwargs, it is expected that they update their
73
73
  blocking_capture function signature to accept named keyword arguments such as custom_params
@@ -181,7 +181,7 @@ class VisualImageSource():
181
181
 
182
182
  def create_capture_thread(self, custom_params=None):
183
183
  """Initialize a background thread to continuously capture images.
184
-
184
+
185
185
  """
186
186
  self.capture_thread = ImageCaptureThread(self.image_source_name, self.capture_function,
187
187
  custom_params=custom_params)
@@ -277,7 +277,7 @@ class VisualImageSource():
277
277
  Args:
278
278
  capture_func (CameraInterface.blocking_capture): The function capturing the image
279
279
  data and timestamp.
280
- custom_params (service_customization_pb2.DictParam): Custom parameters passed to the blocking
280
+ custom_params (service_customization_pb2.DictParam): Custom parameters passed to the blocking
281
281
  capture affecting the resulting capture
282
282
  **capture_func_kwargs: Other keyword arguments for the capture_func
283
283
  """
@@ -309,7 +309,7 @@ class VisualImageSource():
309
309
  """Retrieve the latest captured image and timestamp.
310
310
 
311
311
  Args:
312
- custom_params (service_customization_pb2.DictParam): Custom parameters passed to the object's
312
+ custom_params (service_customization_pb2.DictParam): Custom parameters passed to the object's
313
313
  capture_function affecting the resulting capture
314
314
  **capture_func_kwargs: Other keyword arguments for the capture_function
315
315
 
@@ -343,7 +343,7 @@ class VisualImageSource():
343
343
  image_req (image_pb2.ImageRequest): The image request associated with the image_data.
344
344
 
345
345
  Returns:
346
- image_pb2.ImageResponse.Status indicating if the decode succeeds, or image format conversion or
346
+ image_pb2.ImageResponse.Status indicating if the decoding succeeds, or image format conversion or
347
347
  pixel conversion failed. Throws a decode data fault if the image or
348
348
  pixels cannot be decoded to the desired format. Mutates the image_proto Image proto
349
349
  with the decoded data if successful.
@@ -402,7 +402,7 @@ class VisualImageSource():
402
402
  image_type (image_pb2.ImageType): The type of image (e.g. visual, depth).
403
403
  image_formats (image_pb2.Image.Format): The image formats supported (jpeg, raw)
404
404
  pixel_formats (image_pb2.Image.PixelFormat): The pixel formats supported
405
- param_spec (service_customization_pb2.DictParam.Spec): A set of custom parameters
405
+ param_spec (service_customization_pb2.DictParam.Spec): A set of custom parameters
406
406
  passed into this image source
407
407
  Returns:
408
408
  An ImageSource with the cols, rows, and image type populated.
@@ -431,7 +431,7 @@ class VisualImageSource():
431
431
  which returns the gain as a float.
432
432
  exposure (float | function): The exposure time for an image in seconds. This can be a fixed
433
433
  value or a function which returns the exposure time as a float.
434
- request_custom_params (service_customization_pb2.DictParam): Custom Params associated with the image
434
+ request_custom_params (service_customization_pb2.DictParam): Custom Params associated with the image
435
435
  request. Should not be 'None', but left as an option to accomodate old callers
436
436
  Returns:
437
437
  An instance of the protobuf CaptureParameters message.
@@ -457,11 +457,11 @@ class VisualImageSource():
457
457
  class ThreadCaptureOutput:
458
458
  """Small struct to represent the output of an ImageCaptureThread's get_latest_captured_image
459
459
  in a future-compatible way
460
-
460
+
461
461
  Args:
462
462
  is_valid (Boolean): Whether the latest capture uses the custom parameters and other arguments
463
463
  supplied in the latest request, and is therefore returned
464
- image (Any | None): If the latest capture is valid, the image data in any format
464
+ image (Any | None): If the latest capture is valid, the image data in any format
465
465
  (e.g. numpy, bytes, array)
466
466
  timestamp (float): The timestamp that the latest valid capture was taken
467
467
  """
@@ -547,7 +547,7 @@ class ImageCaptureThread():
547
547
  self.has_updated_capture = True
548
548
 
549
549
  def get_latest_captured_image(self, custom_params=None, **capture_func_kwargs):
550
- """Returns the last found image and timestamp in a ThreadCaptureOutput object if that
550
+ """Returns the last found image and timestamp in a ThreadCaptureOutput object if that
551
551
  image uses the latest params. Otherwise returns a ThreadCaptureOutput object with
552
552
  is_valid = False and capture/timestamp as None.
553
553
  """
@@ -567,8 +567,8 @@ class ImageCaptureThread():
567
567
  # Still, check for the custom_params argument to ensure we accomodate pre-3.3 direct implementations of ImageCaptureThreads
568
568
  if "custom_params" in inspect.signature(capture_func).parameters.keys():
569
569
  #If using a blocking capture function that takes in custom params or kwargs, one should supply those
570
- output_capture_function = lambda: capture_func(custom_params=custom_params, **
571
- capture_func_kwargs)
570
+ def output_capture_function():
571
+ return capture_func(custom_params=custom_params, **capture_func_kwargs)
572
572
  else:
573
573
  output_capture_function = capture_func
574
574
  return output_capture_function
@@ -664,7 +664,7 @@ class CameraBaseImageServicer(image_service_pb2_grpc.ImageServiceServicer):
664
664
  return response
665
665
 
666
666
  def _set_format_and_decode(self, image_data, img_proto, img_req):
667
- """Calls the image_decode_with_error_checking function, which returns a (Boolean, Boolean) if the decode succeeds."""
667
+ """Calls the image_decode_with_error_checking function, which returns a (Boolean, Boolean) if the decoding succeeds."""
668
668
  # This function should set the image data, pixel format, image format, and transform snapshot fields.
669
669
  return self.image_sources_mapped[
670
670
  img_req.image_source_name].image_decode_with_error_checking(
bosdyn/client/lease.py CHANGED
@@ -52,7 +52,7 @@ class ResourceAlreadyClaimedError(LeaseResponseError):
52
52
 
53
53
 
54
54
  class RevokedLeaseError(LeaseResponseError):
55
- """Lease is stale because the lease holder did not check in regularly enough."""
55
+ """Lease is stale because the lease-holder did not check in regularly enough."""
56
56
 
57
57
 
58
58
  class UnmanagedResourceError(LeaseResponseError):
@@ -135,14 +135,11 @@ class Lease(object):
135
135
 
136
136
  Args:
137
137
  lease_proto: bosdyn.api.Lease protobuf object.
138
+ Raises:
139
+ ValueError if lease_proto is not present or valid.
138
140
  """
139
141
 
140
142
  def __init__(self, lease_proto, ignore_is_valid_check=False):
141
- """Initializes a Lease object.
142
-
143
- Raises:
144
- ValueError if lease_proto is not present or valid.
145
- """
146
143
  if not ignore_is_valid_check and not self.is_valid_proto(lease_proto):
147
144
  raise ValueError('invalid lease_proto: {}'.format(lease_proto))
148
145
  self.lease_proto = lease_proto
@@ -289,6 +286,17 @@ class Lease(object):
289
286
 
290
287
 
291
288
  class LeaseState(object):
289
+ """State of lease ownership in the wallet.
290
+
291
+ Args:
292
+ lease_status(LeaseState.Status): The ownership status of the lease.
293
+ lease_owner(lease_pb2.LeaseOwner): The name of the owner of the lease.
294
+ lease (Lease): The original lease used to initialize the LeaseState, before
295
+ applying any subleasing/incrementing.
296
+ lease_current(Lease): The newest version of the lease (subleased, and
297
+ incremented from the original lease).
298
+ client_name(string): The name of the client using this lease.
299
+ """
292
300
 
293
301
  class Status(enum.Enum):
294
302
  UNOWNED = 0
@@ -306,16 +314,7 @@ class LeaseState(object):
306
314
 
307
315
  def __init__(self, lease_status, lease_owner=None, lease=None, lease_current=None,
308
316
  client_name=None):
309
- """
310
- Args:
311
- lease_status(LeaseState.Status): The ownership status of the lease.
312
- lease_owner(lease_pb2.LeaseOwner): The name of the owner of the lease.
313
- lease (Lease): The original lease used to initialize the LeaseState, before
314
- applying any subleasing/incrementing.
315
- lease_current(Lease): The newest version of the lease (subleased, and
316
- incremented from the original lease).
317
- client_name(string): The name of the client using this lease.
318
- """
317
+
319
318
  self.lease_status = lease_status
320
319
  self.lease_owner = lease_owner
321
320
  self.lease_original = lease
@@ -349,10 +348,16 @@ class LeaseState(object):
349
348
  """
350
349
  if lease_use_result.status == lease_use_result.STATUS_OLDER:
351
350
  if self.lease_current:
352
- attempted_lease = Lease(lease_use_result.attempted_lease)
353
- if attempted_lease.compare(self.lease_current) is Lease.CompareResult.SAME:
354
- return LeaseState(LeaseState.Status.OTHER_OWNER,
355
- lease_owner=lease_use_result.owner)
351
+ latest_known_lease = Lease(lease_use_result.latest_known_lease)
352
+ if latest_known_lease.is_valid_lease():
353
+ cmp = self.lease_current.compare(latest_known_lease)
354
+ if cmp is Lease.CompareResult.NEWER or cmp is Lease.CompareResult.SAME:
355
+ # The attempted lease was older, but the lease in the wallet has been updated
356
+ # in the meantime to something that is newer than what the robot has seen, so
357
+ # this OLDER result is no longer relevant.
358
+ return self
359
+ # The lease from the lease wallet was an older lease.
360
+ return LeaseState(LeaseState.Status.OTHER_OWNER, lease_owner=lease_use_result.owner)
356
361
  elif lease_use_result.status == lease_use_result.STATUS_WRONG_EPOCH:
357
362
  if self.lease_current:
358
363
  attempted_lease = Lease(lease_use_result.attempted_lease)
@@ -837,10 +842,10 @@ def add_lease_wallet_processors(client, lease_wallet, resource_list=None):
837
842
  class LeaseKeepAlive(object):
838
843
  """LeaseKeepAlive issues lease liveness checks on a background thread.
839
844
 
840
- The robot's lease system expects lease holders to check in at a regular
845
+ The robot's lease system expects lease-holders to check in at a regular
841
846
  cadence. If the check-ins do not happen, the robot will treat it as a
842
847
  communications loss. Typically this will result in the robot stopping,
843
- powering off, and the lease holder getting their lease revoked.
848
+ powering off, and the lease-holder getting their lease revoked.
844
849
 
845
850
  Using a LeaseKeepAlive object hides most of the details of issuing the
846
851
  lease liveness check. Developers can also manage liveness checks directly
@@ -268,13 +268,13 @@ class LeaseValidator:
268
268
 
269
269
  class LeaseValidatorResponseProcessor: # pylint: disable=too-few-public-methods
270
270
  """LeaseValidatorResponseProcessor updates the lease validator using the
271
- latest_known_lease from the response's LeaseUseResult."""
271
+ latest_known_lease from the response's LeaseUseResult.
272
+
273
+ Args:
274
+ lease_validator (LeaseValidator): validator for a specific robot to be updated.
275
+ """
272
276
 
273
277
  def __init__(self, lease_validator):
274
- """
275
- Args:
276
- lease_validator (LeaseValidator): validator for a specific robot to be updated.
277
- """
278
278
  self.lease_validator = lease_validator
279
279
 
280
280
  def mutate(self, response):
@@ -100,4 +100,4 @@ def _manipulation_api_feedback_error_from_response(response):
100
100
 
101
101
  @handle_common_header_errors
102
102
  def _grasp_override_command_error_from_response(response):
103
- return None
103
+ return None
@@ -34,7 +34,7 @@ class InvalidGraphError(MapProcessingServiceResponseError):
34
34
 
35
35
 
36
36
  class InvalidParamsError(MapProcessingServiceResponseError):
37
- """The parameters passed to the optimizer do not make sense (e.g negative weights)."""
37
+ """The parameters passed to the optimizer do not make sense (e.g. negative weights)."""
38
38
 
39
39
 
40
40
  class MaxIterationsError(MapProcessingServiceResponseError):
@@ -167,11 +167,13 @@ class MapProcessingServiceClient(BaseClient):
167
167
 
168
168
  @staticmethod
169
169
  def _build_process_anchoring_request(params, modify_anchoring_on_server,
170
- stream_intermediate_results, initial_hint):
170
+ stream_intermediate_results, initial_hint,
171
+ apply_gps_results):
171
172
  return map_processing_pb2.ProcessAnchoringRequest(
172
173
  params=params, initial_hint=initial_hint,
173
174
  modify_anchoring_on_server=modify_anchoring_on_server,
174
- stream_intermediate_results=stream_intermediate_results)
175
+ stream_intermediate_results=stream_intermediate_results,
176
+ apply_gps_result_to_waypoints_on_server=apply_gps_results)
175
177
 
176
178
  def process_topology(self, params, modify_map_on_server, **kwargs):
177
179
  """Process the topology of the map on the server, closing loops and producing a
@@ -196,7 +198,7 @@ class MapProcessingServiceClient(BaseClient):
196
198
  **kwargs)
197
199
 
198
200
  def process_anchoring(self, params, modify_anchoring_on_server, stream_intermediate_results,
199
- initial_hint=None, **kwargs):
201
+ initial_hint=None, apply_gps_results=False, **kwargs):
200
202
  """Process the anchoring of the map on the server, producing a metrically consistent anchoring.
201
203
 
202
204
  Args:
@@ -208,13 +210,16 @@ class MapProcessingServiceClient(BaseClient):
208
210
  iterations may be included in the response. If false, only the last iteration will be returned.
209
211
  initial_hint: Initial guess at some number of waypoints and world objects and their anchorings.
210
212
  This field is an AnchoringHint object (see map_processing.proto)
213
+ apply_gps_results: if true, the annotations of waypoints in the graph will be modified to include
214
+ the pose of each waypoint in a GPS centered frame, if the map has GPS (see map_processing.proto)
211
215
  Returns:
212
216
  The ProcessAnchoringResponse containing a new optimized anchoring.
213
217
  Raises:
214
218
  RpcError: Problem communicating with the robot
215
219
  """
216
220
  request = self._build_process_anchoring_request(params, modify_anchoring_on_server,
217
- stream_intermediate_results, initial_hint)
221
+ stream_intermediate_results, initial_hint,
222
+ apply_gps_results)
218
223
 
219
224
  return self.call(self._stub.ProcessAnchoring, request,
220
225
  value_from_response=_get_streamed_anchoring_response,
@@ -36,7 +36,7 @@ def angle_diff_degrees(a1, a2):
36
36
 
37
37
 
38
38
  class Vec2(object):
39
- """Class representing a two dimensional vector."""
39
+ """Class representing a two-dimensional vector."""
40
40
 
41
41
  def __init__(self, x, y):
42
42
  self.x = x
@@ -115,7 +115,7 @@ class Vec2(object):
115
115
 
116
116
 
117
117
  class Vec3(object):
118
- """Class representing a three dimensional vector."""
118
+ """Class representing a three-dimensional vector."""
119
119
 
120
120
  def __init__(self, x, y, z):
121
121
  self.x = x
@@ -183,6 +183,10 @@ class Vec3(object):
183
183
  """Converts the math_helpers.Vec3 into an output of the protobuf geometry_pb2.Vec3."""
184
184
  return geometry_pb2.Vec3(x=self.x, y=self.y, z=self.z)
185
185
 
186
+ def to_numpy(self):
187
+ """Converts the math_helpers.Vec3 into an output of numpy format."""
188
+ return numpy.array([self.x, self.y, self.z])
189
+
186
190
  def dot(self, other):
187
191
  if not isinstance(other, Vec3):
188
192
  raise TypeError("Can't dot types %s and %s." % (type(self), type(other)))
@@ -199,6 +203,11 @@ class Vec3(object):
199
203
  """Create a math_helpers.Vec3 from a geometry_pb2.Vec3 proto."""
200
204
  return Vec3(proto.x, proto.y, proto.z)
201
205
 
206
+ @staticmethod
207
+ def from_numpy(array):
208
+ """Create a math_helpers.Vec3 from a numpy array."""
209
+ return Vec3(array[0], array[1], array[2])
210
+
202
211
 
203
212
  class SE2Pose(object):
204
213
  """Class representing an SE2Pose with position and angle."""
@@ -382,7 +391,7 @@ class SE2Velocity(object):
382
391
  @staticmethod
383
392
  def from_vector(se2_vel_vector):
384
393
  """Converts a 3x1 velocity vector (of either a numpy array or a list) into a math_helpers.SE2Velocity object."""
385
- if type(se2_vel_vector) == list:
394
+ if isinstance(se2_vel_vector, list):
386
395
  if len(se2_vel_vector) != 3:
387
396
  # Must have 3 elements to be a complete SE2Velocity
388
397
  print("Velocity list must have 3 elements. The input has the wrong dimension of: " +
@@ -391,7 +400,7 @@ class SE2Velocity(object):
391
400
  else:
392
401
  return SE2Velocity(x=se2_vel_vector[0], y=se2_vel_vector[1],
393
402
  angular=se2_vel_vector[2])
394
- if type(se2_vel_vector) == numpy.ndarray:
403
+ if isinstance(se2_vel_vector, numpy.ndarray):
395
404
  if se2_vel_vector.shape[0] != 3:
396
405
  # Must have 3 elements to be a complete SE2Velocity
397
406
  print(
@@ -494,7 +503,7 @@ class SE3Velocity(object):
494
503
  @staticmethod
495
504
  def from_vector(se3_vel_vector):
496
505
  """Converts a 6x1 velocity vector (of either a numpy array or a list) into a math_helpers.SE3Velocity object."""
497
- if type(se3_vel_vector) == list:
506
+ if isinstance(se3_vel_vector, list):
498
507
  if len(se3_vel_vector) != 6:
499
508
  # Must have 6 elements to be a complete SE3Velocity
500
509
  print("Velocity list must have 6 elements. The input has the wrong dimension of: " +
@@ -504,7 +513,7 @@ class SE3Velocity(object):
504
513
  return SE3Velocity(lin_x=se3_vel_vector[0], lin_y=se3_vel_vector[1],
505
514
  lin_z=se3_vel_vector[2], ang_x=se3_vel_vector[3],
506
515
  ang_y=se3_vel_vector[4], ang_z=se3_vel_vector[5])
507
- if type(se3_vel_vector) == numpy.ndarray:
516
+ if isinstance(se3_vel_vector, numpy.ndarray):
508
517
  if se3_vel_vector.shape[0] != 6:
509
518
  # Must have 6 elements to be a complete SE3Velocity
510
519
  print(
@@ -526,7 +535,7 @@ class SE3Pose(object):
526
535
  self.z = z
527
536
  # Expect the declaration of math_helpers.SE3Pose to pass a math_helpers.Quat, however we will convert
528
537
  # a protobuf Quaternion into the math_helpers object as well.
529
- if type(rot) == geometry_pb2.Quaternion:
538
+ if isinstance(rot, geometry_pb2.Quaternion):
530
539
  rot = Quat.from_proto(rot)
531
540
  self.rot = rot
532
541
 
@@ -676,6 +685,7 @@ class SE3Pose(object):
676
685
  for the math_helper.SE3Pose."""
677
686
  return geometry_pb2.Vec3(x=self.x, y=self.y, z=self.z)
678
687
 
688
+
679
689
  @property
680
690
  def rotation(self):
681
691
  """Property to allow attribute access of the protobuf message field 'rotation' similar to the geometry_pb2.SE3Pose
@@ -1102,9 +1112,9 @@ def transform_se2velocity(a_adjoint_b_matrix, se2_velocity_in_b):
1102
1112
  Returns:
1103
1113
  math_helpers.SE2Velocity described in frame a. None if the input velocity is an unknown type.
1104
1114
  """
1105
- if type(se2_velocity_in_b) == geometry_pb2.SE2Velocity:
1115
+ if isinstance(se2_velocity_in_b, geometry_pb2.SE2Velocity):
1106
1116
  se2_velocity_in_b_vector = (SE2Velocity.from_proto(se2_velocity_in_b)).to_vector()
1107
- elif type(se2_velocity_in_b) == SE2Velocity:
1117
+ elif isinstance(se2_velocity_in_b, SE2Velocity):
1108
1118
  se2_velocity_in_b_vector = se2_velocity_in_b.to_vector()
1109
1119
  else:
1110
1120
  return None
@@ -1126,9 +1136,9 @@ def transform_se3velocity(a_adjoint_b_matrix, se3_velocity_in_b):
1126
1136
  Returns:
1127
1137
  math_helpers.SE3Velocity described in frame a. None if the input velocity is an unknown type.
1128
1138
  """
1129
- if type(se3_velocity_in_b) == geometry_pb2.SE3Velocity:
1139
+ if isinstance(se3_velocity_in_b, geometry_pb2.SE3Velocity):
1130
1140
  se3_velocity_in_b_vec = (SE3Velocity.from_proto(se3_velocity_in_b)).to_vector()
1131
- elif type(se3_velocity_in_b) == SE3Velocity:
1141
+ elif isinstance(se3_velocity_in_b, SE3Velocity):
1132
1142
  se3_velocity_in_b_vec = se3_velocity_in_b.to_vector()
1133
1143
  else:
1134
1144
  return None
@@ -0,0 +1,147 @@
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
+ """Clients for the metrics logging service."""
8
+ import collections
9
+ import threading
10
+
11
+ from google.protobuf.duration_pb2 import Duration
12
+ from google.protobuf.timestamp_pb2 import Timestamp
13
+
14
+ from bosdyn.api.metrics_logging import (absolute_metrics_pb2, metrics_logging_robot_pb2,
15
+ metrics_logging_robot_service_pb2_grpc)
16
+ from bosdyn.client.common import (BaseClient, common_header_errors, error_factory,
17
+ handle_common_header_errors, handle_unset_status_error)
18
+ from bosdyn.client.data_buffer import make_parameter
19
+ from bosdyn.client.exceptions import ResponseError
20
+
21
+
22
+ class MissingKeysError(ResponseError):
23
+ """Metrics requested from the metrics service did not exist."""
24
+
25
+
26
+ class UnableToOptOutError(ResponseError):
27
+ """Unable to opt-out of metrics logging due to invalid license permissions."""
28
+
29
+
30
+ class MetricsLoggingClient(BaseClient):
31
+ """A client for the metrics logging service on the robot.
32
+ """
33
+ default_service_name = 'metrics-logging'
34
+ service_type = 'bosdyn.api.metrics_logging.MetricsLoggingRobotService'
35
+
36
+ def __init__(self):
37
+ super(MetricsLoggingClient,
38
+ self).__init__(metrics_logging_robot_service_pb2_grpc.MetricsLoggingRobotServiceStub)
39
+
40
+ def get_metrics(self, keys=None, include_events=False, **kwargs):
41
+ """Get metrics from the robot.
42
+
43
+ Args:
44
+ keys(List[strings]): A list of strings representing the keys for metrics that should be returned.
45
+ include_events(bool): Whether events should be included in the response.
46
+
47
+ Returns:
48
+ The GetMetricsResponse response.
49
+
50
+ Raises:
51
+ RpcError: Problem communicating with the robot.
52
+ """
53
+ req = self._get_metrics_request(keys, include_events)
54
+ return self.call(self._stub.GetMetrics, req, None, _get_metrics_error_from_response,
55
+ copy_request=False, **kwargs)
56
+
57
+ def get_metrics_async(self, keys=None, include_events=False, **kwargs):
58
+ """Async version of get_metrics()."""
59
+ req = self._get_metrics_request(keys, include_events)
60
+ return self.call_async(self._stub.GetMetrics, req, None, _get_metrics_error_from_response,
61
+ copy_request=False, **kwargs)
62
+
63
+
64
+ def get_store_sequence_range(self, **kwargs):
65
+ """Determine the range of sequence numbers currently being used by the metrics system's store.
66
+
67
+ Returns:
68
+ A list where the first number represents the starting index (inclusive), and the second number represents
69
+ the final index (inclusive) for the entries in the metrics store.
70
+
71
+ Raises:
72
+ RpcError: Problem communicating with the robot.
73
+ """
74
+ req = metrics_logging_robot_pb2.GetStoreSequenceRangeRequest()
75
+ return self.call(self._stub.GetStoreSequenceRange, req,
76
+ self._store_sequence_range_from_response, common_header_errors,
77
+ copy_request=False, **kwargs)
78
+
79
+ def get_store_sequence_range_async(self, **kwargs):
80
+ """Async version of get_store_sequence_range()."""
81
+ req = metrics_logging_robot_pb2.GetStoreSequenceRangeRequest()
82
+ return self.call_async(self._stub.GetStoreSequenceRange, req,
83
+ self._store_sequence_range_from_response, common_header_errors,
84
+ copy_request=False, **kwargs)
85
+
86
+ def get_absolute_metric_snapshot(self, sequence_numbers, **kwargs):
87
+ """Get absolute metric snapshots for specific sequence numbers' entries.
88
+
89
+ Args:
90
+ sequence_numbers(List(int)): The list of sequence numbers whose entries should be returned as
91
+ absolute metric snapshots.
92
+
93
+ Returns:
94
+ A list of signed_proto_pb2.SignedProto(). Each signed proto will contain the serialized
95
+ absolute_metric_pb2.AbsoluteMetricsSnapshot() for the requested sequence number's timestamp.
96
+ The robot will exclude snapshots if the sequence number does not exist, or the response will exceed
97
+ the grpc limit.
98
+
99
+ Raises:
100
+ RpcError: Problem communicating with the robot.
101
+ """
102
+ req = metrics_logging_robot_pb2.GetAbsoluteMetricSnapshotRequest(
103
+ sequence_numbers=sequence_numbers)
104
+ return self.call(self._stub.GetAbsoluteMetricSnapshot, req,
105
+ self._get_absolute_metric_snapshots_from_response, common_header_errors,
106
+ copy_request=False, **kwargs)
107
+
108
+ def get_absolute_metric_snapshot_async(self, sequence_numbers, **kwargs):
109
+ """Async version of get_absolute_metric_snapshot()."""
110
+ req = metrics_logging_robot_pb2.GetAbsoluteMetricSnapshotRequest(
111
+ sequence_numbers=sequence_numbers)
112
+ return self.call_async(self._stub.GetAbsoluteMetricSnapshot, req,
113
+ self._get_absolute_metric_snapshots_from_response,
114
+ common_header_errors, copy_request=False, **kwargs)
115
+
116
+
117
+ @staticmethod
118
+ def _get_metrics_request(keys, include_events):
119
+ return metrics_logging_robot_pb2.GetMetricsRequest(keys=keys, include_events=include_events)
120
+
121
+ @staticmethod
122
+ def _store_sequence_range_from_response(response):
123
+ return [response.first_sequence_number, response.last_sequence_number]
124
+
125
+ @staticmethod
126
+ def _get_absolute_metric_snapshots_from_response(response):
127
+ return response.snapshots
128
+
129
+
130
+ def make_parameter_update(label, value, is_incremental, units="", notes=""):
131
+ """Create a parameter update proto from the label, param value, and if it is incremental."""
132
+ param_update = absolute_metrics_pb2.ParameterUpdate(incremental=is_incremental)
133
+ param = make_parameter(label, value, units, notes)
134
+ if param is not None:
135
+ param_update.parameter.CopyFrom(param)
136
+ return param_update
137
+
138
+
139
+ @handle_common_header_errors
140
+ def _get_metrics_error_from_response(response):
141
+ """Return an exception based on response from GetMetrics RPC, None if no error."""
142
+ # This could return a MissingKeysError for invalid keys, but at the time of writing
143
+ # this would cause it to throw errors on unreported metrics (not just invalid).
144
+ # Eg, this consistently would throw errors on a new robot with no metrics reported yet.
145
+ return None
146
+
147
+
@@ -28,6 +28,10 @@ class NetworkComputeRotationError(ResponseError):
28
28
  """The robot failed to rotate the image as requested."""
29
29
 
30
30
 
31
+ class NetworkComputeAnalysisFailedError(ResponseError):
32
+ """The model failed to analyze the set of input images, but a retry might work."""
33
+
34
+
31
35
  class NetworkComputeBridgeClient(BaseClient):
32
36
  """Client to either the NetworkComputeBridgeService or the NetworkComputeBridgeWorkerService."""
33
37
 
@@ -143,6 +147,8 @@ _NETWORK_COMPUTE_STATUS_TO_ERROR.update({
143
147
  error_pair(ExternalServerError),
144
148
  network_compute_bridge_pb2.NETWORK_COMPUTE_STATUS_ROTATION_ERROR:
145
149
  error_pair(NetworkComputeRotationError),
150
+ network_compute_bridge_pb2.NETWORK_COMPUTE_STATUS_ANALYSIS_FAILED:
151
+ error_pair(NetworkComputeAnalysisFailedError),
146
152
  })
147
153
 
148
154