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.
- bosdyn/client/__init__.py +5 -6
- bosdyn/client/area_callback_region_handler_base.py +19 -4
- bosdyn/client/area_callback_service_servicer.py +29 -1
- bosdyn/client/area_callback_service_utils.py +45 -51
- bosdyn/client/auth.py +13 -28
- bosdyn/client/autowalk.py +3 -0
- bosdyn/client/channel.py +23 -26
- bosdyn/client/command_line.py +64 -13
- bosdyn/client/common.py +4 -4
- bosdyn/client/data_acquisition.py +47 -6
- bosdyn/client/data_acquisition_plugin.py +12 -2
- bosdyn/client/data_acquisition_plugin_service.py +33 -2
- bosdyn/client/data_acquisition_store.py +38 -0
- bosdyn/client/data_buffer.py +22 -8
- bosdyn/client/data_chunk.py +1 -0
- bosdyn/client/directory_registration.py +1 -14
- bosdyn/client/exceptions.py +0 -4
- bosdyn/client/frame_helpers.py +3 -1
- bosdyn/client/gps/NMEAParser.py +189 -0
- bosdyn/client/gps/__init__.py +6 -0
- bosdyn/client/gps/aggregator_client.py +56 -0
- bosdyn/client/gps/gps_listener.py +153 -0
- bosdyn/client/gps/registration_client.py +48 -0
- bosdyn/client/graph_nav.py +50 -20
- bosdyn/client/image.py +20 -7
- bosdyn/client/image_service_helpers.py +14 -14
- bosdyn/client/lease.py +27 -22
- bosdyn/client/lease_validator.py +5 -5
- bosdyn/client/manipulation_api_client.py +1 -1
- bosdyn/client/map_processing.py +10 -5
- bosdyn/client/math_helpers.py +21 -11
- bosdyn/client/metrics_logging.py +147 -0
- bosdyn/client/network_compute_bridge_client.py +6 -0
- bosdyn/client/power.py +40 -0
- bosdyn/client/recording.py +3 -3
- bosdyn/client/robot.py +15 -16
- bosdyn/client/robot_command.py +341 -203
- bosdyn/client/robot_id.py +6 -5
- bosdyn/client/robot_state.py +6 -0
- bosdyn/client/sdk.py +5 -11
- bosdyn/client/server_util.py +11 -11
- bosdyn/client/service_customization_helpers.py +776 -64
- bosdyn/client/signals_helpers.py +105 -0
- bosdyn/client/spot_cam/compositor.py +6 -2
- bosdyn/client/spot_cam/ptz.py +24 -14
- bosdyn/client/spot_check.py +160 -0
- bosdyn/client/time_sync.py +5 -5
- bosdyn/client/units_helpers.py +39 -0
- bosdyn/client/util.py +100 -64
- bosdyn/client/world_object.py +5 -5
- {bosdyn_client-3.3.2.dist-info → bosdyn_client-4.0.1.dist-info}/METADATA +4 -3
- bosdyn_client-4.0.1.dist-info/RECORD +97 -0
- {bosdyn_client-3.3.2.dist-info → bosdyn_client-4.0.1.dist-info}/WHEEL +1 -1
- bosdyn/client/log_annotation.py +0 -359
- bosdyn_client-3.3.2.dist-info/RECORD +0 -90
- {bosdyn_client-3.3.2.dist-info → bosdyn_client-4.0.1.dist-info}/top_level.txt +0 -0
bosdyn/client/frame_helpers.py
CHANGED
|
@@ -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
|
-
|
|
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
|
bosdyn/client/graph_nav.py
CHANGED
|
@@ -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(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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(
|
|
376
|
-
|
|
377
|
-
|
|
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(
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|