结论: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 之后就绪条件继续成立,下一个循环可能立即再次激活,因此不能把它当作完整状态重置。
延伸阅读
- openpilot 的 100 Hz 控制回路:开源 ADAS 能给车载设备什么 —— 对照本文用循环次数计时的实验进程与量产控制节拍。
- 纵向规划 MPC:求解器的输入契约与失败处理 —— 说明量产规划器与本实验发布进程的职责差异。
- 从使能到车轮:执行链上的八道门,以及它们各自不保证什么 —— 解释为何发布计划与真正驱动车轮之间还有独立安全门。
- 巡航设定车速的状态与使能条件 —— 对照本文
longActive与巡航相关门控。
参考资料
- 项目 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
发表回复