bosdyn-client 4.0.3__py3-none-any.whl → 4.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bosdyn/client/channel.py +3 -1
- bosdyn/client/command_line.py +260 -2
- bosdyn/client/common.py +62 -5
- bosdyn/client/data_acquisition.py +0 -3
- bosdyn/client/data_acquisition_helpers.py +0 -3
- bosdyn/client/data_acquisition_plugin_service.py +28 -1
- bosdyn/client/data_acquisition_store.py +112 -7
- bosdyn/client/frame_helpers.py +2 -0
- bosdyn/client/gps/NMEAParser.py +4 -3
- bosdyn/client/gps/aggregator_client.py +0 -1
- bosdyn/client/gps/gps_listener.py +1 -3
- bosdyn/client/graph_nav.py +108 -37
- bosdyn/client/image.py +5 -2
- bosdyn/client/image_service_helpers.py +1 -3
- bosdyn/client/map_processing.py +0 -3
- bosdyn/client/power.py +5 -13
- bosdyn/client/recording.py +4 -4
- bosdyn/client/robot.py +6 -5
- bosdyn/client/robot_command.py +2 -2
- bosdyn/client/sdk.py +3 -0
- bosdyn/client/server_util.py +2 -0
- bosdyn/client/service_customization_helpers.py +5 -3
- bosdyn/client/spot_cam/streamquality.py +0 -1
- bosdyn/client/spot_check.py +0 -155
- bosdyn/client/time_sync.py +38 -0
- bosdyn/client/util.py +3 -6
- bosdyn/client/world_object.py +6 -39
- {bosdyn_client-4.0.3.dist-info → bosdyn_client-4.1.1.dist-info}/METADATA +4 -4
- {bosdyn_client-4.0.3.dist-info → bosdyn_client-4.1.1.dist-info}/RECORD +31 -31
- {bosdyn_client-4.0.3.dist-info → bosdyn_client-4.1.1.dist-info}/WHEEL +0 -0
- {bosdyn_client-4.0.3.dist-info → bosdyn_client-4.1.1.dist-info}/top_level.txt +0 -0
|
@@ -7,21 +7,24 @@
|
|
|
7
7
|
"""Client implementation for data acquisition store service.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
import
|
|
11
|
-
import
|
|
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
|
|
19
|
-
from bosdyn.
|
|
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.
|
|
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(
|
|
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
|
|
bosdyn/client/frame_helpers.py
CHANGED
|
@@ -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):
|
bosdyn/client/gps/NMEAParser.py
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
bosdyn/client/graph_nav.py
CHANGED
|
@@ -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,
|
|
21
|
-
|
|
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(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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(
|
|
648
|
-
|
|
649
|
-
|
|
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,30 @@ 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
|
+
def _get_streamed_download_graph(response):
|
|
1079
|
+
"""Reads a streamed response to recreate a DownloadGraphRequest"""
|
|
1080
|
+
download_graph = _get_streamed_data(response, graph_nav_pb2.DownloadGraphResponse)
|
|
1081
|
+
return download_graph.graph
|
|
1082
|
+
|
|
1083
|
+
|
|
998
1084
|
def _get_streamed_waypoint_snapshot(response):
|
|
999
1085
|
"""Reads a streamed response to recreate a waypoint snapshot."""
|
|
1000
|
-
|
|
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
|
|
1086
|
+
return _get_streamed_data(response, map_pb2.WaypointSnapshot)
|
|
1012
1087
|
|
|
1013
1088
|
|
|
1014
1089
|
def _get_streamed_edge_snapshot(response):
|
|
1015
1090
|
"""Reads a streamed response to recreate an edge snapshot."""
|
|
1016
|
-
|
|
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
|
|
1091
|
+
return _get_streamed_data(response, map_pb2.EdgeSnapshot)
|
|
1028
1092
|
|
|
1029
1093
|
|
|
1030
1094
|
_UPLOAD_GRAPH_STATUS_TO_ERROR = collections.defaultdict(lambda: (ResponseError, None))
|
|
@@ -1043,6 +1107,7 @@ _UPLOAD_GRAPH_STATUS_TO_ERROR.update({
|
|
|
1043
1107
|
|
|
1044
1108
|
@handle_common_header_errors
|
|
1045
1109
|
@handle_lease_use_result_errors
|
|
1110
|
+
@handle_license_errors_if_present
|
|
1046
1111
|
@handle_unset_status_error(unset='STATUS_UNKNOWN')
|
|
1047
1112
|
def _upload_graph_error(response):
|
|
1048
1113
|
"""Return a custom exception based on upload graph response, None if no error."""
|
|
@@ -1266,6 +1331,12 @@ def _navigate_feedback_error(response):
|
|
|
1266
1331
|
|
|
1267
1332
|
|
|
1268
1333
|
|
|
1334
|
+
@handle_common_header_errors
|
|
1335
|
+
def _download_graph_stream_errors(response):
|
|
1336
|
+
"""Return a custom exception based on download graph streaming response, None if no error."""
|
|
1337
|
+
return None
|
|
1338
|
+
|
|
1339
|
+
|
|
1269
1340
|
@handle_common_header_errors
|
|
1270
1341
|
@handle_unset_status_error(unset='STATUS_UNKNOWN')
|
|
1271
1342
|
def _download_waypoint_snapshot_stream_errors(response):
|
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,
|
|
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.
|
|
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()
|
bosdyn/client/map_processing.py
CHANGED
|
@@ -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,
|
|
19
|
-
handle_lease_use_result_errors,
|
|
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
|
-
|
|
198
|
-
|
|
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
|
bosdyn/client/recording.py
CHANGED
|
@@ -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,
|
|
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
|
|