Source code for bosdyn.mission.util

# Copyright (c) 2023 Boston Dynamics, Inc.  All rights reserved.
#
# Downloading, reproducing, distributing or otherwise using the SDK Software
# is subject to the terms and conditions of the Boston Dynamics Software
# Development Kit License (20191101-BDSDK-SL).

import copy
import json
import logging
import operator
import re
from builtins import str as text
from typing import Dict, Union

import google.protobuf.message
import google.protobuf.struct_pb2
import google.protobuf.text_format
from deprecated.sphinx import deprecated
from google.protobuf import message_factory

from bosdyn.api import alerts_pb2, data_acquisition_pb2, geometry_pb2, gripper_camera_param_pb2
from bosdyn.api.autowalk import walks_pb2
from bosdyn.api.docking import docking_pb2
from bosdyn.api.graph_nav import graph_nav_pb2, map_pb2
from bosdyn.api.mission import mission_pb2, nodes_pb2, util_pb2
from bosdyn.client.util import safe_pb_enum_to_string as _bosdyn_client_safe_pb_enum_to_string
from bosdyn.deprecated import moved_to
from bosdyn.mission import constants

# This is a special dummy message we'll use for TYPE_MESSAGE parameters.
DUMMY_MESSAGE = nodes_pb2.Node(name='dummy-message-for-parameterization')



[docs]class Error(Exception): pass
[docs]class InvalidConversion(Error): """Could not convert the provided value to the destination type.""" def __init__(self, original_value, destination_typename): self.original_value = original_value self.destination_typename = destination_typename def __str__(self): return 'Could not convert "{}" to type "{}"'.format(self.original_value, self.destination_typename)
_python_identifier_regex = re.compile(r'[A-Za-z_]\w*$')
[docs]def tree_to_string(root, start_level=0, include_status=False): """Get a string representation of a Node, interpreted as a tree.""" string = '' if start_level == 0: string += '\n' prefix = '|' + '-' * start_level string += prefix + text(root) + (' ' if text(root) else '') + '(' + root.__class__.__name__ + ')' if include_status: string += '\n' + prefix + 'Status code: [{}]'.format(root.last_result) for child in root.children: string += '\n' + tree_to_string(child, start_level=start_level + 1, include_status=include_status) return string
[docs]def type_to_field_name(type_name): """Use type name to reconstruct field name of bosdyn.api.mission.Node.type Example: SimpleParallel becomes simple_parallel""" node_type = str(type_name).split('.')[-1] field_name = node_type[0].lower() for char in node_type[1:]: if char.isupper(): field_name += f'_{char.lower()}' else: field_name += char oneof_field_names = [x.name for x in nodes_pb2.Node.DESCRIPTOR.oneofs_by_name['type'].fields] assert field_name in oneof_field_names, f'{field_name} is not a field name in bosdyn.api.mission.Node.type' return field_name
[docs]def proto_from_tuple(tup, pack_nodes=True): """Return a bosdyn.api.mission Node from a tuple. EXPERIMENTAL. Example: :: ('do-A-then-B', bosdyn.api.mission.nodes_pb2.Sequence(always_restart=True), [ ('A', foo.bar.Command(...), []), ('B', foo.bar.Command(...), []), ] ) would make a Sequence named 'do-A-then-B' that always restarted, executing some Command named 'A' followed by the Command named 'B'. NOTE: The "List of children tuples" will only work if the parent node has 'child' or 'children' attributes. See tests/test_util.py for a longer example. Args: tup: (Name of node, Instantiated implementation of node protobuf, List of children tuples) """ if isinstance(tup, nodes_pb2.Node): # Sometimes the shorthand doesn't work nicely, and in those cases we allow setting # The node itself. return tup node = nodes_pb2.Node() name_or_dict, inner_proto, children = tup if isinstance(name_or_dict, dict): node.name = name_or_dict.get('name', '') node.reference_id = name_or_dict.get('reference_id', '') node.node_reference = name_or_dict.get('node_reference', '') if 'user_data' in name_or_dict: node.user_data.CopyFrom(name_or_dict['user_data']) for name, pb_type in name_or_dict.get('parameters', {}).items(): parameter = util_pb2.VariableDeclaration(name=name, type=pb_type) node.parameters.add().CopyFrom(parameter) for key, value in name_or_dict.get('parameter_values', {}).items(): parameter_value = util_pb2.KeyValue(key=key) if isinstance(value, str) and value[0] == '$': parameter_value.value.parameter.name = value[1:] # Leave type unspecified, since we can't look it up easily yet. else: parameter_value.value.constant.CopyFrom(python_var_to_value(value)) node.parameter_values.add().CopyFrom(parameter_value) for key, value in name_or_dict.get('overrides', {}).items(): # Rather than keep this matching the compiler functionality, just omit the check. # It's not even a CompileError anyway. #if key not in inner_proto.DESCRIPTOR.fields_by_name: # raise CompileError('Override specified for "{}" but no such field in "{}"'.format( # key, inner_proto.DESCRIPTOR.full_name)) override = util_pb2.KeyValue(key=key) override.value.parameter.name = value # We do need the final field for setting the type accurately... #override.value.parameter.type = field_desc_to_pb_type(inner_proto.DESCRIPTOR.fields_by_name[key]) node.overrides.add().CopyFrom(override) else: node.name = name_or_dict if node.node_reference: return node num_children = len(children) inner_type = inner_proto.DESCRIPTOR.name # Do some sanity checking on the children. if hasattr(inner_proto, 'children'): if num_children == 0: raise Error('Proto "{}" of type "{}" has no children!'.format(node.name, inner_type)) for child_tup in children: child_node = proto_from_tuple(child_tup) inner_proto.children.add().CopyFrom(child_node) elif hasattr(inner_proto, 'child'): if isinstance(inner_proto, nodes_pb2.ForDuration) and num_children == 2: inner_proto.child.CopyFrom(proto_from_tuple(children[0])) inner_proto.timeout_child.CopyFrom(proto_from_tuple(children[1])) elif num_children == 1: inner_proto.child.CopyFrom(proto_from_tuple(children[0])) else: raise Error('Proto "{}" of type "{}" has {} children!'.format( node.name, inner_type, num_children)) elif num_children != 0: raise Error('Proto "{}" of type "{}" was given {} children, but I do not know how to add' ' them!'.format(node.name, inner_type, num_children)) if pack_nodes: node.impl.Pack(inner_proto) else: getattr(node, type_to_field_name(inner_type)).CopyFrom(inner_proto) return node
[docs]def python_var_to_value(var): """Returns a ConstantValue with the appropriate oneof set.""" value = util_pb2.ConstantValue() if isinstance(var, bool): value.bool_value = var elif isinstance(var, int): value.int_value = var elif isinstance(var, float): value.float_value = var elif isinstance(var, str): value.string_value = var elif isinstance(var, google.protobuf.message.Message): value.msg_value.Pack(var) else: raise Error('Invalid type "{}"'.format(type(var))) return value
[docs]def python_type_to_pb_type(var): """Returns the protobuf-schema variable type that corresponds to the given variable.""" if isinstance(var, bool): return util_pb2.VariableDeclaration.TYPE_BOOL elif isinstance(var, int): return util_pb2.VariableDeclaration.TYPE_INT elif isinstance(var, float): # Python floating point is typically a C double. return util_pb2.VariableDeclaration.TYPE_FLOAT elif isinstance(var, str): return util_pb2.VariableDeclaration.TYPE_STRING elif isinstance(var, google.protobuf.message.Message): return util_pb2.VariableDeclaration.TYPE_MESSAGE raise InvalidConversion(var, util_pb2.VariableDeclaration.Type.DESCRIPTOR.full_name)
[docs]def is_string_identifier(string): if hasattr(string, 'isidentifier'): return string.isidentifier() return bool(_python_identifier_regex.match(string))
[docs]def field_desc_to_pb_type(field_desc): """Returns the protobuf-schema variable type that corresponds to the given descriptor.""" if field_desc.type in (field_desc.TYPE_UINT32, field_desc.TYPE_UINT64, field_desc.TYPE_FIXED32, field_desc.TYPE_FIXED64, field_desc.TYPE_INT32, field_desc.TYPE_INT64, field_desc.TYPE_SFIXED64, field_desc.TYPE_SINT32, field_desc.TYPE_SINT64, field_desc.TYPE_SFIXED32): return util_pb2.VariableDeclaration.TYPE_INT elif field_desc.type in (field_desc.TYPE_DOUBLE, field_desc.TYPE_FLOAT): return util_pb2.VariableDeclaration.TYPE_FLOAT elif field_desc.type == field_desc.TYPE_BOOL: return util_pb2.VariableDeclaration.TYPE_BOOL elif field_desc.type == field_desc.TYPE_STRING: return util_pb2.VariableDeclaration.TYPE_STRING elif field_desc.type == field_desc.TYPE_MESSAGE: return util_pb2.VariableDeclaration.TYPE_MESSAGE raise InvalidConversion(field_desc.type, util_pb2.VariableDeclaration.Type.DESCRIPTOR.full_name)
[docs]def safe_pb_type_to_string(pb_type): """Return the stringified VariableDeclaration.Type, or "<unknown>" if the type is invalid.""" try: return util_pb2.VariableDeclaration.Type.Name(pb_type) except ValueError: return '<unknown>'
[docs]def one_line_str(msg): return google.protobuf.text_format.MessageToString(msg, as_one_line=True)
[docs]def node_spec_to_short_string(node_spec, maxlen=15): if node_spec.name: string = node_spec.name else: string = one_line_str(node_spec) if len(string) > maxlen: return string[0:maxlen - 3] + '...' return string
[docs]class ResultFromProto: results_from_proto = { util_pb2.RESULT_FAILURE: constants.Result.FAILURE, util_pb2.RESULT_RUNNING: constants.Result.RUNNING, util_pb2.RESULT_SUCCESS: constants.Result.SUCCESS, util_pb2.RESULT_ERROR: constants.Result.ERROR, } proto_from_results = {v: k for k, v in results_from_proto.items()}
[docs]def proto_enum_to_result_constant(proto_msg): """Returns a Result enum from a util_pb2.Result, or throws InvalidConversion error.""" try: return ResultFromProto.results_from_proto[proto_msg] except KeyError: raise InvalidConversion(proto_msg, 'constants.Result')
[docs]def result_constant_to_proto_enum(result): """Returns a protobuf version of the Result enum, RESULT_UNKNOWN on error.""" if not isinstance(result, constants.Result): raise InvalidConversion(result, util_pb2.Result.DESCRIPTOR.full_name) try: return ResultFromProto.proto_from_results[result] except KeyError: raise InvalidConversion(result, util_pb2.Result.DESCRIPTOR.full_name)
[docs]def most_restrictive_travel_params(travel_params, vel_limit=None, disable_directed_exploration=False, disable_alternate_route_finding=False, path_following_mode=None, ground_clutter_mode=None): if travel_params is None: travel_params = graph_nav_pb2.TravelParams() else: travel_params = copy.deepcopy(travel_params) def take_limiting(mine, other, compare): # This is basically a hack to deal with proto3's handling of unset POD. # All float and integer fields that are unset in a message will have a value of 0. if other == 0: return mine if mine == 0: return other if compare(mine, other): return other return mine def take_velocity_limit(returned, other): # Look at max_vel using >=, then min_vel using <=. for min_max, comp in (('max_vel', operator.ge), ('min_vel', operator.le)): # If the other doesn't even have this field, skip to the next one. if not other.HasField(text(min_max)): continue lim_returned = getattr(returned, min_max) lim_other = getattr(other, min_max) lim_returned.linear.x = take_limiting(lim_returned.linear.x, lim_other.linear.x, comp) lim_returned.linear.y = take_limiting(lim_returned.linear.y, lim_other.linear.y, comp) lim_returned.angular = take_limiting(lim_returned.angular, lim_other.angular, comp) if vel_limit is not None: take_velocity_limit(travel_params.velocity_limit, vel_limit) travel_params.disable_directed_exploration = travel_params.disable_directed_exploration or disable_directed_exploration travel_params.disable_alternate_route_finding = travel_params.disable_alternate_route_finding or disable_alternate_route_finding if path_following_mode == map_pb2.Edge.Annotations.PATH_MODE_STRICT: travel_params.path_following_mode = path_following_mode if ground_clutter_mode == map_pb2.Edge.Annotations.GROUND_CLUTTER_FROM_FOOTFALLS: travel_params.ground_clutter_mode = ground_clutter_mode return travel_params
[docs]def get_value_from_constant_value_message(const_proto): field = const_proto.WhichOneof('value') if field is None: raise AttributeError('Did not have a value set!') value = getattr(const_proto, field) return value
[docs]def get_value_from_value_message(node, blackboard, value_msg, is_validation=False): if value_msg.HasField(text("constant")): constant = value_msg.constant return get_value_from_constant_value_message(constant) elif value_msg.HasField(text("runtime_var")): return blackboard.read(node, value_msg.runtime_var.name) else: raise AttributeError("Value must be a runtime variable or constant.")
safe_pb_enum_to_string = moved_to(_bosdyn_client_safe_pb_enum_to_string, version='4.0.0')
[docs]def create_value( var: Union[bool, int, float, str, google.protobuf.message.Message]) -> util_pb2.Value: """Returns a Value message containing a ConstantValue with the appropriate oneof set. """ return util_pb2.Value(constant=python_var_to_value(var))
[docs]def define_blackboard(dict_values: Dict[str, util_pb2.Value]) -> nodes_pb2.DefineBlackboard: """Returns a DefineBlackboard protobuf message for the key-value pairs in `dict_values`. """ node_to_return = nodes_pb2.DefineBlackboard() for (key, value) in dict_values.items(): node_to_return.blackboard_variables.add().CopyFrom(util_pb2.KeyValue(key=key, value=value)) return node_to_return
[docs]def set_blackboard(dict_values: Dict[str, util_pb2.Value], subfield_values: Dict[str, util_pb2.Value] = {}) -> nodes_pb2.SetBlackboard: """Returns a SetBlackboard protobuf message for the key-value pairs in `dict_values`. """ node_to_return = nodes_pb2.SetBlackboard() for (key, value) in dict_values.items(): node_to_return.blackboard_variables.add().CopyFrom(util_pb2.KeyValue(key=key, value=value)) for (key, value) in subfield_values.items(): node_to_return.blackboard_variable_subfields.add().CopyFrom( util_pb2.KeyValue(key=key, value=value)) return node_to_return
[docs]def severity_to_log_level(text_level): """Converts alert data severity enum to a logger level for printing purposes.""" match text_level: case alerts_pb2.AlertData.SeverityLevel.SEVERITY_LEVEL_INFO: return logging.INFO case alerts_pb2.AlertData.SeverityLevel.SEVERITY_LEVEL_WARN: return logging.WARN case alerts_pb2.AlertData.SeverityLevel.SEVERITY_LEVEL_ERROR: return logging.ERROR case alerts_pb2.AlertData.SeverityLevel.SEVERITY_LEVEL_CRITICAL: # A critical mission prompt or text message does not indicate a critical robot failure, # and is usually expected depending on how a mission plays out. For this reason, we # reduce the servity from CRITICAL to ERROR for logs. # See Prompt.severity in nodes.proto for more info. return logging.ERROR case _: return logging.INFO