maneuversd 纵向动作状态机:为什么第 61 次循环才激活

作者:

在

结论:maneuversd 是 openpilot 中一个实验性的纵向动作发布进程,它用 DT_MDL=0.05 秒按主循环次数推进动作,而不是按真实秒数。它的就绪门要求车速偏离初速小于 0.3 m/s 且纵向处于活动状态,连续超过 60 次合格循环后第 61 次才置为 _active;进入活动后若 longActive 变假,函数仍会继续推进当前动作帧。它替换的是 plannerd 的计划发布者身份,不替换 controlsd、card 与 panda 的执行门。本文为固定源码快照 c8fb906 的静态阅读,未连接车辆与硬件。

进程身份:发布者切换不等于安全许可

在进程配置里,plannerd 与 maneuversd 通过条件互斥:plannerd 使用 not_long_maneuver,maneuversd 使用 long_maneuver,后者要求 started 为真且 LongitudinalManeuverMode 处于开启状态。这是 manager 正常管理路径上的发布者切换,并不是一份独立的安全许可。

main() 先等待 CarParams 存在,再轮询 modelV2,依次取用内置的七个 Maneuver。内置计划固定 allowBrake=True、allowThrottle=True、hasLead=True、speeds=[0.2],并按 should_stop(v_ego, accel) 设置停止位;这些是实验消息的构造字段,不能代表正常纵向规划器的前车识别、速度轨迹或 FCW 能力。

入口 源码事实 边界
process_config.py 进程配置 plannerd 用 not_long_maneuver,maneuversd 用 long_maneuver 是 manager 正常路径的发布者切换,不是独立安全许可
Action.__post_init__() 只断言加速度与时间节点数量一致 未校验节点非空、单调、有限、加速度范围或持续时间
main() 等 CarParams,轮询 modelV2,每轮发三路主题 未在状态推进前用 all_checks() 做门控

就绪门与「第 61 次循环」的由来

Maneuver.get_accel() 在车速偏离初速小于 0.3 m/s、longActive 为真且巡航不处于 standstill 时累加就绪计数;当初速接近零时,还额外要求 carState.standstill 为真。它把状态机推进建立在「连续满足条件」的计数上,而不是单次采样。

只有 _ready_cnt > 3/DT_MDL 才置 _active=True。由于 DT_MDL=0.05,3/DT_MDL 等于 60,而这里用的是严格大于,所以至少需要 61 次合格循环,而不是精确的 3.00 秒墙钟。未进入活动状态时,函数返回一个限幅到 ±2 m/s² 的初速追赶目标。

条件 阈值 说明
车速偏离初速 小于 0.3 m/s 偏离越大越不计入就绪计数
纵向激活 longActive 为真 初速接近零时还要 carState.standstill
就绪计数门 _ready_cnt > 3/DT_MDL 严格大于,至少 61 次合格循环
未活动追赶目标 ±2 m/s² 限幅后的初速追赶,不是最终执行命令

动作插值、重复与 reset 边界

Maneuver._step() 按 _action_frames * DT_MDL 对当前 Action 插值;帧计数加一后,只有超过末节点时间才切换到下一个动作、重做或结束,因此最后一个时间节点需要再推进一帧才会切换。以主循环次数近似时间,意味着卡顿或模型节拍异常都会改变墙钟持续时长。

repeat=2 使内置动作总共执行三轮。reset() 只清 _active、_action_frames、_action_index,不清 _ready_cnt、_repeated、_finished;它与横向子类的 reset() 不同,不能当作完整的状态重置。若 repeat 之后就绪条件继续成立,下一循环可以立即再次激活。

三路发布与有效位语义

每轮 main() 依次发出 alertDebug、longitudinalPlan 和 driverAssistance。计划的 valid=sm.all_checks(),另两路直接为真;其中 driverAssistance 是空默认字段,不提供常规 LDW 结果。动作列表耗尽后,仍会按同样顺序持续发送 aTarget=0 的计划和完成提示。

源码没有在进入 get_accel() 之前用 sm.all_checks() 阻止状态推进,因此无效计划也会先完成计算并填入目标,只是把外层有效位标为假。

主题 有效位 内容 边界
longitudinalPlan sm.all_checks() 按动作插值填入 aTarget、停止位等 有效位不冻结状态机
alertDebug 恒为真 纵向动作模式告警 由 selfdrived 消费,不做执行门
driverAssistance 恒为真 空默认字段 不提供常规 LDW 结果

失效路径与插件化要求

SubMaster.update() 的默认 100 ms 超时也会让循环继续,所以「循环次数 × DT_MDL」并不等于真实经过的时间。sm.all_checks() 只设置发布事件的有效位,不冻结状态机;本进程本身没有「数据失联即重置动作」的独立分支。若模型消息或其他输入失联,外层消息健康和下游自驾状态机可能触发退出,但那是另一层。

若要把这类实验规划做成插件,至少应:用单调时钟或样本时间推进,而不是用消息循环数冒充秒数;建立显式的 idle → armed → active → abort/complete 转移,纵向失活、速度越界、数据失联与进程重启都要撤销动作;把实验规划插件与量产规划插件分开注册、隔离主题与权限,即使替换计划发布者,也不能成为 carControl 或 sendcan 的直接所有者。

常见问题

为什么是「第 61 次循环」而不是精确 3 秒?

就绪门写的是 _ready_cnt > 3/DT_MDL。DT_MDL=0.05 秒,3/DT_MDL 等于 60,而比较是严格大于,所以至少需要 61 次合格循环才置为活动。动作推进也按主循环次数乘 DT_MDL 计时,轮询超时同样会让循环继续,因此它不是精确的 3.00 秒墙钟。

活动后 longActive 变假,动作会立刻停止吗?

函数本身不会在这一点重置 _active。活动后若 longActive 变假或速度越界,get_accel() 仍会进入 _step() 继续推进动作帧;这不等于执行器仍有输出,controlsd 有独立的 CC.longActive 门。

longitudinalPlan.valid 为假时 controlsd 会丢弃目标值吗?

不能这样断言。controlsd 的 publish() 处没有按 self.sm.valid['longitudinalPlan'] 包住目标读取;selfdrived 会对该主题做通信健康检查并可能产生 commIssue,但它与 controlsd 的字段消费是不同层。计划外层 valid=False 不必然让控制器直接丢弃数值。

reset() 是完整的状态重置吗?

不是。reset() 只清 _active、_action_frames、_action_index,不清 _ready_cnt、_repeated、_finished。若 repeat 之后就绪条件继续成立,下一个循环可能立即再次激活,因此不能把它当作完整状态重置。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/LONGITUDINAL_MANEUVER_STATE_MACHINE_DEEP_DIVE.md(取用了进程互斥、就绪门与第 61 次循环阈值、三路发布有效位与失效路径)。
  • 项目 02 —— openpilot-research/LONGITUDINAL_MANEUVER_TEST_MATRIX_DEEP_DIVE.md(关联的回归测试矩阵专题,用于区分发布控制流与测试覆盖,本文未展开其用例)。

说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

更新记录

首次发布 2026-10-11 最近核对:2026-10-11

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注