在 ROS 开发中,某个话题可能会高频发布消息,而业务逻辑希望“消息持续到来时不触发某个操作,消息停止一段时间后再触发”。这类需求本质上是 debounce(防抖)逻辑。

本文以电笛控制系统为例:话题 /electric_horn 发布 std_msgs::Bool,当电笛消息连续到来时只执行电笛控制;当最后一条消息超过 1 秒没有更新时,再触发 HeartBeat(true)

不建议每条消息重建线程

std::thread 在每次回调里启动一个延迟线程,看起来可以实现延迟触发,但容易引入几个问题:

  • 回调里 join() 旧线程可能阻塞 ROS callback,影响消息处理。
  • lastMessageTime_ 如果由回调线程和延迟线程同时读写,需要互斥保护,否则存在数据竞争。
  • 一个共享的 stopDelayThread_ 标志很容易影响新旧线程的生命周期判断。

ROS 自身已经提供 Timer,更适合这种“延迟一段时间后执行一次”的场景。每次收到新消息时重启一个 one-shot timer;如果 1 秒内又收到消息,timer 会被重新计时;如果 1 秒内没有新消息,timer 回调触发。

实现思路

  1. 创建一个 ros::Timer,周期为 1 秒,设置为 one-shot,默认不自动启动。
  2. 每次收到 /electric_horntrue 消息时,执行电笛逻辑,并重启 timer。
  3. 如果后续 1 秒内没有新消息,timer 回调执行 HeartBeat(true)

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <ros/ros.h>
#include <std_msgs/Bool.h>

class TowerController {
public:
explicit TowerController(ros::NodeHandle& nh)
: nh_(nh) {
horn_sub_ = nh_.subscribe(
"/electric_horn",
10,
&TowerController::PressElectricHornCallback,
this
);

idle_timer_ = nh_.createTimer(
ros::Duration(1.0),
&TowerController::OnHornIdle,
this,
true, // oneshot:触发一次后停止
false // autostart:不在构造时自动启动
);
}

private:
ros::NodeHandle nh_;
ros::Subscriber horn_sub_;
ros::Timer idle_timer_;

void PressElectricHornCallback(const std_msgs::Bool::ConstPtr& press) {
if (!press->data) {
return;
}

ROS_INFO("收到电笛按钮按下信号");
ElectricHorn();

// 重启 one-shot timer,相当于把 HeartBeat 延后 1 秒。
idle_timer_.stop();
idle_timer_.start();
}

void OnHornIdle(const ros::TimerEvent&) {
ROS_INFO("消息间隔超过 1 秒,触发心跳信号");
HeartBeat(true);
}

int ElectricHorn() {
ROS_INFO("电笛按钮被触发");
ElectricHornControl();
return 0;
}

void ElectricHornControl() {
// 执行电笛硬件控制。
}

void HeartBeat(bool active) {
// 执行心跳逻辑。
}
};

如果 ElectricHornControl() 可能阻塞,例如访问串口、网络或慢速硬件,建议把硬件操作投递到业务线程池,不要长时间占用 ROS 回调线程。即便如此,延迟触发本身仍建议由 ros::Timer 负责。

注意事项

  • 如果使用 ros::AsyncSpinnerros::MultiThreadedSpinner,共享的硬件状态仍然要用互斥锁或串行任务队列保护。
  • ros::Timer 的回调默认也在 ROS callback 队列中执行,不适合放长时间阻塞任务。
  • 如果只是做“最后一次消息后延迟触发”,one-shot timer 比手动线程、轮询和 sleep 更直接。

总结

这类 ROS 消息延迟处理更适合用 Timer 实现。它避免了手动管理线程生命周期,也减少了数据竞争和回调阻塞风险。只有当业务本身确实需要后台长任务时,再额外引入线程池或任务队列。