第一次接触BehaviorTree.CPP时,最让我惊艳的就是它的异步节点设计。传统行为树在处理长时间任务时经常遇到阻塞问题,比如机器人导航到目标点这个动作,如果使用同步节点,整个行为树会被卡住直到导航完成。而BehaviorTree.CPP的AsyncActionNode和CoroActionNode彻底改变了这个局面。
在实际机器人项目中,我经常遇到这样的场景:机器人正在执行清洁任务时,突然需要响应紧急停止指令。如果使用同步节点,必须等待当前清洁动作完成才能处理停止命令,这显然不符合实际需求。通过异步节点,我们可以让清洁任务在后台执行的同时,行为树继续响应其他事件。
BehaviorTree.CPP的异步机制本质上是通过多线程实现的。当AsyncActionNode被触发时,它会创建一个独立的工作线程执行任务,主线程则可以继续处理其他节点。这里有个关键细节需要注意:异步节点的halt()方法必须正确实现,用于在任务被中断时清理资源。我曾经因为忘记释放ROS action client导致内存泄漏,调试了整整一天。
AsyncActionNode是最基础的异步节点类型,适合处理那些不需要频繁与主线程通信的任务。比如下面这个机器人充电的示例:
cpp复制class ChargeAction : public BT::AsyncActionNode {
public:
ChargeAction(const std::string& name, const BT::NodeConfig& config)
: AsyncActionNode(name, config) {}
static BT::PortsList providedPorts() {
return {BT::InputPort<float>("battery_level")};
}
BT::NodeStatus tick() override {
float battery_level;
if (!getInput("battery_level", battery_level)) {
throw BT::RuntimeError("missing battery_level");
}
// 在后台线程执行充电逻辑
charging_thread_ = std::thread([this, battery_level](){
ros::Rate rate(1);
while(current_battery_ < 100.0) {
current_battery_ += 10.0;
rate.sleep();
}
});
return BT::NodeStatus::RUNNING;
}
void halt() override {
charging_thread_.join(); // 重要:确保线程安全退出
}
private:
std::thread charging_thread_;
float current_battery_ = 0.0;
};
这个例子展示了几个关键点:
CoroActionNode采用了协程模型,特别适合需要与ROS服务交互的场景。比如调用move_base的导航任务:
cpp复制class NavigateToPose : public BT::CoroActionNode {
public:
NavigateToPose(const std::string& name, const BT::NodeConfig& config)
: CoroActionNode(name, config) {}
static BT::PortsList providedPorts() {
return {BT::InputPort<geometry_msgs::PoseStamped>("target_pose")};
}
BT::NodeStatus tick() override {
geometry_msgs::PoseStamped target;
if (!getInput("target_pose", target)) {
throw BT::RuntimeError("missing target_pose");
}
actionlib::SimpleActionClient<move_base_msgs::MoveBaseAction> client("move_base");
client.waitForServer();
move_base_msgs::MoveBaseGoal goal;
goal.target_pose = target;
client.sendGoal(goal);
while (ros::ok()) {
if (isHalted()) {
client.cancelAllGoals();
return BT::NodeStatus::FAILURE;
}
auto state = client.getState();
if (state == actionlib::SimpleClientGoalState::SUCCEEDED) {
return BT::NodeStatus::SUCCESS;
}
if (state == actionlib::SimpleClientGoalState::ABORTED) {
return BT::NodeStatus::FAILURE;
}
setStatusRunningAndYield(); // 关键:让出执行权
}
return BT::NodeStatus::FAILURE;
}
};
CoroActionNode的最大特点是可以在不阻塞的情况下等待ROS action完成,通过setStatusRunningAndYield()实现协程式的挂起和恢复。这种方式比纯线程方案更节省资源。
BehaviorTree.CPP的黑板(Blackboard)系统是节点间通信的核心。在实际项目中,我总结出三种典型使用模式:
xml复制<Sequence>
<GetCurrentPose output_key="current_pose"/>
<CalculateTargetPose input_pose="{current_pose}"
output_key="target_pose"/>
<NavigateToPose target_pose="{target_pose}"/>
</Sequence>
cpp复制static BT::PortsList providedPorts() {
return {
BT::InputPort<std::string>("object_id"),
BT::OutputPort<ObjectInfo>("object_info")
};
}
BT::NodeStatus tick() override {
std::string object_id;
if (!getInput("object_id", object_id)) {
return BT::NodeStatus::FAILURE;
}
// 检查黑板上是否已有缓存
if (auto res = getLockedPort<ObjectInfo>("object_info")) {
if (res->info.object_id == object_id) {
return BT::NodeStatus::SUCCESS;
}
}
// 无缓存则查询数据库
ObjectInfo info = queryDatabase(object_id);
setOutput("object_info", info);
return BT::NodeStatus::SUCCESS;
}
xml复制<ReactiveSequence>
<Condition if="emergency_stop" value="true"/>
<EmergencyStopProcedure/>
</ReactiveSequence>
处理复杂数据类型时,需要实现BT::convertFromString特化版本。比如处理RGB颜色值:
cpp复制struct RGBColor {
uint8_t r, g, b;
};
namespace BT {
template <>
inline RGBColor convertFromString(StringView str) {
auto parts = splitString(str, ',');
if (parts.size() != 3) {
throw RuntimeError("invalid RGB format");
}
RGBColor color;
color.r = convertFromString<uint8_t>(parts[0]);
color.g = convertFromString<uint8_t>(parts[1]);
color.b = convertFromString<uint8_t>(parts[2]);
return color;
}
} // namespace BT
使用时可以直接在XML中配置颜色值:
xml复制<SetLedColor color="255,0,0"/>
结合异步节点和黑板数据流,我们可以构建一个强大的任务中断系统。典型架构包括:
cpp复制class CheckEmergencyStop : public BT::ConditionNode {
public:
CheckEmergencyStop(const std::string& name, const BT::NodeConfig& config)
: ConditionNode(name, config) {}
BT::NodeStatus tick() override {
bool stop_signal;
if (!getInput("stop_signal", stop_signal)) {
return BT::NodeStatus::FAILURE;
}
return stop_signal ? BT::NodeStatus::SUCCESS
: BT::NodeStatus::FAILURE;
}
};
xml复制<Fallback>
<CheckEmergencyStop/>
<RecoverySequence>
<StopAllMotors/>
<PlayWarningSound/>
<SendNotification/>
</RecoverySequence>
<MainTaskSequence>
<!-- 正常任务逻辑 -->
</MainTaskSequence>
</Fallback>
在多线程环境下,资源竞争是需要特别注意的问题。我推荐使用以下几种模式:
cpp复制std::shared_mutex config_mutex_;
void updateConfig(const Config& new_config) {
std::unique_lock lock(config_mutex_);
config_ = new_config;
}
Config getConfig() const {
std::shared_lock lock(config_mutex_);
return config_;
}
cpp复制std::atomic<bool> emergency_stop_{false};
void setEmergencyStop(bool stop) {
emergency_stop_.store(stop);
}
bool isEmergencyStop() const {
return emergency_stop_.load();
}
cpp复制moodycamel::ConcurrentQueue<SensorData> data_queue_;
void processData() {
SensorData data;
while(data_queue_.try_dequeue(data)) {
// 处理数据
}
}
在实际部署中,我发现将行为树与ROS的实时系统结合使用时,合理设置节点执行频率非常重要。过高的频率会导致CPU占用率飙升,而过低的频率又会影响系统响应速度。通常我会将监控类节点设置为10Hz,控制类节点设置为20-30Hz。