BehaviorTree/BehaviorTree.CPP

Using a ReactiveSequence with asynch condition

Closed this issue · 7 comments

TLDR: don't use an asynch action for the condition in a ReactiveSequence.

I have a simple ReactiveSequence like this. It's very similar to the example here:

    <!-- Keep running DummyStatefulAction as long as the monitors are good -->
    <ReactiveSequence>
        <CheckJointVelocityMonitor1/>  <!-- Inherits from BT::RosServiceNode -->
        <DummyStatefulAction/>            <!-- A long-running asynch action -->
    </ReactiveSequence>

The prints when this BT is executing are:

CheckJointVelocityMonitor1 returning SUCCESS  // This BT node always returns success
Starting DummyStatefulAction                                // This is expected, it comes from .onStart(), which returns BT::NodeStatus::RUNNING
DummyStatefulAction halted!                                  // I don't understand where the halt comes from
CheckJointVelocityMonitor1 returning SUCCESS
Starting DummyStatefulAction
DummyStatefulAction halted!
CheckJointVelocityMonitor1 returning SUCCESS
Starting DummyStatefulAction
DummyStatefulAction halted!
CheckJointVelocityMonitor1 returning SUCCESS
Starting DummyStatefulAction
DummyStatefulAction halted!
  ... loop continues

CheckJointVelocityMonitor1 always returns SUCCESS. I've verified with print statements. DummyStatefulAction appears to be ticked once and returns RUNNING, then it gets halted. I don't understand why it gets halted. (Edit: it gets halted because it returns RUNNING.)

Here's the declaration for DummyStatefulAction, which inherits from BT::StatefulActionNode.

class DummyStatefulAction : public BT::StatefulActionNode
{
public:
  DummyStatefulAction(const std::string& name, const BT::NodeConfiguration& config) : StatefulActionNode(name, config)
  {
  }

  static BT::PortsList providedPorts();

  BT::NodeStatus onStart() override;

  BT::NodeStatus onRunning() override;

  void onHalted() override;
};

Here's the definition for DummyStatefulAction. It only returns RUNNING, never SUCCESS or FAILURE.

BT::NodeStatus DummyStatefulAction::onStart()
{
  std::cerr << "Starting DummyStatefulAction" << std::endl;
  return BT::NodeStatus::RUNNING;
}

BT::NodeStatus DummyStatefulAction::onRunning()
{
  std::cerr << "Running DummyStatefulAction" << std::endl;
  return BT::NodeStatus::RUNNING;
}

void DummyStatefulAction::onHalted()
{
  std::cerr << "DummyStatefulAction halted!" << std::endl;
}

I think it must be something related to <CheckJointVelocityMonitor1/> since, if I repalce it with <AlwaysSuccess/>, everything is fine. Like this:

    <ReactiveSequence>
        <!-- CheckJointVelocityMonitor1/ -->
        <AlwaysSuccess/>
        <DummyStatefulAction/>
    </ReactiveSequence>

CheckJointVelocityMonitor1 inherits from BT::RosServiceNode:

class TriggerService : public BT::RosServiceNode<std_srvs::srv::Trigger>
{
public:
  TriggerService() = delete;

  explicit TriggerService(const std::string& name, const BT::NodeConfig& conf, const BT::RosNodeParams& params)
    : RosServiceNode<Trigger>(name, conf, params)
  {
  }

  static BT::PortsList providedPorts()
  {
    return providedBasicPorts({});
  }

  bool setRequest(Trigger::Request::SharedPtr& request) override;

  BT::NodeStatus onResponseReceived(const Trigger::Response::SharedPtr& response) override;

  BT::NodeStatus onFailure(BT::ServiceNodeErrorCode error) override;
};

I'll work on getting a minimal example.

With some more print statement debugging, I see the trouble is that the RosServiceNode is getting halted with every other call to tick()?!

response received: 1
Starting DummyStatefulAction
Ticking RosServiceNode
DummyStatefulAction halted!
Ticking RosServiceNode
response received: 1
Starting DummyStatefulAction
Ticking RosServiceNode
DummyStatefulAction halted!
Ticking RosServiceNode
response received: 1
Starting DummyStatefulAction
Ticking RosServiceNode
DummyStatefulAction halted!
Ticking RosServiceNode
response received: 1
Starting DummyStatefulAction

Well, I can achieve the result I want by hacking the source code of bt_service_node.hpp to block until the service returns, within the tick() function.

It would be nice if the BT could block while the service node is running but I don't see a way to do that.

I can make a PR to check the node is synchronous during parsing, if you want. Or maybe just add a parameter to make BT::RosServiceNode block. Or is there an easier way?

You know what, it doesn't matter for my immediate application. I'll use a topic listener instead and check the timestamp of the latest message. Still curious if there was a better way to do this, though.

With some more print statement debugging, I see the trouble is that the RosServiceNode is getting halted with every other call to tick()?!通过进一步调试打印语句,我发现问题出在每调用一次tick() 时,RosServiceNode 就会停止一次!

response received: 1
Starting DummyStatefulAction
Ticking RosServiceNode
DummyStatefulAction halted!
Ticking RosServiceNode
response received: 1
Starting DummyStatefulAction
Ticking RosServiceNode
DummyStatefulAction halted!
Ticking RosServiceNode
response received: 1
Starting DummyStatefulAction
Ticking RosServiceNode
DummyStatefulAction halted!
Ticking RosServiceNode
response received: 1
Starting DummyStatefulAction

Yes, in ReactiveSequence, when a child node of the current tick returns RUNNING, it halt the other child nodes

// reset the previous children, to make sure that they are
// in IDLE state the next time we tick them
for(size_t i = 0; i < childrenCount(); i++)
{
if(i != index)
{
haltChild(i);
}
}

Yeah, the root cause was using an asynch node for the condition. It was dumb now that I think about it, but also not really clear in the docs.