openpilot 纵向机动性测试工具:maneuversd 机动序列原理与 generate_report 报告生成实战
openpilot 提供了一套名为 longitudinal maneuvers 的专用测试工具,用于验证车辆在纵向(加减速)控制上的调优效果:通过参数 LongitudinalManeuverMode 启动 maneuversd 进程,让车辆依次执行一套内置的加减速机动序列(起步、制动、蠕行、阶跃响应等),再基于行车路线日志用 generate_report.py 生成包含加速度曲线、目标加速度响应时间和统计摘要的 HTML 报告。读完本篇,你将掌握该工具从实车操作、参数开关到底层消息流转、机动状态机、报告解析的完整技术链路,能够独立完成一次完整的纵向调优评测并判读报告。
工具定位与核心组件
该工具的目标场景是车辆纵向控制调参:当你修改了某款车的纵向参数(例如 PCM 加速度补偿值)后,需要一套可重复、可量化对比的加减速测试来验证效果。README(openpilot/tools/longitudinal_maneuvers/README.md)给出的定位是:“Test your vehicle's longitudinal control tuning with this tool”——工具负责让车辆跟随若干纵向机动运行,并提供一个从路线日志生成报告的脚本。
整个工具链由三部分组成:
| 组件 | 文件 | 职责 |
|---|---|---|
| 机动执行守护进程 | maneuversd.py | 订阅车辆状态、按预定义序列下发目标加速度 longitudinalPlan.aTarget |
| 进程调度门禁 | process_config.py | 依据 LongitudinalManeuverMode 参数决定是否拉起 maneuversd |
| 报告生成器 | generate_report.py | 解析路线日志,切分每次机动运行,绘图并输出 HTML 报告 |
开关机制:LongitudinalManeuverMode 参数如何生效
参数的定义与生命周期
该参数在 params_keys.h 中注册为 BOOL 类型,并带有清除策略:
{"LongitudinalManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
即 manager 进程重启(对应 README 中“关车上电”的操作)或状态切回 offroad 时参数会被自动清除,避免上一次测试的开关残留到下次行车。
manager 按参数拉起 maneuversd
进程调度逻辑在 process_config.py 中定义:
def long_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("LongitudinalManeuverMode")
def not_long_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and not params.get_bool("LongitudinalManeuverMode")
maneuversd 的注册条目为 PythonProcess("maneuversd", "openpilot.tools.longitudinal_maneuvers.maneuversd", long_maneuver)(见同文件 L108)——只有“已上电起步(started)且参数为真”时进程才会运行;与之对称的 not_long_maneuver 用于在测试模式下排除互斥的普通纵向流程。这就是为什么 README 要求“先关机写参数、再重新上电”:参数值在 manager 启动那一刻被读取并决定进程集合。
UI 侧的互斥开关
除了在设备上直接写参数(见下文实操步骤),也可以在开发者设置界面操作:developer.py 中注册了 “Longitudinal Maneuver Mode” 开关,其回调 _on_long_maneuver_mode(L171-L176)在打开本模式时会同时关闭 JoystickDebugMode 与 LateralManeuverMode,反向操作也如此——三种“接管/调试模式”互斥,防止多个进程同时下发控制指令。从源码结构看还有两处限制条件:该开关仅在非 release 构建中显示(item.set_visible(not self._is_release)),且需要车辆具备纵向控制能力(ui_state.has_longitudinal_control)并处于 offroad 状态才能操作。这也解释了 README 第 1 步“在 comma 设备上检出 master 等开发分支”的前提。
测试模式的界面提示:alertDebug 消息链
上电进入测试模式后,屏幕上出现的 “Longitudinal Maneuver Mode” 提示并非 maneuversd 直接绘制,而是一条消息驱动的链路:
maneuversd以 20Hz 发布alertDebug消息(services.py 中注册为"alertDebug": (True, 20., 5)),valid恒为真;- selfdrived.py 检测到
alertDebug有帧到达时,追加事件EventName.longitudinalManeuver; - events.py 将该事件映射为永久提示:“Longitudinal Maneuver Mode / Ensure road ahead is clear”。
同时 maneuversd 每帧把当前状态写入 alertText1/alertText2(如 Maneuver Active: 1.50 m/s^2、Setting up to 20.00 mph 或机动描述),这些文本既用于车机调试显示,也是报告生成器切分“运行段”的关键标记(见下文)。
机动序列:MANEUVERS 定义与执行状态机
七个内置机动
maneuversd.py 中的 MANEUVERS 列表定义了完整测试套件。每个机动由若干 Action(加速断点 accel_bp m/s² 与时间断点 time_bp 秒)组成,可重复 repeat 次,并有 initial_speed(m/s)要求的初始车速:
| 机动描述 | 初始车速 | 加速轨迹(m/s²) | 重复次数 | 考察目标 |
|---|---|---|---|---|
| come to stop | 5 m/s | -0.5 持续 12s | 2 | 低速平稳制动到停 |
| start from stop | 0 m/s | +1.5 持续 6s | 2 | 静止起步的牵引响应 |
| creep: alternate between +1m/s^2 and -1m/s^2 | 0 m/s | +1 / -1 各 3s,交替 6 段 | 2 | 低速蠕行时加减速交替 |
| brake step response: -1m/s^2 from 20mph | 20 mph | -1 持续 3s | 2 | 轻制动作阶跃响应 |
| brake step response: -3.5m/s^2 from 20mph | 20 mph | -3.5 持续 3s | 2 | 强制动作阶跃响应 |
| gas step response: +1m/s^2 from 20mph | 20 mph | +1 持续 3s | 2 | 轻加动作阶跃响应 |
| gas step response: +2m/s^2 from 20mph | 20 mph | +2 持续 3s | 2 | 较强加动作阶跃响应 |
README 中报告示例输出的 “runs: 4/2/2” 是历史版本机动的运行次数统计,当前代码中各机动均为 repeat=2,以 maneuversd.py 实际定义为准。
状态机:就绪判定与加速插值
核心逻辑在 Maneuver.get_accel(L62-L74):
def get_accel(self, v_ego: float, long_active: bool, standstill: bool, cruise_standstill: bool, /) -> float:
ready = abs(v_ego - self.initial_speed) < 0.3 and long_active and not cruise_standstill
if self.initial_speed < 0.01:
ready = ready and standstill
self._ready_cnt = (self._ready_cnt + 1) if ready else 0
if self._ready_cnt > (3. / DT_MDL):
self._active = True
if not self._active:
return min(max(self.initial_speed - v_ego, -2.), 2.)
return self._step()
可以拆出三条规则:
- 就绪条件:当前车速
v_ego与机动要求的initial_speed之差小于 0.3 m/s,纵向控制处于激活(long_active)且非巡航静止(cruise_standstill)状态;若机动要求从静止开始(initial_speed < 0.01),还要求车辆确实处于standstill; - 3 秒稳定期:就绪状态必须连续保持 3 秒(
3. / DT_MDL帧)后才置_active,避免车速抖动导致机动中途被触发; - 准备阶段的收敛行为:机动未激活时,返回
clip(initial_speed - v_ego, -2, 2)的限幅差值,相当于一个 P 控制器,把车速推向该动机的初始速度(上限 2 m/s²、下限 -2 m/s²),这也是 README 中“在两次机动之间可手动退出并折返、等车速匹配后自动继续”的底层原因。
激活后的 _step(L37-L60)按 DT_MDL 帧推进,用 np.interp(self._action_frames * DT_MDL, action.time_bp, action.accel_bp) 对当前动作的加速度做时间插值;单动作时长耗尽后进入下一动作,动作序列耗尽后按 repeat 计数重跑(reset() 将 _action_index、_action_frames 清零),全部完成后置 _finished = True,主循环随即把 maneuver 置回 None 并取下一个机动(L195-L196)。
maneuversd 每帧发布了什么
主循环(L151-L196)每帧做三件事:
- 订阅
carState / carControl / controlsState / selfdriveState / modelV2,等待CarParams就绪后才进入循环; - 发布
alertDebug(状态文本)与longitudinalPlan:aTarget = accel、allowBrake = True、allowThrottle = True、hasLead = True(屏蔽基于前车的限制),以及一行值得注意的注释——longitudinalPlan.speeds = [0.2],源码注释说明它是“triggers carControl.cruiseControl.resume in controlsd”,即模拟一个非零目标车速序列,促使 controlsd 的巡航逻辑走 resume 路径,保证起步类机动能够真正发出扭矩; - 发布空的
driverAssistance(valid = True),用于维持下游流程的帧有效状态。
全部机动跑完后,alertText1 变为 Maneuvers Finished,即 README 中描述的 “Maneuvers Finished.” 提示。
实车操作完整流程
以下 8 步完整继承自 README.md,并补充了每步对应的源码依据:
-
在 comma 设备上检出开发分支(如
master)。release 构建中该模式的 UI 开关会被隐藏(见前文_is_release检查),必须使用开发分支。 -
选择场地:找一个空旷的大型停车场或无车无人的道路,平坦笔直的路面最佳。完整机动套件若连续跑完需要 1 英里(约 1.6 公里)以上空间;空间不足时建议在机动之间手动退出 openpilot 并掉头。
-
熄火后写入开关参数,通知 openpilot 启动机动测试守护进程:
echo -n 1 > /data/params/d/LongitudinalManeuverMode注意
echo -n不带换行符,因为该参数是 BOOL 类型,params按字节解析。 -
重新上电。此时 manager 检测到参数为真并拉起
maneuversd(long_maneuver门禁),屏幕上会出现 “Longitudinal Maneuver Mode” 永久提示(alertDebug→EventName.longitudinalManeuver链路)。 -
确认前方道路无遮挡物后,按方向盘 "Set" 键开始测试。README 特别强调:该模式下 openpilot 不会对前方任何障碍物制动。整个测试约运行 4 分钟。过程中可按方向盘 "Cancel" 暂停、按 "Resume" 继续。 GM 车型的建议:所有低速测试(起步、停车、蠕行)都建议长按 resume 键,避免车辆进入厂商的 standstill(静止锁定)状态导致机动无法触发——对应
get_accel中not cruise_standstill的就绪条件。 -
测试结束:看到 “Maneuvers Finished.” 提示后,把车靠边熄火,完成本次路线(route)。
-
上传确认:访问 connect.comma.ai 找到对应路线,时间轴上会出现大量橙色区间(机动期间的控制活动)。确认 “All logs” 显示为 “uploaded”。
-
运行报告生成器(详见下节)。
报告生成:generate_report.py 原理与用法
命令与参数
$ python openpilot/tools/longitudinal_maneuvers/generate_report.py 57048cfce01d9625/0000010e--5b26bc3be7 'pcm accel compensation'
processing report for LEXUS_ES_TSS2
plotting maneuver: start from stop, runs: 4
plotting maneuver: creep: alternate between +1m/s^2 and -1m/s^2, runs: 2
plotting maneuver: gas step response: +1m/s^2 from 20mph, runs: 2
Report written to /home/batman/openpilot/tools/longitudinal_maneuvers/longitudinal_reports/LEXUS_ES_TSS2_57048cfce01d9625_0000010e--5b26bc3be7.html
命令接受两个参数(argparse 定义):
route:路线名,如00000000--5f742174be,也支持deviceid/route的完整格式;description(可选):本次测试的说明文字,会写入报告头部,例如'pcm accel compensation',便于区分不同调参版本的对比报告。
路线读取:本地与云端两种路径
主入口(L158-L166)按路线串里是否含 / 或 | 区分数据源:
if '/' in args.route or '|' in args.route:
lr = LogReader(args.route)
else:
segs = [seg for seg in os.listdir(Paths.log_root()) if args.route in seg]
lr = LogReader([os.path.join(Paths.log_root(), seg, 'rlog.zst') for seg in segs])
- 含
/或|的完整路线标识走LogReader的云端/容器路径; - 仅给路线名时,在设备本地
Paths.log_root()下模糊匹配段目录,直接读取各段的rlog.zst——这意味着你在 comma 设备上跑完测试后即可离线生成报告,无需等待上传。
随后取 CP = lr.first('carParams') 得到车辆指纹(platform),取 ID = lr.first('initData') 得到 git commit/branch/remote,报告头部会打印该路线对应的构建信息,保证“报告 ↔ 代码版本”可追溯。
运行段切分:利用 alertDebug 的上升沿
maneuversd 每次把机动激活时会把 alertText1 置为 Maneuver Active: x.xx m/s^2。报告生成器遍历全部日志帧,在检测到 “Maneuver Active” 文本的上升沿时切分一次新运行(L173-L185):
for msg in lr:
if msg.which() == 'alertDebug':
active = 'Maneuver Active' in msg.alertDebug.alertText1
if active and not active_prev:
if msg.alertDebug.alertText2 == description_prev:
maneuvers[-1][1].append([])
else:
maneuvers.append((msg.alertDebug.alertText2, [[]]))
description_prev = maneuvers[-1][0]
active_prev = active
if active_prev:
maneuvers[-1][1][-1].append(msg)
即按 alertText2(机动描述)分组,同一机动的多次 repeat 各自成为一条 run,激活期间的所有消息帧都归入当前 run。这正是 maneuversd 状态文本与报告格式之间的隐含协议。
有效性判定与 “crossed in Xs” 响应时间
每个 run 的有效性判定(L57-L62):
longActive = [m.longActive for m in carControl]
maneuver_valid = all(longActive) and (not any(cs.cruiseState.standstill for cs in carState) or CP.autoResumeSng)
要求该段内 longActive 全程为真,且期间没有进入巡航静止——除非车辆参数 autoResumeSng 为真(即支持自动恢复的车型,短暂 standstill 不算无效)。无效的 run 在报告中标红 “(invalid maneuver!)” 且默认折叠。
响应时间 “crossed in X.XXXs” 的判定(L71-L84)是报告的核心指标:取该 run 首帧的 longitudinalPlan.aTarget 作为目标加速度,然后扫描 deviceMotion.accelerationDevice.x(IMU 实测加速度),当实测值越过目标值符号方向、且连续两个 20Hz 帧都越过阈值(源码注释:Localizer/IMU 噪声要求双帧确认)时,记录该时刻为 target_cross_time。它量化了“目标加速度下发到整车实际响应”的延迟,是横向对比不同调参版本最直接的数据。
报告内容
report() 函数(L22-L148)生成一个自包含的 HTML 文件,输出到脚本同目录下的 longitudinal_reports/,文件名为 {platform}_{route}.html(如 LEXUS_ES_TSS2_57048cfce01d9625_0000010e--5b26bc3be7.html)。内容包括:
- 头部:平台指纹、路线名、
initData中的 git commit/branch/remote,以及可选的测试描述; - CarParams 折叠块:完整
pprint输出(过滤*DEPRECATED键),记录该次测试的全部车辆标定上下文; - 每个机动一个小节,每次 run 一个可折叠面板,内含一张四子图(matplotlib 渲染后转 base64 webp 内嵌):
- 加速度对比:
carControl.actuators.accel(最终执行器请求)、carOutput.actuatorsOutput.accel、longitudinalPlan.aTarget(maneuversd 下发的目标)、carState.aEgo、deviceMotion.accelerationDevice.x(IMU 实测),并在target_cross_time处画标记圈; carState.vEgo车速曲线;longActive状态条;gasPressed/brakePressed踏板状态,用于排查驾驶员干预;- 另标注首帧
aTarget与全程平均俯仰角(carControl.orientationNED[1],度)——平均俯仰角提醒测试路面是否足够平坦,坡度会污染 IMU 加速度读数;
- 加速度对比:
- Summary 表:按机动汇总
crossed / runs / mean / min / max(crossed 次数、总运行次数、响应时间均值/最小/最大),便于快速横向比较。
生成后脚本会用 webbrowser.open_new_tab 自动打开报告。
注意事项与适用边界
- 安全边界:该模式不针对障碍物制动(README 明确警示),务必在空旷场地进行;IMU 加速度受坡度影响,报告中的平均俯仰角即是为此提供的自检信息。
- 模式互斥:
LongitudinalManeuverMode与JoystickDebugMode、LateralManeuverMode在 UI 回调层面互相关闭,参数本身带CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION清除策略,测试结束熄火后参数即失效,无需手动清理。 - 构建要求:UI 开关仅非 release 构建可见,且需要车辆支持纵向控制;设备直接写参数的方式同样依赖开发分支上的
maneuversd注册项。 - 数据前提:报告解析依赖路线中包含
alertDebug帧,因此只适用于本工具跑出的路线;对横向机动,仓库另有对称的 lateral_maneuvers 工具(LateralManeuverMode参数与lateral_maneuversd进程,见 process_config.py)。
小结
openpilot 的纵向机动测试工具把“调参 → 实车验证 → 量化对比”闭环拆成了三段清晰可查的实现:params 参数经 manager 的 long_maneuver 门禁拉起 maneuversd;maneuversd 用带 3 秒稳定期的状态机执行七个可重复机动,并通过 alertDebug 文本流与 UI、报告生成器共享同一份语义协议;generate_report.py 再以该协议切分运行段,输出含有效性判定、IMU 响应时间、五路加速度对比图和统计摘要的自包含 HTML 报告。三者各自独立又通过消息与参数解耦,是研究 openpilot 进程调度、cereal 消息驱动 UI、以及日志回放分析(LogReader)模式的一个小型而完整的样本。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00