bosdyn-client 4.0.3__py3-none-any.whl → 4.1.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/channel.py +3 -1
- bosdyn/client/command_line.py +260 -2
- bosdyn/client/common.py +62 -5
- bosdyn/client/data_acquisition.py +0 -3
- bosdyn/client/data_acquisition_helpers.py +0 -3
- bosdyn/client/data_acquisition_plugin_service.py +28 -1
- bosdyn/client/data_acquisition_store.py +112 -7
- bosdyn/client/frame_helpers.py +2 -0
- bosdyn/client/gps/NMEAParser.py +4 -3
- bosdyn/client/gps/aggregator_client.py +0 -1
- bosdyn/client/gps/gps_listener.py +1 -3
- bosdyn/client/graph_nav.py +98 -37
- bosdyn/client/image.py +5 -2
- bosdyn/client/image_service_helpers.py +1 -3
- bosdyn/client/map_processing.py +0 -3
- bosdyn/client/power.py +5 -13
- bosdyn/client/recording.py +4 -4
- bosdyn/client/robot.py +6 -5
- bosdyn/client/robot_command.py +2 -2
- bosdyn/client/sdk.py +3 -0
- bosdyn/client/server_util.py +2 -0
- bosdyn/client/service_customization_helpers.py +5 -3
- bosdyn/client/spot_cam/streamquality.py +0 -1
- bosdyn/client/spot_check.py +0 -155
- bosdyn/client/time_sync.py +38 -0
- bosdyn/client/util.py +3 -6
- bosdyn/client/world_object.py +6 -39
- {bosdyn_client-4.0.3.dist-info → bosdyn_client-4.1.0.dist-info}/METADATA +4 -4
- {bosdyn_client-4.0.3.dist-info → bosdyn_client-4.1.0.dist-info}/RECORD +31 -31
- {bosdyn_client-4.0.3.dist-info → bosdyn_client-4.1.0.dist-info}/WHEEL +0 -0
- {bosdyn_client-4.0.3.dist-info → bosdyn_client-4.1.0.dist-info}/top_level.txt +0 -0
bosdyn/client/channel.py
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
# Development Kit License (20191101-BDSDK-SL).
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
import warnings
|
|
9
8
|
|
|
10
9
|
import grpc
|
|
11
10
|
|
|
@@ -25,6 +24,9 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
25
24
|
# creating channels in the bosdyn.client.Robot class.
|
|
26
25
|
DEFAULT_MAX_MESSAGE_LENGTH = 100 * (1024**2)
|
|
27
26
|
|
|
27
|
+
# Useful for clients to ensure there is enough room left in a message for other fields like headers
|
|
28
|
+
DEFAULT_HEADER_BUFFER_LENGTH = 4 * (1024**2)
|
|
29
|
+
|
|
28
30
|
# Period in milliseconds after which a keepalive ping is sent on the transport.
|
|
29
31
|
DEFAULT_KEEP_ALIVE_TIME_MS = 5000
|
|
30
32
|
|
bosdyn/client/command_line.py
CHANGED
|
@@ -13,16 +13,19 @@ import argparse
|
|
|
13
13
|
import datetime
|
|
14
14
|
import os
|
|
15
15
|
import signal
|
|
16
|
+
import socket
|
|
16
17
|
import sys
|
|
17
18
|
import threading
|
|
18
19
|
import time
|
|
19
20
|
|
|
21
|
+
from deprecated.sphinx import deprecated
|
|
20
22
|
from google.protobuf import json_format
|
|
21
23
|
|
|
22
24
|
import bosdyn.client
|
|
23
25
|
from bosdyn.api import data_acquisition_pb2, image_pb2
|
|
24
26
|
from bosdyn.api.data_buffer_pb2 import Event, TextMessage
|
|
25
27
|
from bosdyn.api.data_index_pb2 import EventsCommentsSpec
|
|
28
|
+
from bosdyn.api.keepalive import keepalive_pb2
|
|
26
29
|
from bosdyn.api.robot_state_pb2 import BehaviorFault
|
|
27
30
|
from bosdyn.util import duration_str, timestamp_to_datetime
|
|
28
31
|
|
|
@@ -38,6 +41,7 @@ from .estop import EstopClient, EstopEndpoint, EstopKeepAlive
|
|
|
38
41
|
from .exceptions import Error, InvalidRequestError, ProxyConnectionError
|
|
39
42
|
from .image import (ImageClient, ImageResponseError, UnknownImageSourceError, build_image_request,
|
|
40
43
|
save_images_as_files)
|
|
44
|
+
from .keepalive import KeepaliveClient
|
|
41
45
|
from .lease import LeaseClient
|
|
42
46
|
from .license import LicenseClient
|
|
43
47
|
from .local_grid import LocalGridClient
|
|
@@ -1704,6 +1708,81 @@ class LeaseListCommand(Command):
|
|
|
1704
1708
|
return str(resource)
|
|
1705
1709
|
|
|
1706
1710
|
|
|
1711
|
+
class EstopCommands(Subcommands):
|
|
1712
|
+
"""Commands for interacting with robot estop service."""
|
|
1713
|
+
|
|
1714
|
+
NAME = 'estop'
|
|
1715
|
+
|
|
1716
|
+
def __init__(self, subparsers, command_dict):
|
|
1717
|
+
"""Commands for interacting with robot estop service.
|
|
1718
|
+
|
|
1719
|
+
Args:
|
|
1720
|
+
subparsers: List of argument parsers.
|
|
1721
|
+
command_dict: Dictionary of command names which take parsed options.
|
|
1722
|
+
"""
|
|
1723
|
+
super(EstopCommands,
|
|
1724
|
+
self).__init__(subparsers, command_dict,
|
|
1725
|
+
[BecomeEstopCommand, GetEstopConfigCommand, GetEstopStatusCommand])
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
class GetEstopConfigCommand(Command):
|
|
1729
|
+
"""Get estop config of estop service."""
|
|
1730
|
+
|
|
1731
|
+
NAME = 'config'
|
|
1732
|
+
|
|
1733
|
+
def __init__(self, subparsers, command_dict):
|
|
1734
|
+
"""Call EstopService GetEstopConfig RPC.
|
|
1735
|
+
|
|
1736
|
+
Args:
|
|
1737
|
+
subparsers: List of argument parsers.
|
|
1738
|
+
command_dict: Dictionary of command names which take parsed options.
|
|
1739
|
+
"""
|
|
1740
|
+
super(GetEstopConfigCommand, self).__init__(subparsers, command_dict)
|
|
1741
|
+
|
|
1742
|
+
def _run(self, robot, options):
|
|
1743
|
+
"""Implementation of the command.
|
|
1744
|
+
|
|
1745
|
+
Args:
|
|
1746
|
+
robot: Robot object on which to run the command.
|
|
1747
|
+
options: Parsed command-line arguments.
|
|
1748
|
+
|
|
1749
|
+
Returns:
|
|
1750
|
+
True.
|
|
1751
|
+
"""
|
|
1752
|
+
client = robot.ensure_client(EstopClient.default_service_name)
|
|
1753
|
+
config = client.get_config()
|
|
1754
|
+
print(config)
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
class GetEstopStatusCommand(Command):
|
|
1758
|
+
"""Get estop status of estop service."""
|
|
1759
|
+
|
|
1760
|
+
NAME = 'status'
|
|
1761
|
+
|
|
1762
|
+
def __init__(self, subparsers, command_dict):
|
|
1763
|
+
"""Call EstopService GetEstopSystemStatus RPC.
|
|
1764
|
+
|
|
1765
|
+
Args:
|
|
1766
|
+
subparsers: List of argument parsers.
|
|
1767
|
+
command_dict: Dictionary of command names which take parsed options.
|
|
1768
|
+
"""
|
|
1769
|
+
super(GetEstopStatusCommand, self).__init__(subparsers, command_dict)
|
|
1770
|
+
|
|
1771
|
+
def _run(self, robot, options):
|
|
1772
|
+
"""Implementation of the command.
|
|
1773
|
+
|
|
1774
|
+
Args:
|
|
1775
|
+
robot: Robot object on which to run the command.
|
|
1776
|
+
options: Parsed command-line arguments.
|
|
1777
|
+
|
|
1778
|
+
Returns:
|
|
1779
|
+
True.
|
|
1780
|
+
"""
|
|
1781
|
+
client = robot.ensure_client(EstopClient.default_service_name)
|
|
1782
|
+
status = client.get_status()
|
|
1783
|
+
print(status)
|
|
1784
|
+
|
|
1785
|
+
|
|
1707
1786
|
class BecomeEstopCommand(Command):
|
|
1708
1787
|
"""Grab and hold estop until Ctl-C."""
|
|
1709
1788
|
|
|
@@ -1754,7 +1833,7 @@ class BecomeEstopCommand(Command):
|
|
|
1754
1833
|
# Create the endpoint to the robot estop system.
|
|
1755
1834
|
# Timeout should be chosen to balance safety considerations with expected service latency.
|
|
1756
1835
|
# See the estop documentation for details.
|
|
1757
|
-
endpoint = EstopEndpoint(client,
|
|
1836
|
+
endpoint = EstopEndpoint(client, f"command-line-{socket.gethostname()}", options.timeout)
|
|
1758
1837
|
# Have this endpoint to set up the robot's estop system such that it is the sole estop.
|
|
1759
1838
|
# See the function's docstring and the estop documentation for details.
|
|
1760
1839
|
endpoint.force_simple_setup()
|
|
@@ -1788,6 +1867,14 @@ class BecomeEstopCommand(Command):
|
|
|
1788
1867
|
return True
|
|
1789
1868
|
|
|
1790
1869
|
|
|
1870
|
+
class OldBecomeEstopCommand(BecomeEstopCommand):
|
|
1871
|
+
"""Old version of BecomeEstopCommand."""
|
|
1872
|
+
|
|
1873
|
+
def run(self, robot, options):
|
|
1874
|
+
print('DEPRECATION WARNING: This command is now "bosdyn.client estop become-estop"')
|
|
1875
|
+
return BecomeEstopCommand.run(self, robot, options)
|
|
1876
|
+
|
|
1877
|
+
|
|
1791
1878
|
class ImageCommands(Subcommands):
|
|
1792
1879
|
"""Commands for querying images."""
|
|
1793
1880
|
|
|
@@ -2276,6 +2363,175 @@ class PowerCommand(Subcommands):
|
|
|
2276
2363
|
])
|
|
2277
2364
|
|
|
2278
2365
|
|
|
2366
|
+
class KeepaliveCommand(Subcommands):
|
|
2367
|
+
"""Send keepalive commands to the robot."""
|
|
2368
|
+
|
|
2369
|
+
NAME = 'keepalive'
|
|
2370
|
+
|
|
2371
|
+
def __init__(self, subparsers, command_dict):
|
|
2372
|
+
"""Send keepalive commands to the robot.
|
|
2373
|
+
|
|
2374
|
+
Args:
|
|
2375
|
+
subparsers: List of argument parsers.
|
|
2376
|
+
command_dict: Dictionary of command names which take parsed options.
|
|
2377
|
+
"""
|
|
2378
|
+
super(KeepaliveCommand, self).__init__(subparsers, command_dict, [
|
|
2379
|
+
KeepaliveGetStatusCommand,
|
|
2380
|
+
KeepaliveRemovePoliciesCommand,
|
|
2381
|
+
])
|
|
2382
|
+
|
|
2383
|
+
|
|
2384
|
+
def lease_details(leases):
|
|
2385
|
+
"""Returns list of <resource_name>:<sequence>, ...N."""
|
|
2386
|
+
lease_strings = []
|
|
2387
|
+
for lease in leases:
|
|
2388
|
+
sequence_string = ", ".join([str(lease_seq) for lease_seq in lease.sequence])
|
|
2389
|
+
lease_strings.append(f"{lease.resource}:[{sequence_string}]")
|
|
2390
|
+
return ", ".join(lease_strings)
|
|
2391
|
+
|
|
2392
|
+
|
|
2393
|
+
class KeepaliveGetStatusCommand(Command):
|
|
2394
|
+
"""Get status of keepalive service."""
|
|
2395
|
+
|
|
2396
|
+
NAME = 'status'
|
|
2397
|
+
|
|
2398
|
+
def __init__(self, subparsers, command_dict):
|
|
2399
|
+
"""Call KeepaliveService GetStatus RPC.
|
|
2400
|
+
|
|
2401
|
+
Args:
|
|
2402
|
+
subparsers: List of argument parsers.
|
|
2403
|
+
command_dict: Dictionary of command names which take parsed options.
|
|
2404
|
+
"""
|
|
2405
|
+
super(KeepaliveGetStatusCommand, self).__init__(subparsers, command_dict)
|
|
2406
|
+
self._parser.add_argument('-f', '--full', action='store_true', default=False,
|
|
2407
|
+
help='Show full GetStatus proto as json.')
|
|
2408
|
+
|
|
2409
|
+
def _run(self, robot, options):
|
|
2410
|
+
"""Implementation of the command.
|
|
2411
|
+
|
|
2412
|
+
Args:
|
|
2413
|
+
robot: Robot object on which to run the command.
|
|
2414
|
+
options: Parsed command-line arguments.
|
|
2415
|
+
|
|
2416
|
+
Returns:
|
|
2417
|
+
True.
|
|
2418
|
+
"""
|
|
2419
|
+
client = robot.ensure_client(KeepaliveClient.default_service_name)
|
|
2420
|
+
status = client.get_status()
|
|
2421
|
+
|
|
2422
|
+
# Give the option to print out the full message.
|
|
2423
|
+
if options.full:
|
|
2424
|
+
print(json_format.MessageToJson(status))
|
|
2425
|
+
return True
|
|
2426
|
+
|
|
2427
|
+
# If there are no policies, there probably isn't anything interesting here.
|
|
2428
|
+
if len(status.status) == 0:
|
|
2429
|
+
print("No active policies")
|
|
2430
|
+
return True
|
|
2431
|
+
|
|
2432
|
+
# List all live policies in a concise message.
|
|
2433
|
+
if len(status.status) == 1:
|
|
2434
|
+
print("Robot returned 1 live policy:")
|
|
2435
|
+
else:
|
|
2436
|
+
print(f"Robot returned {len(status.status)} live policies:")
|
|
2437
|
+
for live_policy in status.status:
|
|
2438
|
+
rough_robot_timestamp = status.header.response_timestamp.ToSeconds()
|
|
2439
|
+
last_checkin = live_policy.last_checkin.ToSeconds()
|
|
2440
|
+
time_elapsed = rough_robot_timestamp - last_checkin
|
|
2441
|
+
# Go through each action, and create a helpful message describing the status.
|
|
2442
|
+
action_list = []
|
|
2443
|
+
for action in live_policy.policy.actions:
|
|
2444
|
+
name = action.WhichOneof("action")
|
|
2445
|
+
# If this is a lease_stale or auto_return action, include some lease info.
|
|
2446
|
+
name_maybe_with_details = name
|
|
2447
|
+
if action.HasField("lease_stale"):
|
|
2448
|
+
name_maybe_with_details = f"{name_maybe_with_details} ({lease_details(action.lease_stale.leases)})"
|
|
2449
|
+
if action.HasField("auto_return"):
|
|
2450
|
+
name_maybe_with_details = f"{name_maybe_with_details} ({lease_details(action.auto_return.leases)})"
|
|
2451
|
+
# Add an indicator if the policy action is active or not.
|
|
2452
|
+
action_after_time = action.after.ToSeconds()
|
|
2453
|
+
active_message = "NOT active"
|
|
2454
|
+
if time_elapsed > action.after.ToSeconds():
|
|
2455
|
+
active_message = "ACTIVE"
|
|
2456
|
+
action_list.append(
|
|
2457
|
+
f"{name_maybe_with_details} after {action_after_time} seconds, {active_message}"
|
|
2458
|
+
)
|
|
2459
|
+
formatted_string = f"id: {live_policy.policy_id}\n client_name: {live_policy.client_name}\n last checkin {time_elapsed}s ago"
|
|
2460
|
+
# Only the supervisor policy seems to have a name.
|
|
2461
|
+
if live_policy.policy.name:
|
|
2462
|
+
formatted_string = formatted_string + f"\n policy name: '{live_policy.policy.name}'"
|
|
2463
|
+
if live_policy.policy.user_id:
|
|
2464
|
+
formatted_string = formatted_string + f"\n user_id: '{live_policy.policy.user_id}'"
|
|
2465
|
+
if len(live_policy.policy.associated_leases) > 0:
|
|
2466
|
+
formatted_string = formatted_string + f"\n associated_leases: {lease_details(live_policy.policy.associated_leases)}"
|
|
2467
|
+
print(formatted_string)
|
|
2468
|
+
print(" actions:")
|
|
2469
|
+
for index, action in enumerate(action_list):
|
|
2470
|
+
print(f" {action}")
|
|
2471
|
+
print("")
|
|
2472
|
+
|
|
2473
|
+
# If there is an action control action, we should indicate that.
|
|
2474
|
+
if len(status.active_control_actions) == 0:
|
|
2475
|
+
return True
|
|
2476
|
+
for action in status.active_control_actions:
|
|
2477
|
+
enum_name = keepalive_pb2.GetStatusResponse.PolicyControlAction.Name(action)
|
|
2478
|
+
print(f"Active control action: {enum_name}")
|
|
2479
|
+
return True
|
|
2480
|
+
|
|
2481
|
+
|
|
2482
|
+
class KeepaliveRemovePoliciesCommand(Command):
|
|
2483
|
+
"""Remove keepalive policies."""
|
|
2484
|
+
|
|
2485
|
+
NAME = 'remove'
|
|
2486
|
+
|
|
2487
|
+
def __init__(self, subparsers, command_dict):
|
|
2488
|
+
"""Remove keepalive policies.
|
|
2489
|
+
|
|
2490
|
+
Args:
|
|
2491
|
+
subparsers: List of argument parsers.
|
|
2492
|
+
command_dict: Dictionary of command names which take parsed options.
|
|
2493
|
+
"""
|
|
2494
|
+
super(KeepaliveRemovePoliciesCommand, self).__init__(subparsers, command_dict)
|
|
2495
|
+
self._parser.add_argument('--policy-id', type=int, nargs='+',
|
|
2496
|
+
help='Specify specific policy ids to remove.')
|
|
2497
|
+
|
|
2498
|
+
def _run(self, robot, options):
|
|
2499
|
+
"""Implementation of the command.
|
|
2500
|
+
|
|
2501
|
+
Args:
|
|
2502
|
+
robot: Robot object on which to run the command.
|
|
2503
|
+
options: Parsed command-line arguments.
|
|
2504
|
+
|
|
2505
|
+
Returns:
|
|
2506
|
+
True.
|
|
2507
|
+
"""
|
|
2508
|
+
client = robot.ensure_client(KeepaliveClient.default_service_name)
|
|
2509
|
+
current_policies = client.get_status().status
|
|
2510
|
+
current_policy_ids = [s.policy_id for s in current_policies]
|
|
2511
|
+
|
|
2512
|
+
# If there are no policies, there probably isn't anything interesting here.
|
|
2513
|
+
if len(current_policies) == 0:
|
|
2514
|
+
print("No active policies")
|
|
2515
|
+
return True
|
|
2516
|
+
|
|
2517
|
+
to_rm = []
|
|
2518
|
+
if options.policy_id:
|
|
2519
|
+
# Remove specific policies.
|
|
2520
|
+
for policy_id in options.policy_id:
|
|
2521
|
+
if policy_id in current_policy_ids:
|
|
2522
|
+
to_rm.append(policy_id)
|
|
2523
|
+
else:
|
|
2524
|
+
print(f"Policy '{policy_id}' not found.")
|
|
2525
|
+
else:
|
|
2526
|
+
# Remove all current policies.
|
|
2527
|
+
to_rm = current_policy_ids
|
|
2528
|
+
|
|
2529
|
+
client.modify_policy(policy_ids_to_remove=to_rm)
|
|
2530
|
+
print(f"Removed {len(to_rm)} policies")
|
|
2531
|
+
|
|
2532
|
+
return True
|
|
2533
|
+
|
|
2534
|
+
|
|
2279
2535
|
class PowerRobotCommand(Command):
|
|
2280
2536
|
"""Control the power of the entire robot."""
|
|
2281
2537
|
|
|
@@ -2405,12 +2661,14 @@ def main(args=None):
|
|
|
2405
2661
|
DataServiceCommands(subparsers, command_dict)
|
|
2406
2662
|
TimeSyncCommand(subparsers, command_dict)
|
|
2407
2663
|
LeaseCommands(subparsers, command_dict)
|
|
2408
|
-
|
|
2664
|
+
OldBecomeEstopCommand(subparsers, command_dict)
|
|
2665
|
+
EstopCommands(subparsers, command_dict)
|
|
2409
2666
|
ImageCommands(subparsers, command_dict)
|
|
2410
2667
|
LocalGridCommands(subparsers, command_dict)
|
|
2411
2668
|
DataAcquisitionCommand(subparsers, command_dict)
|
|
2412
2669
|
HostComputerIPCommand(subparsers, command_dict)
|
|
2413
2670
|
PowerCommand(subparsers, command_dict)
|
|
2671
|
+
KeepaliveCommand(subparsers, command_dict)
|
|
2414
2672
|
|
|
2415
2673
|
options = parser.parse_args(args=args)
|
|
2416
2674
|
|
bosdyn/client/common.py
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
# Development Kit License (20191101-BDSDK-SL).
|
|
6
6
|
|
|
7
7
|
"""Contains elements common to all service clients."""
|
|
8
|
+
import concurrent
|
|
8
9
|
import copy
|
|
9
10
|
import functools
|
|
10
11
|
import logging
|
|
11
|
-
import math
|
|
12
12
|
import socket
|
|
13
13
|
import types
|
|
14
14
|
|
|
@@ -232,6 +232,37 @@ def handle_custom_params_errors(*args, status_value=None, status_field_name='sta
|
|
|
232
232
|
return decorator
|
|
233
233
|
|
|
234
234
|
|
|
235
|
+
def handle_license_errors(func):
|
|
236
|
+
"""Decorate "error from response" functions to handle typical license errors."""
|
|
237
|
+
|
|
238
|
+
@functools.wraps(func)
|
|
239
|
+
def wrapper(*args, **kwargs):
|
|
240
|
+
return common_license_errors(*args) or func(*args, **kwargs)
|
|
241
|
+
|
|
242
|
+
return wrapper
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def handle_license_errors_if_present(func):
|
|
246
|
+
"""Decorate "error from response" functions to handle typical license errors.
|
|
247
|
+
Does not raise an error for STATUS_UNKNOWN.
|
|
248
|
+
Use for responses that may only sometimes fill out the license status."""
|
|
249
|
+
|
|
250
|
+
@functools.wraps(func)
|
|
251
|
+
def wrapper(*args, **kwargs):
|
|
252
|
+
return common_license_errors(*args, allow_unset=True) or func(*args, **kwargs)
|
|
253
|
+
|
|
254
|
+
return wrapper
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def common_license_errors(response, allow_unset=False):
|
|
258
|
+
license_status = response.license_status
|
|
259
|
+
if allow_unset and license_status == license_pb2.LicenseInfo.STATUS_UNKNOWN:
|
|
260
|
+
return None
|
|
261
|
+
elif license_status != license_pb2.LicenseInfo.STATUS_VALID:
|
|
262
|
+
return LicenseError(response)
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
235
266
|
def maybe_raise(exc):
|
|
236
267
|
"""raise the provided exception if it is not None"""
|
|
237
268
|
if exc is not None:
|
|
@@ -300,6 +331,7 @@ class BaseClient(object):
|
|
|
300
331
|
self.response_processors = []
|
|
301
332
|
self.lease_wallet = None
|
|
302
333
|
self.client_name = None
|
|
334
|
+
self.executor = None
|
|
303
335
|
|
|
304
336
|
@staticmethod
|
|
305
337
|
@deprecated(reason='Forces serialization even if the logging is not happening. Do not use.',
|
|
@@ -331,6 +363,7 @@ class BaseClient(object):
|
|
|
331
363
|
self.logger = other.logger.getChild(self._name or self._service_type_short)
|
|
332
364
|
self.lease_wallet = other.lease_wallet
|
|
333
365
|
self.client_name = other.client_name
|
|
366
|
+
self.executor = other.executor
|
|
334
367
|
|
|
335
368
|
def update_request_iterator(self, request_iterator, logger, rpc_method, is_blocking,
|
|
336
369
|
copy_request=True):
|
|
@@ -444,7 +477,7 @@ class BaseClient(object):
|
|
|
444
477
|
|
|
445
478
|
value_from_response and error_from_response should not raise their own exceptions!
|
|
446
479
|
|
|
447
|
-
|
|
480
|
+
call_async does not accept streaming rpcs, see 'call_async_streaming'.
|
|
448
481
|
"""
|
|
449
482
|
request = self._apply_request_processors(request, copy_request=copy_request)
|
|
450
483
|
logger = self._get_logger(rpc_method)
|
|
@@ -468,6 +501,25 @@ class BaseClient(object):
|
|
|
468
501
|
response_future.add_done_callback(on_finish)
|
|
469
502
|
return FutureWrapper(response_future, value_from_response, error_from_response)
|
|
470
503
|
|
|
504
|
+
@process_kwargs
|
|
505
|
+
def call_async_streaming(self, rpc_method, request, value_from_response=None,
|
|
506
|
+
error_from_response=None, assemble_type=None, copy_request=False,
|
|
507
|
+
**kwargs):
|
|
508
|
+
"""Returns a Future for rpc_method(request, kwargs) after running processors.
|
|
509
|
+
|
|
510
|
+
value_from_response and error_from_response should not raise their own exceptions.
|
|
511
|
+
|
|
512
|
+
A version of 'call_async' for streaming rpcs. True async streaming calls are not supported by
|
|
513
|
+
python grpc. Instead, this call creates a thread that runs the synchronous 'call' function.
|
|
514
|
+
"""
|
|
515
|
+
request = self._apply_request_processors(request, copy_request=copy_request)
|
|
516
|
+
if self.executor is None:
|
|
517
|
+
self.executor = concurrent.futures.ThreadPoolExecutor()
|
|
518
|
+
|
|
519
|
+
future = self.executor.submit(self.call, rpc_method, request, assemble_type=assemble_type,
|
|
520
|
+
copy_request=copy_request, **kwargs)
|
|
521
|
+
return FutureWrapper(future, value_from_response, error_from_response, is_streaming=True)
|
|
522
|
+
|
|
471
523
|
def _apply_request_processors(self, request, copy_request=True):
|
|
472
524
|
if request is None:
|
|
473
525
|
return
|
|
@@ -498,10 +550,11 @@ class BaseClient(object):
|
|
|
498
550
|
class FutureWrapper():
|
|
499
551
|
"""Wraps a Future to aid more complicated clients' async calls."""
|
|
500
552
|
|
|
501
|
-
def __init__(self, future, value_from_response, error_from_response):
|
|
553
|
+
def __init__(self, future, value_from_response, error_from_response, is_streaming=False):
|
|
502
554
|
self.original_future = future
|
|
503
555
|
self._error_from_response = error_from_response
|
|
504
556
|
self._value_from_response = value_from_response
|
|
557
|
+
self._is_streaming = is_streaming
|
|
505
558
|
|
|
506
559
|
def __repr__(self):
|
|
507
560
|
return self.original_future.__repr__()
|
|
@@ -521,7 +574,7 @@ class FutureWrapper():
|
|
|
521
574
|
def traceback(self, **kwargs):
|
|
522
575
|
return self.original_future.traceback(**kwargs)
|
|
523
576
|
|
|
524
|
-
def add_done_callback(self, cb):
|
|
577
|
+
def add_done_callback(self, cb, assemble_type=None):
|
|
525
578
|
"""Add callback executed on FutureWrapper when future is done."""
|
|
526
579
|
self.original_future.add_done_callback(lambda not_used_original_future: cb(self))
|
|
527
580
|
|
|
@@ -535,7 +588,6 @@ class FutureWrapper():
|
|
|
535
588
|
|
|
536
589
|
if self._value_from_response is None:
|
|
537
590
|
return base_result
|
|
538
|
-
|
|
539
591
|
return self._value_from_response(base_result)
|
|
540
592
|
|
|
541
593
|
def exception(self, **kwargs):
|
|
@@ -547,6 +599,11 @@ class FutureWrapper():
|
|
|
547
599
|
return None
|
|
548
600
|
return self._error_from_response(self.original_future.result())
|
|
549
601
|
|
|
602
|
+
# 'call_async_streaming' uses the non-async 'call' function. 'call' does all of it's
|
|
603
|
+
# own error handling so just return any errors from that call as is.
|
|
604
|
+
if self._is_streaming:
|
|
605
|
+
return error
|
|
606
|
+
|
|
550
607
|
return translate_exception(error)
|
|
551
608
|
|
|
552
609
|
|
|
@@ -42,7 +42,6 @@ However, the data_collect_fn function should update the status to STATUS_SAVING
|
|
|
42
42
|
storing the data.
|
|
43
43
|
"""
|
|
44
44
|
|
|
45
|
-
import concurrent.futures
|
|
46
45
|
import logging
|
|
47
46
|
import threading
|
|
48
47
|
import time
|
|
@@ -242,6 +241,34 @@ class DataAcquisitionStoreHelper(object):
|
|
|
242
241
|
future = self.store_client.store_data_async(message, data_id, file_extension)
|
|
243
242
|
self.data_id_future_pairs.append((data_id, future))
|
|
244
243
|
|
|
244
|
+
def store_data_as_chunks(self, message, data_id, file_extension=None):
|
|
245
|
+
"""Store a data message by streaming with the data acquisition store service.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
message (bytes): Data to store.
|
|
249
|
+
data_id (bosdyn.api.DataIdentifier) : Data identifier to use for storing this data.
|
|
250
|
+
file_extension (string) : File extension to use for writing the data to a file.
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
RPCError: Problem communicating with the robot.
|
|
254
|
+
"""
|
|
255
|
+
future = self.store_client.store_data_as_chunks_async(message, data_id, file_extension)
|
|
256
|
+
self.data_id_future_pairs.append((data_id, future))
|
|
257
|
+
|
|
258
|
+
def store_file(self, file_path, data_id, file_extension=None):
|
|
259
|
+
"""Store a file with the data acquisition store service.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
file_path (string): Path to the file to store.
|
|
263
|
+
data_id (bosdyn.api.DataIdentifier) : Data identifier to use for storing this file.
|
|
264
|
+
file_extension (string) : File extension to use for writing the data to the file.
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
RPCError: Problem communicating with the robot.
|
|
268
|
+
"""
|
|
269
|
+
future = self.store_client.store_file_async(file_path, data_id, file_extension)
|
|
270
|
+
self.data_id_future_pairs.append((data_id, future))
|
|
271
|
+
|
|
245
272
|
def cancel_check(self):
|
|
246
273
|
"""Raises RequestCancelledError if the request has already been cancelled."""
|
|
247
274
|
self.state.cancel_check()
|