行为树BehaviorTree学习记录3_黑板和端口

(为免误导,特免责声明如下:本文所有内容,只是基于个人当前理解和实际做法,后面亦会有更正和修订,但任何版本都不免有个人能力有限、理解有误或者工作环境不同的状况,故文中内容仅供参考。任何人都可以借鉴或者直接使用代码片段,但对任何直接引用或者借鉴产生的技术问题等后果,作者不承担任何责任。)

  • 简介
    这部分内容是讲述:行为树节点之间如何传递参数(或者从xml传入)和交换数据的。
    在这里插入图片描述
  • 黑板
    黑板是所有节点都可以访问的键/值存储的共享信息。
    与黑板的接入形式就是键值对。
    节点的输入端口可以从黑板读取数据,节点的输出端口可以写入黑板。

如上图中,行为节点ThinkWhatToSay有个输出端口,会写入the_answer:“xxxx”, 然后另外一个行为节点SaySomething有个输入端口,会从黑板中读取这个键值对的数据。

  • 输入端口 inputs ports
    输入端口可以是:

    • 静态的字符串,节点可以读取和解析
    • 一个指向黑板的指针,被当做键值对的键(key)
      比如行为节点SaySomething需要打印出一个字符串,为了传递这个字符串,我们用了一个叫做message的端口。
      xml文件里可以写成下面的任何一种:
<SaySomething name="fist"   message="hello world" />
<SaySomething name="second" message="{greetings}" />

第一种就是直接读取字符串,本例为“hello world”
第二种是指向了黑板的,greetings这个键,需要再运行时候读取黑板内容。
SaySomething的代码实现如下:

// SyncActionNode (synchronous action) with an input port.
class SaySomething : public SyncActionNode
{
public:
  // If your Node has ports, you must use this constructor signature 
  SaySomething(const std::string& name, const NodeConfig& config)
    : SyncActionNode(name, config)
  { }

  // It is mandatory to define this STATIC method.
  static PortsList providedPorts()
  {
    // This action has a single input port called "message"
    return { InputPort<std::string>("message") };
  }

  // Override the virtual function tick()
  NodeStatus tick() override
  {
    Expected<std::string> msg = getInput<std::string>("message");
    // Check if expected is valid. If not, throw its error
    if (!msg)
    {
      throw BT::RuntimeError("missing required input [message]: ", 
                              msg.error() );
    }
    // use the method value() to extract the valid message.
    std::cout << "Robot says: " << msg.value() << std::endl;
    return NodeStatus::SUCCESS;
  }
};

When a custom TreeNode has input and/or output ports, these ports must be declared in the static method:
当自定义的树节点需要用到输入输出端口的时候,这些端口必须声明在下面这个静态函数里:
static MyCustomNode::PortsList providedPorts();
使用输入端口(这里是message)进行读取的时候,可以用模板方法:
TreeNode::getInput(key).
这个方法有可能失败,用户可以根据需要进行处理,比如:返回错误、抛出异常、或者使用缺省值等。
建议在tick里面调用getInput(),而不是在类的构造函数中,因为真实值可能在运行中被修改,所以要定期去更新。

  • 输出端口Output ports
    当输入端口指向黑板入口时,必须先有节点通过输出端口已经在该入口写入了一些数据,才会有效。
    下例中,ThinkWhatToSay这个节点就是通过输出端口往text这个入口写字符串的。
class ThinkWhatToSay : public SyncActionNode
{
public:
  ThinkWhatToSay(const std::string& name, const NodeConfig& config)
    : SyncActionNode(name, config)
  { }

  static PortsList providedPorts()
  {
    return { OutputPort<std::string>("text") };
  }

  // This Action writes a value into the port "text"
  NodeStatus tick() override
  {
    // the output may change at each tick(). Here we keep it simple.
    setOutput("text", "The answer is 42" );
    return NodeStatus::SUCCESS;
  }
};

Alternatively, most of the time for debugging purposes, it is possible to write a static value into an entry using the built-in Actions called Script.
另外,很多时候为了调试,可以在xml文件里用脚本(Script)往黑板写一个静态的数值。脚本相当于执行了一个内建的动作。

<Script code=" the_answer:='The answer is 42' " />
  • 完整的例子
<root BTCPP_format="4" >
    <BehaviorTree ID="MainTree">
       <Sequence name="root_sequence">
           <SaySomething     message="hello" />
           <ThinkWhatToSay   text="{the_answer}"/>
           <SaySomething     message="{the_answer}" />
       </Sequence>
    </BehaviorTree>
</root>

In this example, a Sequence of 3 Actions is executed:
本例中一个顺序控制节点有3个行动子节点会被执行。

- Action 1 从静态字符串中读取message.
- Action 2  通过输出端口text,写入数据到黑板入口:the_answer.
- Action 3 从黑板入口:the_answer,读取到输入端口:message .
#include "behaviortree_cpp/bt_factory.h"

// file that contains the custom nodes definitions
#include "dummy_nodes.h"
using namespace DummyNodes;

int main()
{  
  BehaviorTreeFactory factory;
  factory.registerNodeType<SaySomething>("SaySomething");
  factory.registerNodeType<ThinkWhatToSay>("ThinkWhatToSay");

  auto tree = factory.createTreeFromFile("./my_tree.xml");
  tree.tickWhileRunning();
  return 0;
}

/*  Expected output:
  Robot says: hello
  Robot says: The answer is 42
*/

连接输入与输出端口的是相同的键(Key):the_answer, 就是说指向黑板的相同入口。
这些端口可以相连是因为他们的类型都是相同的,这里都是字符串,如果类型不通在factory.createTreeFromFile 的时候会抛出异常。

  • 通用类型端口

解析字符串: 除了前面例子里的string类型,BehaviorTree.CPP 支持自动把字符串转换为通用的类型,比如int,long,double,bool,NodeStatus等等。
而用户自定义类型也可以简单的获得支持。
比如有个自定义的类型:

// We want to use this custom type
struct Position2D 
{ 
  double x;
  double y; 
};

To allow the XML loader to instantiate a Position2D from a string, we need to provide a template specialization of BT::convertFromString(StringView).
为了xml读取器能从字符串中实例化Position2D,我们需要提供一个模板特例化:
BT::convertFromString(StringView)
这个取决于你是如何把Position2D序列化(serialized )成字符串的,本例中,我们简单的把他用分号分成2个数字。

// Template specialization to converts a string to Position2D.
namespace BT
{
    template <> inline Position2D convertFromString(StringView str)
    {
        // We expect real numbers separated by semicolons
        auto parts = splitString(str, ';');
        if (parts.size() != 2)
        {
            throw RuntimeError("invalid input)");
        }
        else
        {
            Position2D output;
            output.x     = convertFromString<double>(parts[0]);
            output.y     = convertFromString<double>(parts[1]);
            return output;
        }
    }
} // end namespace BT

StringView是一个 C++11版的std::string_view. 你也可以传递std::string 或者 const char*. 库中提供了简单的字符串分割函数splitString.也可以随便用其他,比如: boost::algorithm::split.
通用类型的话会直接调用特例化,比如转换成double,convertFromString().

  • 实例
class CalculateGoal: public SyncActionNode
{
  public:
    CalculateGoal(const std::string& name, const NodeConfig& config):
      SyncActionNode(name,config)
    {}

    static PortsList providedPorts()
    {
      return { OutputPort<Position2D>("goal") };
    }

    NodeStatus tick() override
    {
      Position2D mygoal = {1.1, 2.3};
      setOutput<Position2D>("goal", mygoal);
      return NodeStatus::SUCCESS;
    }
};

class PrintTarget: public SyncActionNode
{
  public:
    PrintTarget(const std::string& name, const NodeConfig& config):
        SyncActionNode(name,config)
    {}

    static PortsList providedPorts()
    {
      // Optionally, a port can have a human readable description
      const char*  description = "Simply print the goal on console...";
      return { InputPort<Position2D>("target", description) };
    }
      
    NodeStatus tick() override
    {
      auto res = getInput<Position2D>("target");
      if( !res )
      {
        throw RuntimeError("error reading port [target]:", res.error());
      }
      Position2D target = res.value();
      printf("Target positions: [ %.1f, %.1f ]\n", target.x, target.y );
      return NodeStatus::SUCCESS;
    }
};

现在可以正常连接输入输出端口,都指向黑板相同的入口。
下例中树是一个顺序控制,有4个行为子节点:

  • 用行为CalculateGoal存储一个Position2D 到黑板入口:GoalPosition
    Call PrintTarget. The input “target” will be read from the Blackboard entry GoalPosition.
  • 调用PrintTarget,输入端口target会读取黑板入口:GoalPosition
  • 采用内建行为脚本,指定字符串"-1;3" 到黑板的键:OtherGoal,字符串到Position2D 的转换会自动进行。
  • 再次调用PrintTarget,输入端口读取的黑板入口是:OtherGoal
static const char* xml_text = R"(
 <root BTCPP_format="4" >
     <BehaviorTree ID="MainTree">
        <Sequence name="root">
            <CalculateGoal goal="{GoalPosition}" />
            <PrintTarget   target="{GoalPosition}" />
            <Script        code=" OtherGoal:='-1;3' " />
            <PrintTarget   target="{OtherGoal}" />
        </Sequence>
     </BehaviorTree>
 </root>
 )";

int main()
{
  BT::BehaviorTreeFactory factory;
  factory.registerNodeType<CalculateGoal>("CalculateGoal");
  factory.registerNodeType<PrintTarget>("PrintTarget");

  auto tree = factory.createTreeFromText(xml_text);
  tree.tickWhileRunning();

  return 0;
}
/* Expected output:

    Target positions: [ 1.1, 2.3 ]
    Converting string: "-1;3"
    Target positions: [ -1.0, 3.0 ]
*/

这次树结构式定义在字符串(语法和xml一样)中的,用createTreeFromText读取。
★ 在运行时的共享是在内存中的,类型转换可以自动完成,需要指定特例化函数是因为需要从xml字符串中读取自定义类型的对象数据。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在Navigation2中,行为树黑板传递数据的方式是通过在行为树黑板文件中定义变量,并在行为树的各个节点中访问这些变量来实现的。具体步骤如下: 1. 在行为树黑板文件中定义变量:在行为树黑板文件(如`nav2_behavior_tree.xml`)中使用`<param>`标签定义需要传递的参数,例如: ``` <param name="distance_to_goal" value="0.3" type="double"/> ``` 这个例子中定义了一个名为`distance_to_goal`的参数,类型为`double`,初始值为`0.3`。 2. 在行为树节点中读取或写入变量:在行为树的各个节点中,可以使用`<param>`或`<param_ref>`标签来访问行为树黑板中的参数,例如: ``` <param name="distance_to_goal" value="0.3" type="double"/> <condition class="DistanceToGoal" distance="{distance_to_goal}"> ``` 这个例子中,`DistanceToGoal`节点的`distance`参数使用了行为树黑板中的`distance_to_goal`参数,节点会读取该参数的值并进行判断。 3. 在行为树启动时加载行为树黑板:在Navigation2中,行为树黑板是在行为树启动时加载的。加载行为树黑板的代码位于`nav2_bt_navigator`模块的`bt_navigator.cpp`文件中。加载黑板的代码如下: ``` std::string bt_xml_file; node->declare_parameter("bt_xml_filename", bt_xml_file, rclcpp::ParameterValue(std::string("nav2_behavior_tree.xml"))); auto bt_xml = file_utils::read_file(bt_xml_file); blackboard->setUserData("behavior_tree_xml", std::string(bt_xml.data(), bt_xml.size())); ``` 这段代码从ROS参数服务器中读取`bt_xml_filename`参数,加载行为树黑板文件并将其写入行为树黑板中,节点在运行时可以访问该黑板中的参数。 综上所述,Navigation2中行为树黑板传递数据的方式是通过在行为树黑板文件中定义变量,并在行为树的各个节点中访问这些变量来实现的。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值