首页
/ openpilot 纵向机动性测试工具:maneuversd 机动序列原理与 generate_report 报告生成实战

openpilot 纵向机动性测试工具:maneuversd 机动序列原理与 generate_report 报告生成实战

2026-09-04 16:53:35作者:史锋燃Gardner

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_modeL171-L176)在打开本模式时会同时关闭 JoystickDebugModeLateralManeuverMode,反向操作也如此——三种“接管/调试模式”互斥,防止多个进程同时下发控制指令。从源码结构看还有两处限制条件:该开关仅在非 release 构建中显示(item.set_visible(not self._is_release)),且需要车辆具备纵向控制能力(ui_state.has_longitudinal_control)并处于 offroad 状态才能操作。这也解释了 README 第 1 步“在 comma 设备上检出 master 等开发分支”的前提。

测试模式的界面提示:alertDebug 消息链

上电进入测试模式后,屏幕上出现的 “Longitudinal Maneuver Mode” 提示并非 maneuversd 直接绘制,而是一条消息驱动的链路:

  1. maneuversd 以 20Hz 发布 alertDebug 消息(services.py 中注册为 "alertDebug": (True, 20., 5)),valid 恒为真;
  2. selfdrived.py 检测到 alertDebug 有帧到达时,追加事件 EventName.longitudinalManeuver
  3. events.py 将该事件映射为永久提示:“Longitudinal Maneuver Mode / Ensure road ahead is clear”。

同时 maneuversd 每帧把当前状态写入 alertText1/alertText2(如 Maneuver Active: 1.50 m/s^2Setting 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_accelL62-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 中“在两次机动之间可手动退出并折返、等车速匹配后自动继续”的底层原因。

激活后的 _stepL37-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)每帧做三件事:

  1. 订阅 carState / carControl / controlsState / selfdriveState / modelV2,等待 CarParams 就绪后才进入循环;
  2. 发布 alertDebug(状态文本)与 longitudinalPlanaTarget = accelallowBrake = TrueallowThrottle = TruehasLead = True(屏蔽基于前车的限制),以及一行值得注意的注释——longitudinalPlan.speeds = [0.2],源码注释说明它是“triggers carControl.cruiseControl.resume in controlsd”,即模拟一个非零目标车速序列,促使 controlsd 的巡航逻辑走 resume 路径,保证起步类机动能够真正发出扭矩;
  3. 发布空的 driverAssistancevalid = True),用于维持下游流程的帧有效状态。

全部机动跑完后,alertText1 变为 Maneuvers Finished,即 README 中描述的 “Maneuvers Finished.” 提示。

实车操作完整流程

以下 8 步完整继承自 README.md,并补充了每步对应的源码依据:

  1. 在 comma 设备上检出开发分支(如 master)。release 构建中该模式的 UI 开关会被隐藏(见前文 _is_release 检查),必须使用开发分支。

  2. 选择场地:找一个空旷的大型停车场或无车无人的道路,平坦笔直的路面最佳。完整机动套件若连续跑完需要 1 英里(约 1.6 公里)以上空间;空间不足时建议在机动之间手动退出 openpilot 并掉头。

  3. 熄火后写入开关参数,通知 openpilot 启动机动测试守护进程:

    echo -n 1 > /data/params/d/LongitudinalManeuverMode
    

    注意 echo -n 不带换行符,因为该参数是 BOOL 类型,params 按字节解析。

  4. 重新上电。此时 manager 检测到参数为真并拉起 maneuversdlong_maneuver 门禁),屏幕上会出现 “Longitudinal Maneuver Mode” 永久提示(alertDebugEventName.longitudinalManeuver 链路)。

  5. 确认前方道路无遮挡物后,按方向盘 "Set" 键开始测试。README 特别强调:该模式下 openpilot 不会对前方任何障碍物制动。整个测试约运行 4 分钟。过程中可按方向盘 "Cancel" 暂停、按 "Resume" 继续。 GM 车型的建议:所有低速测试(起步、停车、蠕行)都建议长按 resume 键,避免车辆进入厂商的 standstill(静止锁定)状态导致机动无法触发——对应 get_accelnot cruise_standstill 的就绪条件。

  6. 测试结束:看到 “Maneuvers Finished.” 提示后,把车靠边熄火,完成本次路线(route)。

  7. 上传确认:访问 connect.comma.ai 找到对应路线,时间轴上会出现大量橙色区间(机动期间的控制活动)。确认 “All logs” 显示为 “uploaded”。

  8. 运行报告生成器(详见下节)。

报告生成: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)。内容包括:

  1. 头部:平台指纹、路线名、initData 中的 git commit/branch/remote,以及可选的测试描述;
  2. CarParams 折叠块:完整 pprint 输出(过滤 *DEPRECATED 键),记录该次测试的全部车辆标定上下文;
  3. 每个机动一个小节,每次 run 一个可折叠面板,内含一张四子图(matplotlib 渲染后转 base64 webp 内嵌):
    • 加速度对比:carControl.actuators.accel(最终执行器请求)、carOutput.actuatorsOutput.accellongitudinalPlan.aTarget(maneuversd 下发的目标)、carState.aEgodeviceMotion.accelerationDevice.x(IMU 实测),并在 target_cross_time 处画标记圈;
    • carState.vEgo 车速曲线;
    • longActive 状态条;
    • gasPressed / brakePressed 踏板状态,用于排查驾驶员干预;
    • 另标注首帧 aTarget 与全程平均俯仰角(carControl.orientationNED[1],度)——平均俯仰角提醒测试路面是否足够平坦,坡度会污染 IMU 加速度读数;
  4. Summary 表:按机动汇总 crossed / runs / mean / min / max(crossed 次数、总运行次数、响应时间均值/最小/最大),便于快速横向比较。

生成后脚本会用 webbrowser.open_new_tab 自动打开报告。

注意事项与适用边界

  • 安全边界:该模式不针对障碍物制动(README 明确警示),务必在空旷场地进行;IMU 加速度受坡度影响,报告中的平均俯仰角即是为此提供的自检信息。
  • 模式互斥LongitudinalManeuverModeJoystickDebugModeLateralManeuverMode 在 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 门禁拉起 maneuversdmaneuversd 用带 3 秒稳定期的状态机执行七个可重复机动,并通过 alertDebug 文本流与 UI、报告生成器共享同一份语义协议;generate_report.py 再以该协议切分运行段,输出含有效性判定、IMU 响应时间、五路加速度对比图和统计摘要的自包含 HTML 报告。三者各自独立又通过消息与参数解耦,是研究 openpilot 进程调度、cereal 消息驱动 UI、以及日志回放分析(LogReader)模式的一个小型而完整的样本。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341