bosdyn-client 4.1.0__py3-none-any.whl → 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bosdyn/client/__init__.py +1 -0
- bosdyn/client/command_line.py +15 -7
- bosdyn/client/data_acquisition_helpers.py +1 -1
- bosdyn/client/data_acquisition_store.py +2 -2
- bosdyn/client/gps/NMEAParser.py +16 -6
- bosdyn/client/gps/gps_listener.py +31 -1
- bosdyn/client/gps/ntrip_client.py +240 -0
- bosdyn/client/graph_nav.py +26 -1
- bosdyn/client/gripper_camera_param.py +34 -0
- bosdyn/client/image.py +16 -0
- bosdyn/client/image_service_helpers.py +88 -68
- bosdyn/client/log_status.py +6 -0
- bosdyn/client/math_helpers.py +15 -1
- bosdyn/client/payload_software_update.py +185 -0
- bosdyn/client/payload_software_update_initiation.py +79 -0
- bosdyn/client/point_cloud.py +9 -0
- bosdyn/client/robot.py +5 -4
- bosdyn/client/sdk.py +4 -2
- bosdyn/client/service_customization_helpers.py +18 -5
- bosdyn/client/spot_cam/__init__.py +2 -0
- bosdyn/client/spot_cam/ptz.py +20 -24
- bosdyn/client/util.py +1 -1
- {bosdyn_client-4.1.0.dist-info → bosdyn_client-5.0.0.dist-info}/METADATA +4 -4
- {bosdyn_client-4.1.0.dist-info → bosdyn_client-5.0.0.dist-info}/RECORD +26 -23
- {bosdyn_client-4.1.0.dist-info → bosdyn_client-5.0.0.dist-info}/WHEEL +0 -0
- {bosdyn_client-4.1.0.dist-info → bosdyn_client-5.0.0.dist-info}/top_level.txt +0 -0
bosdyn/client/__init__.py
CHANGED
bosdyn/client/command_line.py
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
# is subject to the terms and conditions of the Boston Dynamics Software
|
|
5
5
|
# Development Kit License (20191101-BDSDK-SL).
|
|
6
6
|
|
|
7
|
+
# Boston Dynamics, Inc. Confidential Information.
|
|
8
|
+
# Copyright 2025. All Rights Reserved.
|
|
7
9
|
"""Command-line utility code for interacting with robot services."""
|
|
8
10
|
|
|
9
11
|
from __future__ import division
|
|
@@ -705,13 +707,16 @@ class LogStatusCommands(Subcommands):
|
|
|
705
707
|
subparsers: List of argument parsers.
|
|
706
708
|
command_dict: Dictionary of command names which take parsed options.
|
|
707
709
|
"""
|
|
708
|
-
super(LogStatusCommands, self).__init__(
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
710
|
+
super(LogStatusCommands, self).__init__(
|
|
711
|
+
subparsers,
|
|
712
|
+
command_dict,
|
|
713
|
+
[
|
|
714
|
+
GetLogCommand,
|
|
715
|
+
GetActiveLogStatusesCommand,
|
|
716
|
+
ExperimentLogCommand,
|
|
717
|
+
StartRetroLogCommand,
|
|
718
|
+
TerminateLogCommand,
|
|
719
|
+
])
|
|
715
720
|
|
|
716
721
|
|
|
717
722
|
class GetLogCommand(Command):
|
|
@@ -947,6 +952,8 @@ class TerminateLogCommand(Command):
|
|
|
947
952
|
return True
|
|
948
953
|
|
|
949
954
|
|
|
955
|
+
|
|
956
|
+
|
|
950
957
|
class RobotIdCommand(Command):
|
|
951
958
|
"""Show robot-id."""
|
|
952
959
|
|
|
@@ -2646,6 +2653,7 @@ def main(args=None):
|
|
|
2646
2653
|
parser = argparse.ArgumentParser(prog='bosdyn.client', description=main.__doc__)
|
|
2647
2654
|
add_common_arguments(parser, credentials_no_warn=True)
|
|
2648
2655
|
|
|
2656
|
+
|
|
2649
2657
|
command_dict = {} # command name to fn which takes parsed options
|
|
2650
2658
|
subparsers = parser.add_subparsers(title='commands', dest='command')
|
|
2651
2659
|
|
|
@@ -167,7 +167,7 @@ def clean_filename(filename):
|
|
|
167
167
|
filename(string): Original filename to clean.
|
|
168
168
|
|
|
169
169
|
Returns:
|
|
170
|
-
Valid filename with removed characters
|
|
170
|
+
Valid filename with removed characters :?<>|*
|
|
171
171
|
"""
|
|
172
172
|
|
|
173
173
|
return "".join(i for i in filename if i not in ":*?<>|")
|
|
@@ -338,8 +338,8 @@ class DataAcquisitionStoreClient(BaseClient):
|
|
|
338
338
|
def query_max_capture_id(self, **kwargs):
|
|
339
339
|
"""Query max capture id from the robot.
|
|
340
340
|
Returns:
|
|
341
|
-
QueryMaxCaptureIdResult, which has a max_capture_id uint64, corresponding to the
|
|
342
|
-
greatest capture id on the robot. Used for
|
|
341
|
+
QueryMaxCaptureIdResult, which has a max_capture_id uint64, corresponding to the
|
|
342
|
+
greatest capture id on the robot. Used for skipping DAQ synchronization
|
|
343
343
|
on connect.
|
|
344
344
|
"""
|
|
345
345
|
request = data_acquisition_store.QueryMaxCaptureIdRequest()
|
bosdyn/client/gps/NMEAParser.py
CHANGED
|
@@ -14,7 +14,7 @@ from google.protobuf.any_pb2 import Any
|
|
|
14
14
|
from google.protobuf.timestamp_pb2 import Timestamp
|
|
15
15
|
|
|
16
16
|
from bosdyn.api.gps.gps_pb2 import GpsDataPoint
|
|
17
|
-
from bosdyn.util import RobotTimeConverter,
|
|
17
|
+
from bosdyn.util import RobotTimeConverter, now_sec, seconds_to_timestamp
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
has_warned_no_zda = False
|
|
@@ -36,6 +36,7 @@ class NMEAParser(object):
|
|
|
36
36
|
# If your GPS outputs data at 20 Hz, this constant must be less than 0.050 seconds.
|
|
37
37
|
self.grouping_timeout = 0.025
|
|
38
38
|
self.last_failed_read_log_time = None
|
|
39
|
+
self.last_gga = None
|
|
39
40
|
|
|
40
41
|
def nmea_message_group_to_gps_data_point(self, nmea_messages: List[Tuple[str, str, int]],
|
|
41
42
|
time_converter: RobotTimeConverter):
|
|
@@ -53,12 +54,13 @@ class NMEAParser(object):
|
|
|
53
54
|
data, raw_nmea_msg, client_timestamp = nmea_message_list
|
|
54
55
|
|
|
55
56
|
if data.sentence_type == 'GGA':
|
|
57
|
+
self.last_gga = raw_nmea_msg
|
|
56
58
|
if data.latitude is not None and data.longitude is not None and data.altitude is not None:
|
|
57
59
|
data_point.llh.latitude = data.latitude
|
|
58
60
|
data_point.llh.longitude = data.longitude
|
|
59
61
|
data_point.llh.height = data.altitude
|
|
60
62
|
|
|
61
|
-
if data.num_sats is not None and int(data.num_sats) > 0:
|
|
63
|
+
if data.num_sats is not None and data.num_sats != '' and int(data.num_sats) > 0:
|
|
62
64
|
for _ in range(int(data.num_sats)):
|
|
63
65
|
sat = data_point.satellites.add()
|
|
64
66
|
|
|
@@ -86,7 +88,12 @@ class NMEAParser(object):
|
|
|
86
88
|
data_point.accuracy.vertical = data.std_dev_altitude
|
|
87
89
|
|
|
88
90
|
elif data.sentence_type == 'ZDA':
|
|
89
|
-
|
|
91
|
+
try:
|
|
92
|
+
gps_timestamp = data.datetime
|
|
93
|
+
except:
|
|
94
|
+
self.logger.exception("Failed to extract datetime from ZDA message.")
|
|
95
|
+
continue
|
|
96
|
+
|
|
90
97
|
# Protobuf timestamp does not use timezone aware timestamps.
|
|
91
98
|
gps_timestamp_no_tz = gps_timestamp.replace(tzinfo=None)
|
|
92
99
|
data_point.timestamp_gps.FromDatetime(gps_timestamp_no_tz)
|
|
@@ -111,7 +118,7 @@ class NMEAParser(object):
|
|
|
111
118
|
|
|
112
119
|
def parse(self, new_data: str, time_converter: RobotTimeConverter,
|
|
113
120
|
check: bool = True) -> List[GpsDataPoint]:
|
|
114
|
-
timestamp =
|
|
121
|
+
timestamp = now_sec() # Client timestamp when received.
|
|
115
122
|
self.data = self.data + new_data
|
|
116
123
|
|
|
117
124
|
if len(self.data) == 0:
|
|
@@ -143,14 +150,14 @@ class NMEAParser(object):
|
|
|
143
150
|
except Exception as e:
|
|
144
151
|
# Parsing error, log and skip.
|
|
145
152
|
# Throttle the logs.
|
|
146
|
-
now =
|
|
153
|
+
now = now_sec()
|
|
147
154
|
if self.last_failed_read_log_time is None or (
|
|
148
155
|
now - self.last_failed_read_log_time) > self.LOG_THROTTLE_TIME:
|
|
149
156
|
self.logger.exception(f"Failed to parse {stripped}. Is it NMEA?")
|
|
150
157
|
self.last_failed_read_log_time = now
|
|
151
158
|
continue
|
|
152
159
|
|
|
153
|
-
#
|
|
160
|
+
# If the message does not contain a timestamp attribute, abandon the rest of the logic
|
|
154
161
|
# and go to the beginning of the loop
|
|
155
162
|
if not hasattr(nmea_msg, 'timestamp'):
|
|
156
163
|
continue
|
|
@@ -193,3 +200,6 @@ class NMEAParser(object):
|
|
|
193
200
|
break
|
|
194
201
|
|
|
195
202
|
return [self.nmea_message_group_to_gps_data_point(x, time_converter) for x in found_subsets]
|
|
203
|
+
|
|
204
|
+
def get_latest_gga(self):
|
|
205
|
+
return self.last_gga
|
|
@@ -17,6 +17,7 @@ from bosdyn.api.gps.gps_pb2 import GpsDataPoint, GpsDevice
|
|
|
17
17
|
from bosdyn.client.exceptions import ProxyConnectionError
|
|
18
18
|
from bosdyn.client.gps.aggregator_client import AggregatorClient
|
|
19
19
|
from bosdyn.client.gps.NMEAParser import NMEAParser
|
|
20
|
+
from bosdyn.client.gps.ntrip_client import NtripClient, NtripClientParams
|
|
20
21
|
from bosdyn.client.robot import UnregisteredServiceNameError
|
|
21
22
|
from bosdyn.util import RobotTimeConverter, duration_to_seconds
|
|
22
23
|
|
|
@@ -70,6 +71,9 @@ class NMEAStreamReader(object):
|
|
|
70
71
|
|
|
71
72
|
return new_points
|
|
72
73
|
|
|
74
|
+
def get_latest_gga(self):
|
|
75
|
+
return self.parser.get_latest_gga()
|
|
76
|
+
|
|
73
77
|
|
|
74
78
|
class GpsListener:
|
|
75
79
|
|
|
@@ -77,10 +81,22 @@ class GpsListener:
|
|
|
77
81
|
self.logger = logger
|
|
78
82
|
self.robot = robot
|
|
79
83
|
self.time_converter = time_converter
|
|
84
|
+
self.stream = stream
|
|
80
85
|
self.reader = NMEAStreamReader(logger, stream, body_tform_gps, verbose)
|
|
81
86
|
self.gps_device = GpsDevice()
|
|
82
87
|
self.gps_device.name = name
|
|
83
88
|
self.aggregator_client = None
|
|
89
|
+
self.ntrip_client = None
|
|
90
|
+
|
|
91
|
+
def run_ntrip_client(self, ntrip_params: NtripClientParams):
|
|
92
|
+
self.ntrip_client = NtripClient(self.stream, ntrip_params, self.logger)
|
|
93
|
+
self.ntrip_client.start_stream()
|
|
94
|
+
|
|
95
|
+
def stop_ntrip_client(self):
|
|
96
|
+
if self.ntrip_client is None:
|
|
97
|
+
return
|
|
98
|
+
self.ntrip_client.stop_stream()
|
|
99
|
+
self.ntrip_client = None
|
|
84
100
|
|
|
85
101
|
def run(self):
|
|
86
102
|
# It is possible for a payload to come up faster than the service. Loop a few times
|
|
@@ -118,7 +134,7 @@ class GpsListener:
|
|
|
118
134
|
agg_future = None
|
|
119
135
|
|
|
120
136
|
# Attach and run until a SIGINT is received.
|
|
121
|
-
self.logger.info('
|
|
137
|
+
self.logger.info('Listening for GPS data.')
|
|
122
138
|
try:
|
|
123
139
|
while True:
|
|
124
140
|
try:
|
|
@@ -156,5 +172,19 @@ class GpsListener:
|
|
|
156
172
|
else:
|
|
157
173
|
time_passed_since_last_rpc = time.time() - timestamp_of_last_rpc
|
|
158
174
|
|
|
175
|
+
# If we are running an NTRIP client, pass it the latest GGA message.
|
|
176
|
+
if self.ntrip_client is not None:
|
|
177
|
+
# If the NTRIP Client's stream has been closed, restart it.
|
|
178
|
+
if not self.ntrip_client.is_streaming():
|
|
179
|
+
self.logger.info("Restarting NTRIP Client!")
|
|
180
|
+
self.ntrip_client.start_stream()
|
|
181
|
+
|
|
182
|
+
latest_gga = self.reader.get_latest_gga()
|
|
183
|
+
if latest_gga is not None:
|
|
184
|
+
self.ntrip_client.handle_nmea_gga(latest_gga)
|
|
185
|
+
|
|
159
186
|
except KeyboardInterrupt:
|
|
160
187
|
print() # Get past the ^C in the console output
|
|
188
|
+
|
|
189
|
+
# Just in case there is an NTRIP client still running, stop it here.
|
|
190
|
+
self.stop_ntrip_client()
|
|
@@ -0,0 +1,240 @@
|
|
|
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 base64
|
|
8
|
+
import socket
|
|
9
|
+
import ssl
|
|
10
|
+
import time
|
|
11
|
+
from threading import Thread
|
|
12
|
+
|
|
13
|
+
SERVER_RECONNECT_DELAY = 60 # seconds of delay before retry after server error
|
|
14
|
+
SOCKET_TIMEOUT = 10 # socket timeout in seconds
|
|
15
|
+
SOCKET_MAX_RECV_TIMEOUTS = 12 # number of timeouts before reconnecting
|
|
16
|
+
|
|
17
|
+
DEFAULT_NTRIP_SERVER = ""
|
|
18
|
+
DEFAULT_NTRIP_PORT = 2101
|
|
19
|
+
DEFAULT_NTRIP_TLS_PORT = 2102
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NtripClientParams:
|
|
23
|
+
"""
|
|
24
|
+
Class for storing parameters for connecting an NTRIP client to an NTRIP server.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, server=DEFAULT_NTRIP_SERVER, port=DEFAULT_NTRIP_PORT, user="", password="",
|
|
28
|
+
mountpoint="", tls=False):
|
|
29
|
+
"""
|
|
30
|
+
Constructor.
|
|
31
|
+
"""
|
|
32
|
+
self.server = server
|
|
33
|
+
self.port = port
|
|
34
|
+
self.user = user
|
|
35
|
+
self.password = password
|
|
36
|
+
self.mountpoint = mountpoint
|
|
37
|
+
self.tls = tls
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class NtripClient:
|
|
41
|
+
"""
|
|
42
|
+
Client used to connect to an NTRIP server to download GPS corrections. These corrections are
|
|
43
|
+
then forwarded on to the GPS device using the given stream.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, device, params: NtripClientParams, logger):
|
|
47
|
+
"""
|
|
48
|
+
Constructor.
|
|
49
|
+
"""
|
|
50
|
+
self.device = device
|
|
51
|
+
self.host = params.server
|
|
52
|
+
self.port = params.port
|
|
53
|
+
self.user = params.user
|
|
54
|
+
self.password = params.password
|
|
55
|
+
self.mountpoint = params.mountpoint
|
|
56
|
+
self.tls = params.tls
|
|
57
|
+
|
|
58
|
+
self.thread = None
|
|
59
|
+
self.streaming = False
|
|
60
|
+
self.sock = None
|
|
61
|
+
|
|
62
|
+
self.logger = logger
|
|
63
|
+
|
|
64
|
+
def make_request(self):
|
|
65
|
+
"""
|
|
66
|
+
Make a connection request to an NTRIP server.
|
|
67
|
+
"""
|
|
68
|
+
auth_str = base64.b64encode(f"{self.user}:{self.password}".encode()).decode()
|
|
69
|
+
request = (f"GET /{self.mountpoint} HTTP/1.1\r\n"
|
|
70
|
+
f"Host: {self.host}:{self.port}\r\n"
|
|
71
|
+
"User-Agent: NTRIP Python Client\r\n"
|
|
72
|
+
"Accept: */*\r\n"
|
|
73
|
+
f"Authorization: Basic {auth_str}\r\n"
|
|
74
|
+
"Connection: close\r\n"
|
|
75
|
+
"\r\n")
|
|
76
|
+
return request.encode()
|
|
77
|
+
|
|
78
|
+
def start_stream(self):
|
|
79
|
+
"""
|
|
80
|
+
Start streaming data from an NTRIP server to a GPS receiver.
|
|
81
|
+
"""
|
|
82
|
+
if self.streaming:
|
|
83
|
+
self.stop_stream()
|
|
84
|
+
self.thread = Thread(target=self.stream_data_worker, daemon=True)
|
|
85
|
+
self.thread.start()
|
|
86
|
+
|
|
87
|
+
def stop_stream(self):
|
|
88
|
+
"""
|
|
89
|
+
Stop streaming NTRIP data.
|
|
90
|
+
"""
|
|
91
|
+
self.streaming = False
|
|
92
|
+
if self.thread:
|
|
93
|
+
self.thread.join()
|
|
94
|
+
self.thread = None
|
|
95
|
+
|
|
96
|
+
def is_streaming(self):
|
|
97
|
+
"""
|
|
98
|
+
Determine if we are streaming NTRIP data.
|
|
99
|
+
"""
|
|
100
|
+
return self.streaming
|
|
101
|
+
|
|
102
|
+
def send_gga(self, gga):
|
|
103
|
+
"""
|
|
104
|
+
Given a GPGGA message, send it to the NTRIP server. This helps the NTRIP server send
|
|
105
|
+
corrections that are applicable to the area in which the receiver is operating.
|
|
106
|
+
"""
|
|
107
|
+
if self.sock:
|
|
108
|
+
try:
|
|
109
|
+
self.sock.send(gga.encode())
|
|
110
|
+
except OSError:
|
|
111
|
+
self.logger.warning("Error sending GGA")
|
|
112
|
+
return False
|
|
113
|
+
return True
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def create_icy_session(self):
|
|
117
|
+
"""
|
|
118
|
+
NTRIP Rev1 uses Shoutcast (ICY). Create an ICY session to stream RTCM data.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
# Create a socket connection to the host and port.
|
|
122
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
123
|
+
if self.tls:
|
|
124
|
+
self.logger.info("Using secure connection")
|
|
125
|
+
context = ssl.create_default_context()
|
|
126
|
+
sock = context.wrap_socket(sock, server_hostname=self.host)
|
|
127
|
+
sock.settimeout(SOCKET_TIMEOUT)
|
|
128
|
+
self.logger.info("Connecting to NTRIP Server.")
|
|
129
|
+
sock.connect((self.host, self.port))
|
|
130
|
+
self.logger.info("Connected to NTRIP Server.")
|
|
131
|
+
|
|
132
|
+
# Send the request.
|
|
133
|
+
self.logger.info("Sending NTRIP Request.")
|
|
134
|
+
sock.send(self.make_request())
|
|
135
|
+
# Read a chunk of data, expecting to get the status line and headers.
|
|
136
|
+
response = sock.recv(1024)
|
|
137
|
+
|
|
138
|
+
response_lines = response.split(b"\r\n")
|
|
139
|
+
if len(response_lines) < 2:
|
|
140
|
+
self.logger.error("Invalid response from server: %s", response)
|
|
141
|
+
return False
|
|
142
|
+
status = response_lines[0].decode().split(" ")
|
|
143
|
+
self.logger.info(response_lines[0].decode())
|
|
144
|
+
if status[1] != "200":
|
|
145
|
+
self.logger.error("HTTP Error: %s, retrying in %d seconds",
|
|
146
|
+
response_lines[0].decode(), SERVER_RECONNECT_DELAY)
|
|
147
|
+
sock.close()
|
|
148
|
+
time.sleep(SERVER_RECONNECT_DELAY)
|
|
149
|
+
return False
|
|
150
|
+
self.logger.info("NTRIP Request Response received.")
|
|
151
|
+
for line in range(1, len(response_lines)):
|
|
152
|
+
if response_lines[line] == b"":
|
|
153
|
+
# Empty line, end of headers.
|
|
154
|
+
# The rest of the response contains data.
|
|
155
|
+
response = b"\r\n".join(response_lines[line + 1:])
|
|
156
|
+
break
|
|
157
|
+
self.logger.info(response_lines[line])
|
|
158
|
+
|
|
159
|
+
if response:
|
|
160
|
+
self.handle_ntrip_data(response)
|
|
161
|
+
|
|
162
|
+
# Make the socket available for sending GGA.
|
|
163
|
+
self.sock = sock
|
|
164
|
+
return True
|
|
165
|
+
except (socket.herror, socket.gaierror):
|
|
166
|
+
self.logger.warning("Error connecting to server %s:%d", self.host, self.port)
|
|
167
|
+
return False
|
|
168
|
+
except (TimeoutError, socket.timeout):
|
|
169
|
+
self.logger.warning("Connection timeout")
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
def stream_data(self):
|
|
173
|
+
"""
|
|
174
|
+
Stream NTRIP data from a connected server and send it to a GPS receiver.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
# NTRIP Rev1 uses Shoutcast (ICY), which looks a lot like HTTP but isn't quite the same.
|
|
178
|
+
# Create an ICY session here to talk to an NTRIP Rev1 caster.
|
|
179
|
+
ok = self.create_icy_session()
|
|
180
|
+
if not ok:
|
|
181
|
+
self.logger.error("Failed to create NTRIP Rev1 ICY session.")
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
timeouts = 0
|
|
185
|
+
has_data = False
|
|
186
|
+
while self.streaming and timeouts < SOCKET_MAX_RECV_TIMEOUTS:
|
|
187
|
+
try:
|
|
188
|
+
response = self.sock.recv(2048)
|
|
189
|
+
if response:
|
|
190
|
+
timeouts = 0
|
|
191
|
+
self.handle_ntrip_data(response)
|
|
192
|
+
if not has_data:
|
|
193
|
+
self.logger.info("Received NTRIP data.")
|
|
194
|
+
has_data = True
|
|
195
|
+
else:
|
|
196
|
+
self.logger.warning("Connection closed by server")
|
|
197
|
+
break
|
|
198
|
+
except (socket.herror, socket.gaierror):
|
|
199
|
+
self.logger.warning("Error connecting to server %s:%d", self.host, self.port)
|
|
200
|
+
except (TimeoutError, socket.timeout):
|
|
201
|
+
self.logger.exception("Socket timeout.")
|
|
202
|
+
timeouts += 1
|
|
203
|
+
|
|
204
|
+
self.logger.info("NTRIP stream closed. Closing socket.")
|
|
205
|
+
# Close the socket.
|
|
206
|
+
try:
|
|
207
|
+
self.sock.close()
|
|
208
|
+
except OSError:
|
|
209
|
+
pass
|
|
210
|
+
self.sock = None
|
|
211
|
+
|
|
212
|
+
self.logger.info("NTRIP client finished.")
|
|
213
|
+
|
|
214
|
+
def stream_data_worker(self):
|
|
215
|
+
"""
|
|
216
|
+
NTRIP client worker thread function.
|
|
217
|
+
"""
|
|
218
|
+
self.streaming = True
|
|
219
|
+
while self.streaming:
|
|
220
|
+
self.stream_data()
|
|
221
|
+
|
|
222
|
+
def handle_ntrip_data(self, data):
|
|
223
|
+
"""
|
|
224
|
+
Callback for handling NTRIP data.
|
|
225
|
+
"""
|
|
226
|
+
# Send to receiver as is.
|
|
227
|
+
self.device.write(data)
|
|
228
|
+
|
|
229
|
+
def handle_nmea_gga(self, sentence):
|
|
230
|
+
"""
|
|
231
|
+
Process an NMEA-GGA sentence passed in as a string.
|
|
232
|
+
"""
|
|
233
|
+
fields = sentence.split(",")
|
|
234
|
+
if len(fields) < 7:
|
|
235
|
+
return
|
|
236
|
+
quality = int(fields[6] or 0)
|
|
237
|
+
if quality not in (0, 6): # No fix or estimated.
|
|
238
|
+
if not self.send_gga(sentence + "\r\n"):
|
|
239
|
+
# Something went wrong, restart the stream.
|
|
240
|
+
self.start_stream()
|
bosdyn/client/graph_nav.py
CHANGED
|
@@ -413,11 +413,26 @@ class GraphNavClient(BaseClient):
|
|
|
413
413
|
request = self._build_navigate_to_anchor_request(
|
|
414
414
|
seed_tform_goal, travel_params, route_params, cmd_duration, leases, used_endpoint,
|
|
415
415
|
command_id, goal_waypoint_rt_seed_ewrt_seed_tolerance, gps_navigation_params)
|
|
416
|
-
return self.call_async(self._stub.
|
|
416
|
+
return self.call_async(self._stub.NavigateToAnchor, request,
|
|
417
417
|
value_from_response=_command_id_from_navigate_route_response,
|
|
418
418
|
error_from_response=_navigate_to_anchor_error, copy_request=False,
|
|
419
419
|
**kwargs)
|
|
420
420
|
|
|
421
|
+
def navigate_to_anchor_full_async(self, seed_tform_goal, cmd_duration, route_params=None,
|
|
422
|
+
travel_params=None, leases=None, timesync_endpoint=None,
|
|
423
|
+
goal_waypoint_rt_seed_ewrt_seed_tolerance=None,
|
|
424
|
+
command_id=None, gps_navigation_params=None, **kwargs):
|
|
425
|
+
"""Async version of navigate_to_anchor() that returns the full response."""
|
|
426
|
+
used_endpoint = timesync_endpoint or self._timesync_endpoint
|
|
427
|
+
if not used_endpoint:
|
|
428
|
+
raise GraphNavServiceResponseError(response=None, error_message='No timesync endpoint!')
|
|
429
|
+
request = self._build_navigate_to_anchor_request(
|
|
430
|
+
seed_tform_goal, travel_params, route_params, cmd_duration, leases, used_endpoint,
|
|
431
|
+
command_id, goal_waypoint_rt_seed_ewrt_seed_tolerance, gps_navigation_params)
|
|
432
|
+
return self.call_async(self._stub.NavigateToAnchor, request,
|
|
433
|
+
error_from_response=_navigate_to_anchor_error, copy_request=False,
|
|
434
|
+
**kwargs)
|
|
435
|
+
|
|
421
436
|
def navigation_feedback(self, command_id=0, **kwargs):
|
|
422
437
|
"""Returns the feedback corresponding to the active route follow command.
|
|
423
438
|
|
|
@@ -1075,6 +1090,10 @@ def _get_streamed_data(response, data_type):
|
|
|
1075
1090
|
return proto_instance
|
|
1076
1091
|
|
|
1077
1092
|
|
|
1093
|
+
def _get_streamed_download_graph(response):
|
|
1094
|
+
"""Reads a streamed response to recreate a DownloadGraphRequest"""
|
|
1095
|
+
download_graph = _get_streamed_data(response, graph_nav_pb2.DownloadGraphResponse)
|
|
1096
|
+
return download_graph.graph
|
|
1078
1097
|
|
|
1079
1098
|
|
|
1080
1099
|
def _get_streamed_waypoint_snapshot(response):
|
|
@@ -1327,6 +1346,12 @@ def _navigate_feedback_error(response):
|
|
|
1327
1346
|
|
|
1328
1347
|
|
|
1329
1348
|
|
|
1349
|
+
@handle_common_header_errors
|
|
1350
|
+
def _download_graph_stream_errors(response):
|
|
1351
|
+
"""Return a custom exception based on download graph streaming response, None if no error."""
|
|
1352
|
+
return None
|
|
1353
|
+
|
|
1354
|
+
|
|
1330
1355
|
@handle_common_header_errors
|
|
1331
1356
|
@handle_unset_status_error(unset='STATUS_UNKNOWN')
|
|
1332
1357
|
def _download_waypoint_snapshot_stream_errors(response):
|
|
@@ -52,3 +52,37 @@ class GripperCameraParamClient(BaseClient):
|
|
|
52
52
|
"""Async version of gripper_camera_get_param_service_command()."""
|
|
53
53
|
return self.call_async(self._stub.GetParams, gripper_camera_get_param_request,
|
|
54
54
|
error_from_response=common_header_errors, **kwargs)
|
|
55
|
+
|
|
56
|
+
def set_camera_calib(self, set_gripper_camera_calib_request, **kwargs):
|
|
57
|
+
"""Issue gripper camera calibration
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
set_gripper_camera_calib_request (gripper_camera_params_pb2.GripperCameraCalibrationRequest) : The command request to set gripper camera calibration
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The GripperCameraCalibrationResponse message.
|
|
64
|
+
"""
|
|
65
|
+
return self.call(self._stub.SetCamCalib, set_gripper_camera_calib_request,
|
|
66
|
+
error_from_response=common_header_errors, **kwargs)
|
|
67
|
+
|
|
68
|
+
def set_camera_calib_async(self, set_gripper_camera_calib_request, **kwargs):
|
|
69
|
+
"""Async version of set_camera_calib()."""
|
|
70
|
+
return self.call_async(self._stub.SetCamCalib, set_gripper_camera_calib_request,
|
|
71
|
+
error_from_response=common_header_errors, **kwargs)
|
|
72
|
+
|
|
73
|
+
def get_camera_calib(self, get_gripper_camera_calib_request, **kwargs):
|
|
74
|
+
"""Issue gripper camera get calibration
|
|
75
|
+
|
|
76
|
+
Arge:
|
|
77
|
+
get_gripper_camera_calib_request (gripper_camera_params_pb2.GripperCameraGetCalibrationRequest) : The command reqeust to get gripper camera calibration
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The GripperCameraGetCalibrationResponse message, which contains the GripperCameraCalibrationProto
|
|
81
|
+
"""
|
|
82
|
+
return self.call(self._stub.GetCamCalib, get_gripper_camera_calib_request,
|
|
83
|
+
error_from_response=common_header_errors, **kwargs)
|
|
84
|
+
|
|
85
|
+
def get_camera_calib_asnyc(self, get_gripper_camera_calib_request, **kwargs):
|
|
86
|
+
"""Asnyc version of get_camera_calib()."""
|
|
87
|
+
return self.call_async(self._stub.GetCamCalib, get_gripper_camera_calib_request,
|
|
88
|
+
error_from_response=common_header_errors, **kwargs)
|
bosdyn/client/image.py
CHANGED
|
@@ -204,6 +204,22 @@ def _get_image_value(response):
|
|
|
204
204
|
return response.image_responses
|
|
205
205
|
|
|
206
206
|
|
|
207
|
+
def pixel_format_to_numpy_type(pixel_format):
|
|
208
|
+
if pixel_format == image_pb2.Image.PIXEL_FORMAT_GREYSCALE_U8:
|
|
209
|
+
dtype = np.uint8
|
|
210
|
+
elif pixel_format == image_pb2.Image.PIXEL_FORMAT_RGB_U8:
|
|
211
|
+
dtype = np.uint8
|
|
212
|
+
elif pixel_format == image_pb2.Image.PIXEL_FORMAT_RGBA_U8:
|
|
213
|
+
dtype = np.uint8
|
|
214
|
+
elif pixel_format == image_pb2.Image.PIXEL_FORMAT_DEPTH_U16:
|
|
215
|
+
dtype = np.uint16
|
|
216
|
+
elif pixel_format == image_pb2.Image.PIXEL_FORMAT_GREYSCALE_U16:
|
|
217
|
+
dtype = ">u2" # Big endian
|
|
218
|
+
else:
|
|
219
|
+
raise Exception('Image PixelFormat type {} not supported'.format(pixel_format))
|
|
220
|
+
return dtype
|
|
221
|
+
|
|
222
|
+
|
|
207
223
|
def write_pgm_or_ppm(image_response, filename="", filepath=".", include_pixel_format=False):
|
|
208
224
|
"""Write raw data from image_response to a PGM file.
|
|
209
225
|
|