bosdyn-client 5.0.1.2__py3-none-any.whl → 5.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/access_controlled_door_util.py +206 -0
- bosdyn/client/arm_surface_contact.py +2 -2
- bosdyn/client/async_tasks.py +3 -2
- bosdyn/client/audio_visual_helpers.py +3 -2
- bosdyn/client/autowalk.py +0 -2
- bosdyn/client/command_line.py +72 -15
- bosdyn/client/common.py +1 -1
- bosdyn/client/data_acquisition.py +3 -5
- bosdyn/client/data_acquisition_helpers.py +0 -3
- bosdyn/client/data_acquisition_plugin.py +1 -2
- bosdyn/client/data_acquisition_plugin_service.py +3 -2
- bosdyn/client/data_acquisition_store.py +1 -7
- bosdyn/client/data_buffer.py +5 -4
- bosdyn/client/directory_registration.py +3 -2
- bosdyn/client/estop.py +3 -2
- bosdyn/client/fault.py +1 -1
- bosdyn/client/gps/aggregator_client.py +2 -4
- bosdyn/client/gps/gps_listener.py +5 -7
- bosdyn/client/gps/ntrip_client.py +12 -3
- bosdyn/client/graph_nav.py +67 -13
- bosdyn/client/hazard_avoidance.py +119 -0
- bosdyn/client/image.py +5 -4
- bosdyn/client/image_service_helpers.py +6 -7
- bosdyn/client/ir_enable_disable.py +1 -1
- bosdyn/client/keepalive.py +4 -2
- bosdyn/client/lease.py +3 -2
- bosdyn/client/lease_validator.py +0 -1
- bosdyn/client/log_status.py +57 -3
- bosdyn/client/map_processing.py +2 -4
- bosdyn/client/network_compute_bridge_client.py +4 -6
- bosdyn/client/payload.py +2 -3
- bosdyn/client/payload_registration.py +11 -10
- bosdyn/client/power.py +84 -27
- bosdyn/client/processors.py +27 -2
- bosdyn/client/recording.py +3 -3
- bosdyn/client/robot_command.py +22 -22
- bosdyn/client/robot_state.py +1 -1
- bosdyn/client/sdk.py +2 -3
- bosdyn/client/service_customization_helpers.py +1 -1
- bosdyn/client/spot_cam/audio.py +1 -2
- bosdyn/client/spot_cam/health.py +1 -1
- bosdyn/client/spot_cam/lighting.py +1 -1
- bosdyn/client/spot_cam/media_log.py +1 -1
- bosdyn/client/spot_cam/network.py +3 -2
- bosdyn/client/spot_cam/power.py +1 -1
- bosdyn/client/spot_cam/ptz.py +1 -1
- bosdyn/client/spot_cam/streamquality.py +1 -1
- bosdyn/client/spot_cam/version.py +1 -1
- bosdyn/client/spot_check.py +5 -6
- bosdyn/client/url_validation_util.py +220 -0
- bosdyn/client/util.py +2 -4
- bosdyn/client/world_object.py +1 -1
- {bosdyn_client-5.0.1.2.dist-info → bosdyn_client-5.1.1.dist-info}/METADATA +3 -3
- bosdyn_client-5.1.1.dist-info/RECORD +106 -0
- bosdyn_client-5.0.1.2.dist-info/RECORD +0 -103
- {bosdyn_client-5.0.1.2.dist-info → bosdyn_client-5.1.1.dist-info}/WHEEL +0 -0
- {bosdyn_client-5.0.1.2.dist-info → bosdyn_client-5.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
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 json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from string import Template
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from bosdyn.client.url_validation_util import safe_api_call
|
|
14
|
+
|
|
15
|
+
API_TIMEOUT_DEFAULT = 30
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def file_to_json(file_path):
|
|
19
|
+
"""
|
|
20
|
+
Helper to read and parse JSON files.
|
|
21
|
+
Args:
|
|
22
|
+
file_path (Path): Path to JSON file.
|
|
23
|
+
Returns:
|
|
24
|
+
dict: Parsed JSON data.
|
|
25
|
+
"""
|
|
26
|
+
return json.loads(file_path.read_text())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Functions to handle api calls to access control systems
|
|
32
|
+
def door_action(api_calls, door_id, action, path_to_cert=None, is_robot=True):
|
|
33
|
+
"""Executes a sequence of API calls required to perform an action (e.g., open or close) on a specified door,
|
|
34
|
+
handling data substitutions and certificate verification as needed.
|
|
35
|
+
|
|
36
|
+
api_calls (list of dict): List of API call specifications, where each dict contains information
|
|
37
|
+
such as 'method', 'url', 'action', 'sni_hostname', 'route', 'request_data', and 'responses'.
|
|
38
|
+
door_id (str): Identifier of the door to perform the action on.
|
|
39
|
+
action (list of str): The action(s) to perform (e.g., "open", "close"). Only calls matching
|
|
40
|
+
the specified action(s) will be executed.
|
|
41
|
+
path_to_cert (str, optional): Path to a certificate file for SSL verification. If None, no certificate
|
|
42
|
+
is used.
|
|
43
|
+
is_robot (bool, optional): Indicates if the API calls are being made on behalf of a robot. Defaults to True.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
dict: An error message dictionary containing details about any error encountered during the API calls.
|
|
47
|
+
If all calls succeed, returns an empty dictionary.
|
|
48
|
+
"""
|
|
49
|
+
error_msg = {}
|
|
50
|
+
cross_call_substitutions = {"door_id": door_id}
|
|
51
|
+
|
|
52
|
+
for call_data in api_calls:
|
|
53
|
+
|
|
54
|
+
if call_data.get("action") not in action:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
method = call_data.get("method")
|
|
58
|
+
url = call_data.get("url")
|
|
59
|
+
sni_hostname = call_data.get("sni_hostname")
|
|
60
|
+
route = call_data.get("route")
|
|
61
|
+
request_data = call_data.get("request_data", {})
|
|
62
|
+
responses = call_data.get("responses", {})
|
|
63
|
+
call_action = call_data.get("action", "")
|
|
64
|
+
|
|
65
|
+
if not method or not url:
|
|
66
|
+
error_msg["extra_message"] = "API call error: missing method or url"
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
# Format URL and data - note: substitutions are case-INsensitive
|
|
70
|
+
try:
|
|
71
|
+
# This will only replace instances of values formatted like $variable or ${variable}
|
|
72
|
+
url = Template(url).safe_substitute(cross_call_substitutions)
|
|
73
|
+
|
|
74
|
+
templated_request_data = {}
|
|
75
|
+
for k, v in request_data.items():
|
|
76
|
+
if isinstance(v, dict): # Handle nested dicts, like headers
|
|
77
|
+
templated_request_data[k] = {
|
|
78
|
+
k2:
|
|
79
|
+
Template(v2).safe_substitute(cross_call_substitutions) if isinstance(
|
|
80
|
+
v2, str) else v2 for k2, v2 in v.items()
|
|
81
|
+
}
|
|
82
|
+
elif isinstance(v, str): # Handle top level string values
|
|
83
|
+
templated_request_data[k] = Template(v).safe_substitute(
|
|
84
|
+
cross_call_substitutions)
|
|
85
|
+
else:
|
|
86
|
+
templated_request_data[k] = v
|
|
87
|
+
# Check if cert file was included at the configuration, if yes use it
|
|
88
|
+
if path_to_cert:
|
|
89
|
+
templated_request_data["verify"] = path_to_cert
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f"{e} has no value, couldn't make substitution")
|
|
92
|
+
|
|
93
|
+
result_of_call, api_error_msg = make_access_control_system_api_call(
|
|
94
|
+
method, url, templated_request_data, responses, sni_hostname=sni_hostname,
|
|
95
|
+
is_robot=is_robot, route=route)
|
|
96
|
+
if api_error_msg:
|
|
97
|
+
error_msg["action"] = call_action
|
|
98
|
+
error_msg["api_error"] = api_error_msg
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
# If the call returned data, store it for subsequent calls
|
|
102
|
+
if result_of_call:
|
|
103
|
+
try:
|
|
104
|
+
for result in result_of_call:
|
|
105
|
+
tag = result[0]
|
|
106
|
+
content = result[1]
|
|
107
|
+
cross_call_substitutions[tag] = content
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"API call error: couldn't read data, got exception {e}")
|
|
110
|
+
error_msg["extra_message"] = f"API call error: couldn't read data, got exception"
|
|
111
|
+
break
|
|
112
|
+
return error_msg
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def make_access_control_system_api_call(method, url, request_data, store_responses=None,
|
|
118
|
+
sni_hostname=None, is_robot=True, route=None):
|
|
119
|
+
"""Makes an HTTP request and optionally extracts specific fields from the JSON response.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
method (str): HTTP method to use (e.g., 'GET', 'POST')
|
|
123
|
+
url (str): The endpoint URL for the API call
|
|
124
|
+
request_data (dict): Request configuration including headers, body, etc.
|
|
125
|
+
store_responses (Optional[Dict[str, str]]): Dictionary mapping response field names to
|
|
126
|
+
JSON paths. For example:
|
|
127
|
+
{
|
|
128
|
+
"token": "auth.token", # Store response's auth.token as "token"
|
|
129
|
+
"session_id": "data.session" # Store response's data.session as "session_id"
|
|
130
|
+
}
|
|
131
|
+
If None, no data will be extracted from the response.
|
|
132
|
+
sni_hostname (str|None): If specified, this parameter provides the hostname declared by
|
|
133
|
+
and expected by the access control server during TLS negotiation. This should only be
|
|
134
|
+
required if the server's hostname is not resolvable via DNS.
|
|
135
|
+
route (str|None): Route type to use ("WIFI", "LTE").
|
|
136
|
+
If None, default interface (WIFI) will be used.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Tuple[Optional[List[Tuple[str, Any]]], Optional[Dict]]:
|
|
140
|
+
- First element: If store_responses was provided and matching data was found,
|
|
141
|
+
returns list of tuples [(field_name, value), ...]. None otherwise.
|
|
142
|
+
- Second element: If error occurred, returns dict with error details:
|
|
143
|
+
{
|
|
144
|
+
'status_code': int,
|
|
145
|
+
'reason': str,
|
|
146
|
+
'elapsed': float
|
|
147
|
+
}
|
|
148
|
+
None if successful.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
>>> store_responses = {"auth_token": "data.token"}
|
|
152
|
+
>>> data, error = make_access_control_system_api_call("POST", "https://api.door/auth",
|
|
153
|
+
... {"json": {"key": "value"}},
|
|
154
|
+
... store_responses)
|
|
155
|
+
>>> if data:
|
|
156
|
+
... # data might be [("auth_token", "abc123")]
|
|
157
|
+
... token = dict(data)["auth_token"]
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
response, status_message = safe_api_call(method, url, sni_hostname,
|
|
161
|
+
timeout=API_TIMEOUT_DEFAULT, is_robot=is_robot,
|
|
162
|
+
interface=route, **request_data)
|
|
163
|
+
|
|
164
|
+
if response is None:
|
|
165
|
+
# There was an error during the API call
|
|
166
|
+
return None, status_message
|
|
167
|
+
|
|
168
|
+
if response.status_code == 200:
|
|
169
|
+
if store_responses:
|
|
170
|
+
# Only try to parse JSON and extract data if we need to store responses
|
|
171
|
+
data_json = response.json()
|
|
172
|
+
return [(tag, get_value_by_path(data_json, path))
|
|
173
|
+
for tag, path in store_responses.items()
|
|
174
|
+
if get_value_by_path(data_json, path) is not None], None
|
|
175
|
+
# If we don't need to store responses, just return success
|
|
176
|
+
return None, None
|
|
177
|
+
|
|
178
|
+
# If the status code isn't 200, something went wrong
|
|
179
|
+
return None, {
|
|
180
|
+
'status_code': response.status_code,
|
|
181
|
+
'reason': response.reason,
|
|
182
|
+
'elapsed': response.elapsed.total_seconds(),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
except requests.exceptions.RequestException as e:
|
|
186
|
+
print(f"API call failed: {str(e)}")
|
|
187
|
+
return None, {
|
|
188
|
+
'status_code': getattr(e.response, 'status_code', None),
|
|
189
|
+
'reason': getattr(e.response, 'reason', str(e)),
|
|
190
|
+
'elapsed': getattr(e.response, 'elapsed', None)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_value_by_path(data_json, path_to_info):
|
|
195
|
+
"""Takes in a json representing the data of a call response, and a string representing the
|
|
196
|
+
path through the json to the desired data. Traverses the json and returns the data.
|
|
197
|
+
"""
|
|
198
|
+
keys = path_to_info.split('.')
|
|
199
|
+
result = data_json
|
|
200
|
+
|
|
201
|
+
for key in keys:
|
|
202
|
+
result = result.get(key, None)
|
|
203
|
+
if result is not None:
|
|
204
|
+
return result
|
|
205
|
+
else:
|
|
206
|
+
return False
|
|
@@ -4,8 +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
|
-
from bosdyn.api import
|
|
8
|
-
from bosdyn.client.common import BaseClient
|
|
7
|
+
from bosdyn.api import arm_surface_contact_service_pb2_grpc
|
|
8
|
+
from bosdyn.client.common import BaseClient
|
|
9
9
|
from bosdyn.client.robot_command import NoTimeSyncError, _edit_proto, _TimeConverter
|
|
10
10
|
|
|
11
11
|
from .lease import add_lease_wallet_processors
|
bosdyn/client/async_tasks.py
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
"""Utilities for managing periodic tasks consisting of asynchronous GRPC calls."""
|
|
8
8
|
import abc
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
from bosdyn.util import now_sec
|
|
10
11
|
|
|
11
12
|
from .exceptions import ResponseError, RpcError
|
|
12
13
|
|
|
@@ -79,7 +80,7 @@ class AsyncGRPCTask(object, metaclass=abc.ABCMeta):
|
|
|
79
80
|
|
|
80
81
|
def update(self):
|
|
81
82
|
"""Call this periodically to manage execution of task represented by this object."""
|
|
82
|
-
now_sec =
|
|
83
|
+
now_sec = now_sec()
|
|
83
84
|
if self._future is not None:
|
|
84
85
|
if self._future.original_future.done():
|
|
85
86
|
try:
|
|
@@ -13,6 +13,7 @@ import bosdyn.client
|
|
|
13
13
|
from bosdyn.api import audio_visual_pb2
|
|
14
14
|
from bosdyn.client.audio_visual import (AudioVisualClient, BehaviorExpiredError, DoesNotExistError,
|
|
15
15
|
InvalidClientError)
|
|
16
|
+
from bosdyn.util import now_sec
|
|
16
17
|
|
|
17
18
|
_LOGGER = logging.getLogger(__name__)
|
|
18
19
|
|
|
@@ -84,7 +85,7 @@ class AudioVisualHelper:
|
|
|
84
85
|
# Run the AV behavior until the stop_event is triggered
|
|
85
86
|
while not self.stop_event.wait(self.refresh_rate):
|
|
86
87
|
try:
|
|
87
|
-
end_time_secs =
|
|
88
|
+
end_time_secs = now_sec() + self.refresh_rate + 0.10 # add 100ms margin
|
|
88
89
|
result = self.av_client.run_behavior(self.behavior_name, end_time_secs)
|
|
89
90
|
set_future_result(
|
|
90
91
|
result.run_result == audio_visual_pb2.RunBehaviorResponse.RESULT_BEHAVIOR_RUN)
|
|
@@ -109,4 +110,4 @@ class AudioVisualHelper:
|
|
|
109
110
|
try:
|
|
110
111
|
self.av_client.stop_behavior(self.behavior_name)
|
|
111
112
|
except InvalidClientError:
|
|
112
|
-
_LOGGER.warning('Failed to stop behavior, run by a different client.')
|
|
113
|
+
_LOGGER.warning('Failed to stop behavior, run by a different client.')
|
bosdyn/client/autowalk.py
CHANGED
|
@@ -14,8 +14,6 @@ from bosdyn.client.common import BaseClient, error_factory, error_pair
|
|
|
14
14
|
from bosdyn.client.exceptions import ResponseError
|
|
15
15
|
from bosdyn.client.lease import add_lease_wallet_processors
|
|
16
16
|
|
|
17
|
-
from .data_chunk import chunk_message
|
|
18
|
-
|
|
19
17
|
|
|
20
18
|
class AutowalkResponseError(ResponseError):
|
|
21
19
|
"""General class of errors for autowalk service."""
|
bosdyn/client/command_line.py
CHANGED
|
@@ -12,6 +12,7 @@ from __future__ import division
|
|
|
12
12
|
|
|
13
13
|
import abc
|
|
14
14
|
import argparse
|
|
15
|
+
import copy
|
|
15
16
|
import datetime
|
|
16
17
|
import os
|
|
17
18
|
import signal
|
|
@@ -19,17 +20,19 @@ import socket
|
|
|
19
20
|
import sys
|
|
20
21
|
import threading
|
|
21
22
|
import time
|
|
23
|
+
import uuid
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
import google.protobuf.timestamp_pb2
|
|
24
26
|
from google.protobuf import json_format
|
|
25
27
|
|
|
26
28
|
import bosdyn.client
|
|
27
29
|
from bosdyn.api import data_acquisition_pb2, image_pb2
|
|
28
30
|
from bosdyn.api.data_buffer_pb2 import Event, TextMessage
|
|
29
31
|
from bosdyn.api.data_index_pb2 import EventsCommentsSpec
|
|
32
|
+
from bosdyn.api.header_pb2 import CommonError
|
|
30
33
|
from bosdyn.api.keepalive import keepalive_pb2
|
|
31
34
|
from bosdyn.api.robot_state_pb2 import BehaviorFault
|
|
32
|
-
from bosdyn.util import duration_str, timestamp_to_datetime
|
|
35
|
+
from bosdyn.util import duration_str, now_sec, timestamp_to_datetime
|
|
33
36
|
|
|
34
37
|
from .auth import InvalidLoginError, InvalidTokenError
|
|
35
38
|
from .data_acquisition import DataAcquisitionClient
|
|
@@ -694,14 +697,14 @@ class FaultWatchCommand(Command):
|
|
|
694
697
|
|
|
695
698
|
|
|
696
699
|
class LogStatusCommands(Subcommands):
|
|
697
|
-
"""Start, update and terminate experiment logs, start and terminate retro logs and check status
|
|
698
|
-
active logs for robot."""
|
|
700
|
+
"""Start, update and terminate experiment logs, start and terminate retro logs and check status
|
|
701
|
+
of active logs for robot."""
|
|
699
702
|
|
|
700
703
|
NAME = 'log-status'
|
|
701
704
|
NEED_AUTHENTICATION = True
|
|
702
705
|
|
|
703
706
|
def __init__(self, subparsers, command_dict):
|
|
704
|
-
"""Interact with logs for robot
|
|
707
|
+
"""Interact with logs for robot.
|
|
705
708
|
|
|
706
709
|
Args:
|
|
707
710
|
subparsers: List of argument parsers.
|
|
@@ -715,6 +718,7 @@ class LogStatusCommands(Subcommands):
|
|
|
715
718
|
GetActiveLogStatusesCommand,
|
|
716
719
|
ExperimentLogCommand,
|
|
717
720
|
StartRetroLogCommand,
|
|
721
|
+
StartConcurrentLogCommand,
|
|
718
722
|
TerminateLogCommand,
|
|
719
723
|
])
|
|
720
724
|
|
|
@@ -725,7 +729,7 @@ class GetLogCommand(Command):
|
|
|
725
729
|
NAME = 'get'
|
|
726
730
|
|
|
727
731
|
def __init__(self, subparsers, command_dict):
|
|
728
|
-
"""Get log status from robot
|
|
732
|
+
"""Get log status from robot.
|
|
729
733
|
|
|
730
734
|
Args:
|
|
731
735
|
subparsers: List of argument parsers.
|
|
@@ -805,7 +809,7 @@ class StartTimedExperimentLogCommand(Command):
|
|
|
805
809
|
NAME = 'timed'
|
|
806
810
|
|
|
807
811
|
def __init__(self, subparsers, command_dict):
|
|
808
|
-
"""Start timed experiment log
|
|
812
|
+
"""Start timed experiment log.
|
|
809
813
|
|
|
810
814
|
Args:
|
|
811
815
|
subparsers: List of argument parsers.
|
|
@@ -837,7 +841,8 @@ class StartContinuousExperimentLogCommand(Command):
|
|
|
837
841
|
NAME = 'continuous'
|
|
838
842
|
|
|
839
843
|
def __init__(self, subparsers, command_dict):
|
|
840
|
-
"""Start continuous experiment log, defaulted to update keep alive time by 10 seconds every
|
|
844
|
+
"""Start continuous experiment log, defaulted to update keep alive time by 10 seconds every
|
|
845
|
+
5 seconds.
|
|
841
846
|
|
|
842
847
|
Args:
|
|
843
848
|
subparsers: List of argument parsers.
|
|
@@ -894,7 +899,7 @@ class StartRetroLogCommand(Command):
|
|
|
894
899
|
NAME = 'retro'
|
|
895
900
|
|
|
896
901
|
def __init__(self, subparsers, command_dict):
|
|
897
|
-
"""Start a retro log
|
|
902
|
+
"""Start a retro log.
|
|
898
903
|
|
|
899
904
|
Args:
|
|
900
905
|
subparsers: List of argument parsers.
|
|
@@ -920,13 +925,61 @@ class StartRetroLogCommand(Command):
|
|
|
920
925
|
return True
|
|
921
926
|
|
|
922
927
|
|
|
928
|
+
class StartConcurrentLogCommand(Command):
|
|
929
|
+
"""Start a concurrent experiment log, with event-derived data."""
|
|
930
|
+
|
|
931
|
+
NAME = 'concurrent'
|
|
932
|
+
|
|
933
|
+
def __init__(self, subparsers, command_dict):
|
|
934
|
+
"""Start data log
|
|
935
|
+
Args:
|
|
936
|
+
subparsers: List of argument parsers.
|
|
937
|
+
command_dict: Dictionary of command names which take parsed options.
|
|
938
|
+
"""
|
|
939
|
+
super(StartConcurrentLogCommand, self).__init__(subparsers, command_dict)
|
|
940
|
+
self._parser.add_argument('seconds', type=float, help='how long should the experiment run?')
|
|
941
|
+
self._parser.add_argument('event_type',
|
|
942
|
+
help='name of the event type we want to match a recipe against')
|
|
943
|
+
|
|
944
|
+
def _run(self, robot, options):
|
|
945
|
+
"""Implementation of the command.
|
|
946
|
+
|
|
947
|
+
Args:
|
|
948
|
+
robot: Robot object on which to run the command.
|
|
949
|
+
options: Parsed command-line arguments.
|
|
950
|
+
Returns:
|
|
951
|
+
True
|
|
952
|
+
"""
|
|
953
|
+
|
|
954
|
+
client = robot.ensure_client(LogStatusClient.default_service_name)
|
|
955
|
+
time_sync_endpoint = TimeSyncEndpoint(
|
|
956
|
+
robot.ensure_client(TimeSyncClient.default_service_name))
|
|
957
|
+
if not time_sync_endpoint.establish_timesync():
|
|
958
|
+
print("Failed to establish time sync with the robot.")
|
|
959
|
+
return False
|
|
960
|
+
|
|
961
|
+
robot_now = time_sync_endpoint.robot_timestamp_from_local_secs(now_sec())
|
|
962
|
+
|
|
963
|
+
event = Event()
|
|
964
|
+
event.type = options.event_type
|
|
965
|
+
event.description = 'Triggering a recipe data log'
|
|
966
|
+
event.source = 'LogStatus CLI'
|
|
967
|
+
event.id = uuid.uuid4().hex
|
|
968
|
+
event.start_time.CopyFrom(robot_now)
|
|
969
|
+
event.end_time.CopyFrom(robot_now)
|
|
970
|
+
|
|
971
|
+
response = client.start_concurrent_log(options.seconds, event)
|
|
972
|
+
print(response.log_status)
|
|
973
|
+
return True
|
|
974
|
+
|
|
975
|
+
|
|
923
976
|
class TerminateLogCommand(Command):
|
|
924
977
|
"""Terminate log gathering process."""
|
|
925
978
|
|
|
926
979
|
NAME = 'terminate'
|
|
927
980
|
|
|
928
981
|
def __init__(self, subparsers, command_dict):
|
|
929
|
-
"""Terminate log on robot
|
|
982
|
+
"""Terminate log on robot.
|
|
930
983
|
|
|
931
984
|
Args:
|
|
932
985
|
subparsers: List of argument parsers.
|
|
@@ -1054,7 +1107,7 @@ class TextMsgCommand(Command):
|
|
|
1054
1107
|
if options.timestamp:
|
|
1055
1108
|
try:
|
|
1056
1109
|
robot_timestamp = robot.time_sync.robot_timestamp_from_local_secs(
|
|
1057
|
-
|
|
1110
|
+
now_sec(), timesync_timeout_sec=1.0)
|
|
1058
1111
|
except TimeSyncError as err:
|
|
1059
1112
|
print("Failed to send message with timestamp: {}.".format(err))
|
|
1060
1113
|
return False
|
|
@@ -1107,7 +1160,7 @@ class OperatorCommentCommand(Command):
|
|
|
1107
1160
|
"""
|
|
1108
1161
|
client_timestamp = None
|
|
1109
1162
|
if options.timestamp:
|
|
1110
|
-
client_timestamp =
|
|
1163
|
+
client_timestamp = now_sec()
|
|
1111
1164
|
try:
|
|
1112
1165
|
robot.time_sync.wait_for_sync(timeout_sec=1.0)
|
|
1113
1166
|
except TimeSyncError as err:
|
|
@@ -1123,7 +1176,7 @@ class DataServiceCommands(Subcommands):
|
|
|
1123
1176
|
NAME = 'data'
|
|
1124
1177
|
|
|
1125
1178
|
def __init__(self, subparsers, command_dict):
|
|
1126
|
-
"""Commands for querying the data-service
|
|
1179
|
+
"""Commands for querying the data-service.
|
|
1127
1180
|
|
|
1128
1181
|
Args:
|
|
1129
1182
|
subparsers: List of argument parsers.
|
|
@@ -1295,7 +1348,7 @@ class GetDataBufferStatusCommand(Command):
|
|
|
1295
1348
|
NAME = 'status'
|
|
1296
1349
|
|
|
1297
1350
|
def __init__(self, subparsers, command_dict):
|
|
1298
|
-
"""Get status of data-buffer on robot
|
|
1351
|
+
"""Get status of data-buffer on robot.
|
|
1299
1352
|
|
|
1300
1353
|
Args:
|
|
1301
1354
|
subparsers: List of argument parsers.
|
|
@@ -1538,7 +1591,6 @@ class MetricsCommand(Command):
|
|
|
1538
1591
|
|
|
1539
1592
|
Returns:
|
|
1540
1593
|
Timestamp string in ISO 8601 format
|
|
1541
|
-
|
|
1542
1594
|
"""
|
|
1543
1595
|
# The json format of a timestamp is a string that looks like '"2022-01-12T21:56:05Z"',
|
|
1544
1596
|
# so we strip off the outer quotes and return that.
|
|
@@ -2648,6 +2700,10 @@ class PowerWifiRadioCommand(Command):
|
|
|
2648
2700
|
|
|
2649
2701
|
|
|
2650
2702
|
|
|
2703
|
+
|
|
2704
|
+
|
|
2705
|
+
|
|
2706
|
+
|
|
2651
2707
|
def main(args=None):
|
|
2652
2708
|
"""Command-line interface for interacting with robot services."""
|
|
2653
2709
|
parser = argparse.ArgumentParser(prog='bosdyn.client', description=main.__doc__)
|
|
@@ -2678,6 +2734,7 @@ def main(args=None):
|
|
|
2678
2734
|
PowerCommand(subparsers, command_dict)
|
|
2679
2735
|
KeepaliveCommand(subparsers, command_dict)
|
|
2680
2736
|
|
|
2737
|
+
|
|
2681
2738
|
options = parser.parse_args(args=args)
|
|
2682
2739
|
|
|
2683
2740
|
setup_logging(verbose=options.verbose)
|
bosdyn/client/common.py
CHANGED
|
@@ -25,7 +25,7 @@ from .exceptions import (CustomParamError, Error, InternalServerError, InvalidRe
|
|
|
25
25
|
|
|
26
26
|
_LOGGER = logging.getLogger(__name__)
|
|
27
27
|
|
|
28
|
-
from bosdyn.api import
|
|
28
|
+
from bosdyn.api import license_pb2
|
|
29
29
|
|
|
30
30
|
DEFAULT_RPC_TIMEOUT = 30 # seconds
|
|
31
31
|
|
|
@@ -8,15 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
import collections
|
|
10
10
|
|
|
11
|
-
from google.protobuf import json_format
|
|
12
|
-
|
|
13
11
|
from bosdyn.api import data_acquisition_pb2 as data_acquisition
|
|
14
12
|
from bosdyn.api import data_acquisition_service_pb2_grpc as data_acquisition_service
|
|
15
13
|
from bosdyn.client.common import (BaseClient, common_header_errors, custom_params_error,
|
|
16
14
|
error_factory, error_pair, handle_common_header_errors,
|
|
17
|
-
|
|
18
|
-
from bosdyn.client.exceptions import
|
|
19
|
-
from bosdyn.util import
|
|
15
|
+
handle_unset_status_error)
|
|
16
|
+
from bosdyn.client.exceptions import InternalServerError, ResponseError
|
|
17
|
+
from bosdyn.util import now_sec, now_timestamp, seconds_to_duration
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
class DataAcquisitionResponseError(ResponseError):
|
|
@@ -12,11 +12,8 @@ from urllib.error import URLError
|
|
|
12
12
|
from urllib.parse import urlencode
|
|
13
13
|
from urllib.request import Request, urlopen
|
|
14
14
|
|
|
15
|
-
from google.protobuf import json_format
|
|
16
15
|
from google.protobuf.struct_pb2 import Struct
|
|
17
16
|
|
|
18
|
-
import bosdyn.client
|
|
19
|
-
import bosdyn.client.util
|
|
20
17
|
from bosdyn.api import data_acquisition_pb2, data_acquisition_store_pb2
|
|
21
18
|
from bosdyn.client.exceptions import ResponseError
|
|
22
19
|
|
|
@@ -15,8 +15,7 @@ from bosdyn.client.common import (BaseClient, error_factory, error_pair,
|
|
|
15
15
|
handle_unset_status_error)
|
|
16
16
|
from bosdyn.client.data_acquisition import (DataAcquisitionClient, DataAcquisitionResponseError,
|
|
17
17
|
UnknownCaptureTypeError, _get_live_data_error,
|
|
18
|
-
acquire_data_error,
|
|
19
|
-
from bosdyn.util import now_timestamp
|
|
18
|
+
acquire_data_error, metadata_to_proto)
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class DataAcquisitionPluginClient(BaseClient):
|
|
@@ -54,6 +54,7 @@ from bosdyn.client.data_acquisition_store import DataAcquisitionStoreClient
|
|
|
54
54
|
from bosdyn.client.data_buffer import DataBufferClient
|
|
55
55
|
from bosdyn.client.server_util import ResponseContext, populate_response_header
|
|
56
56
|
from bosdyn.client.service_customization_helpers import create_value_validator
|
|
57
|
+
from bosdyn.util import now_sec
|
|
57
58
|
|
|
58
59
|
_LOGGER = logging.getLogger(__name__)
|
|
59
60
|
|
|
@@ -590,7 +591,7 @@ class RequestManager:
|
|
|
590
591
|
"""
|
|
591
592
|
state = self.get_request_state(request_id)
|
|
592
593
|
with state._lock:
|
|
593
|
-
state._completion_time =
|
|
594
|
+
state._completion_time = now_sec()
|
|
594
595
|
|
|
595
596
|
def cleanup_requests(self, older_than_time=None):
|
|
596
597
|
"""Remove all requests that were completed farther in the past than older_than_time.
|
|
@@ -600,7 +601,7 @@ class RequestManager:
|
|
|
600
601
|
Args:
|
|
601
602
|
older_than_time (float): Optional time (in seconds) that requests will be removed after.
|
|
602
603
|
"""
|
|
603
|
-
older_than_time = older_than_time or
|
|
604
|
+
older_than_time = older_than_time or now_sec() - kDefaultRequestExpiration
|
|
604
605
|
with self._lock:
|
|
605
606
|
# Grab the contents to iterate through outside of the lock
|
|
606
607
|
requests = list(self._requests.items())
|
|
@@ -10,18 +10,12 @@
|
|
|
10
10
|
from os import fstat
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
-
from google.protobuf import json_format
|
|
14
|
-
|
|
15
13
|
from bosdyn.api import data_acquisition_store_pb2 as data_acquisition_store
|
|
16
14
|
from bosdyn.api import data_acquisition_store_service_pb2_grpc as data_acquisition_store_service
|
|
17
15
|
from bosdyn.api import data_chunk_pb2 as data_chunk
|
|
18
|
-
from bosdyn.api import header_pb2, image_pb2
|
|
19
16
|
from bosdyn.client.channel import DEFAULT_HEADER_BUFFER_LENGTH, DEFAULT_MAX_MESSAGE_LENGTH
|
|
20
|
-
from bosdyn.client.common import
|
|
21
|
-
handle_common_header_errors, handle_unset_status_error)
|
|
17
|
+
from bosdyn.client.common import BaseClient, common_header_errors
|
|
22
18
|
from bosdyn.client.data_chunk import split_serialized
|
|
23
|
-
from bosdyn.client.exceptions import Error, ResponseError
|
|
24
|
-
from bosdyn.util import now_timestamp
|
|
25
19
|
|
|
26
20
|
DEFAULT_CHUNK_SIZE_BYTES = int(DEFAULT_MAX_MESSAGE_LENGTH - DEFAULT_HEADER_BUFFER_LENGTH)
|
|
27
21
|
|
bosdyn/client/data_buffer.py
CHANGED
|
@@ -28,6 +28,7 @@ from bosdyn.api import parameter_pb2
|
|
|
28
28
|
from bosdyn.client import time_sync
|
|
29
29
|
from bosdyn.client.common import BaseClient, common_header_errors
|
|
30
30
|
from bosdyn.client.exceptions import Error, ResponseError, RpcError
|
|
31
|
+
from bosdyn.util import now_sec
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
class InvalidArgument(Error):
|
|
@@ -356,7 +357,7 @@ class DataBufferClient(BaseClient):
|
|
|
356
357
|
(msg_type if msg_type is not None else
|
|
357
358
|
(proto.DESCRIPTOR.full_name if proto is not None else 'Unknown')))
|
|
358
359
|
else:
|
|
359
|
-
return converter.robot_timestamp_from_local_secs(
|
|
360
|
+
return converter.robot_timestamp_from_local_secs(now_sec())
|
|
360
361
|
return None
|
|
361
362
|
|
|
362
363
|
|
|
@@ -430,7 +431,7 @@ class LoggingHandler(logging.Handler): # pylint: disable=too-many-instance-attr
|
|
|
430
431
|
msg = self.record_to_msg(record)
|
|
431
432
|
with self._lock:
|
|
432
433
|
self._msg_queue.append(msg)
|
|
433
|
-
self._last_emit_time =
|
|
434
|
+
self._last_emit_time = now_sec()
|
|
434
435
|
|
|
435
436
|
def flush(self):
|
|
436
437
|
self._flush_event.set()
|
|
@@ -487,7 +488,7 @@ class LoggingHandler(logging.Handler): # pylint: disable=too-many-instance-attr
|
|
|
487
488
|
while (self._num_failed_sends_sequential < self._limit_failed_sends_sequential and
|
|
488
489
|
not self._shutdown_event.is_set()):
|
|
489
490
|
flush = self._flush_event.wait(self._flush_event_wait_time)
|
|
490
|
-
msg_age =
|
|
491
|
+
msg_age = now_sec() - self._last_emit_time
|
|
491
492
|
with self._lock:
|
|
492
493
|
num_msgs = len(self._msg_queue)
|
|
493
494
|
to_send = self._msg_queue[:num_msgs]
|
|
@@ -555,7 +556,7 @@ class LoggingHandler(logging.Handler): # pylint: disable=too-many-instance-attr
|
|
|
555
556
|
if self.time_sync_endpoint is not None:
|
|
556
557
|
try:
|
|
557
558
|
msg.timestamp.CopyFrom(
|
|
558
|
-
self.time_sync_endpoint.robot_timestamp_from_local_secs(
|
|
559
|
+
self.time_sync_endpoint.robot_timestamp_from_local_secs(now_sec()))
|
|
559
560
|
except time_sync.NotEstablishedError:
|
|
560
561
|
# If timestamp is not set in the proto, data-buffer will timestamp it on receipt.
|
|
561
562
|
msg.message = '(No time sync!): ' + msg.message
|
|
@@ -18,6 +18,7 @@ from bosdyn.api import (directory_pb2, directory_registration_pb2,
|
|
|
18
18
|
directory_registration_service_pb2_grpc)
|
|
19
19
|
from bosdyn.client.common import (BaseClient, error_factory, error_pair,
|
|
20
20
|
handle_common_header_errors, handle_unset_status_error)
|
|
21
|
+
from bosdyn.util import now_sec
|
|
21
22
|
|
|
22
23
|
from .error_callback_result import ErrorCallbackResult
|
|
23
24
|
from .exceptions import ResponseError, RetryableUnavailableError, RpcError, TimedOutError
|
|
@@ -442,7 +443,7 @@ class DirectoryRegistrationKeepAlive(object):
|
|
|
442
443
|
|
|
443
444
|
self.logger.info('Starting directory registration loop for {}'.format(self.directory_name))
|
|
444
445
|
while not self._end_reregister_signal.wait(wait_time):
|
|
445
|
-
exec_start =
|
|
446
|
+
exec_start = now_sec()
|
|
446
447
|
action = ErrorCallbackResult.RESUME_NORMAL_OPERATION
|
|
447
448
|
try:
|
|
448
449
|
self.dir_reg_client.register(
|
|
@@ -476,7 +477,7 @@ class DirectoryRegistrationKeepAlive(object):
|
|
|
476
477
|
# Log all other exceptions, but continue looping in hopes that it resolves itself
|
|
477
478
|
self.logger.exception('Caught general exception')
|
|
478
479
|
|
|
479
|
-
elapsed =
|
|
480
|
+
elapsed = now_sec() - exec_start
|
|
480
481
|
if action == ErrorCallbackResult.RETRY_IMMEDIATELY:
|
|
481
482
|
wait_time = 0.0
|
|
482
483
|
elif action == ErrorCallbackResult.ABORT:
|