核心课程 / ROS2 应用与开发 / EXP-11
EXP-11 · 第 3 章 · 验证性 · 2 学时

时间 API 与双时钟体系

IMU 200Hz、相机 30Hz,时间戳差一点数据就对不上。本课讲清 ROS2 为什么自建时间抽象层、系统时钟与 /clock 仿真时钟如何透明切换,以及 create_timer 与 Header.stamp 的正确姿势。

难度 ⭐⭐2 学时验证性关键词 Clock · Timer · stamp

目标本课学完,你应该能做到

① 说清架构
一个开关、一个出口

能阐述 Clock 的双时间源机制:use_sim_time 是唯一开关,get_clock().now() 是唯一出口;系统时钟与 /clock 仿真时钟如何透明切换。

② 掌握 API
Time / Duration / Timer

能正确使用 get_clock().now()、create_timer(period, callback),以及 Time 的 nanoseconds / seconds_nanoseconds 转换。

③ 独立完成
给数据盖上时刻

独立编写一个用定时器周期采样 MQ5、并为每条数据的 Header.stamp 打上采集时刻时间戳的发布节点。

④ 迁移应用
诊断时间问题

能分析"消息时间戳不一致"的成因(系统时钟 vs 传感器硬件时钟),并用 ros2 topic delay 测量端到端延迟、识别负延迟的含义。

本课的重点与难点 重点:stamp 应该盖在"采集那一刻",而不是"发布那一刻";以及双时钟只用一个出口的意义。
难点:开着 use_sim_time=true 却没有 /clock 发布者时,节点会永久阻塞——这个现象很难自己猜出来。
和前后的关系 本课的时间模型是 EXP-16 rosbag 回放与 EXP-17 launch 传 use_sim_time 的前提,务必学扎实。

01问题引入:时间不对齐,数据全白采

1
IMU 每秒 50 条、相机每秒 30 帧——融合前必须按时间戳配对,配错了姿态就算飞。
2
直接用 Python 的 time.time()?仿真里"1 秒"可能只要真实 0.1 秒,录包回放时两条消息的时间戳也根本对不上。
3
多机部署时,两台电脑系统时钟不同步,延迟甚至算出负值。
4
ROS2 的答案:自建时间抽象层——Clock 类统一出口,底层自动在系统时钟与 /clock 仿真时钟间切换,上层代码一行不改。

生活化地说:多传感器融合就像一场合唱——IMU 是快声部(200 拍/秒),相机是慢声部(30 拍/秒),必须都看着同一个指挥(统一时钟)才能合上拍。谁自己偷偷看手机计时(time.time()),整团就乱。

😤 各看各的表:time.time() 混用四宗罪
  • 仿真失真:Gazebo 0.5 倍速时,仿真"1 秒"= 真实 2 秒,时间戳全错
  • 回放失效:录包回放时 stamp 是"当时录制时刻",和新数据永远对不上
  • 跨机负延迟:两台电脑钟差 50ms,算出 -50ms 的"延迟"
  • 配对失败:温度 1.002s、湿度 1.008s 到达,谁配谁?
😎 全团看指挥:get_clock().now()
  • 所有节点问同一个 Clock,答案永远一致
  • 仿真模式自动换"指挥"为 /clock 话题,代码零改动
  • rosbag 回放发 /clock,历史时间轴整体复现
  • stamp 盖在采集时刻,延迟测量才有物理意义
时间 = 一等公民机器人系统里时间有两类来源(真实墙上时 / 仿真逻辑时),Clock 把差异吸收在底层:get_clock().now() 永远返回"当前正确的时间"。

02先看现象:让时间看得见

跑起 time_demo_node,用三条命令把"时间"变成看得见的数据。命令背后的代码在 05 节逐行拆。

终端 A启动双定时器演示节点
1启动节点终端 A

先进入源码目录(与实验 6 的 common.py 同目录,才能直接 import 它读 MQ5),再启动节点——1Hz 采样定时器 + 5s 摘要定时器同时开跑,日志里直接打印 stamp:

cd ~/ros2_ws/src/xflz_eai_car1/driver/ros_sensor/ros_sensor/ros_sensor
python3 time_demo_node.py
$ python3 time_demo_node.py [INFO] [time_demo_node]: 时间演示节点已启动 [INFO] [time_demo_node]: 系统时钟模式: use_sim_time=False [INFO] 采样 #0001 | stamp: 1786615681.044442152 | frame_id: sensor_frame [INFO] 采样 #0002 | stamp: 1786615682.044580476 | frame_id: sensor_frame # 每秒一条;sec 是接近当前时刻的 Unix 时间
终端 B外部视角:读 stamp、测频率、测延迟新终端
2echo:看消息里的时间戳终端 B

stamp 的 sec 一格一格 +1——这就是"时间在数据里流动"的样子:

ros2 topic echo /sensor_data
$ ros2 topic echo /sensor_data stamp: sec: 1786615681 nanosec: 444421522 frame_id: sensor_frame --- stamp: sec: 1786615682 nanosec: 445804769 frame_id: sensor_frame # sec 逐条递增,nanosec 在 0~999999999 内补齐亚秒精度
ros2 topic echo 显示 stamp
终端 B:ros2 topic echo /sensor_data —— 每条消息的 stamp.sec 逐秒递增,frame_id 固定 sensor_frame(指导书实验 11 · 步骤 2)
3hz:验证定时器节拍终端 B

平均频率应贴近 1.000 —— 偏差大说明回调太重拖了后腿(03 节原理):

ros2 topic hz /sensor_data
$ ros2 topic hz /sensor_data average rate: 1.000 min: 0.997s max: 1.002s std dev: 0.00187s window: 3
ros2 topic hz 输出
终端 B:ros2 topic hz —— average rate 1.000,抖动 ±2ms(Linux 调度器精度),定时器"活着且很准"(指导书实验 11 · 步骤 3)
4delay:测端到端延迟终端 B

delay = 收到时刻 − stamp,反映通信栈实时性能;出现负值先怀疑时钟不同步:

ros2 topic delay /sensor_data
$ ros2 topic delay /sensor_data average delay: 0.002 min: 0.002s max: 0.002s std dev: 0.00000s window: 1 average delay: 0.002 min: 0.001s max: 0.002s std dev: 0.00054s window: 2 # 本机栈开销约 2ms,滚动窗口持续统计;负值 = 发收两端钟差(先 NTP 对时再查代码)

✔ 三条命令读完,你已经把"时间"从抽象概念变成了三个可测量的数:stamp、rate、delay。

03原理讲解

一行调用,两套时钟,一个 bool 开关

① 调用:想获取现在的时间戳,命令行里写 now = self.get_clock().now(),功能就一句话——问当前的钟"现在几点"。
② now 是什么:now 是一个 ROS 2 时间对象(rclpy 的 Time),里面装着 sec(秒)和 nanosec(纳秒)两段数值;它不是字符串,调用 now.to_msg() 就能变成 Header.stamp 那样的时间戳,直接盖到消息上。
③ 和 /clock 什么关系:这个 now 的时间从哪来,由钟自己决定——当 use_sim_time 开成 true 时,get_clock().now() 读的就是 /clock 话题馈进来的时间;普通模式下读的则是操作系统墙上钟。
④ 两套钟,一个开关:系统墙上钟、仿真钟两套并存,靠 use_sim_time 这个 bool 值切换(false 用墙钟、true 用 /clock)——你自己的代码一行都不用改,永远只有上面那一行调用。先看框图,再看详解:

极简框图 · 一个开关,二选一
node.get_clock().now()
      │
      ├── use_sim_time = false(默认)
      │     时间源:操作系统墙钟(现实时间)
      │     特征:和现实世界一秒一秒同步,不能快进/暂停
      │     适用:真机、真实传感器(USB 相机 / IMU / 雷达)
      │
      └── use_sim_time = true
            时间源:订阅 /clock 话题
            特征:Gazebo / rosbag play 说了算——可 0.5 倍速、可暂停、可回退
            适用:仿真、录包回放

⚠️ 坑:true 了却没人发 /clock → 节点拿不到时间,死等不走(04 节实操 A)
一句速记use_sim_time 就是时间源选择开关:关 = 看电脑自带的钟;开 = 听 /clock 话题的钟。整套系统开关必须统一。
两个时间到底差在哪|一张表看完
项目use_sim_time=false(墙钟)use_sim_time=true(仿真时间)
时间来源Linux/Windows 系统时钟话题 /clock(消息里的时间)
时间流速固定 = 现实时间任意:可快、可慢、暂停、回退
rosbag 回放回放时代码里的时间还在往前走,不跟着 bag 放慢回放时间跟着 bag 走,算法行为和录制时一模一样
适用场景真机,真实 IMU / 相机 / 雷达Gazebo 仿真、rosbag 离线回放、算法复现
两条铁律 ① 它只是开关,不是两套时间同时生效:get_clock().now() 按开关自动选数据源,业务代码一行不改,也不需要把两个时间对齐——同一节点同一时刻永远只用一套。
② 所有参与融合的节点,开关必须统一:激光 true + IMU 融合 false = 两个节点时间轴不一样,时间戳错乱,融合直接炸——真机全 false,仿真全 true。
随堂思考(先自己想,再展开) Q1 · Gazebo 仿真,你忘记给节点加 use_sim_time:=true,会怎样?
查看答案节点继续读系统墙钟;仿真加速跑时消息疯狂涌出、算法来不及处理,容易丢数据、融合发散。若别的节点开了 true,还会和它时间轴对不上——开关混用的后果见坑表「开关不统一」行。

Q2 · launch 里写了 use_sim_time:=true,但没有启动 Gazebo / rosbag play,节点会怎样?
查看答案没人发 /clock,get_clock().now() 拿不到时间——节点阻塞在那里不工作(04 节实操 A 复现的就是它)。
图 11-2 · 双时钟框架|本图回答:切换 use_sim_time 后,now() 的时间从哪里来?
✅ 结论:两源、一开关、一个出口⚠️ 风险:true 而无 /clock → 永久等待🔍 验证:04 节实操 A/B
怎么设置 use_sim_time(唯一主场 · 先设后用) ① 启动时设(正道,两条照抄):命令行 python3 time_demo_node.py --ros-args -p use_sim_time:=true;或 launch 字典里写 'use_sim_time': True(EXP-17 用这个口子)。
② 真机默认 false 不动;一旦开了 true,必须有节点往 /clock 发消息,否则本节点永久等待——04 节实操 A 就是在复现它。
③ 运行中也能改,改了立即生效:节点跑起来后执行 ros2 param set /节点名 use_sim_time true,TimeSource 的参数回调当场切换时间源(订阅/退订 /clock),不用重启。但切换瞬间 now() 会从墙上钟绝对秒(17 亿级)跳到仿真起点(或反跳),跨切换点的时间差、超时、TF 外推全会算错——所以稳妥做法仍是启动时定好(先设后用)。04 节实操 A 用的就是①里的命令。

下面的交互演示可以亲手切换时间源,建议把每个按钮都点一遍:

术语风险:没有 ROS_TIME / ROS_SYSTEM 两个常量 早期讲义把两套时钟写成 ROS_TIME / ROS_SYSTEM,容易误以为业务代码要自己判断时钟类型。实际只有一个开关 use_sim_time、一个出口 get_clock()——分辨时间源是 Clock 的内部实现,业务代码不许自己判断。
now = self.get_clock().now()← 你的代码从头到尾只有这一行,永远不改
真实世界秒表(墙上钟)
0.0 s
操作系统 · 永远匀速 · 谁都拦不住它
节点盖 stamp 用的钟(get_clock().now() 的输出)
0.0 s
时间源:① 操作系统 · 读墙上钟
/clock 话题 当前时间源①——用不到这个话题(真机的常态)
节点日志 · 每秒一条采样(盯住 stamp 的数值来自哪个时间源)
👉 先点「时间源②」,看时间轴怎么被仿真器接管

图 11-1 · 一行代码,两种世界 | 回答:切换时间源后节点的 stamp 跟着怎么变?

✅ 结论:出口始终唯一,切换时间源不改代码⚠️ 风险:切到②却等不到 /clock → stamp 冻结🔍 验证:04 节实操 A/B(stamp 从 100 递增)
时间源① · 操作系统
use_sim_time = false · 默认

Clock 读墙上钟,stamp 与真实世界同步——真机 B2 实验箱的唯一正确选择,也是你前十个实验一直在用的模式。

时间源② · /clock 话题
use_sim_time = true

Clock 改听 rosgraph_msgs/msg/Clock:仿真器推进一步发一次,stamp 跟着走一步——可快、可慢、可停、可回到录制时的历史。

最大的坑
true 却没人发 /clock

演示里「卡 4 秒」只是预演;真机上误开 true 且没有仿真器,Clock 会永久等待——节点启动后一片寂静(06 节实操 B 复现)。

妙处:代码零改动
切换时间源是 Clock 内部的事

整个演示里顶部那行代码从未变过——真机 ↔ 仿真 ↔ 回放切换,一行代码都不用改,这就是时间抽象层的全部意义。

时间的两副面孔:代码里一副,网络上一副

同一个"时刻"在两个场合长得不一样:写代码时要能加减比较(好算逻辑),发消息时要紧凑可序列化(好传输)。ROS2 干脆拆成两个类型,分工明确:

🧮 代码里 · rclpy.time.Time(API 类型)
now = clock.now()
later = now + Duration(seconds=5.0)  # Time ± Duration
dt = t2 - t1                          # → 得 Duration
now > past                            # 可直接比较

像个计算器:能加减、能比较。回调里判断"超时没?差多久?"全靠它——但没法直接塞进消息发出去。

to_msg() ↓ 📦 ↑ Time.from_msg() 过界必转
📦 消息里 · builtin_interfaces/Time(传输类型)
stamp:
  sec: 1786615681     # int32 · Unix 秒
  nanosec: 44442152  # uint32 · 纳秒

像个快递单:只有两个字段,紧凑、可序列化,嵌在 Header.stamp 里跨机传输——但自己不会算术。

过界必转两个类型不能互相直接赋值:API → 消息用 to_msg(),消息 → API 用 Time.from_msg()。05 节代码精讲里 now.to_msg() 那一行干的就是这件事。

create_timer 的真实行为与 stamp 规范

定时器原理
挂在 spin 事件循环上的周期源

create_timer(period, cb) 注册触发源;spin 每轮检查到点就把回调加入执行队列——没有 spin 就永远不触发,取代 ROS1 的 while+Rate.sleep()。

名义周期
回调耗时可以拖垮频率

周期只是名义下限:回调 C=0.3s > 周期 T=0.2s 时,上一回调结束下一触发点早已过期 → 实际间隔≈C,f ≈ 1/max(T,C) = 1/0.3 ≈ 3.3Hz,永远到不了 5Hz。I/O 密集任务要异步化。

多定时器
同节点内串行执行

demo 节点同时挂 1s 采样定时器 + 5s 摘要定时器,各自独立计时,但回调在同一事件循环里排队——一个堵全堵。

stamp 规范
盖"采集时刻"不是"发布时刻"

在读传感器的那行代码处取 now();等处理完再盖戳,等于把处理延迟算进了传输延迟——排查延迟问题时会误导自己。

✅ 结论:定时器靠 spin 驱动、同线程串行、f=1/max(T,C)⚠️ 风险:回调超时 → 频率被压低;stamp 盖晚 → 延迟失真🔍 验证:02 节实测 hz≈1.000、delay=0.3ms
端到端延迟公式:delay = 订阅端收到时刻 − stamp 采集时刻,减去 stamp 才是"通信+排队"的真延迟。局域网有线通常 < 1ms(02 节实测 0.3ms);多机出现负值说明两台电脑系统时钟没对齐——先 NTP 再查代码。

04实验步骤

工程化顺序:先看全景(图 11-3)→ 再看节点内流程(图 11-4)→ 最后逐行读代码。

图 11-3 · 工程全景|本图回答:本实验动了哪些文件和话题,数据从哪流到哪?
✅ 结论:一个节点、两定时器、时间只经 Clock 出口⚠️ 风险:依赖 common.py 与仿真器,缺则功能不可用🔍 验证:02 节 echo / hz / delay 实测

time_demo_node 逐行拆解

源码来自指导手册实验 11 的 time_demo_node.py:全文件只做三件事——两个定时器、盖时间戳、发摘要。

图 11-4 · 节点主流程|本图回答:一次采样从触发到发布经过哪几行,没有 spin 会怎样?
✅ 结论:①取时刻 → ②转类型 → ③盖戳发布⚠️ 风险:忘 spin 全不触发;回调串行一个堵全堵🔍 验证:02 节 hz≈1.000 + 下方逐行代码

第一站 · __init__:发布器 + 两个定时器

# 来源:time_demo_node.py __init__
self._publisher = self.create_publisher(Header, 'sensor_data', 10)
# 用 Header 消息演示 stamp 字段——它正是所有传感器消息"头部"的原型

self._timer1 = self.create_timer(1.0, self._timer1_callback)   # 1Hz:采样发布
self._sample_count = 0
self._timer2 = self.create_timer(5.0, self._timer2_callback)   # 0.2Hz:状态摘要

self.get_logger().info(f'系统时钟模式: use_sim_time={self.get_parameter("use_sim_time").value}')
# use_sim_time 是系统预声明参数——不用 declare 就能 get,它由 --ros-args -p 控制

第二站 · timer1_callback:盖时间戳的规范姿势

# 来源:time_demo_node.py _timer1_callback(每秒一次)
now = self.get_clock().now()      # ① 在"采集动作"这一行取时间——不是函数末尾
now_msg = now.to_msg()            # ② API 类型 → 消息类型(rclpy.time.Time → builtin_interfaces/Time)

msg = Header()
msg.stamp = now_msg               # ③ ⭐ 核心:把采集时刻盖进消息头
msg.frame_id = 'sensor_frame'    #     坐标系名,TF2 章节会再见到它
self._publisher.publish(msg)

self.get_logger().info(
    f'采样 #{self._sample_count:04d} | stamp: {now_msg.sec}.{now_msg.nanosec:09d} | ...')
# nanosec 用 :09d 补零到 9 位——否则 44442152 会被误读成更高精度

第三站 · timer2_callback 与时间运算

# 来源:time_demo_node.py _timer2_callback(每 5 秒摘要)
self.get_logger().info(
    f'[状态摘要] 累计采样次数: {self._sample_count}, '
    f'当前ROS2时间: {self.get_clock().now().nanoseconds} ns')
# nanoseconds:Unix 纪元起的总纳秒数(int,无溢出风险;C++ 要用 uint64_t)

# 时间运算三件套(可加进节点练习)
from rclpy.time import Duration
future = now + Duration(seconds=5.0)   # Time ± Duration = Time
diff   = now - past                    # Time − Time = Duration(.nanoseconds/1e9 得秒)
print(now > past)                      # Time 之间可直接比较
两个 Time 别搞混rclpy.time.Time(API 对象,可加减)与 builtin_interfaces/msg/Time(传输结构,字段 sec/nanosec)——过界必须 to_msg() / Time.from_msg() 转一道,直接赋值字段会类型报错。
没有 spin 就没有节拍两个定时器都挂在 rclpy.spin(node) 的事件循环上:spin 不跑,定时器永远不触发。同一节点多定时器在同一线程串行——一个回调堵 3 秒,另一个也跟着饿 3 秒。

亲手"接管"一个节点的时间轴

让节点不再看墙上钟,改听手动发布的 /clock——卡在哪、怎么复活,看图 11-5:

图 11-5 · 仿真接管流程|本图回答:节点卡在哪一步,/clock 一来为什么立刻复活?
✅ 结论:卡点是"等首条 /clock",复活零改码⚠️ 风险:真机误开 true = 假死🔍 验证:下方实操 A/B
终端 A以仿真模式启动(先看它卡住)
1true 开着、没人发 /clock终端 A

复现 02 节演示器预演过的场景——节点启动后一片寂静:

cd ~/ros2_ws/src/xflz_eai_car1/driver/ros_sensor/ros_sensor/ros_sensor
python3 time_demo_node.py --ros-args -p use_sim_time:=true
$ python3 time_demo_node.py --ros-args -p use_sim_time:=true [INFO] [time_demo_node]: 时间演示节点已启动 [INFO] [time_demo_node]: 系统时钟模式: use_sim_time=True (之后一片寂静——进程活着,但第一条采样永远不来)
终端 B手动扮演"仿真器"
2发布 /clock,节点瞬间复活终端 B

从仿真秒 100 开始,每秒推进 1 仿真秒——观察终端 A 的 stamp 被"接管":

ros2 topic pub /clock rosgraph_msgs/msg/Clock "{clock: {sec: 100, nanosec: 0}}" --rate 1
# 终端 A 立刻活跃起来: [INFO] 采样 #0001 | stamp: 100.000000000 | frame_id: sensor_frame [INFO] 采样 #0002 | stamp: 101.000000000 | frame_id: sensor_frame # stamp 从 100 开始递增 —— 时间轴已完全由 /clock 话题决定

✔ 再试 --rate 0.5(0.5 倍速):stamp 推进变慢,1Hz 定时器真实触发频率约 0.5Hz——仿真时间与真实时间彻底解耦。

#证据项记录方式合格标准
1stamp 可见topic echo 截图sec 逐条递增、nanosec 补齐 9 位
2节拍准确topic hz 截图average rate ≈ 1.000
3延迟量级topic delay 截图正值、毫秒级;能解释负值含义
4卡死复现true 无 /clock 时的终端截图节点启动后无任何采样输出
5时间轴接管pub /clock 后终端 A 截图stamp 从 sec=100 开始递增
6变速体验--rate 0.5 后对比截图说出真实频率与仿真频率的关系

05注意事项与验收清单

  • 默画双时钟架构:use_sim_time 是源开关,get_clock().now() 是统一出口
  • 说清 false=墙钟(真机)、true=/clock(仿真),并强调同一系统所有节点开关必须统一
  • 讲清 Time(API)与 builtin_interfaces/Time(消息)的互转关系
  • 实测 topic hz ≈ 1.000、topic delay 正常量级,理解负值的含义
  • 亲手体验一次:true 开着没 /clock 的阻塞,以及手动 pub 的"复活"
  • 说出 stamp 应该盖在采集时刻的哪一行代码
  • 解释回调超时为什么会拖低实际频率(名义周期 ≠ 实际频率)
坑症状解法
忘调 spin定时器永远不触发节点创建后必须 rclpy.spin(node) 启动事件循环
init 顺序错get_clock().now() 报 Clock not initialized先 rclpy.init() 再建节点,之后才能取钟
真机误开 use_sim_time节点卡死没有任何输出无 /clock 发布者即永久等待——真机保持 false
delay 出现负值跨机测量越测越离谱两机 NTP 对时;单机则查 stamp 是不是盖晚了
topic delay 报 no headermsg does not have headerdelay 要的是嵌套 .header 字段——String、裸 Header 都会这样报错(EXP-16 的 /chatter 同理),换带 header 的话题即可
运行中切换 use_sim_time时间大跳变(墙钟 17 亿秒 ↔ 仿真 0),超时/TF 一串算错ros2 param set 改了立即生效、无需重启;代价是切换跳变,稳妥仍是启动时定好
开关不统一融合节点时间戳错乱、数据对不上同一系统所有节点 use_sim_time 必须设成同一个值——真机全 false,仿真全 true
先给结论:use_sim_time 到底是什么 false → 系统墙钟(现实时间):读操作系统时间,跟着真实世界一秒一秒走——真机、真实传感器默认用这个。
true → 仿真时间(/clock 话题时间):不读系统钟,专门订阅 /clock 拿时间,由仿真器 / rosbag play 发布——可暂停、倍速、回退。
不需要"两个时间保持一致":同一节点同一时刻只选一套时间源,二选一,不会同时用。
对应关系:真机实车真实传感器 → false;Gazebo 仿真、rosbag 回放 → true——你理解的这套就是标准答案;唯一强制要求是全系统开关统一。
一句话记住本实验 时间只有一个出口(get_clock()),只有一个开关(use_sim_time)。凡是绕过出口直接读 time.time() 的代码,在仿真与回放里一定会出错;凡是开着 use_sim_time=true 却没人发 /clock 的机器,节点一定会永久卡住。这两条覆盖了本课 90% 的故障。

06本课总结与验证题

抽象
双时钟一个出口

Clock 吸收系统时钟与 /clock 的差异,上层代码仿真真机零改动。

类型
API Time ⇄ 消息 Time

to_msg()/from_msg() 互转;sec+nanosec 结构贯穿所有 Header。

节拍
create_timer 挂 spin

名义周期受回调耗时拖累;多定时器同循环串行。

规范
stamp 盖采集时刻

delay/hz 诊断通信质量;负延迟先怀疑时钟未对齐。

验证题(改动型 · 每题先自己作答,再展开核对) 1(目标①·改参数)节点带 -p use_sim_time:=true 运行,/clock 正以 1Hz 持续发布。把参数改成 false 重启后,stamp 的数值来源和步进会怎么变?
参考答案与得分点 得分点:①来源切回系统墙上钟(Unix 真实时间)②stamp 立即跳到当前真实时刻并按真实秒递增 ③不再受 /clock 影响(①②各 40%,③ 20%)
2(目标②③·改代码)把 timer1_callback 里的 now = self.get_clock().now() 挪到 publish() 之前,且取 now 前先 sleep(0.2)。ros2 topic delay(读数 = 收到时刻 − stamp,见 02 节)的读数会偏大还是偏小?还测得准"采集→送达"的真实延迟吗?
参考答案与得分点 得分点:①stamp 盖晚 0.2s → delay = 收到 − stamp 变偏小,极端时可为负 ②测不到"采集时刻",读数丢失处理段,不再是采集→送达延迟 ③印证 stamp 必须盖在采集行。(①②各 40%,③ 20%)
3(目标④·改数据解释)跨机测量得到 delay = -50ms。写出等式成因,并给出两种修复。
参考答案与得分点 得分点:①t_recv − t_stamp = −0.05s → 钟差/盖晚(本机钟慢 50ms 或 stamp 晚于真实采集)②修复 A:两机 chrony/NTP 对时 ③修复 B:核查 stamp 是否在采集那一刻取。(① 50%,②③各 25%)
4(目标①·改流程)实操 B 中把 --rate 1 改成 --rate 0.5。以真实秒计,1Hz 采样定时器的实际触发频率约多少?每次采样的 stamp 相比上一条增加多少?
参考答案与得分点 得分点:①仿真时间流速 = 0.5 仿真秒/真实秒 → 定时器周期 1.0 仿真秒 = 2.0 真实秒 → 实际约 0.5Hz ②每次触发 stamp 读数 +1.0 仿真秒(按仿真轴走)③真实频率减半、仿真步进不变。(①②各 40%,③ 20%)
5(目标②·改类型)删掉 now_msg = now.to_msg(),直接写 msg.stamp = now。会发生什么?正确写法里 msg.stamp 的类型是什么?
参考答案与得分点 得分点:①类型不匹配报错:rclpy.time.Time ≠ 消息字段类型 ②正确类型 builtin_interfaces/msg/Time(sec + nanosec)③过界必转 to_msg()/from_msg()。(①②各 40%,③ 20%)