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 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
 
@@ -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, 'command-line', options.timeout)
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
- BecomeEstopCommand(subparsers, command_dict)
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
- Asynchronous calls cannot be done with streaming rpcs right now.
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
 
@@ -7,9 +7,6 @@
7
7
  """General client implementation for the main, on-robot data-acquisition service."""
8
8
 
9
9
  import collections
10
- import functools
11
- import json
12
- import time
13
10
 
14
11
  from google.protobuf import json_format
15
12
 
@@ -4,10 +4,7 @@
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
- import io
8
- import json
9
7
  import logging
10
- import os
11
8
  import ssl
12
9
  import time
13
10
  from pathlib import Path
@@ -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()