bosdyn-client 4.0.2__py3-none-any.whl → 4.1.0__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.
@@ -7,21 +7,24 @@
7
7
  """Client implementation for data acquisition store service.
8
8
  """
9
9
 
10
- import collections
11
- import functools
12
- import json
10
+ from os import fstat
11
+ from pathlib import Path
13
12
 
14
13
  from google.protobuf import json_format
15
14
 
16
15
  from bosdyn.api import data_acquisition_store_pb2 as data_acquisition_store
17
16
  from bosdyn.api import data_acquisition_store_service_pb2_grpc as data_acquisition_store_service
18
- from bosdyn.api import image_pb2
19
- from bosdyn.client import data_chunk
17
+ from bosdyn.api import data_chunk_pb2 as data_chunk
18
+ from bosdyn.api import header_pb2, image_pb2
19
+ from bosdyn.client.channel import DEFAULT_HEADER_BUFFER_LENGTH, DEFAULT_MAX_MESSAGE_LENGTH
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.data_chunk import split_serialized
22
23
  from bosdyn.client.exceptions import Error, ResponseError
23
24
  from bosdyn.util import now_timestamp
24
25
 
26
+ DEFAULT_CHUNK_SIZE_BYTES = int(DEFAULT_MAX_MESSAGE_LENGTH - DEFAULT_HEADER_BUFFER_LENGTH)
27
+
25
28
 
26
29
  class DataAcquisitionStoreClient(BaseClient):
27
30
  """A client for triggering data acquisition store methods."""
@@ -255,11 +258,64 @@ class DataAcquisitionStoreClient(BaseClient):
255
258
  error_from_response=common_header_errors, copy_request=False,
256
259
  **kwargs)
257
260
 
261
+ def store_data_as_chunks(self, data, data_id, file_extension=None, **kwargs):
262
+ """Store data using streaming, supports storing of large data that is too large for a single store_data rpc. Note: using this rpc means that the data must be loaded into memory.
263
+
264
+ Args:
265
+ data (bytes) : Arbitrary data to store.
266
+ data_id (bosdyn.api.DataIdentifier) : Data identifier to use for storing this data.
267
+ file_extension (string) : File extension to use for writing the data to a file.
268
+
269
+ Returns:
270
+ StoreDataResponse final successful response or first failed response.
271
+ """
272
+ return self.call(self._stub.StoreDataStream,
273
+ _iterate_data_chunks(data, data_id, file_extension),
274
+ error_from_response=common_header_errors, value_from_response=None,
275
+ copy_request=False, **kwargs)
276
+
277
+ def store_data_as_chunks_async(self, data, data_id, file_extension=None, **kwargs):
278
+ """Async version of the store_data_as_chunks() RPC."""
279
+ return self.call_async_streaming(
280
+ self._stub.StoreDataStream, _iterate_data_chunks(data, data_id, file_extension),
281
+ error_from_response=common_header_errors, value_from_response=None,
282
+ assemble_type=data_acquisition_store.StoreStreamResponse, copy_request=False, **kwargs)
283
+
284
+ def store_file(self, file_path, data_id, file_extension=None, **kwargs):
285
+ """Store file using file path, supports storing of large files that are too large for a single store_data rpc.
286
+
287
+ Args:
288
+ file_path (string) : File path to arbitrary data to store.
289
+ data_id (bosdyn.api.DataIdentifier) : Data identifier to use for storing this data.
290
+ file_extension (string) : File extension to use for writing the data to a file.
291
+
292
+ Returns:
293
+ StoreDataResponse final successful response or first failed response.
294
+ """
295
+
296
+ file_abs = Path(file_path).absolute()
297
+ file = open(file_abs, "rb")
298
+ return self.call(self._stub.StoreDataStream,
299
+ _iterate_store_file(file, data_id, file_extension=file_extension),
300
+ error_from_response=common_header_errors, value_from_response=None,
301
+ copy_request=False, **kwargs)
302
+
303
+ def store_file_async(self, file_path, data_id, file_extension=None, **kwargs):
304
+ """Async version of the store_file() RPC."""
305
+
306
+ file_abs = Path(file_path).absolute()
307
+ file = open(file_abs, "rb")
308
+ return self.call_async_streaming(
309
+ self._stub.StoreDataStream,
310
+ _iterate_store_file(file, data_id, file_extension=file_extension),
311
+ error_from_response=common_header_errors, value_from_response=None,
312
+ assemble_type=data_acquisition_store.StoreStreamResponse, copy_request=False, **kwargs)
313
+
258
314
  def query_stored_captures(self, query=None, **kwargs):
259
315
  """Query stored captures from the robot.
260
316
 
261
317
  Args:
262
- query (bosdyn.api.DataQueryParams) : Query parameters.
318
+ query (bosdyn.api.QueryParameters) : Query parameters.
263
319
  Raises:
264
320
  RpcError: Problem communicating with the robot.
265
321
  """
@@ -270,6 +326,15 @@ class DataAcquisitionStoreClient(BaseClient):
270
326
  assemble_type=data_acquisition_store.QueryStoredCapturesResponse,
271
327
  copy_request=False, **kwargs)
272
328
 
329
+ def query_stored_captures_async(self, query=None, **kwargs):
330
+ """Async version of the query_stored_captures() RPC."""
331
+ request = data_acquisition_store.QueryStoredCapturesRequest(query=query)
332
+ self._apply_request_processors(request, copy_request=False)
333
+ return self.call_async_streaming(
334
+ self._stub.QueryStoredCaptures, request, error_from_response=common_header_errors,
335
+ assemble_type=data_acquisition_store.QueryStoredCapturesResponse, copy_request=False,
336
+ **kwargs)
337
+
273
338
  def query_max_capture_id(self, **kwargs):
274
339
  """Query max capture id from the robot.
275
340
  Returns:
@@ -284,13 +349,53 @@ class DataAcquisitionStoreClient(BaseClient):
284
349
 
285
350
  def query_max_capture_id_async(self, **kwargs):
286
351
  """Async version of the query_max_capture_id() RPC."""
287
- request = data_acquisition_store.QueryMaxCaptureIdRequest(query=query)
352
+ request = data_acquisition_store.QueryMaxCaptureIdRequest()
288
353
  return self.call_async(self._stub.QueryMaxCaptureId, request,
289
354
  value_from_response=_get_max_capture_id,
290
355
  error_from_response=common_header_errors, copy_request=False,
291
356
  **kwargs)
292
357
 
293
358
 
359
+ def _iterate_store_file(file, data_id, file_extension=None):
360
+ """Iterator over file data and create multiple StoreStreamRequest
361
+
362
+ Args:
363
+ file (BufferedReader) : Reader to the file for arbitrary data to store.
364
+ data_id (bosdyn.api.DataIdentifier) : Data identifier to use for storing this data.
365
+ file_extension (string) : File extension to use for writing the data to a file.
366
+ Returns:
367
+ StoreStreamRequests iterates over these requests.
368
+ """
369
+ total_size = fstat(file.fileno()).st_size
370
+ while True:
371
+ chunk = file.read(DEFAULT_CHUNK_SIZE_BYTES)
372
+ if not chunk:
373
+ # No more data
374
+ break
375
+ data = data_chunk.DataChunk(data=chunk, total_size=total_size)
376
+ request = data_acquisition_store.StoreStreamRequest(chunk=data, data_id=data_id,
377
+ file_extension=file_extension)
378
+ yield request
379
+
380
+
381
+ def _iterate_data_chunks(data, data_id, file_extension=None):
382
+ """Iterator over data and create multiple StoreDataRequest
383
+
384
+ Args:
385
+ data (bytes) : Arbitrary data to store.
386
+ data_id (bosdyn.api.DataIdentifier) : Data identifier to use for storing this data.
387
+ file_extension (string) : File extension to use for writing the data to a file.
388
+ Returns:
389
+ StoreDataRequests iterates over these requests.
390
+ """
391
+ total_size = len(data)
392
+ for chunk in split_serialized(data, DEFAULT_CHUNK_SIZE_BYTES):
393
+ chunk_data = data_chunk.DataChunk(data=chunk, total_size=total_size)
394
+ request = data_acquisition_store.StoreStreamRequest(chunk=chunk_data, data_id=data_id,
395
+ file_extension=file_extension)
396
+ yield request
397
+
398
+
294
399
  def _get_action_ids(response):
295
400
  return response.action_ids
296
401
 
@@ -14,6 +14,7 @@ BODY_FRAME_NAME = "body"
14
14
  # string identifier.
15
15
  GRAV_ALIGNED_BODY_FRAME_NAME = "flat_body"
16
16
  ODOM_FRAME_NAME = "odom"
17
+ SEED_FRAME_NAME = "seed"
17
18
  GROUND_PLANE_FRAME_NAME = "gpe"
18
19
  HAND_FRAME_NAME = "hand"
19
20
  UNKNOWN_FRAME_NAME = "unknown"
@@ -33,6 +34,7 @@ FOOT_FRAME_NAMES = [
33
34
  HIND_RIGHT_FOOT_FRAME_NAME
34
35
  ]
35
36
  WR1_FRAME_NAME = "arm0.link_wr1"
37
+ WAYPOINT_FRAME_NAME = "waypoint"
36
38
 
37
39
 
38
40
  class Error(Exception):
@@ -92,13 +92,14 @@ class NMEAParser(object):
92
92
  data_point.timestamp_gps.FromDatetime(gps_timestamp_no_tz)
93
93
  has_timestamp = True
94
94
 
95
- # Populate client and robot timestamps.
95
+ # Populate client and robot timestamps. If we are not using TimeSync, the robot
96
+ # timestamp will be the same as the client timestamp.
96
97
  data_point.timestamp_client.CopyFrom(seconds_to_timestamp(client_timestamp))
97
98
  if time_converter is not None:
98
99
  data_point.timestamp_robot.CopyFrom(
99
100
  time_converter.robot_timestamp_from_local_secs(client_timestamp))
100
101
  else:
101
- data_point.timestamp_robot.CopyFrom(now_timestamp())
102
+ data_point.timestamp_robot.CopyFrom(data_point.timestamp_client)
102
103
 
103
104
 
104
105
  if not has_timestamp and not has_warned_no_zda:
@@ -110,8 +111,8 @@ class NMEAParser(object):
110
111
 
111
112
  def parse(self, new_data: str, time_converter: RobotTimeConverter,
112
113
  check: bool = True) -> List[GpsDataPoint]:
113
- self.data = self.data + new_data
114
114
  timestamp = time.time() # Client timestamp when received.
115
+ self.data = self.data + new_data
115
116
 
116
117
  if len(self.data) == 0:
117
118
  return [] # Protection because empty_string.splitlines() returns empty array
@@ -5,7 +5,6 @@
5
5
  # Development Kit License (20191101-BDSDK-SL).
6
6
 
7
7
  """For clients to use the Gps Aggregator service."""
8
- import collections
9
8
 
10
9
  from bosdyn.api.gps import aggregator_pb2, aggregator_service_pb2_grpc, gps_pb2
11
10
  from bosdyn.client.common import (BaseClient, common_header_errors, error_factory, error_pair,
@@ -6,7 +6,6 @@
6
6
 
7
7
  """Reads GPS data from a tcp/udp stream, and sends to aggregator service."""
8
8
 
9
- import logging
10
9
  import signal
11
10
  import socket
12
11
  import time
@@ -40,7 +39,7 @@ class NMEAStreamReader(object):
40
39
  try:
41
40
  raw_data = self.stream.readline()
42
41
  # If the rawdata is a bytes or bytearray object, decode it into a string.
43
- if type(raw_data) is not str:
42
+ if not isinstance(raw_data, str):
44
43
  raw_data = str(raw_data, "utf-8")
45
44
  except UnicodeDecodeError:
46
45
  # Throttle the logs.
@@ -159,4 +158,3 @@ class GpsListener:
159
158
 
160
159
  except KeyboardInterrupt:
161
160
  print() # Get past the ^C in the console output
162
- pass
@@ -17,8 +17,9 @@ from bosdyn.api.graph_nav import (graph_nav_pb2, graph_nav_service_pb2, graph_na
17
17
  map_pb2, nav_pb2)
18
18
  from bosdyn.client.common import (BaseClient, common_header_errors, common_lease_errors,
19
19
  error_factory, error_pair, handle_common_header_errors,
20
- handle_lease_use_result_errors, handle_unset_status_error)
21
- from bosdyn.client.exceptions import Error, InvalidRequestError, ResponseError
20
+ handle_lease_use_result_errors, handle_license_errors_if_present,
21
+ handle_unset_status_error)
22
+ from bosdyn.client.exceptions import Error, InvalidRequestError, ResponseError, UnimplementedError
22
23
  from bosdyn.client.lease import add_lease_wallet_processors
23
24
 
24
25
 
@@ -31,6 +32,7 @@ class GraphNavClient(BaseClient):
31
32
  super(GraphNavClient, self).__init__(graph_nav_service_pb2_grpc.GraphNavServiceStub)
32
33
  self._timesync_endpoint = None
33
34
  self._data_chunk_size = 1024 * 1024 # bytes = 1 MB
35
+ self._use_streaming_graph_upload = True
34
36
 
35
37
  def update_from(self, other):
36
38
  super(GraphNavClient, self).update_from(other)
@@ -236,18 +238,31 @@ class GraphNavClient(BaseClient):
236
238
  return self.call(self._stub.NavigateRoute, request,
237
239
  error_from_response=_navigate_route_error, copy_request=False, **kwargs)
238
240
 
239
- def navigate_route_full_async(self, route, cmd_duration, route_follow_params=None,
240
- travel_params=None, leases=None, timesync_endpoint=None,
241
- command_id=None, destination_waypoint_tform_body_goal=None,
242
- **kwargs):
241
+ def navigate_route_full_async(
242
+ self,
243
+ route,
244
+ cmd_duration,
245
+ route_follow_params=None,
246
+ travel_params=None,
247
+ leases=None,
248
+ timesync_endpoint=None,
249
+ command_id=None,
250
+ destination_waypoint_tform_body_goal=None,
251
+ **kwargs):
243
252
  """Async version of navigate_route_full()."""
244
253
  used_endpoint = timesync_endpoint or self._timesync_endpoint
245
254
  if not used_endpoint:
246
255
  raise GraphNavServiceResponseError(response=None, error_message='No timesync endpoint!')
247
- request = self._build_navigate_route_request(route, route_follow_params, travel_params,
248
- cmd_duration, leases, used_endpoint,
249
- command_id,
250
- destination_waypoint_tform_body_goal)
256
+ request = self._build_navigate_route_request(
257
+ route,
258
+ route_follow_params,
259
+ travel_params,
260
+ cmd_duration,
261
+ leases,
262
+ used_endpoint,
263
+ command_id,
264
+ destination_waypoint_tform_body_goal,
265
+ )
251
266
  return self.call_async(self._stub.NavigateRoute, request,
252
267
  error_from_response=_navigate_route_error, copy_request=False,
253
268
  **kwargs)
@@ -457,9 +472,30 @@ class GraphNavClient(BaseClient):
457
472
  The response, which includes waypoint and edge id's sorted by whether it was cached.
458
473
  Raises:
459
474
  RpcError: Problem communicating with the robot.
475
+ UploadGraphError: Indicates a problem with the map provided.
476
+ IncompatibleSensorsError: The map was recorded with different sensors than the robot.
477
+ AreaCallbackError: The map includes area callback services not present on the robot.
460
478
  LeaseUseError: Error using provided lease.
479
+ LicenseError: The robot's license is not valid.
461
480
  """
462
481
  request = self._build_upload_graph_request(lease, graph, generate_new_anchoring)
482
+ # Use streaming to upload the graph, if applicable.
483
+ if self._use_streaming_graph_upload:
484
+ # Need to manually apply request processors since this will be serialized and chunked.
485
+ self._apply_request_processors(request, copy_request=False)
486
+ serialized = request.SerializeToString()
487
+ try:
488
+ return self.call(
489
+ self._stub.UploadGraphStreaming,
490
+ GraphNavClient._data_chunk_iterator_upload_graph(serialized,
491
+ self._data_chunk_size),
492
+ value_from_response=_get_response, error_from_response=_upload_graph_error,
493
+ **kwargs)
494
+ except UnimplementedError:
495
+ print('UploadGraphStreaming unimplemented. Old robot release?')
496
+ # Recreate the request so that we clear any state that might have happened during our attempt to stream.
497
+ request = self._build_upload_graph_request(lease, graph, generate_new_anchoring)
498
+ # Continue to regular UploadGraph.
463
499
  return self.call(self._stub.UploadGraph, request, value_from_response=_get_response,
464
500
  error_from_response=_upload_graph_error, copy_request=False, **kwargs)
465
501
 
@@ -520,6 +556,17 @@ class GraphNavClient(BaseClient):
520
556
  RpcError: Problem communicating with the robot
521
557
  """
522
558
  request = self._build_download_graph_request()
559
+ # Use streaming to download the graph, if applicable.
560
+ if self._use_streaming_graph_upload:
561
+ try:
562
+ resp = self.call(self._stub.DownloadGraphStreaming, request,
563
+ value_from_response=_get_streamed_download_graph,
564
+ error_from_response=_download_graph_stream_errors,
565
+ copy_request=False, **kwargs)
566
+ return resp
567
+ except UnimplementedError:
568
+ print('DownloadGraphStreaming unimplemented. Old robot release?')
569
+ # Continue to regular DownloadGraph.
523
570
  return self.call(self._stub.DownloadGraph, request, value_from_response=_get_graph,
524
571
  error_from_response=common_header_errors, copy_request=False, **kwargs)
525
572
 
@@ -644,9 +691,16 @@ class GraphNavClient(BaseClient):
644
691
  request_gps_state=request_gps_state)
645
692
 
646
693
  @staticmethod
647
- def _build_navigate_route_request(route, route_follow_params, travel_params, end_time_secs,
648
- leases, timesync_endpoint, command_id,
649
- destination_waypoint_tform_body_goal):
694
+ def _build_navigate_route_request(
695
+ route,
696
+ route_follow_params,
697
+ travel_params,
698
+ end_time_secs,
699
+ leases,
700
+ timesync_endpoint,
701
+ command_id,
702
+ destination_waypoint_tform_body_goal,
703
+ ):
650
704
  converter = timesync_endpoint.get_robot_time_converter()
651
705
  request = graph_nav_pb2.NavigateRouteRequest(
652
706
  route=route, route_follow_params=route_follow_params,
@@ -719,6 +773,22 @@ class GraphNavClient(BaseClient):
719
773
  return graph_nav_pb2.UploadGraphRequest(lease=lease, graph=graph,
720
774
  generate_new_anchoring=generate_new_anchoring)
721
775
 
776
+ @staticmethod
777
+ def _data_chunk_iterator_upload_graph(serialized_upload_graph, data_chunk_byte_size):
778
+ """Converts a serialized UploadGraphRequest into a series of UploadGraphStreamingRequests."""
779
+ total_bytes_size = len(serialized_upload_graph)
780
+ num_chunks = math.ceil(total_bytes_size / data_chunk_byte_size)
781
+ for i in range(num_chunks):
782
+ start_index = i * data_chunk_byte_size
783
+ end_index = (i + 1) * data_chunk_byte_size
784
+ chunk = data_chunk_pb2.DataChunk(total_size=total_bytes_size)
785
+ if (end_index > total_bytes_size):
786
+ chunk.data = serialized_upload_graph[start_index:total_bytes_size]
787
+ else:
788
+ chunk.data = serialized_upload_graph[start_index:end_index]
789
+ req = graph_nav_pb2.UploadGraphStreamingRequest(chunk=chunk)
790
+ yield req
791
+
722
792
  @staticmethod
723
793
  def _data_chunk_iterator_upload_waypoint_snapshot(serialized_waypoint_snapshot, lease,
724
794
  data_chunk_byte_size):
@@ -995,36 +1065,26 @@ def _get_graph(response):
995
1065
  return response.graph
996
1066
 
997
1067
 
1068
+ def _get_streamed_data(response, data_type):
1069
+ """Given a list of streamed responses, return an instance of the given data type that is parsed from those responses."""
1070
+ data = bytes()
1071
+ for resp in response:
1072
+ data += resp.chunk.data
1073
+ proto_instance = data_type()
1074
+ proto_instance.ParseFromString(data)
1075
+ return proto_instance
1076
+
1077
+
1078
+
1079
+
998
1080
  def _get_streamed_waypoint_snapshot(response):
999
1081
  """Reads a streamed response to recreate a waypoint snapshot."""
1000
- data = ''
1001
- num_chunks = 0
1002
- for resp in response:
1003
- if num_chunks == 0:
1004
- data = resp.chunk.data
1005
- else:
1006
- data += resp.chunk.data
1007
- num_chunks += 1
1008
- waypoint_snapshot = map_pb2.WaypointSnapshot()
1009
- if (num_chunks > 0):
1010
- waypoint_snapshot.ParseFromString(data)
1011
- return waypoint_snapshot
1082
+ return _get_streamed_data(response, map_pb2.WaypointSnapshot)
1012
1083
 
1013
1084
 
1014
1085
  def _get_streamed_edge_snapshot(response):
1015
1086
  """Reads a streamed response to recreate an edge snapshot."""
1016
- data = ''
1017
- num_chunks = 0
1018
- for resp in response:
1019
- if num_chunks == 0:
1020
- data = resp.chunk.data
1021
- else:
1022
- data += resp.chunk.data
1023
- num_chunks += 1
1024
- edge_snapshot = map_pb2.EdgeSnapshot()
1025
- if (num_chunks > 0):
1026
- edge_snapshot.ParseFromString(data)
1027
- return edge_snapshot
1087
+ return _get_streamed_data(response, map_pb2.EdgeSnapshot)
1028
1088
 
1029
1089
 
1030
1090
  _UPLOAD_GRAPH_STATUS_TO_ERROR = collections.defaultdict(lambda: (ResponseError, None))
@@ -1043,6 +1103,7 @@ _UPLOAD_GRAPH_STATUS_TO_ERROR.update({
1043
1103
 
1044
1104
  @handle_common_header_errors
1045
1105
  @handle_lease_use_result_errors
1106
+ @handle_license_errors_if_present
1046
1107
  @handle_unset_status_error(unset='STATUS_UNKNOWN')
1047
1108
  def _upload_graph_error(response):
1048
1109
  """Return a custom exception based on upload graph response, None if no error."""
bosdyn/client/image.py CHANGED
@@ -170,7 +170,7 @@ class ImageClient(BaseClient):
170
170
 
171
171
 
172
172
  def build_image_request(image_source_name, quality_percent=75, image_format=None, pixel_format=None,
173
- resize_ratio=None):
173
+ resize_ratio=None, fallback_formats=None):
174
174
  """Helper function which builds an ImageRequest from an image source name.
175
175
 
176
176
  By default the robot will choose an appropriate format when no image format
@@ -184,13 +184,16 @@ def build_image_request(image_source_name, quality_percent=75, image_format=None
184
184
  data, such as JPEG, RAW, or RLE.
185
185
  pixel_format (image_pb2.Image.PixelFormat) The pixel format of the image.
186
186
  resize_ratio (double): Resize ratio for image dimensions.
187
+ fallback_formats (image_pb2.Image.PixelFormat) Fallback pixel formats to use
188
+ if the pixel_format is invalid.
187
189
 
188
190
  Returns:
189
191
  The ImageRequest protobuf message for the given parameters.
190
192
  """
191
193
  return image_pb2.ImageRequest(image_source_name=image_source_name,
192
194
  quality_percent=quality_percent, image_format=image_format,
193
- pixel_format=pixel_format, resize_ratio=resize_ratio)
195
+ pixel_format=pixel_format, fallback_formats=fallback_formats,
196
+ resize_ratio=resize_ratio)
194
197
 
195
198
 
196
199
  def _list_image_sources_value(response):
@@ -76,7 +76,6 @@ class CameraInterface(ABC):
76
76
  A tuple with image data (in any format), and the capture timestamp in seconds (float) in the
77
77
  service computer's clock.
78
78
  """
79
- pass
80
79
 
81
80
  @abstractmethod
82
81
  def image_decode(self, image_data, image_proto, image_req):
@@ -92,7 +91,6 @@ class CameraInterface(ABC):
92
91
  pixel format, image format, and potentially the transform snapshot fields within the image_proto
93
92
  protobuf message.
94
93
  """
95
- pass
96
94
 
97
95
 
98
96
  class VisualImageSource():
@@ -575,7 +573,7 @@ class ImageCaptureThread():
575
573
 
576
574
  def _do_image_capture(self):
577
575
  """Main loop for the image capture thread, which requests and saves images."""
578
- while not self.stop_capturing_event.isSet():
576
+ while not self.stop_capturing_event.is_set():
579
577
  # Get the image by calling the blocking capture function.
580
578
  start_time = time.time()
581
579
  capture, capture_time = self.capture_function()
@@ -6,9 +6,6 @@
6
6
 
7
7
  """For clients of the graph_nav map processing service."""
8
8
 
9
- import collections
10
- from enum import Enum
11
-
12
9
  from bosdyn.api.graph_nav import map_pb2, map_processing_pb2, map_processing_service_pb2
13
10
  from bosdyn.api.graph_nav import map_processing_service_pb2_grpc as map_processing
14
11
  from bosdyn.client.common import (BaseClient, common_header_errors, error_factory,
bosdyn/client/power.py CHANGED
@@ -15,8 +15,9 @@ from google.protobuf.duration_pb2 import Duration
15
15
 
16
16
  from bosdyn.api import (basic_command_pb2, full_body_command_pb2, license_pb2, power_pb2,
17
17
  power_service_pb2_grpc, robot_command_pb2, robot_state_pb2)
18
- from bosdyn.client.common import (BaseClient, error_factory, handle_common_header_errors,
19
- handle_lease_use_result_errors, handle_unset_status_error)
18
+ from bosdyn.client.common import (BaseClient, common_license_errors, error_factory,
19
+ handle_common_header_errors, handle_lease_use_result_errors,
20
+ handle_unset_status_error)
20
21
  from bosdyn.client.exceptions import (Error, InternalServerError, LicenseError, ResponseError,
21
22
  TimedOutError)
22
23
 
@@ -190,19 +191,10 @@ def _handle_license_errors(func):
190
191
 
191
192
  def _common_license_errors(response):
192
193
  """Return an exception based on license status. None if no error."""
193
-
194
- license_status = None
195
194
  if response.status != power_pb2.STATUS_LICENSE_ERROR:
196
195
  return None
197
- if hasattr(response, 'license_status'):
198
- license_status = response.license_status
199
- else:
200
- # This means you're using the wrong error handler.
201
- return InternalServerError(response, 'No LicenseInfo.Status field found!')
202
-
203
- if license_status != license_pb2.LicenseInfo.STATUS_VALID:
204
- return LicenseError(response)
205
- return None
196
+
197
+ return common_license_errors(response)
206
198
 
207
199
 
208
200
  @handle_common_header_errors
@@ -13,7 +13,7 @@ from bosdyn.api.graph_nav import map_pb2, nav_pb2, recording_pb2, recording_serv
13
13
  from bosdyn.api.graph_nav import recording_service_pb2_grpc as recording_service
14
14
  from bosdyn.client.common import (BaseClient, common_header_errors, error_factory,
15
15
  handle_common_header_errors, handle_lease_use_result_errors,
16
- handle_unset_status_error)
16
+ handle_license_errors_if_present, handle_unset_status_error)
17
17
  from bosdyn.client.exceptions import ResponseError
18
18
 
19
19
 
@@ -275,14 +275,13 @@ class GraphNavRecordingServiceClient(BaseClient):
275
275
  @staticmethod
276
276
  def make_edge_environment(
277
277
  vel_limit=None, direction_constraint=map_pb2.Edge.Annotations.DIRECTION_CONSTRAINT_NONE,
278
- require_alignment=False, flat_ground=False, ground_mu_hint=.8, grated_floor=False):
278
+ require_alignment=False, ground_mu_hint=.8, grated_floor=False):
279
279
  """Create an edge environment.
280
280
 
281
281
  Args:
282
282
  vel_limit: A SE2VelocityLimit to use while traversing the edge. Note this is not a target speed, just a max/min.
283
283
  direction_constraint: A direction constraints on the robot's orientation when traversing the edge.
284
284
  require_alignment: Boolean where if true, the robot must be aligned with the edge in yaw before traversing it.
285
- flat_ground: Boolean where if true, the edge crosses flat ground and the robot shouldn't try to climb over obstacles.
286
285
  ground_mu_hint: Terrain coefficient of friction user hint. Suggested values lie between [.4, .8].
287
286
  grated_floor: Boolean where if true, the edge crosses over grated metal.
288
287
  Returns:
@@ -290,7 +289,6 @@ class GraphNavRecordingServiceClient(BaseClient):
290
289
  """
291
290
  edge_env = map_pb2.Edge.Annotations()
292
291
  edge_env.require_alignment.value.CopyFrom(require_alignment)
293
- edge_env.flat_ground.value.CopyFrom(flat_ground)
294
292
  edge_env.grated_floor.value.CopyFrom(grated_floor)
295
293
  if (ground_mu_hint > 0):
296
294
  edge_env.ground_mu_hint.value.CopyFrom(ground_mu_hint)
@@ -438,6 +436,7 @@ _START_RECORDING_STATUS_TO_ERROR.update({
438
436
 
439
437
  @handle_common_header_errors
440
438
  # @handle_lease_use_result_errors
439
+ @handle_license_errors_if_present
441
440
  @handle_unset_status_error(unset='STATUS_UNKNOWN')
442
441
  def _start_recording_error(response):
443
442
  """Return a custom exception based on start recording response, None if no error."""
@@ -482,6 +481,7 @@ _CREATE_WAYPOINT_STATUS_TO_ERROR.update({
482
481
 
483
482
  @handle_common_header_errors
484
483
  # @handle_lease_use_result_errors
484
+ @handle_license_errors_if_present
485
485
  @handle_unset_status_error(unset='STATUS_UNKNOWN')
486
486
  def _create_waypoint_error(response):
487
487
  """Return a custom exception based on create waypoint response, None if no error."""
bosdyn/client/robot.py CHANGED
@@ -129,6 +129,7 @@ class Robot(object):
129
129
  self.cert = None
130
130
  self.lease_wallet = LeaseWallet()
131
131
  self._time_sync_thread = None
132
+ self.executor = None
132
133
 
133
134
  # Set default max message length for sending and receiving. These values are used when
134
135
  # creating channels.
@@ -198,6 +199,7 @@ class Robot(object):
198
199
  self.max_receive_message_length = other.max_receive_message_length
199
200
  self.client_name = other.client_name
200
201
  self.lease_wallet.set_client_name(self.client_name)
202
+ self.executor = other.executor
201
203
 
202
204
  def ensure_client(self, service_name, channel=None, options=[], service_endpoint=None):
203
205
  """Ensure a Client for a given service.
@@ -247,7 +249,7 @@ class Robot(object):
247
249
  for channel_from_auth in self.channels_by_authority.values():
248
250
  channel_from_auth.close()
249
251
 
250
- def get_cached_robot_id(self):
252
+ def get_cached_robot_id(self, timeout=None):
251
253
  """Return the RobotId proto for this robot, querying it from the robot if not yet cached.
252
254
 
253
255
  Raises:
@@ -255,10 +257,10 @@ class Robot(object):
255
257
  """
256
258
  if not self._robot_id:
257
259
  robot_id_client = self.ensure_client('robot-id')
258
- self._robot_id = robot_id_client.get_id()
260
+ self._robot_id = robot_id_client.get_id(timeout=timeout)
259
261
  return self._robot_id
260
262
 
261
- def get_cached_hardware_hardware_configuration(self):
263
+ def get_cached_hardware_hardware_configuration(self, timeout=None):
262
264
  """Return the HardwareConfiguration proto for this robot, querying it from the robot if not
263
265
  yet cached.
264
266
 
@@ -267,7 +269,7 @@ class Robot(object):
267
269
  """
268
270
  if not self._hardware_config:
269
271
  client = self.ensure_client(RobotStateClient.default_service_name)
270
- self._hardware_config = client.get_robot_hardware_configuration()
272
+ self._hardware_config = client.get_robot_hardware_configuration(timeout=timeout)
271
273
  return self._hardware_config
272
274
 
273
275
 
@@ -411,7 +413,6 @@ class Robot(object):
411
413
  self.logger.warning(
412
414
  'Payload is not authorized. Authentication will block until an'
413
415
  ' operator authorizes the payload in the Admin Console.')
414
- pass
415
416
  time.sleep(0.1)
416
417
  self.update_user_token(user_token)
417
418