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
@@ -32,6 +32,7 @@ FOOT_FRAME_NAMES = [
32
32
  FRONT_LEFT_FOOT_FRAME_NAME, FRONT_RIGHT_FOOT_FRAME_NAME, HIND_LEFT_FOOT_FRAME_NAME,
33
33
  HIND_RIGHT_FOOT_FRAME_NAME
34
34
  ]
35
+ WR1_FRAME_NAME = "arm0.link_wr1"
35
36
 
36
37
 
37
38
  class Error(Exception):
@@ -178,7 +179,7 @@ def get_se2_a_tform_b(frame_tree_snapshot, frame_a, frame_b, validate=True):
178
179
 
179
180
  Returns:
180
181
  math_helpers.SE2Pose between frame_a and frame_b if they exist in the tree and
181
- frame a is a gravity aligned frame. None otherwise.
182
+ frame_a is a gravity aligned frame. None otherwise.
182
183
  """
183
184
  # Validate that the transform is in a gravity aligned frame based on the string name.
184
185
  if not is_gravity_aligned_frame_name(frame_a):
@@ -274,6 +275,7 @@ class ChildFrameInTree(GenerateTreeError):
274
275
 
275
276
  def add_edge_to_tree(frame_tree_snapshot, parent_tform_child, parent_frame_name, child_frame_name):
276
277
  """Appends a child/parent and the transform to the FrameTreeSnapshot.
278
+
277
279
  Args:
278
280
  frame_tree_snapshot (dict) dictionary representing the child_to_parent_edge_map
279
281
  parent_tform_child (SE3Pose proto)
@@ -0,0 +1,189 @@
1
+ # Copyright (c) 2023 Boston Dynamics, Inc. All rights reserved.
2
+ #
3
+ # Downloading, reproducing, distributing or otherwise using the SDK Software
4
+ # is subject to the terms and conditions of the Boston Dynamics Software
5
+ # Development Kit License (20191101-BDSDK-SL).
6
+
7
+ import datetime
8
+ import math
9
+ import time
10
+ from typing import List, Tuple
11
+
12
+ import pynmea2
13
+ from google.protobuf.any_pb2 import Any
14
+ from google.protobuf.timestamp_pb2 import Timestamp
15
+
16
+ from bosdyn.api.gps.gps_pb2 import GpsDataPoint
17
+ from bosdyn.util import RobotTimeConverter, now_timestamp, seconds_to_timestamp
18
+
19
+
20
+ has_warned_no_zda = False
21
+
22
+
23
+ class NMEAParser(object):
24
+
25
+ # The amount of time to wait before logging another decode error.
26
+ LOG_THROTTLE_TIME = 2.0 # seconds.
27
+
28
+ def __init__(self, logger):
29
+ self.data = ''
30
+ self.full_lines = []
31
+ self.logger = logger
32
+ # NMEA strings come in in "groups" we are just trying to
33
+ # figure out which group each message belongs to. We do this
34
+ # by checking if their times are near to one another.
35
+ #
36
+ # If your GPS outputs data at 20 Hz, this constant must be less than 0.050 seconds.
37
+ self.grouping_timeout = 0.025
38
+ self.last_failed_read_log_time = None
39
+
40
+ def nmea_message_group_to_gps_data_point(self, nmea_messages: List[Tuple[str, str, int]],
41
+ time_converter: RobotTimeConverter):
42
+ """Convert a NMEA message group with the same UTC timestamp to a GpsDataPoint."""
43
+
44
+ global has_warned_no_zda
45
+
46
+ data_point = GpsDataPoint()
47
+ has_timestamp = False
48
+
49
+
50
+ # The NMEA message group should at least have a ZDA message.
51
+ # Parse each message depending on the NMEA sentence_type.
52
+ for nmea_message_list in nmea_messages:
53
+ data, raw_nmea_msg, client_timestamp = nmea_message_list
54
+
55
+ if data.sentence_type == 'GGA':
56
+ if data.latitude is not None and data.longitude is not None and data.altitude is not None:
57
+ data_point.llh.latitude = data.latitude
58
+ data_point.llh.longitude = data.longitude
59
+ data_point.llh.height = data.altitude
60
+
61
+ if data.num_sats is not None and int(data.num_sats) > 0:
62
+ for _ in range(int(data.num_sats)):
63
+ sat = data_point.satellites.add()
64
+
65
+ # GPS Quality indicator:
66
+ # 0: Fix not valid
67
+ # 1: GPS fix
68
+ # 2: Differential GPS fix, OmniSTAR VBS
69
+ # 4: Real-Time Kinematic, fixed integers
70
+ # 5: Real-Time Kinematic, float integers, OmniSTAR XP/HP or Location RTK
71
+ if data.gps_qual is not None:
72
+ data_point.mode.value = data.gps_qual
73
+
74
+ if not has_timestamp:
75
+ # If there is no ZDA message to provide a date, assume today's date.
76
+ gps_timestamp = datetime.datetime.combine(datetime.date.today(), data.timestamp)
77
+ data_point.timestamp_gps.FromDatetime(gps_timestamp)
78
+
79
+ elif data.sentence_type == 'GST':
80
+ if data.std_dev_latitude is not None:
81
+ # Horizontal Root Mean Squared. Note we are not using "twice distance rms" or "2drms".
82
+ hrms = math.sqrt((
83
+ (math.pow(data.std_dev_latitude, 2) + math.pow(data.std_dev_longitude, 2)) /
84
+ 2))
85
+ data_point.accuracy.horizontal = hrms
86
+ data_point.accuracy.vertical = data.std_dev_altitude
87
+
88
+ elif data.sentence_type == 'ZDA':
89
+ gps_timestamp = data.datetime
90
+ # Protobuf timestamp does not use timezone aware timestamps.
91
+ gps_timestamp_no_tz = gps_timestamp.replace(tzinfo=None)
92
+ data_point.timestamp_gps.FromDatetime(gps_timestamp_no_tz)
93
+ has_timestamp = True
94
+
95
+ # Populate client and robot timestamps.
96
+ data_point.timestamp_client.CopyFrom(seconds_to_timestamp(client_timestamp))
97
+ if time_converter is not None:
98
+ data_point.timestamp_robot.CopyFrom(
99
+ time_converter.robot_timestamp_from_local_secs(client_timestamp))
100
+ else:
101
+ data_point.timestamp_robot.CopyFrom(now_timestamp())
102
+
103
+
104
+ if not has_timestamp and not has_warned_no_zda:
105
+ self.logger.warning("GPS data does not include ZDA. Timestamp may be inaccurate.")
106
+ has_warned_no_zda = True
107
+
108
+
109
+ return data_point
110
+
111
+ def parse(self, new_data: str, time_converter: RobotTimeConverter,
112
+ check: bool = True) -> List[GpsDataPoint]:
113
+ self.data = self.data + new_data
114
+ timestamp = time.time() # Client timestamp when received.
115
+
116
+ if len(self.data) == 0:
117
+ return [] # Protection because empty_string.splitlines() returns empty array
118
+
119
+ lines = self.data.splitlines(True)
120
+ self.data = lines[-1]
121
+ len_lines = len(lines)
122
+
123
+ # Parse each line.
124
+ for idx, line in enumerate(lines):
125
+ # Only parse the last line if it ends with a \n
126
+ if idx == len_lines - 1:
127
+ if len(line) > 0:
128
+ if line[-1] == '\n':
129
+ # This line is complete, any new data
130
+ # will be on a new line. Parse it.
131
+ self.data = ""
132
+ else:
133
+ break
134
+ else:
135
+ break
136
+ stripped = line.strip()
137
+ try:
138
+
139
+ nmea_msg = pynmea2.parse(stripped, check)
140
+ except KeyboardInterrupt:
141
+ raise
142
+ except Exception as e:
143
+ # Parsing error, log and skip.
144
+ # Throttle the logs.
145
+ now = time.time()
146
+ if self.last_failed_read_log_time is None or (
147
+ now - self.last_failed_read_log_time) > self.LOG_THROTTLE_TIME:
148
+ self.logger.exception(f"Failed to parse {stripped}. Is it NMEA?")
149
+ self.last_failed_read_log_time = now
150
+ continue
151
+
152
+ # Only use NMEA messages that have a timestamp.
153
+ # For example, GSA and GST messages are not supported.
154
+ if isinstance(nmea_msg.timestamp, datetime.time):
155
+ self.full_lines.append((nmea_msg, stripped, timestamp))
156
+ elif isinstance(nmea_msg.timestamp, str) and nmea_msg.timestamp == '.':
157
+ # pynmea2 will set the timestamp to the string '.' when GPS
158
+ # spits out: "$GPZDA,.,,,,,00*66". Silently ignore.
159
+ continue
160
+ elif nmea_msg.timestamp is not None:
161
+ self.logger.error("Invalid timestamp for \"" + stripped + "\" \"")
162
+ continue
163
+
164
+ found = True
165
+ found_subsets = []
166
+ # Group a subset of NMEA messages based on timestamp.
167
+ while found:
168
+ if len(self.full_lines) < 2:
169
+ break
170
+ first_time = self.full_lines[0][0].timestamp
171
+ found = False
172
+ for idx in range(1, len(self.full_lines)):
173
+
174
+ date = datetime.date(1, 1, 1)
175
+ datetime1 = datetime.datetime.combine(date, first_time)
176
+ datetime2 = datetime.datetime.combine(date, self.full_lines[idx][0].timestamp)
177
+
178
+ # Mod the total seconds by 3600 so that checking 23:59:59.99 and 00:00:00.00
179
+ # evaluate as close to each other.
180
+ time_elapsed = (datetime2 - datetime1).total_seconds() % 3600
181
+
182
+ if (time_elapsed < 0) or (time_elapsed > self.grouping_timeout):
183
+ subset = self.full_lines[0:idx]
184
+ self.full_lines = self.full_lines[idx:]
185
+ found_subsets.append(subset)
186
+ found = True
187
+ break
188
+
189
+ return [self.nmea_message_group_to_gps_data_point(x, time_converter) for x in found_subsets]
@@ -0,0 +1,6 @@
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
+
@@ -0,0 +1,56 @@
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
+ """For clients to use the Gps Aggregator service."""
8
+ import collections
9
+
10
+ from bosdyn.api.gps import aggregator_pb2, aggregator_service_pb2_grpc, gps_pb2
11
+ from bosdyn.client.common import (BaseClient, common_header_errors, error_factory, error_pair,
12
+ handle_common_header_errors, handle_unset_status_error)
13
+ from bosdyn.client.exceptions import ResponseError
14
+
15
+
16
+ class AggregatorClient(BaseClient):
17
+ """Client for the Gps Aggregator service."""
18
+ default_service_name = 'gps-aggregator'
19
+ service_type = 'bosdyn.api.gps.AggregatorService'
20
+
21
+ def __init__(self):
22
+ super(AggregatorClient, self).__init__(aggregator_service_pb2_grpc.AggregatorServiceStub)
23
+
24
+ def new_gps_data(self, data_points, gps_device, **kwargs):
25
+ """Tell the robot about new GPS data that was collected.
26
+
27
+ Args:
28
+ data_points (collection of bosdyn.api.gps.GpsDataPoint): All the data you want to send.
29
+ gps_device (collection of bosdyn.api.gps.GpsDevice): The identifier of this device.
30
+
31
+ Returns:
32
+ An instance of bosdyn.api.NewGpsDataResponse
33
+
34
+ Raises:
35
+ RpcError: Problem communicating with the robot.
36
+ """
37
+ req = aggregator_pb2.NewGpsDataRequest()
38
+ req.data_points.extend(data_points)
39
+ req.gps_device.CopyFrom(gps_device)
40
+ return self.call(self._stub.NewGpsData, req, None, error_from_response=_new_gps_data_error,
41
+ copy_request=False, **kwargs)
42
+
43
+ def new_gps_data_async(self, data_points, gps_device, **kwargs):
44
+ """Async version of new_gps_data()"""
45
+ req = aggregator_pb2.NewGpsDataRequest()
46
+ req.data_points.extend(data_points)
47
+ req.gps_device.CopyFrom(gps_device)
48
+ return self.call_async(self._stub.NewGpsData, req, None,
49
+ error_from_response=_new_gps_data_error, copy_request=False,
50
+ **kwargs)
51
+
52
+
53
+ @handle_common_header_errors
54
+ def _new_gps_data_error(response):
55
+ """Return an exception based on response from New GPS Data RPC, None if no error."""
56
+ return None
@@ -0,0 +1,153 @@
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
+ """Reads GPS data from a tcp/udp stream, and sends to aggregator service."""
8
+
9
+ import logging
10
+ import signal
11
+ import socket
12
+ import time
13
+ from typing import List
14
+
15
+ import bosdyn.api
16
+ import bosdyn.client.util
17
+ from bosdyn.api.gps.gps_pb2 import GpsDataPoint, GpsDevice
18
+ from bosdyn.client.exceptions import ProxyConnectionError
19
+ from bosdyn.client.gps.aggregator_client import AggregatorClient
20
+ from bosdyn.client.gps.NMEAParser import NMEAParser
21
+ from bosdyn.client.robot import UnregisteredServiceNameError
22
+ from bosdyn.util import RobotTimeConverter, duration_to_seconds
23
+
24
+
25
+ class NMEAStreamReader(object):
26
+
27
+ # The amount of time to wait before logging another decode error.
28
+ LOG_THROTTLE_TIME = 2.0 # seconds.
29
+
30
+ def __init__(self, logger, stream, body_tform_gps):
31
+ self.logger = logger
32
+ self.stream = stream
33
+ self.parser = NMEAParser(logger)
34
+ self.body_tform_gps = body_tform_gps.to_proto()
35
+ self.last_failed_read_log_time = None
36
+
37
+ def read_data(self, time_converter: RobotTimeConverter) -> List[GpsDataPoint]:
38
+ """This function returns an array of new GpsDataPoints."""
39
+ try:
40
+ raw_data = self.stream.readline()
41
+ except UnicodeDecodeError:
42
+ # Throttle the logs.
43
+ now = time.time()
44
+ if self.last_failed_read_log_time is None or (
45
+ now - self.last_failed_read_log_time) > self.LOG_THROTTLE_TIME:
46
+ self.logger.exception("Failed to decode NMEA message. Is it not Unicode?")
47
+ self.last_failed_read_log_time = now
48
+ return None
49
+
50
+ if '$' not in raw_data:
51
+ # Not NMEA
52
+ return None
53
+
54
+ # Trim any leading characters before the NMEA sentence.
55
+ raw_data = raw_data[raw_data.index('$'):]
56
+ self.logger.info(f"Read: {raw_data}")
57
+ new_points = self.parser.parse(raw_data, time_converter, check=False)
58
+
59
+ # Offset for the GPS
60
+ for data_point in new_points:
61
+ data_point.body_tform_gps.CopyFrom(self.body_tform_gps)
62
+
63
+ return new_points
64
+
65
+
66
+ class GpsListener:
67
+
68
+ def __init__(self, robot, time_converter, stream, name, body_tform_gps, logger):
69
+ self.logger = logger
70
+ self.robot = robot
71
+ self.time_converter = time_converter
72
+ self.reader = NMEAStreamReader(logger, stream, body_tform_gps)
73
+ self.gps_device = GpsDevice()
74
+ self.gps_device.name = name
75
+ self.aggregator_client = None
76
+
77
+ def run(self):
78
+ # It is possible for a payload to come up faster than the service. Loop a few times
79
+ # to give it time to come up.
80
+ MAX_ATTEMPTS = 45
81
+ SECS_PER_ATTEMPT = 2
82
+ num_attempts = 0
83
+
84
+ while self.aggregator_client is None and num_attempts < MAX_ATTEMPTS:
85
+ num_attempts += 1
86
+ try:
87
+ self.aggregator_client = self.robot.ensure_client(
88
+ AggregatorClient.default_service_name)
89
+ except (UnregisteredServiceNameError, ProxyConnectionError):
90
+ self.logger.info("Waiting for the Aggregator Service")
91
+ time.sleep(SECS_PER_ATTEMPT)
92
+ except:
93
+ self.logger.exception(
94
+ "Unexpected exception while waiting for the Aggregator Service")
95
+ time.sleep(SECS_PER_ATTEMPT)
96
+ if num_attempts == MAX_ATTEMPTS:
97
+ self.logger.error("Failed to connect to the Aggregator Service!")
98
+ return False
99
+
100
+ # Continue to send an empty GPS data request if connected to a device without GPS signal.
101
+ # These variables control the frequency with which these empty messages are sent.
102
+ every_x_seconds = 5
103
+ time_passed_since_last_rpc = 0
104
+ timestamp_of_last_rpc = 0
105
+
106
+ # Ensure that KeyboardInterrupt is raised on a SIGINT.
107
+ signal.signal(signal.SIGINT, signal.default_int_handler)
108
+
109
+ accumulated_data = []
110
+ agg_future = None
111
+
112
+ # Attach and run until a SIGINT is received.
113
+ self.logger.info('Looping')
114
+ try:
115
+ while True:
116
+ try:
117
+ new_data = self.reader.read_data(self.time_converter)
118
+ except socket.timeout:
119
+ self.logger.warn(
120
+ """Socket timed out while reading GPS data. This may be normal, there
121
+ could be a problem with the GPS receiver, or there may be a loose hardware
122
+ connection.""")
123
+ return False
124
+ except:
125
+ self.logger.exception(
126
+ "Caught exception while attempting to read from GPS stream.")
127
+ return False
128
+
129
+ if new_data is None:
130
+ continue
131
+
132
+ accumulated_data.extend(new_data)
133
+
134
+ if len(accumulated_data) > 0:
135
+ if agg_future is None or agg_future.done():
136
+ agg_future = self.aggregator_client.new_gps_data_async(
137
+ accumulated_data, self.gps_device)
138
+ accumulated_data.clear()
139
+ timestamp_of_last_rpc = time.time()
140
+ time_passed_since_last_rpc = 0
141
+ else:
142
+ if time_passed_since_last_rpc > every_x_seconds:
143
+ if agg_future is None or agg_future.done():
144
+ agg_future = self.aggregator_client.new_gps_data_async([],
145
+ self.gps_device)
146
+ timestamp_of_last_rpc = time.time()
147
+ time_passed_since_last_rpc = 0
148
+ else:
149
+ time_passed_since_last_rpc = time.time() - timestamp_of_last_rpc
150
+
151
+ except KeyboardInterrupt:
152
+ print() # Get past the ^C in the console output
153
+ pass
@@ -0,0 +1,48 @@
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
+ from bosdyn.api.gps import registration_pb2, registration_service_pb2_grpc
8
+ from bosdyn.client.common import BaseClient, handle_common_header_errors
9
+
10
+
11
+ class RegistrationClient(BaseClient):
12
+ """ Client for the GPS Registration service. """
13
+ default_service_name = 'gps-registration'
14
+ service_type = 'bosdyn.api.gps.RegistrationService'
15
+
16
+ def __init__(self):
17
+ super(RegistrationClient,
18
+ self).__init__(registration_service_pb2_grpc.RegistrationServiceStub)
19
+
20
+ def get_location(self):
21
+ req = registration_pb2.GetLocationRequest()
22
+ return self.call(self._stub.GetLocation, req, None, error_from_response=_get_location_error,
23
+ copy_request=False)
24
+
25
+ def get_location_async(self):
26
+ req = registration_pb2.GetLocationRequest()
27
+ return self.call_async(self._stub.GetLocation, req, None,
28
+ error_from_response=_get_location_error, copy_request=False)
29
+
30
+ def reset_registration(self):
31
+ req = registration_pb2.ResetRegistrationRequest()
32
+ return self.call(self._stub.ResetRegistration, req, None,
33
+ error_from_response=_get_location_error, copy_request=False)
34
+
35
+ def reset_registration_async(self):
36
+ req = registration_pb2.ResetRegistrationRequest()
37
+ return self.call_async(self._stub.ResetRegistration, req, None,
38
+ error_from_response=_get_location_error, copy_request=False)
39
+
40
+
41
+ @handle_common_header_errors
42
+ def _get_location_error(response):
43
+ return None
44
+
45
+
46
+ @handle_common_header_errors
47
+ def _reset_registration_error(response):
48
+ return None
@@ -128,6 +128,7 @@ class GraphNavClient(BaseClient):
128
128
  request_live_world_objects=False,
129
129
  request_live_robot_state=False,
130
130
  waypoint_id=None,
131
+ request_gps_state=False,
131
132
  **kwargs):
132
133
  """Obtain current localization state of the robot.
133
134
 
@@ -141,21 +142,23 @@ class GraphNavClient(BaseClient):
141
142
  request_live_images=request_live_images,
142
143
  request_live_terrain_maps=request_live_terrain_maps,
143
144
  request_live_world_objects=request_live_world_objects,
144
- request_live_robot_state=request_live_robot_state, waypoint_id=waypoint_id)
145
+ request_live_robot_state=request_live_robot_state, waypoint_id=waypoint_id,
146
+ request_gps_state=request_gps_state)
145
147
  return self.call(self._stub.GetLocalizationState, req, None, common_header_errors,
146
148
  copy_request=False, **kwargs)
147
149
 
148
- def get_localization_state_async(self, request_live_point_cloud=False,
149
- request_live_images=False, request_live_terrain_maps=False,
150
- request_live_world_objects=False,
151
- request_live_robot_state=False, waypoint_id=None, **kwargs):
150
+ def get_localization_state_async(
151
+ self, request_live_point_cloud=False, request_live_images=False,
152
+ request_live_terrain_maps=False, request_live_world_objects=False,
153
+ request_live_robot_state=False, waypoint_id=None, request_gps_state=False, **kwargs):
152
154
  """Async version of get_localization_state()."""
153
155
  req = self._build_get_localization_state_request(
154
156
  request_live_point_cloud=request_live_point_cloud,
155
157
  request_live_images=request_live_images,
156
158
  request_live_terrain_maps=request_live_terrain_maps,
157
159
  request_live_world_objects=request_live_world_objects,
158
- request_live_robot_state=request_live_robot_state, waypoint_id=waypoint_id)
160
+ request_live_robot_state=request_live_robot_state, waypoint_id=waypoint_id,
161
+ request_gps_state=request_gps_state)
159
162
  return self.call_async(self._stub.GetLocalizationState, req, None, common_header_errors,
160
163
  copy_request=False, **kwargs)
161
164
 
@@ -302,7 +305,8 @@ class GraphNavClient(BaseClient):
302
305
  raise GraphNavServiceResponseError(response=None, error_message='No timesync endpoint!')
303
306
  request = self._build_navigate_to_request(destination_waypoint_id, travel_params,
304
307
  route_params, cmd_duration, leases, used_endpoint,
305
- command_id, destination_waypoint_tform_body_goal)
308
+ command_id, destination_waypoint_tform_body_goal,
309
+ route_blocked_behavior)
306
310
  return self.call_async(self._stub.NavigateTo, request,
307
311
  value_from_response=_command_id_from_navigate_route_response,
308
312
  error_from_response=_navigate_to_error, copy_request=False, **kwargs)
@@ -340,7 +344,7 @@ class GraphNavClient(BaseClient):
340
344
  def navigate_to_anchor(self, seed_tform_goal, cmd_duration, route_params=None,
341
345
  travel_params=None, leases=None, timesync_endpoint=None,
342
346
  goal_waypoint_rt_seed_ewrt_seed_tolerance=None, command_id=None,
343
- **kwargs):
347
+ gps_navigation_params=None, **kwargs):
344
348
  """Navigate to a pose in seed frame along a route chosen by the GraphNav service.
345
349
 
346
350
  Args:
@@ -353,6 +357,9 @@ class GraphNavClient(BaseClient):
353
357
  goal_waypoint_rt_seed_ewrt_seed_tolerance: Vec3 protobuf of the tolerances for goal waypoint selection.
354
358
  command_id: If not None, this continues an existing navigate_to command with the given ID. If None,
355
359
  a new command_id will be used.
360
+ gps_navigation_params: API GPSNavigationParams. If not None, this will be interpreted as a GPS-based
361
+ navigation command. seed_tform_goal will be ignored and whatever goal is passed in using the GPS
362
+ navigation params will be used.
356
363
  Returns:
357
364
  int: Command ID to use in feedback lookup.
358
365
  Raises:
@@ -372,10 +379,9 @@ class GraphNavClient(BaseClient):
372
379
  used_endpoint = timesync_endpoint or self._timesync_endpoint
373
380
  if not used_endpoint:
374
381
  raise GraphNavServiceResponseError(response=None, error_message='No timesync endpoint!')
375
- request = self._build_navigate_to_anchor_request(seed_tform_goal, travel_params,
376
- route_params, cmd_duration, leases,
377
- used_endpoint, command_id,
378
- goal_waypoint_rt_seed_ewrt_seed_tolerance)
382
+ request = self._build_navigate_to_anchor_request(
383
+ seed_tform_goal, travel_params, route_params, cmd_duration, leases, used_endpoint,
384
+ command_id, goal_waypoint_rt_seed_ewrt_seed_tolerance, gps_navigation_params)
379
385
  return self.call(self._stub.NavigateToAnchor, request,
380
386
  value_from_response=_command_id_from_navigate_route_response,
381
387
  error_from_response=_navigate_to_anchor_error, copy_request=False,
@@ -384,15 +390,14 @@ class GraphNavClient(BaseClient):
384
390
  def navigate_to_anchor_async(self, seed_tform_goal, cmd_duration, route_params=None,
385
391
  travel_params=None, leases=None, timesync_endpoint=None,
386
392
  goal_waypoint_rt_seed_ewrt_seed_tolerance=None, command_id=None,
387
- **kwargs):
393
+ gps_navigation_params=None, **kwargs):
388
394
  """Async version of navigate_to_anchor()."""
389
395
  used_endpoint = timesync_endpoint or self._timesync_endpoint
390
396
  if not used_endpoint:
391
397
  raise GraphNavServiceResponseError(response=None, error_message='No timesync endpoint!')
392
- request = self._build_navigate_to_anchor_request(seed_tform_goal, travel_params,
393
- route_params, cmd_duration, leases,
394
- used_endpoint, command_id,
395
- goal_waypoint_rt_seed_ewrt_seed_tolerance)
398
+ request = self._build_navigate_to_anchor_request(
399
+ seed_tform_goal, travel_params, route_params, cmd_duration, leases, used_endpoint,
400
+ command_id, goal_waypoint_rt_seed_ewrt_seed_tolerance, gps_navigation_params)
396
401
  return self.call_async(self._stub.NavigateTo, request,
397
402
  value_from_response=_command_id_from_navigate_route_response,
398
403
  error_from_response=_navigate_to_anchor_error, copy_request=False,
@@ -628,13 +633,15 @@ class GraphNavClient(BaseClient):
628
633
  @staticmethod
629
634
  def _build_get_localization_state_request(request_live_point_cloud, request_live_images,
630
635
  request_live_terrain_maps, request_live_world_objects,
631
- request_live_robot_state, waypoint_id):
636
+ request_live_robot_state, waypoint_id,
637
+ request_gps_state):
632
638
  return graph_nav_pb2.GetLocalizationStateRequest(
633
639
  request_live_point_cloud=request_live_point_cloud,
634
640
  request_live_images=request_live_images,
635
641
  request_live_terrain_maps=request_live_terrain_maps,
636
642
  request_live_world_objects=request_live_world_objects,
637
- request_live_robot_state=request_live_robot_state, waypoint_id=waypoint_id)
643
+ request_live_robot_state=request_live_robot_state, waypoint_id=waypoint_id,
644
+ request_gps_state=request_gps_state)
638
645
 
639
646
  @staticmethod
640
647
  def _build_navigate_route_request(route, route_follow_params, travel_params, end_time_secs,
@@ -677,12 +684,16 @@ class GraphNavClient(BaseClient):
677
684
  @staticmethod
678
685
  def _build_navigate_to_anchor_request(seed_tform_goal, travel_params, route_params,
679
686
  end_time_secs, leases, timesync_endpoint, command_id,
680
- goal_waypoint_rt_seed_ewrt_seed_tolerance):
687
+ goal_waypoint_rt_seed_ewrt_seed_tolerance,
688
+ gps_navigation_params):
681
689
  converter = timesync_endpoint.get_robot_time_converter()
682
690
  request = graph_nav_pb2.NavigateToAnchorRequest(
683
691
  seed_tform_goal=seed_tform_goal,
684
692
  goal_waypoint_rt_seed_ewrt_seed_tolerance=goal_waypoint_rt_seed_ewrt_seed_tolerance,
685
693
  clock_identifier=timesync_endpoint.clock_identifier)
694
+ # Note that this overrides the seed_tform_goal, which is a OneOf.
695
+ if gps_navigation_params:
696
+ request.gps_navigation_params.CopyFrom(gps_navigation_params)
686
697
  request.end_time.CopyFrom(
687
698
  converter.robot_timestamp_from_local_secs(time.time() + end_time_secs))
688
699
  if travel_params is not None:
@@ -925,6 +936,23 @@ class RobotLostError(RouteNavigationError):
925
936
  """Cannot issue a navigation request when the robot is already lost."""
926
937
 
927
938
 
939
+ class InvalidGPSError(RouteNavigationError):
940
+ """Cannot issue the GPS command because it is invalid."""
941
+
942
+ def _gps_status_to_string(self, status):
943
+ if status == graph_nav_pb2.NavigateToAnchorResponse.GPS_STATUS_OK:
944
+ return 'OK'
945
+ elif status == graph_nav_pb2.NavigateToAnchorResponse.GPS_STATUS_NO_COORDS_IN_MAP:
946
+ return 'The uploaded map did not contain any valid GPS coordinates.'
947
+ elif status == graph_nav_pb2.NavigateToAnchorResponse.GPS_STATUS_TOO_FAR_FROM_MAP:
948
+ return 'The given coordinates were too far from any coordinates in the uploaded map.'
949
+ else:
950
+ return 'Unknown error'
951
+
952
+ def __str__(self):
953
+ return f'{self.error_message} (reason: {self._gps_status_to_string(self.response.gps_status)})'
954
+
955
+
928
956
  class RobotNotLocalizedToRouteError(RouteNavigationError):
929
957
  """The current localization doesn't refer to any waypoint in the route (possibly uninitialized localization)."""
930
958
 
@@ -1213,6 +1241,8 @@ _NAVIGATE_TO_ANCHOR_STATUS_TO_ERROR.update({
1213
1241
  error_pair(UnrecognizedCommandError),
1214
1242
  graph_nav_pb2.NavigateToAnchorResponse.STATUS_AREA_CALLBACK_ERROR:
1215
1243
  error_pair(AreaCallbackMapError),
1244
+ graph_nav_pb2.NavigateToAnchorResponse.STATUS_INVALID_GPS_COMMAND:
1245
+ error_pair(InvalidGPSError)
1216
1246
  })
1217
1247
 
1218
1248