标签: panda

  • 开发工具链的四种权限:摇杆、机动、消息桥与仿真各能做什么

    结论:这套开发工具链包含四种不同权限等级的入口:摇杆调试控制(替代 controlsd)、纵向机动(替代 plannerd)、横向机动(覆盖期望曲率)、以及 msgq/ZMQ 消息桥。它们都能影响 carControl 或 sendcan 路径,但配置层的「实验开关互斥」不是安全门。此外「bridge」在本项目有两个不同含义:受管进程 bridge 是 msgq/ZMQ 主题桥,tools/sim/bridge 是物理仿真与 openpilot 的适配器,二者不能混为一个控制入口。

    四类调试入口与运行条件

    摇杆输入由 joystick_control.main() 提供,车载由 process_config.py 管理;脚本要求驻车启动或在 ZMQ 模式下运行,并设置 JoystickDebugMode——但这不是安全固件许可。摇杆控制器 joystickd_thread() 在 JoystickDebugMode 或 notCar 时运行,模式启用时 controlsd 不运行;testJoystick 超过 0.2 秒未收到会把轴归零。

    纵向机动由 maneuversd.main() 在 LongitudinalManeuverMode 时运行,与 plannerd 互斥,按目标速度及纵向激活连续满足约三秒后执行预置加速度曲线——它不是一般规划器的插件实现。横向机动由 lateral_maneuversd.main() 在 LateralManeuverMode 时运行,只在目标速度、直路、低侧倾且横向激活时起动,转向/油门输入或速度偏差会重置动作。

    组件 入口与条件 输入 → 输出 关键限制
    摇杆输入 joystick_control.main() 键盘/游戏手柄 → testJoystick 要求驻车启动或 ZMQ 模式;设置调试模式不等于固件许可
    摇杆控制器 joystickd_thread() testJoystick/carState 等 → carControl 启用时 controlsd 不运行;0.2 秒超时轴归零
    纵向机动 maneuversd.main() 模型时钟/车速/carControl → longitudinalPlan 与 plannerd 互斥;约三秒条件后执行预置曲线
    横向机动 lateral_maneuversd.main() 车速/曲率/驾驶员输入 → lateralManeuverPlan 仅在目标速度、直路、低侧倾且横向激活时起动
    msgq/ZMQ bridge bridge.main() msgq 服务 ↔ ZMQ 网络主题 只传输消息,不校验驾驶控制语义
    崩溃收集 tombstoned.main() apport crash → cloudlog 摘要/日志副本 仅处理 .crash,限制 100 MB;不在实时闭环内

    开发者界面把三个实验参数设置为互斥,但这只是配置层保障;插件架构仍应在进程启动层做独立冲突检测。selfdrived 对收到的机动主题添加专用事件,如果外部插件冒充这些主题,就会影响使能和告警路径。

    关键执行路径

    Keyboard.update() 与 Joystick.update() 把人工输入变成双轴,send_thread() 以 100 Hz 发布;游戏手柄使用动态记录的轴范围、中心死区和三次曲线,重插拔异常时轴归零,而键盘输入是逐键累积、不是弹簧归中。joystickd_thread() 同样以 100 Hz 发布 carControl:纵轴经系数 4 转加速度,横轴经车辆模型转换曲率及转角,latActive 依赖 selfdriveState.active 与转向故障,longActive 还依赖纵向控制能力和覆盖事件。软件层后续仍走 card 和 panda 安全钩子。

    纵向 Maneuver.get_accel() 等待初始车速、纵向激活与驻车状态,再由 _step() 插值并推进动作/重复次数;main() 发布目标加速度、停止条件及调试文本。横向 Maneuver.get_accel() 等待约两秒稳定条件、记下基准曲率,main() 把横向加速度换算成 baseline + a/v²;Controls.state_control() 仅在该消息有效时替代模型期望曲率,随后调用 clip_curvature()。这里的「加速度」是横向试验扰动,不是纵向油门命令。

    入口 节拍/条件 语义要点
    Keyboard.update() / Joystick.update() 输入事件 键盘逐键累积;手柄动态轴范围、死区与三次曲线
    send_thread() 100 Hz 发布 testJoystick
    joystickd_thread() 100 Hz 纵轴系数 4 转加速度;横轴经车辆模型转曲率/转角
    横向 main() 稳定约两秒后 baseline + a/v² 覆盖期望曲率;未发消息则不替代

    网络消息桥与崩溃诊断

    get_services() 决定转发主题,zmq_to_msgq() 建立网络订阅到本地发布。白名单使用子串查找,不是严格集合匹配;get_port() 按主题名散列端口,源码注明可能冲突。新增跨设备插件必须显式定义主题允许集、身份鉴别和冲突处理,不能把此调试桥当生产控制 API。

    崩溃收集侧,get_tombstones() 只扫描至多 1000 个目录项,report_tombstone_apport() 提取可执行路径、信号和首个相关栈帧,清洗文件名后复制崩溃文件,main() 每五秒比较增量。报告路径适合诊断插件订阅,不能影响实时安全控制。

    仿真闭环与其证据边界

    run_bridge.create_bridge() 启动 MetaDrive 进程,SimulatorBridge._run() 创建车、传感器与各自周期线程,读取人工输入及 carControl,按使能状态决定应用人工还是 openpilot 输出。SimulatedCar.send_can_messages() 生成 Honda Civic 2022 风格 CAN,send_panda_state() 发布虚拟 pandaStates,SimulatedSensors.update() 发布 IMU、GPS、驾驶员状态和周边设备状态,send_camera_images() 送 VisionIPC 图像;launch_openpilot.sh 使用 NOBOARD=1、SIMULATION=1 和强制 Honda 指纹。

    但这条路径没有证明真实 panda 安全钩子、线束、电气故障、实际相机时序或目标车 DBC 行为。test_sim_bridge.py 的驾驶闭环测试被 unittest.skip 明确跳过,test_metadrive_bridge.py 继承该测试且还可能因未安装 MetaDrive 跳过。因此测试文件存在也不能声称当前仿真闭环通过。调试控制、纵向规划替代、横向曲率覆盖和网络消息桥分别是不同权限等级;测试插件不能将 UI 的互斥开关视为唯一安全门,仿真适配器也应保持独立于生产硬件抽象。

    常见问题

    开启了 JoystickDebugMode 是否就等于可以实车控制?

    不等于。源文档明确脚本设置 JoystickDebugMode「不是安全固件许可」,且脚本要求驻车启动或在 ZMQ 模式下运行。使能、故障和纵向覆盖状态仍参与计算,软件层后续仍走 card 和 panda 安全钩子。

    UI 里三个实验参数互斥,是否已经足够安全?

    不足。开发者界面的互斥只是配置层保障,源文档建议插件架构仍应在进程启动层做独立冲突检测。另外若外部插件冒充机动主题,会影响使能和告警路径,因此需要进程级仲裁与回放基线。

    仿真测试文件存在,是否说明仿真闭环已经通过?

    不能这样推断。test_sim_bridge.py 的驾驶闭环测试被 unittest.skip 明确跳过,test_metadrive_bridge.py 继承该测试且可能因未安装 MetaDrive 跳过。而且仿真路径没有证明真实 panda 安全钩子、线束、电气故障、实际相机时序或目标车 DBC 行为。

    msgq/ZMQ bridge 能不能当作生产控制接口?

    不能。源文档明确它「传输消息,不校验驾驶控制语义」,白名单使用子串查找而非严格集合匹配,端口按主题名散列且源码注明可能冲突。新增跨设备插件必须显式定义主题允许集、身份鉴别和冲突处理。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DEVELOPMENT_TOOLS_DEEP_DIVE.md(取用了四类调试入口的运行条件、关键执行路径、消息桥与崩溃诊断边界、仿真闭环的证据边界)。
    • 项目 02 —— openpilot-research/PLUGIN_REFACTOR.md(仅用于确认调试工具在插件化中的权限分级约束)。

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

    更新记录

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

  • panda Python 设备 API:连接副作用、CAN 传输与健康协议

    结论:panda 的 Python API 不是一组只读查询函数,而是一条会改变设备状态的维护接口:Panda() 构造时立即调用 connect(),连接成功后会读取健康与协议版本、重置 CAN 通信、关闭三路总线的 CAN FD 自动切换并写入默认速率。CAN 传输层用六字节头加 XOR 校验组织报文,返回帧把总线号加 128、被拒帧加 192。XOR 与版本检查只检测传输格式与意外损坏,不是对报文来源的密码学认证,也不能替代 panda MCU 的车型 TX 约束。本文为固定源码快照 c8fb906 的静态阅读,未连接设备、未发送 CAN。

    连接不是只读操作

    Panda.__init__() 可按序列号或命令行选择设备,并立即调用 connect()。连接先试 USB 后试 SPI:USB 侧匹配 VID/PID、读取序列号并可能独占接口;SPI 侧检查 bootstub 状态与协议版本。没有设备时 connect() 抛异常,wait=True 会重试。close() 关闭句柄及 USB context,reset() 发送设备复位请求并可重连。

    连接成功后,驱动会读取 MCU 的健康与 CAN 包版本;默认 disable_checks=True 会禁用心跳检查和省电,随后重置 CAN 通信、关闭三个总线的 CAN FD 自动切换并写入默认 CAN 速率。因此普通 Panda() 本身就带有设备状态副作用,不适合作为插件的「探测对象」。正式 pandad 的 Python 包装器只在健康检查与固件维护时临时打开设备,运行时收发由 C++ pandad 承担。

    get_packets_versions() 读取两个协议版本,短响应时返回 (0,0);health()、can_health() 和 CAN 收发入口都用装饰器对比本地与设备版本,不匹配则拒绝解释数据或发送。这意味着自制 MCU 固件不能只实现同名端点,还必须保持头格式、健康结构布局与版本 hash 一致。

    CAN 包格式与传输

    pack_can_buffer() 只接受 DLC_TO_LEN 中列出的长度;头部编码 DLC、总线、FD、地址及 XOR 校验。USB 写入时按约 256 字节分块,SPI 不走这条分块开关。unpack_can_buffer() 验证校验和,跨传输的不完整帧会留在 can_rx_overflow_buffer 等下次拼接;返回帧把 bus 加 128,被拒绝帧加 192,不能误认作真实物理总线编号。

    can_send_many() 经版本检查后调用底层 bulkWrite,在短写时重复提交剩余字节;默认写超时为 10 ms,源码注明 timeout=0 意味无限。can_recv() 读取后重组帧,对 USB IO 与 overflow 错误会无限重试并休眠,函数自身没有总重试上限。发送函数没有处理 bulkWrite 返回零但不抛异常的情况,按控制流可能不前进,是否发生需用具体传输实现验证。

    环节 实现事实 边界
    pack_can_buffer() 头部含 DLC、总线、FD、地址与 XOR 未对车型允许地址、速率或驾驶状态裁决
    USB 写入 按约 256 字节分块 SPI 不走这条分块开关
    unpack_can_buffer() 验证校验和,残帧留待下次拼接 返回帧 bus 加 128,被拒帧加 192
    can_recv() IO/overflow 错误无限重试并休眠 函数本身没有总重试上限

    健康与配置 API

    health() 解码点火、controls_allowed、安全模式与参数、心跳丢失、RX 校验无效、缓冲溢出、故障和温度等,它是点时状态,不证明下一帧仍允许发送,也不能只看 controls_allowed 而忽略 safety mode 与 fault。can_health() 报告每总线的 bus-off、错误被动、收发丢帧、校验错误与速率/CAN FD 信息,其计数器是硬件/固件视角,不能代替车型信号新鲜度。

    配置类方法与维护类方法有明确的能力差别:set_safety_mode()、set_alternative_experience() 向 MCU 发送安全模式与体验标志请求,但 Python 方法本身不做车型许可判断;send_heartbeat() 会重新开启心跳检查。get_signature() 读取设备签名字节并与本地固件尾部比对,属于版本一致性比较,并不是单凭 Python 层就完成的固件可信启动验证。

    函数簇 返回/动作 信任边界
    health() 点火、控制许可、安全模式、心跳、溢出、故障、温度 点时状态,不能只看 controls_allowed
    can_health() 每总线 bus-off、错误被动、丢帧、校验错误、速率 计数器是硬件/固件视角,协议版本须一致
    set_safety_mode() 发送安全模式与体验标志控制请求 Python 方法未做车型许可,插件不应直接获得
    set_obd() 与 CAN 配置 切 OBD 复用、收发器使能、CAN/CAN FD 速率 对布线与收帧可见性有副作用
    get_signature() 读设备签名并与本地固件尾部比对 版本一致性比较,不等于固件可信启动

    面向自制硬件与插件的约束

    源码给出的设计建议(不是现有代码已实现)是:把设备接口分成枚举与只读健康、接收 CAN、发送 CAN、改写总线与安全模式、固件维护五级能力,只有受信任的单一网关持有发送、心跳和 safety mode 权限。自制网关至少要复现包格式、残帧处理、返回/拒绝标记、版本握手、错误计数和独立的 MCU TX 安全策略,并在异常时由硬件默认停止自动控制,而不是依赖 Python 进程及时恢复。

    还需要注意,pandad.py 的 Python 包装器会在固件签名不同或处于 bootstub 状态时调用 flash/recover,之后再启动 C++ pandad;它是会改写固件的恢复链,因此研究时不应把实例化 Panda 或运行包装器视为无害操作。协议 ABI、线束拓扑、CAN 速率及固件镜像应绑定到具体硬件版本,设备发现不得自动刷写或切换安全模式。

    常见问题

    为什么说 Panda() 一实例化就有副作用?

    因为它按序列号或 CLI 选设备后立即调用 connect();连接成功会读取运动健康与协议版本,默认 disable_checks=True 时禁用心跳检查与省电,并重置 CAN 通信、写入默认 CAN 速率。所以它不适合作为插件的「只读探测」入口。

    can_send_many() 与 can_recv() 会无限重试吗?

    can_recv() 对 USB IO 与 overflow 错误会无限重试并休眠,函数自身没有总重试上限;can_send_many() 在短写时重复提交剩余字节,默认写超时 10 ms,源码注明 timeout=0 表示无限。因此需要调用方自己设定超时与失败处理。

    CAN 包的 XOR 校验能证明报文来源可信吗?

    不能。XOR 与包版本检查用于检测传输格式错误或意外损坏,不是对 CAN 报文来源的密码学认证,也不能替代 panda MCU 的车型 TX 约束。pack_can_buffer() 本身不对车型允许地址、速率或驾驶状态做裁决。

    自制 MCU 固件只实现同名端点就够了吗?

    不够。Python 层用装饰器对比本地与设备协议版本,不匹配就拒绝解释或发送,所以固件还须保持头格式、健康结构布局及版本 hash 一致;此外还应复现残帧处理、返回/拒绝标记、错误计数与独立的 MCU TX 安全策略。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PANDA_PYTHON_DEVICE_API_DEEP_DIVE.md(取用了连接副作用、CAN 包格式与传输、健康与配置 API 分层、自制硬件约束)。
    • 项目 02 —— openpilot-research/PANDA_BOARD_DEEP_DIVE.md(关联的板级安全链专题,用于区分 Python 层能力与 MCU 执行门,本文未展开其函数细节)。

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

    更新记录

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

  • openpilot 系统底座:manager、cereal 与 msgq 分工

    结论:openpilot 的系统底座由四类互不等价的服务组成:manager 是进程生命周期的控制平面,cereal/msgq 是实时结构化消息数据平面,VisionIPC 单独传图像缓冲,Params 保存跨重启配置。不能把它们合并理解为一个「事件总线」,因为四者对可靠性、持久性和时序的承诺完全不同:msgq 是可被新写入覆盖的实时队列,而 Params 才承担持久化。本文为固定源码快照 c8fb906 的静态阅读,未在真实设备上启动完整系统、未验证断电与共享内存溢出的实际时序。

    控制平面与数据平面:四类服务的分工

    把 manager、cereal、VisionIPC 和 Params 当成同一类基础设施,会导致对可靠性和时序的错误假设。它们的职责边界在源码里是清晰分离的:manager 负责进程启停,cereal/msgq 负责实时消息,VisionIPC 负责图像缓冲的文件描述符传递,Params 负责跨重启的键值配置。

    层 代表入口 语义边界
    manager 控制平面 manager_init()、manager_thread()、ensure_running() 订阅 deviceState、carParams、pandaStates,按 started 与点火边沿管理进程;状态消息是报告,不是安全固件许可
    cereal/msgq 数据平面 new_message()、PubMaster.send()、SubMaster.update() 实时结构化消息;新插件若依赖每帧必达,需自建可靠传输,不能把该队列当事务消息系统
    VisionIPC VisionIpcServer::listener()、VisionIpcClient::recv() 只传图像缓冲索引与元数据,图像内容不复制进普通 Event
    Params Params::put()、Params::get()、Params::clearAll() 持久配置与少量控制状态,按生命周期标志清理;不适合替换高频 cereal 消息

    启动与进程管理

    manager_init() 清理按生命周期标志定义的参数,填默认值和版本/设备信息,建立共享内存路径并注册设备;注册失败会抛异常,因此不能假定 manager_thread() 已经开始。manager_thread() 订阅 deviceState、carParams、pandaStates,按 started 与点火边沿清理参数、更新 IsOffroad、调用 ensure_running() 并发布 managerState。NOBOARD、BLOCK 等配置会排除对应进程。

    入口 已核对行为 关键边界
    ensure_running() 用 enabled、排除列表及 should_run(started, params, CP) 判定每个进程的目标状态,先停后启 条件按每次循环重算,不能假定 44 个进程同时运行
    ManagerProcess.stop() 先发 SIGINT 或指定信号,等待后必要时 SIGKILL block=False 只发起停止,下次启动会等待停止完成
    PythonProcess.start() / NativeProcess.start() 分别用多进程包装 Python 模块与原生可执行文件 插件进程若纳入管理,需保留进程状态与退出语义
    DaemonProcess.start() 用持久 PID 与 /proc 命令行子串判断是否复用守护进程 不设置普通 proc 对象,stop() 为空

    进程的入口和运行条件由 process_config.py 的静态表决定,因此插件化不能只实现动态 import,还需定义能力声明、依赖、健康状态及受管退出协议。收到卸载、关机、重启参数会退出循环,manager_cleanup() 清理受管进程后由 main() 调硬件关机或重启。此外,manager_thread() 仅在 deviceState 的存活、平均频率和有效位检查通过时更新电源监控看门狗文件,但进程启停仍读取缓存的 started,不受该健康门直接阻断。

    消息契约与底层队列

    log.capnp 与 services.py 分别定义 Event 的结构字段与主题清单、标称频率、日志选项和队列容量,改 schema 与服务表要同步消费者。new_message() 新建消息默认 valid=False 并写单调时钟 logMonoTime,生产者需要显式设置有效性;SubMaster.update() 轮询指定主题,更新帧号、消息时间与有效位;SubMaster.all_checks() 合并存活、平均频率和消息有效位,模拟模式会放宽部分时序检查。频率追踪以长期和近期平均频率判断接收速率,不是硬实时截止时间证明。

    底层 msgq_new_queue() 在 /dev/shm(macOS 为 /tmp)建立 mmap 队列,msgq_msg_send() 检查发布者 UID 后向环形缓冲写入长度与负载并唤醒读者,msgq_msg_recv() 在读者被逐出或读指针失效时重置到当前写指针,旧消息不保证可补读。SubMaster 的 conflate=True 订阅意味着主要消费最新状态。因此 services.py 里的频率只是契约/健康检查参考,并不表示所有消息在该频率上严格同步。图像则单独走 VisionIPC:VisionIpcServer::send() 只向 msgq 发送缓冲索引、服务器 ID 和帧附加元数据,VisionIpcClient::recv() 用服务器 ID 与索引确认缓冲仍属于当前服务端。

    Params 与生命周期配置

    Params 是 C++ 实现的 Python 绑定。底层 Params::put() 先写临时文件、fsync、加锁、原子 rename 并同步目录;Params::get() 可阻塞等待非空值;Params::clearAll() 依键标志清除指定生命周期数据,并清理未登记键。它适合持久配置和少量控制状态,不适合替换高频 cereal 消息。插件应声明键名、类型、默认值、清理标志与迁移版本,避免私自写未登记键后被 clearAll() 删除。

    设备启动门、记录与空间回收

    hardware_thread() 订阅 panda、外设、定位与辅助驾驶状态,生成 deviceState;点火/连接状态形成行车条件,热状态、启动训练、条款、空间、系统启动完成、更新状态及设备注册等构成启动门。进入行车后仍持续检查点火与严重过热,panda 超时会清除点火条件。需要强调:deviceState.started 是 manager 启停 onroad 进程的信号,不是 selfdriveState.enabled 或 panda 的 controls_allowed。硬件移植必须实现 HardwareBase 的平台能力并处理相机、IMU、温度、供电及看门狗;当前硬件选择仅按 /AGNOS 在设备与 PC 两个实现间切换。

    组件 关键函数 边界
    loggerd loggerd_thread()、rotate_if_needed() 记录服务消息并按编码器就绪或超时旋转分段,无相机包时有兜底旋转
    encoderd encoder_thread() 图像流编码与分段配合;日志索引和视频负载是不同文件/主题
    uploader Uploader.list_upload_files()、upload() 跳过锁定或已上传文件;计费网络限制视频上传
    deleter deleter_step() 空间不足时优先删除可回收旧分段,避开锁定目录

    日志、上传与删除是旁路服务,不应成为转向/制动控制的同步前置条件。插件若新增高频主题,还需决定是否记录、抽样、队列容量与磁盘配额——仅在 services.py 注册主题并不足以保证长期可运维。

    常见问题

    msgq 队列能保证每条消息都被消费到吗?

    不能。msgq 是可被新写入覆盖的实时队列,读者被逐出或读指针失效时会重置到当前写指针,旧消息不保证可补读。依赖每帧必达的插件应自行定义可靠传输或落盘机制。

    deviceState.started 等于车辆已被使能吗?

    不等于。started 是 manager 启停 onroad 进程的信号,与 selfdriveState.enabled 和 panda 的 controls_allowed 是不同层概念,不能相互替代。

    为什么 Params 不适合传高频数据?

    Params 每次写入都要写临时文件、fsync、加锁并原子 rename 并同步目录,面向持久配置而非高频流。高频结构化消息应走 cereal/msgq。

    插件的参数键可以随便写吗?

    不建议。Params::clearAll() 会依生命周期标志清理未登记键,插件应声明键名、类型、默认值、清理标志与迁移版本,否则可能被意外删除。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/SYSTEM_FOUNDATION_DEEP_DIVE.md(取用了控制平面与数据平面分工、启动与进程管理、消息契约与 msgq 队列、Params 生命周期,以及设备启动门与旁路服务)。
    • 项目 02 —— openpilot-research/HARDWARE_BACKEND_USB_SNAPSHOT_CONTRACT_DEEP_DIVE.md(仅用于确认硬件选择与 deviceState 的衔接位置)。

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

    更新记录

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

  • PowerMonitoring 电量估算与 11.8 V 低压关机契约

    结论:PowerMonitoring 的「电量」是估算值而非 BMS 读数:容量单位是 uWh,代码里的 30,000,000 uWh 只是设定的 30 Wh 上限,不是车辆电池实际容量;Params 缺失或过低时按 3 Wh 起步。估算器的失败路径不向调用方返回失效状态——它在积分前裁剪容量、缺测时不更新低通电压、写盘失败时延后积分时钟,而关机判定表达式不检查这些输入的有效位与年龄。它只写一个 DoShutdown 请求位,本身不切断电源;真正断电由 manager 接力。固定快照基线为 c8fb906。

    输入、容量估算与数据来源

    估算器读 CarBatteryCapacity,初始容量至少 3,000,000 uWh;电压滤波器初值 12,000 mV。电量积分的核心式子是 power_used = power_W * 1,000,000 * elapsed_h:驻车时按硬件功率积分耗电并减少估算容量,点火时按固定的 45 W 增加估算容量,并把本次驻车累计耗电清零。

    功率来源因平台而异。comma 平台从 hwmon1/power1_input 读微瓦并转瓦;read_param_file() 读取失败默认返回 0,PC 基类也返回 0。这意味着「功率缺测」和「真实零功率」在数值上无法区分——这是估算偏差风险的来源之一,而不代表设备上已测到该偏差。

    入口 实际行为
    PowerMonitoring.__init__() 读 CarBatteryCapacity,容量至少 3,000,000 uWh;电压滤波器初值 12,000 mV。
    calculate() 电压可用时做固定增益低通、容量裁剪与约 10 秒保存;点火按 45 W 充电,驻车按硬件功率积分;异常只记录,不返回失效状态。
    _perform_integration() 用单调时间差把瓦换算为 uWh,增加驻车耗电并减少估算容量;负耗电抛错后在内部被捕获。
    should_shutdown() 根据驻车时长、低通电压、容量、点火、线束、Params 开关和启动历史返回布尔判定。
    hardware_thread() peripheralState.pandaType=unknown 时传 voltage=None;把估算值写 deviceState,判定后写 DoShutdown。
    manager.main() 看到 DoShutdown 后清理受管进程,再调用 HARDWARE.shutdown()。

    采样时序与低通滤波的实际行为

    calculate() 在积分之前把容量裁剪至 [0, 30 Wh],所以一次积分之后可能暂时高于 30 Wh 或低于 0;现有上下限测试也按一帧越界的行为断言。发布到 deviceState.carBatteryCapacityUwh 时又用 max(0, ...) 遮掉负值,这不代表内部估算已回到零。

    当 voltage=None,函数清空 last_measurement_time、next_pulsed_measurement_time 和 power_used_uWh,但不更新容量、低通电压或瞬时电压;下一次有效电压只重新设置积分时间,不补算缺测区间。离线探针确认旧的低通电压可在缺测后继续触发 should_shutdown(),因为关机表达式不检查电压年龄。

    电压低通系数固定 0.011,每次有效 calculate() 调用推进一次,代码不按实际 dt 重算系数;探针把两次有效样本间隔拉长到 1000 秒,第二次仍只推进一次固定系数。注释的约 45 秒时间常数依赖近似稳定的调用频率。hardware_thread() 名义上按约 2 Hz 更新,但点火边沿会额外触发,调度延迟也会改变低通的实际物理时间。新平台若更换采样频率或电压测点,11.8 V 阈值和滤波参数必须重新标定。

    关机布尔门与边界

    若 offroad_timestamp is None,函数立即返回 False,即使 ForcePowerDown=True 也不走后续判定。普通关机要求:驻车超过 300 秒、未点火、已连接车端线束、DisablePowerDown=False,并且三个触发条件至少一项为真。

    条件 阈值 / 备注
    驻车时长 超过 300 秒(普通门共同前提)
    触发条件一 驻车超过 30 小时
    触发条件二 低通电压低于 11.8 V 且驻车超过 60 秒
    触发条件三 估算容量不大于零
    强制位 ForcePowerDown 以 OR 加入,可绕过普通门,但仍受最终启动门约束
    最终启动门 started_seen 或系统单调时间已超过 3600 秒

    ForcePowerDown 在这些普通门之后以 OR 方式加入,故可绕过点火、线束、禁用关机、五分钟和三个触发条件;但仍受最末尾的 started_seen or now > 3600 约束。离线探针确认强制位在点火中、无车端线束且禁用普通关机时仍可通过,但无驻车时间或未过最终启动门时不能通过。这里的 now 是系统单调时钟绝对值,不是本进程或本次开机管理器的局部计时;启动后已超过一小时的设备即使 started_seen=False 也通过该门。

    两条失败路径都不提供失败区间的功率历史:put() 抛错时积分时刻都不推进,下次成功时把当下功率应用到从上次积分起的整段间隔;负功率会让 _perform_integration() 记录错误并保留旧积分时刻,后续正功率同样覆盖这段间隔。hardwared 写入 DoShutdown 后由 manager 接力;该 Params 位是动作请求,不是电源已经切断的回执。

    面向新硬件与插件化的接口建议

    新硬件电源插件应返回 {voltage, current_power, sample_time, valid, source},明确瓦数符号、供电位置和校准精度;缺测不能静默映射为 0 W。估算器与关机策略应分离,低压门使用有界年龄的有效电压、实际 dt 和可追踪阈值,强制关机另设授权与审计。

    台架至少测试点火抖动、USB / 线束脱落、功率传感器读失败、低压瞬降、长时间驻车、manager 重启、请求已写但硬件未关机,以及真实 12 V 电气负载下的最坏关机延迟。现有单测覆盖无 peripheral、点火充电、驻车积分、上下限、低压、禁用关机、线束与延迟门;本轮未运行。

    常见问题

    30 Wh 是车辆电池的真实容量吗?

    不是。30,000,000 uWh 是代码里的设定上限,不是车辆电池 BMS 读数;若 Params 缺失或过低,初始化按 3 Wh 起步。容量单位是 uWh,且积分前会裁剪到 [0, 30 Wh],一次积分后可能暂时越界。

    为什么功率读不到时估算不会停?

    因为两种平台行为不同:comma 读 sysfs 失败默认 0 W,有效电压但功率缺测会被当成零耗电;若平台实现直接抛错,calculate() 捕获后不推进积分时刻,恢复后的一个功率读数会被应用到整个失败间隔。两条路径都不提供失败区间的功率历史。

    电压缺测后还能触发低压关机吗?

    可能。当 voltage=None,函数不更新低通电压;离线探针确认旧的低通电压可在缺测后继续参与 should_shutdown(),因为关机表达式不检查电压年龄。反过来,缺功率读数会让容量消耗被低估。

    写了 DoShutdown 就等于断电了吗?

    不等于。DoShutdown 是一个动作请求位,PowerMonitoring 本身不切断电源;hardwared 写入后由 manager 看到并清理受管进程,再调用 HARDWARE.shutdown()。请求已写但硬件未关机属于必须单独台架验证的场景。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/POWER_MONITORING_CAPACITY_SHUTDOWN_CONTRACT_DEEP_DIVE.md(取用了容量估算公式、采样时序、低通滤波行为、关机布尔门与硬件插件建议)。

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

    更新记录

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

  • Hyundai 经典 CAN panda safety:三模式权限矩阵

    结论:Hyundai 经典 CAN panda safety 把「标准雷达 SCC / 标准相机 SCC / Legacy」三种模式做成一张权限矩阵:同一辆车的按钮 RX 固定在 bus 0,而 TX 可能落在 bus 0 或 bus 2,纵向能力只在调试构建生效。Legacy 也不是全面关闭 RX 校验,放宽的只有 0x386 与 0x394,0x260、0x4F1、0x421 仍保留各自的校验或计数门。以下为固定源码快照 c8fb906 的函数级静态分析,不是目标固件或实车验证;bus 号指单个 safety 实例的本地总线编号,车型、线束与 ECU 版本需另行确认。

    初始化与模式选择

    CarInterface._get_params() 在 hyundai 与 hyundaiLegacy 之间选择,并设置相机 SCC 与纵向能力。需要注意的是 openpilotLongitudinalControl 只是上层请求,不是固件准入证明。hyundai_init() 再依据纵向、相机 SCC、FCEV 标志选择 RX 检查表与 TX 白名单;hyundai_legacy_init() 使用 Legacy RX 表,并强制关闭纵向和相机 SCC,仅保留基础 TX 表。

    共享的 hyundai_common_init() 在非调试构建中强制 hyundai_longitudinal=false。也就是说纵向链在源码层面就带着 ALLOW_DEBUG 编译门,不能由 Python 侧 CarParams 位推定量产固件可发。

    模式 SCC12 RX CLU11 TX 纵向/附属 TX 关键差异
    标准雷达 SCC、非纵向 bus 0 bus 0,4 字节 无 SCC TX 巡航接合沿使能;按钮只许受门控的 Resume/Cancel。
    标准相机 SCC、非纵向 bus 2 bus 2,4 字节 无 SCC TX 按钮 RX 仍是 bus 0,不能把 TX 所在总线误当按钮输入来源。
    标准雷达 SCC、纵向 不列为必需 RX bus 0,4 字节 SCC11/12/13/14、FRT_RADAR11、FCA11/12、0x7D0 在 bus 0 LONG 仅 ALLOW_DEBUG 有效;不经原车 SCC 接合沿使能。
    标准相机 SCC、纵向 不列为必需 RX bus 2,4 字节 SCC11/12/13/14、FRT_RADAR11 在 bus 0 相机 SCC 纵向不使用雷达静默诊断白名单。
    Legacy bus 0 bus 0,4 字节 无 SCC TX 强制非纵向、非相机 SCC;仅部分 RX 的完整性检查放宽。

    所有模式均可发送 bus 0 的 0x340 LKAS11 与 0x485 LFAHDA_MFC。纵向 TX 表中的 SCC11/12/13/14 对 bus 0 标记 check_relay=true;若原 ECU 在该总线继续发送同地址帧,共享继电器故障逻辑可阻断 TX。safety_fwd_hook() 只提供通用 bus 0↔2 转发,并静态阻断将 check_relay 地址送往本机 TX 总线,这不是 ECU 已被成功静默的证明。

    RX 完整性与状态消费

    各地址使用不同的计数位与半字节校验:0x386 使用位计数/XOR,其余适用帧使用半字节和。RX 完整性检查先于车型钩子执行,通用 safety_rx_hook() 只在消息匹配、校验有效且属于 RX 表时才调用 hyundai_rx_hook()。

    帧 检查表 实际消费
    0x260 EMS16 或 0x371 E_EMS11 同一备选组;0x260 检查 2 位计数和校验,0x371 忽略二者 ICE 从 0x260 读油门,EV/HEV 从 0x371 读油门。
    0x386 WHL_SPD11 标准模式校验加 4 位计数;Legacy 忽略二者 只用左前、右后轮速判断移动,阈值原始值 12。
    0x394 TCS13 标准模式校验加 3 位计数;Legacy 忽略二者 字节 5 的 DriverOverride 等于 2 判制动。
    0x251 MDPS12 忽略校验/计数,但监视存活 11 位驾驶员扭矩进入共享采样窗口。
    0x4F1 CLU11 忽略校验、检查 4 位计数 bus 0 上解析按钮和主开关,更新近期交互或纵向按钮边沿。
    0x421 SCC12 只在非纵向为必需;校验加 4 位计数 按相机 SCC 标志从 bus 2 或 bus 0 读取 ACC 模式。
    0x91 FCEV 油门 仅特定标准雷达 SCC 或纵向分支附加;忽略校验/计数 FCEV 标志下读取字节 6;标准相机 SCC 非纵向表没有该附加检查。

    FCEV 分支中 0x91 更新油门,但 0x260 的 ICE 回退条件仅排除 EV/HEV;若车辆也出现 0x260,后续该帧仍可能改写油门状态,须用目标车报文核对,不能把单帧观测当成最终状态。

    TX 字段边界与使能条件

    转向 0x340 从 11 位字段还原扭矩与请求位:普通/ALT_LIMITS/ALT_LIMITS_2 的最大扭矩分别为 384/270/170,每帧上升分别为 3/2/2、下降为 7/3/3,均使用实时差 112、驾驶员扭矩余量 50 与请求位节拍容忍;两种替代标志同时存在时 ALT_LIMITS_2 优先。非纵向的 0x4F1 只允许 controls_allowed 时 Resume,或 cruise_engaged_prev 时 Cancel;纵向模式跳过整个按钮 TX 分支,白名单内的 0x4F1 不受该专属按钮码限制,这是插件权限设计必须额外收紧的差异。

    纵向加速度 0x421 同时核对 aReqRaw 与 aReqValue,单位是 0.01 m/s²,活动范围为 -350 到 200,并禁止 AEB 减速度命令、AEB 请求和停止请求。共享加速度检查允许未激活编码,不能仅用数值落在范围内推断准入。雷达 SCC 纵向表包含 0x38D FCA11,专属钩子禁止其减速度/AEB 激活字段;0x483 FCA12 仅有地址/总线/长度白名单;0x7D0 仅雷达 SCC 纵向可发送,载荷必须是 02 3E 80 00 00 00 00 00。SCC11/13/14、FRT_RADAR11 和 HUD 没有该模式的逐字段校验,白名单准入不代表 ECU 接受,也不代表原车 AEB、FCW 或仪表状态保持不变。

    面向自制硬件的插件契约

    源文档给出的是一组建议设计而非已实现能力:车型身份插件应输出带证据的标准/Legacy、SCC 来源、动力类型与转向限制,未知或冲突组合不授予 TX;总线插件同时报告 RX/TX 的物理接口与本地 bus,尤其记录相机 SCC 模式下按钮 RX bus 0、TX bus 2 的差异。

    纵向插件只能提交控制意图;可信任网关需核对构建类型、MCU mode/param 读回、ECU 静默回执、原车 AEB 功能与失联降级,再交由独立 MCU safety 门裁决。对纵向 CLU11、FCA12、HUD 与 SCC 附属帧应另设最小载荷模板和速率配额,不能因原模式白名单存在就向普通插件开放原始 CAN 写入。现有 test_hyundai.py 覆盖标准/Legacy、替代转向限值、相机 SCC、FCEV 与纵向 AEB/诊断路径,但本文未运行这些单测,模型实体、目标固件、台架及实车行为仍未验证。

    常见问题

    Hyundai Legacy 模式是不做 RX 校验吗?

    不是。Legacy 只放宽 0x386 和 0x394 的校验与计数检查;0x260、0x4F1、0x421 仍保留各自的校验或计数门。同时它强制关闭纵向和相机 SCC。

    相机 SCC 模式下按钮输入在哪个总线?

    按钮 RX 仍是 bus 0,只是 CLU11 的 TX 落在 bus 2。源码明确说明不能把 TX 所在总线误当成按钮输入来源。

    Hyundai 纵向控制是不是打开参数就能用?

    不是。LONG 仅在 ALLOW_DEBUG 编译条件下有效,共享 hyundai_common_init() 在非调试构建中强制关闭纵向。Python 侧 CarParams 位不能作为量产固件可发的证明。

    0x7D0 的载荷能自由填吗?

    不能。源文档说明 0x7D0 仅雷达 SCC 纵向可发送,载荷必须是固定的 02 3E 80 00 00 00 00 00。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/HYUNDAI_CLASSIC_PANDA_SAFETY_DEEP_DIVE.md(模式权限矩阵、RX 完整性表、TX 字段边界与插件契约)。
    • 项目 02 —— openpilot-research/ 同目录 Hyundai/Honda 品牌总览(SCC 与纵向能力作为相邻边界参考)。

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

    更新记录

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

  • Hyundai 控制输出:经典 CAN 与 CAN FD 两条构帧路径与独立安全门

    结论:Hyundai 的控制输出不是一条「转向命令」链路,而是同一份车型控制器代码按经典 CAN / CAN FD、原车纵向 / 自控纵向、LKA / LFA 架构分支,形成不同候选帧,再交给 panda 固件的 hyundai_tx_hook 或 hyundai_canfd_tx_hook 独立复核。主机侧 CANPacker 编码成功、CarOutput.accel 回报正确、tester-present 发出,都不等于 ECU 接受。下述节拍以 card 目标 100 Hz 且每轮调用一次控制器为前提,是代码名义节拍,不是实测总线频率。

    公共控制周期:力矩、加速度与请求位

    每一控制轮,CarController.update() 先把 actuators.torque 乘以车型 STEER_MAX,再以最近一次软件输出和 CS.out.steeringTorque 调用驾驶员力矩/变化率限制。横向未活动时力矩清零;经典 CAN 分支在横向活动但请求位被切断时还会设置 torque_fault,CAN FD 分支没有这个额外字段。这是两条分支在同一控制器里的第一处实现差异。

    common_fault_avoidance() 在转角绝对值达到 85 度时管理请求位,目的在于避免 EPS 长时间大角度带扭矩故障。加速度只做 [-3.5, 2.0] m/s² 夹限,再传给具体纵向报文;CarOutput 回报的 accel 就是这一软件夹限值,不是 CAN FD 内部 aReqValue、MCU 发送回执或车辆实测加速度。

    诊断保活方面,若启用 openpilot 纵向且非 CANFD_CAMERA_SCC,每 100 帧向经典 CAN 雷达 0x7d0、CAN FD E-CAN 雷达 0x7d0 或 LKA 架构 ADAS ECU 0x730 形成一条 tester-present 候选。它的目的是维持此前请求的 ECU 静默,不能证明 ECU 已静默;CarInterface.init() 使用通用 disable_ecu(),其成功/失败返回值没有被 card.ControlsReady 闭环消费。取消请求持续时 cancel_counter 逐轮加一,解除请求即归零——源码注释指出部分车辆的取消键也可能是暂停/恢复键,因此原车纵向的按钮取消被延迟。

    经典 CAN 输出:LKAS11 与 SCC 家族

    LKAS11 每控制轮形成候选:继承 CS.lkas11 中一批原车相机字段,覆盖转向力矩、请求位、HUD 警告、车道线和计数,再按车型选择 CRC8、前 6 字节和、或前 6+末字节和三种校验之一。它不是泛用的「转向命令」结构,不同车型的显示字段也有例外,process_hud_alert() 对部分 Genesis 警告采用不同编码。原车纵向下,持续取消超过 10 帧后发送一条 CLU11 取消候选;恢复请求则在距上次按钮帧大于 0.1 秒时一次追加 25 个相同恢复候选,而 last_button_frame 只在间隔至少 0.15 秒时更新。

    自控纵向每偶数帧形成 SCC11、SCC12、SCC14;USE_FCA 且非 CAMERA_SCC 时还生成 FCA11。SCC12 的 aReqRaw / aReqValue 都取软件加速度,计数器及 nibble checksum 由构帧函数处理;SCC14 传入 jerk 上限和 HUD 前车状态。非 FCA 路径在 SCC12 设置 AEB disabled 字段,FCA 路径在 FCA11 设置相应禁用状态。

    经典 CAN 报文 触发条件与节拍 关键边界
    LKAS11 每控制轮 覆盖力矩/请求位/HUD;校验方式按车型三分支
    CLU11 取消 原车纵向且持续取消超过 10 帧 「最多 10 Hz」注释不等于严格上限
    恢复候选 ×25 距上次按钮帧 > 0.1 秒 不构成 25 条 ECU 接受证明
    SCC11 / SCC12 / SCC14 自控纵向每偶数帧 SCC12 双加速度字段;SCC14 传 jerk 与前车状态
    FCA11 USE_FCA 且非 CAMERA_SCC 设置制动执行禁用状态
    SCC13 / FCA12 5 Hz 选项帧 额外选项帧,非原车 AEB 保留保证
    FRT_RADAR11 2 Hz 选项帧 同上,单独验证
    LFA 图标 SEND_LFA 平台每 5 帧 仅图标候选

    经典 panda hook 对 LKAS11 限扭矩/请求位,对 SCC12 检查两路加速度并禁止 AEB 执行字段,对 FCA11 阻止制动执行位;诊断地址只允许固定 tester-present 载荷。不同 hyundai、hyundaiLegacy、相机 SCC 和纵向模式选择不同 RX 检查与 TX 白名单,软件字段编码正确仍可能被 MCU 拒绝。

    CAN FD 输出:LKA/LFA 路由与 SCC_CONTROL

    CanBus 按 LKA/LFA 转向架构决定 A-CAN 或 E-CAN。LKA 平台生成 LKAS / LKAS_ALT,仅在自控纵向时同时补 LFA,其他平台直接生成 LFA。LKA 平台每 5 帧还用最近缓存的 CAM_0x2a4 / CAM_0x362 字段构造「无车道线」报文来抑制原车 LFA——这属于 ADAS 能力修改,应单独验证缓存年龄与原车功能影响。图标候选按 LKA/纵向组合每 5 帧输出;若 flag 开启,LKA 平台可生成 SPAS1 / SPAS2 转向灯候选。

    自控纵向每偶数帧生成一条 SCC_CONTROL:aReqRaw 用夹限后的目标,aReqValue 用与上次记录 accel_last 相差不超过 5/50 的值;accel_last 随后记录当前原始目标,而不是编码出的 aReqValue。所以这个运算不能简单称为持续的输出速率限制,阶跃目标须回放核对两个字段和 ECU 响应。LKA 架构另发 ADRV 系列报文以配合禁用 ADAS ECU,LFA 架构按偶数帧生成 AEB disabled 图标报文。

    CAN FD 路径 生成条件 需单独验证的点
    LKAS / LKAS_ALT LKA 架构每轮 总线归属(A-CAN/E-CAN)
    LFA 自控纵向补发,或非 LKA 平台直接生成 架构判定是否正确
    无车道线抑制报文 LKA 平台每 5 帧 缓存 CAM 报文年龄、原车功能影响
    SCC_CONTROL 自控纵向每偶数帧 aReqRaw 与 aReqValue 的 5/50 差值语义
    ADRV 系列 LKA 架构 与禁用 ADAS ECU 的配合
    AEB disabled 图标 LFA 架构每偶数帧 仅图标,不等于 AEB 状态
    SPAS1 / SPAS2 flag 开启且 LKA 平台 本快照未见设置该 flag 的车型

    原车纵向 CAN FD 用按钮或 SCC_CONTROL 取消:备用按钮平台直接复用最近 CS.cruise_info 的计数/校验及状态字段,把 ACCMode 改为 4、加速度置零;标准按钮平台须取消持续超过 10 帧、距上次按钮超过 0.25 秒,才一次追加 20 个相同的取消按钮候选。标准按钮的恢复也可一次追加 20 个候选;备用按钮平台的恢复分支只是 pass,本快照未实现。MCU safety 仍要求原车纵向 SCC_CONTROL 为取消模式且加速度为零。

    插件化与硬件验收边界

    把转向、纵向、巡航按钮、HUD、ADAS 抑制和诊断保活声明为不同能力,各自绑定平台、DBC、线束、节拍及安全模式,是这套实现的自然结论。车型控制插件只提交结构化候选;唯一受信任网关验证 CAN 地址、物理总线、长度、年龄、限幅和 ECU 静默状态,并由独立 MCU 保留不可绕过的 TX safety。禁止把 CANPacker 成功、CarOutput 回报或 tester-present 发送等同于 ECU 接受。

    无车验证应覆盖经典 CAN/CAN FD、LKA/LFA、原车/自控纵向、相机 SCC、备用按钮、AEB 图标、缓存断流、大转角请求位、加速度阶跃和诊断失效,再逐帧与 DBC、panda hook 对照。CANFD_ENABLE_BLINKERS 还存在对 0x7b1 的保活及 SPAS 构帧代码,但本快照静态平台表未见设置该 flag 的车型,不能把代码分支当成已启用的实车能力。

    常见问题

    为什么 CAN FD 里的 aReqValue 不能当作真实加速度限幅?

    因为 SCC_CONTROL 的 aReqValue 取的是与上次记录 accel_last 相差不超过 5/50 的值,而 accel_last 记录的是当前原始目标而非编码结果。这个运算在阶跃目标下不是持续速率限制,须回放核对两字段与 ECU 响应;CarOutput 回报的 accel 也只是软件夹限值,不是车辆实测加速度。

    tester-present 发出去,是否说明雷达 ECU 已被静默?

    不代表。每 100 帧形成的 tester-present 候选只用于维持此前请求的 ECU 静默状态,本身不能证明 ECU 已进入静默;CarInterface.init() 使用的 disable_ecu() 返回值也没有被 card.ControlsReady 闭环消费,因此不能由「帧已构造」推断「ECU 已接受」。

    经典 CAN 与 CAN FD 的转向输出差在哪?

    经典 CAN 每控制轮形成 LKAS11,并在横向活动但请求位被切断时设置 torque_fault;CAN FD 按 LKA/LFA 架构生成 LKAS/LKAS_ALT 或 LFA,没有这个额外字段。此外 CAN FD 的 LKA 平台还会每 5 帧构造「无车道线」报文抑制原车 LFA,属于 ADAS 能力修改。

    备用按钮平台的恢复功能是否已实现?

    未实现。源文档明确:原车纵向 CAN FD 中,标准按钮平台的恢复可一次追加 20 个候选,而备用按钮平台的恢复分支只是 pass,本快照未实现。取消分支则复用最近 CS.cruise_info 的字段并把 ACCMode 改为 4、加速度置零。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/HYUNDAI_CONTROLLER_CAN_OUTPUT_DEEP_DIVE.md(取用了公共控制周期、经典 CAN 与 CAN FD 构帧节拍、panda hook 复核点与插件化边界)。
    • 项目 02 —— openpilot-research/CAR_PROTOCOL_IMPLEMENTATIONS.md(仅用于确认 Hyundai 品牌目录与 CAN/CAN FD 链路的总体位置)。

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

    更新记录

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

  • 19 条迁移规则:migrate_all() 的合成字段不等于原始测量

    结论:process_replay 的历史日志迁移是离线回放输入的兼容层,不是车辆实时消息改写路径。migrate_all() 默认列出 15 条规则,manager_states 加 1 条、panda_states 加 2 条、camera_states 加 1 条,共最多 19 条。核心判定有两条:迁移产出的是合成字段,不等于原始测量;而 migrate() 让所有规则读取同一份原始列表,前一条的替换/新增不会成为后一条的输入——这是兼容性批处理,不是依次变换的管道。本文仅做静态源码与 Python AST 检查,本机缺 capnp,未运行日志迁移、整段回放、模型或实车。

    调度契约

    replay_process() 始终打开 manager 迁移,并按进程发布主题开启 panda 与相机迁移。migrate() 先把输入全量转列表并按原始 which() 分组:每条规则由 @migration 标注输入主题和可选 product,原始日志中只要出现一次该 product,整条规则跳过。结束时先替换、再倒序删除、再追加,最后按 logMonoTime 稳定排序。

    开关 规则数变化 触发条件
    默认 15 条 migrate_all() 的基础规则集
    manager_states +1 条 replay_process() 始终打开 manager 迁移
    panda_states +2 条 按进程发布主题开启
    camera_states +1 条 按进程发布主题开启

    由于每条规则都独立读取同一份原始列表,规则之间的书写顺序在语义上无关紧要;真正决定结果的是「哪些 product 出现在原始日志里」以及各规则自身的触发条件。也正因为如此,同一份日志在不同开关组合下会得到不同的迁移结果,比较前必须先固定开关集合。

    此外,规则跳过是「整条跳过」而非「逐条补齐」:新主题一旦在原始日志中出现,对应旧消息在整个路段都不会被迁移生成。

    逐函数规则表

    下表是按规则入口整理的全量清单。它们的共同特征是:多数规则只在「原始日志缺少对应新主题」时才生成消息,一旦新主题已存在就整条跳过;生成的字段往往直接置 valid=True 或沿用旧时间戳,因此回放里看到的「有效」并不等于原始采集时该值真实有效。

    规则入口 输入到输出或修改 证据/注意点
    migrate_sensorEvents() 旧聚合 sensorEventsDEPRECATED 拆为加速度、陀螺仪、光、磁力、温度消息,删旧消息 新消息无条件置 valid=True,即使旧 Event 无效
    migrate_carParams() 用 opendbc MIGRATION 重写车型指纹,并把每项 carFw.brand 设为当前品牌 为回放统一身份,不是 ECU 固件真实性校验
    migrate_gpsLocation() 当 flags==1 且无 fix 时补 hasFix=True,覆盖内部/外部 GPS 两主题 只做布尔兼容,不重新评估 GNSS 质量
    migrate_deviceState() 取首条 initData.deviceType 写到所有 deviceState 缺任一主题时不操作
    migrate_carOutput() 从旧 carControl.deprecated.actuatorsOutput 合成 carOutput 原始日志只要已有一条 carOutput 就跳过全部缺失时点
    migrate_controlsState() 从旧 controlsState.deprecated 合成 selfdriveState 的使能、告警等字段 合成状态沿用旧 valid/时间
    migrate_carState() 以先前 controlsState.deprecated.vCruise 修正 carState.vCruise/cluster 只在差值大于 0.1 时改;依赖原始遍历顺序
    migrate_liveLocationKalman() 旧姿态、速度、加速度、角速度及健康位转 deviceMotion 缺失向量改为 NaN,时间戳取 Event 的 logMonoTime
    migrate_deviceMotion() 若所有已有 deviceMotion.timestamp 均为 0,逐条补 logMonoTime 混合零/非零时整条规则不操作
    migrate_liveTracks() 旧 liveTracksDEPRECATED 换为 radarTracks.points 仅保留 trackId/dRel/yRel/vRel 等子集
    migrate_driverAssistance() 每条 longitudinalPlan 旁合成空 driverAssistance 存在并不证明旧算法输出了该辅助结果
    migrate_drivingModelData() 从 modelV2 提取帧、动作、车道线与轨迹多项式 meta 列表重复 laneChangeState,遗漏 laneChangeDirection
    migrate_onroadEvents() 旧事件列表换为新 onroadEvents,过滤 DEPRECATED 与无法映射枚举 空成员 RuntimeError 会打印并跳过
    migrate_driverMonitoringState() 旧 DMS 模式、注意力、事件映射为 vision/wheeltouch 策略、三级告警及 lockout 百分比与标定值由旧字段估算,不能当原生轨迹
    migrate_longitudinalPlan() 所有 aTarget==0 且存在 carParams 时,用 speeds/accels 估算 aTarget 与 shouldStop 若任一计划已有非零 aTarget,整条不操作
    migrate_managerState() 用当前 managed_processes 名单重建每条进程列表,统一置 running=True 合成结果不是当时真实进程健康快照
    migrate_pandaStates() 从旧单 panda 转列表,或修改现有列表最后一项的 safetyParam 与 alternativeExperience 需要 carParams,不能拿回放值当 MCU 实际配置读回
    migrate_peripheralState() 按原始 pandaState(s) 的时间与 valid 合成空 peripheralState 不能据此证明原设备供电/风扇等外围指标真实存在
    migrate_cameraStates() 从三路 EncodeIdx 的 frameId 找 segmentId,重建 CameraState 的 frameId/encodeId,缺 SOF 时估算 已有索引但某帧缺匹配则删除该相机状态

    从变换方向看,这些规则大致分三类:拆分与改名(如传感器事件、雷达轨迹、onroad 事件)、估算与合成(如纵向计划、驾驶员监测、外设状态与 carOutput)、统一身份与默认填充(如车型参数、manager 状态、deviceMotion 时间戳)。三类对回放结论的可信度不同,若把合成值当原始值,回归比较会得出错误结论。

    精确发现与证据级别

    1. migrate_drivingModelData() 的循环 for meta_field in ["laneChangeState", "laneChangeState"] 复制同一字段两次;源与目标均有该方向字段,故迁移方向保持 schema 默认值(源码静态确认)。
    2. product 判断只看原始日志主题是否出现,不逐时间戳补齐;新主题存在即阻止该规则为旧消息生成对应时点。
    3. migrate_pandaStates() 启用时无条件要求 carParams;回放需提供完整车辆身份输入。
    4. migrate_cameraStates() 用已见最小 frameId 作无索引回退,既非全段预扫描最小值,也非视频帧实体校验。

    AST 无依赖检查确认了 19 个装饰器规则及其输入/product 声明;本机 import capnp 失败,故未执行 Cap’n Proto 迁移单测。本文不能证明 19 条规则的运行结果、性能或跨版本 schema 兼容。

    对硬件与插件化重构的意义

    历史日志迁移应属于离线验证数据适配插件,与车端实时消息源严格隔离。插件清单要声明输入 schema、产出 schema、字段级变换、合成/丢弃策略与版本,并把「原始观测」「迁移估算」「默认填充」分别标注。车型、panda safety、相机时间与模型输出的迁移数据可以帮助回归比较,但不能给新硬件或车辆授予控制资格;新增硬件版本须保留原始日志、迁移后的日志和差异报告,尤其复核 valid、logMonoTime、方向枚举与安全参数。

    具体到回放实验,建议保留原始日志、迁移后的日志与差异报告三份产物;在比较前先核对被迁移主题的数量、顺序与时间是否单调,再判断差异来自算法改动还是迁移策略本身。缺少这三份产物时,一次「通过」既无法定位回归,也无法解释合成字段的来源。

    常见问题

    迁移后的日志能当原始测量吗?

    不能。迁移产出的是合成字段:如 migrate_sensorEvents() 无条件置 valid=True、migrate_controlsState() 沿用旧 valid/时间、migrate_peripheralState() 合成空结构,因此须把「原始观测」「迁移估算」「默认填充」分别标注。

    一条规则会用到前一条规则生成的消息吗?

    不会。migrate() 让所有规则读取同一份原始列表,前一条的替换/新增不会成为后一条的输入,结束时才统一替换、倒序删除、追加并按 logMonoTime 排序——这是兼容性批处理,不是管道。

    laneChangeDirection 迁移后为什么是默认值?

    migrate_drivingModelData() 的循环 for meta_field in ["laneChangeState", "laneChangeState"] 复制同一字段两次,未处理 laneChangeDirection,因此方向保持 schema 默认值。这是源码静态确认,尚无动态回放证据。

    迁移规则一共是多少条?

    最多 19 条:migrate_all() 默认 15 条,manager_states 加 1 条,panda_states 加 2 条,camera_states 加 1 条;replay_process() 始终打开 manager 迁移,panda 与相机迁移按进程发布主题开启。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PROCESS_REPLAY_SCHEMA_MIGRATION_RULES_OUTPUT_VALIDITY_DEEP_DIVE.md(19 条迁移规则、调度契约与证据级别)
    • 项目 02 —— openpilot-research/PROCESS_REPLAY_VALIDATION_DEEP_DIVE.md(迁移规则所处的回放主链路)

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

    更新记录

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

  • 车载硬件状态:300 秒驻车、11.8 V 与 45 W 的关机链

    结论:硬件状态的信任缺口集中在两处:缺测被静默当成 0(ThermalZone.read() 在温度文件不存在时返回 0,HardwareBase 多数方法是空操作或零值默认),以及慢速链没有采样时间戳(hw_state_thread() 消费端无采样时间,旧网络/USB 状态可能继续出现在 deviceState)。此外驻车容量是估算值而非车载 BMS 实测电量。以下基于固定源码快照 c8fb906 的静态分析与离线探针,不是实板验证。

    平台抽象与 hardwared 线程

    平台选择在导入时就固定:有 /AGNOS 文件则用 HardwareComma,否则用 HardwarePc,并非运行时可插拔发现。HardwareBase 除 get_device_type() 外多数方法有空操作或零值默认实现——移植时不能把「继承成功」等同于「能力可用」。

    组件 / 函数 输入与输出 实际职责
    HARDWARE 选择 /AGNOS 文件 → HardwareComma 或 HardwarePc 导入时固定平台实例,并非运行时可插拔发现
    ThermalZone.read()、ThermalConfig.get_msg() sysfs 温区 → *TempC 字段 按温区名称查找并读取;温度文件不存在返回 0,不是显式失效标志
    hw_state_thread() 网络、调制解调器、USB → 容量 1 的队列 每约 10 秒或 USB 拓扑变化时抓取慢速状态;队列满或采集失败会消耗本次触发,主线程沿用旧值
    hardware_thread() pandaStates、peripheralState、温度、Params → deviceState 点火边沿和约 2 Hz 周期更新,处理启动门、热状态、风扇目标、功耗与关机请求
    FanController.update() 温度、点火 → 目标风扇百分比 积分 PID 加前馈,点火变化重置
    PowerMonitoring.calculate() 车端电压、点火 → 累计耗电 / 剩余容量估计 驻车按设备功率积分,点火时按固定 45 W 充电率估计;约 10 秒保存 CarBatteryCapacity

    点火、热状态与行车资格

    pandaStates 中任一已知 panda 的点火线或 CAN 点火信号可建立点火;超过 5 秒未收到状态则清除点火,但线束连接状态 in_car 仅在收到 panda 状态时更新。行车资格分两层:已有行车状态要求点火、无 onroad cycle、未达到 critical;从驻车新启动还要求条款/培训、空间、设备已启动、温度低于 overheated、非卸载等启动条件。

    因此热状态迟滞允许「过热但未严重」的在途状态暂时保持,不能把「禁止进入行车」误写成「立即退出行车」。温度源分两组:驻车危险检查用内存、CPU、GPU 的最大值,行车热状态再加入 PMIC,两者都经 5 秒一阶滤波;mici 与其他设备采用不同热带阈值,驻车高温可直接升到 critical。风扇目标在温度告警时被强制为 100%,随后经 pandad 发给 panda;panda 回报的转速另由 selfdrived 检查。

    风扇 PID 与温度带

    风扇与温度构成一条闭环:温度决定目标百分比,panda 执行并回报转速,故障事件则由 selfdrive 侧产生。关键参数如下。

    项目 参数 / 行为
    风扇控制 积分 PID 加前馈,点火变化时重置
    行车风扇范围 30–100%
    驻车风扇范围 0–30%
    温度告警 风扇目标强制 100%
    温度滤波 5 秒一阶滤波
    温度源 驻车用内存/CPU/GPU 最大值;行车再加入 PMIC

    电源估算与关机条件

    普通关机不是单条件触发,而是「前置状态 + 多个触发之一」的组合;ForcePowerDown 能绕过普通条件,但仍受末尾约束。

    条件 阈值 / 机制
    普通关机前置 驻车、连接车辆、未禁用关机、驻车超过 300 秒
    触发之一 驻车超过 30 小时
    触发之二 低通电压低于 11.8 V
    触发之三 估计容量耗尽
    点火充电率 按固定 45 W 估计
    ForcePowerDown 绕过上述普通条件,但仍受末尾的 started_seen / 最短开机时间约束

    hardwared 只写 DoShutdown;真正调用关机的是 manager.main()。

    缺测的零值风险与插件契约

    两个具体风险值得单独记住:hw_state_thread() 消费端无采样时间戳,旧网络/USB 状态可能继续出现在 deviceState;ThermalZone.read() 把缺失温区当 0。这类慢速状态之所以能继续沿用旧值,正是因为消费端没有采样时刻可供判断新鲜度;新平台应给每条状态附上采样时间与失效哨兵。这些是移植设计风险,需要在目标平台上显式处理,而不是沿用零值默认。

    因此新平台要显式声明能力与缺测状态:标定 sysfs 温区名、单位、采样频率、失效哨兵;定义电压测点、功率方向与关机电气动作;点火、温度、关机和风扇属于可信任设备服务,需要时效性、故障降级和唯一发布者。硬件插件不得自行宣布 deviceState.started 或绕开独立 panda 安全门。

    外置计算模块与触摸记录

    同一条 hardwared 线程里还挂着外置计算模块与触摸记录两条支线。Chestnut.update() 在驻车时限次刷固件,并对外置 GPU 的丢电、链路、过热、模型失败分别生成不同告警;chestnut_state_thread() 读取供电与 PCIe 状态,并按有效位合并 GPU 状态。也就是说「外置模块可用」本身也是一组需要区分失效类型的判定位,而不是一个布尔值。

    触摸记录则只属诊断路径:touch_thread() 仅在 comma 硬件启动,按事件帧分组发布到原始 touch 主题,UI 交互另由 raylib 采集,两者不应混为一谈。此外,hardware_thread() 里的 ign_edge 是「已启动状态」与 onroad 条件的持续差异,不一定是单次点火边沿:启动条件受阻而 onroad 条件仍真时,它会持续绕过约 2 Hz 的周期门,device_booted 一旦为真也会在本进程锁存。

    常见问题

    温度读不到会怎样?

    ThermalZone.read() 在温度文件不存在时返回 0,这不是显式失效标志。缺测不能被静默当作 0 °C,新硬件平台必须明确失效处理。

    普通关机需要满足哪些条件?

    前置是驻车、连接车辆、未禁用关机且驻车超过 300 秒;再由超过 30 小时、低通电压低于 11.8 V、或估计容量耗尽三者之一触发。

    驻车容量是车载 BMS 实测电量吗?

    不是。它是估算值:无点火时按平台报告的设备功率积分,有点火时按固定 45 W 充电率估算,并非测得的真实电量。

    过热会立即退出行车吗?

    不一定。热状态迟滞允许「过热但未严重」的在途状态暂时保持;「禁止进入行车」与「立即退出行车」是两条不同的判定。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/HARDWARE_STATE_DEEP_DIVE.md(硬件状态、供电与热管理,含源码行号索引)
    • 项目 02 —— openpilot-research/probes/test_power_monitoring_contract.py(电源估算与关机条件的离线探针)
    • 项目 02 —— openpilot-research/probes/test_fan_controller_contract.py(风扇 PID 上下限与积分的离线探针)

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

    更新记录

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

  • pandad 的 100 Hz 主循环:connected 不等于链路健康

    结论:原生 pandad 用 100 Hz 主节拍派发约 20/10/2 Hz 的低频任务,但它暴露的「存活证据」必须分层看:进程存活、句柄 connected、CAN 更新、控制读成功、MCU 安全模式读回是五个不同层级。PandaSpiHandle::connected 在构造时为真,而在此 SPI 实现中未见出错后置假,因此 check_connected() 不是可靠的链路探针;comms_healthy 则在 bulk 负返回时锁存为假,即使随后成功也不自动恢复。本文为对本地 source/ 快照 c8fb906 的静态追踪,未启动进程、未接入实板、未发送车辆控制帧。

    构建、进程所有权与运行开关

    SConscript 在非 Darwin 平台构建原生程序;但 SCons 构建出二进制不等于 manager 直接管理它——进程表把原生 _pandad 设为 enabled=False,常驻的是 Python pandad 包装器。包装器通过签名门后 Popen(["./pandad"]),等待其退出再恢复,原生进程的退出码不作为包装器的成功判据。macOS 不构建此目录的原生程序,但进程表仍定义 Python 包装器,两者是独立的构建/运行条件。

    开关 源码入口 对运行态的影响
    BOARDD_LOOPBACK connect() 只要变量存在就请求 panda CAN 回环,不是只读诊断
    BOARDD_SKIP_FW_CHECK connect() 跳过原生 up_to_date() 签名门;不跳过外层 Python 包装器的刷写/最终签名检查
    FAKESEND pandad_run() 阻止发送线程调 Panda::can_send(),但主循环仍可下发安全模式、心跳、风扇与 IR 命令
    STARTED send_panda_states() 在本地 health 副本中强制点火线位,影响点火/省电/NO_OUTPUT 分支与发布值,不改变真实引脚
    NO_FAN_CONTROL process_peripheral_state() 跳过风扇目标下发,仍处理 IR 与其他设备命令
    PANDAD_MAXOUT can_receive() 收 CAN 后追加一次向 0xab 端点的丢弃缓冲读取,返回值不单独作为健康判据
    SPI_ERR_PROB lltransfer() 正值时随机改 SPI 长度、TX 或 RX 数据,属实传输注错开关,不可在行车部署

    这些开关不是可分配给普通插件的能力:FAKESEND 只约束候选 CAN 发送,STARTED 与回环/注错会改变网关控制流或真实总线行为。设备配置应由受信任启动器使用固定清单生成,并在上车前核对实际进程环境。

    调度和观测语义

    主循环先收 CAN,然后每 5 帧处理风扇/IR,每 10 帧更新 selfdriveState、deviceState 并处理 panda 健康/心跳/安全模式,每 50 帧发布外设状态;每轮还持续消费 MCU 串口日志。RateKeeper 名义基频是 100 Hz,首次 frame=0 即运行低频分支。

    节拍 帧模数 任务
    100 Hz 每帧 先接收 CAN 并发布,逐轮读取串口日志;RateKeeper 调整帧计数与睡眠
    约 20 Hz 每 5 帧 处理风扇目标与 cabinCameraState IR 目标
    约 10 Hz 每 10 帧 更新 selfdriveState、deviceState,处理 MCU/CAN 健康、心跳与 PandaSafety 配置
    约 2 Hz 每 50 帧 发布 peripheralState(电压、电流、风扇)

    若 SPI 交易、串口读取或消息处理超期,实际频率由运行时决定,RateKeeper 不能保证硬实时或补发错过的状态帧。engaged 每个 10 Hz 周期由 selfdriveState 的 alive/valid 与 enabled 联合计算;is_onroad 仅在 deviceState 本次 updated 时更新,否则沿用上次布尔值。因此「设备消息未更新」与「确认仍在行车」不能等同。

    send_panda_states() 对 health 或任一路 CAN health 返回空值时直接跳过发布,process_panda_state() 随之返回,本轮不发心跳,也不执行它的通信故障退出门;主循环随后仍调用 configureSafetyMode(is_onroad)。若读到了结构体但 comms_healthy=false,事件标无效;只在 ignition_local=false 时设置 do_exit,点火真时继续运行并依赖其他安全层。

    失联、退出与故障路径

    PandaSpiHandle::connected 构造时为真,在此 SPI 实现中未见出错后置假,因此 check_connected() 不是可靠的链路探针——进程存活、句柄 connected、CAN 更新、控制读成功、MCU 安全模式读回是不同层级的证据。默认 SPI 交易重试还可能没有总时限。退出主循环后,只有 is_onroad && !engaged && connected 才尝试发 NO_OUTPUT,且控制写结果没有回执;接着 join() 发送线程——若线程正阻塞于无总截止期的 SPI 交易,退出也可能被推迟。

    Python 包装器会在原生进程退出后进入自己的恢复循环,但不能代替 MCU 固件的独立心跳/安全钩子。固件过期异常在 connect() 的构造 try 之外抛出,原生内部 100 ms 重试只覆盖构造失败,外层包装器才处理进程退场。

    自制硬件和插件验收

    应把网关状态拆成 process_alive、transport_available、can_rx_fresh、health_snapshot_complete、mcu_safety_mode_confirmed 和 heartbeat_age,每项带设备 UID、采样时间、错误原因与独立阈值。插件只能订阅这些观测或提交受约束候选帧,不能自行改变心跳/安全模式;退出和断电路径应由 MCU 侧默认安全态兜底,而非依赖主机退出时的一次控制写。

    相邻的 test_pandad_spi.py 在实板注入小概率 SPI 错误,检查 CAN、pandaStates、peripheralState 的数据与平均/最大间隔;它没有覆盖永久 SPI 失败、health 只有短读、点火真假两种退出分支或 join() 被无限交易阻塞。本轮未运行该硬件测试,新平台应先在无车台架做这些故障注入。

    常见问题

    check_connected() 能证明 SPI 链路正常吗?

    不能。PandaSpiHandle::connected 在构造时为真,而在此 SPI 实现中未见出错后置假,所以 check_connected() 不是可靠的链路探针;进程存活、句柄状态、CAN 更新、控制读成功与 MCU 安全模式读回是不同层级的证据。

    comms_healthy 变假后会自动恢复吗?

    不会。comms_healthy 只在 bulk 返回负值时被置假,本句柄内未见恢复为真,即使随后调用成功也不自动恢复;控制读写本身也不更新该位。

    health 读失败时会发心跳吗?

    不会。send_panda_states() 对 health 或任一路 CAN health 返回空值时直接跳过发布,process_panda_state() 随之返回,本轮不发心跳,也不执行它的通信故障退出门。

    pandad 退出时一定会发 NO_OUTPUT 吗?

    不一定。退出主循环后只有 is_onroad && !engaged && connected 同时成立才尝试发 NO_OUTPUT,且控制写结果没有回执;join() 若阻塞在无总截止期的 SPI 交易,退出也可能被推迟。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PANDAD_NATIVE_MAIN_LOOP_LIVENESS_TELEMETRY_DEEP_DIVE.md(原生 pandad 入口、主/发送线程、调度分频与失联降级)
    • 项目 02 —— openpilot-research/PANDAD_NATIVE_SPI_RESPONSE_BOUNDS_RETRY_CONTRACT_DEEP_DIVE.md(源文档指向的原生 SPI 响应边界与重试专题)
    • 项目 02 —— openpilot-research/PROCESS_SUPERVISION_DEEP_DIVE.md(源文档指向的进程监督与设备恢复包装器专题)

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

    更新记录

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

  • 横向目标曲率有三道限幅:jerk 5、加速度 3、曲率 0.2 各拦什么

    结论:从目标曲率到车轮,controlsd 要经过「来源仲裁 → 变化率/加速度/绝对曲率三道限幅 → 选定的横向控制器 → 车型编码 → panda 固件」这几层。两个容易被误读的边界是:来源选择只看 lateralManeuverPlan 的消息 valid 位,不看 updated、alive 或消息年龄,因此发布者停发而进程不重启时,最后一条有效计划可能被持续选为目标;另外变化率限幅本身不会置 curvature_limited 标志,不能用它判断「是否发生过裁剪」。以下为固定源码快照 c8fb906 的静态追踪。

    两条目标来源与 valid / alive 的差别

    正常目标来自 modelV2.action.desiredCurvature;若 controlsd 看到 lateralManeuverPlan 的消息 valid 位为真,则优先采用该计划;横向未激活时两条来源都改用实测曲率,以降低再使能时的阶跃。模型侧 get_action_from_model() 有两条版本兼容路径:没有 action 输出时从预测轨迹的 yaw/yaw rate 估算曲率,有 action 时把输出横向量除以 max(1,vEgo)^2;纵向加速度作 0.3 s 平滑,横向平滑常量为 0,但 vEgo<=0.3 m/s 时保持上一帧曲率。

    实验来源 lateral_maneuversd 只有机动 active 且未完成才把计划标为有效,曲率以基线曲率加「目标横向加速度 / max(vEgo,1)^2」形成;进程表只在横向机动条件下管理它,生产运行不应把它当作常驻规划器。

    失效语义的核心在消息健康位:SubMaster.update_msgs() 只在收到新消息时更新 valid;alive 按收包时间重新计算;all_checks() 才同时检查存活、频率与有效位。而 controlsd 的来源选择只检查 sm.valid['lateralManeuverPlan'],未检查该主题的 updated、alive 或消息年龄。因此曾收到有效实验计划后,若发布者停发而进程不重启,最后一条计划仍可能被持续选为目标——其后仍经过限幅、latActive、车型与 panda 门,不等于可以任意发送转向。

    controlsd 的逐层处理

    Controls.__init__() 读取 CarParams,按转向接口类型优先选 angle/curvature,否则按横向调参选 PID/torque;update() 以 selfdriveState 为轮询主题更新异步输入与姿态数据。state_control() 更新车辆模型、活动门、目标曲率与执行器,并对数值字段做有限性检查。VehicleModel.calc_curvature() 把扣除在线角偏置的方向盘角换成实测曲率,考虑转向比、速度与路面横滚;controlsd 在调用前取负号,方向约定需用实车校验。

    函数/组件 源码作用 契约或边界
    Controls.__init__() 按转向接口类型优先选 angle/curvature,否则按横向调参选 PID/torque。 不同输出单位不能在插件间直接互换。
    state_control() 更新车辆模型、活动门、目标曲率与执行器,做有限性检查。 生成的是 CarControl,不是 CAN 或执行许可。
    VehicleModel.calc_curvature() 扣除在线角偏置的转角换算实测曲率,考虑转向比/速度/横滚。 调用前取负号,方向约定需实车校验。
    clip_curvature() 顺序限制变化率、考虑 roll 的横向加速度范围、绝对曲率。 curvature_limited 只由后两道限幅标记,变化率限幅不置位。
    Controls.publish() 发布 controlsState 与 carControl,比较请求与 carOutput。 顶层 valid=CS.canValid,不是 sm.all_checks()。
    Car.controls_update() carControl 存活且非被动/已初始化时调用车型 apply() 并发布 sendcan。 仍须通过车型编码与 panda 固件安全约束。

    三道限幅与标志位陷阱

    clip_curvature() 按顺序做三件事:用至少 1 m/s 的速度计算 5 m/s^3 的近似横向 jerk 限幅;再按 ±3 m/s^2 加 roll 补偿限加速度;最后按 ±0.2 1/m 限绝对曲率。源码明确标注 jerk 公式为近似,这些常量不是可以脱离车型和硬件直接复用的认证约束。

    隔离探针给出一个典型陷阱:当变化率单独生效时,输出已从 0.1 改成 0.0005,但 curvature_limited=False。也就是说这个标志不能作为「无裁剪」的诊断依据。探针只验证纯数值函数,不代表 controlsd 消息时序、车型控制器或板端限幅已通过。

    限幅 阈值 是否置 curvature_limited
    变化率(近似横向 jerk) 5 m/s^3,速度下限 1 m/s 否
    横向加速度(含 roll 补偿) ±3 m/s^2 是
    绝对曲率 ±0.2 1/m 是

    输出单位与有限性清理

    控制器输出按 steerControlType 分流:通用第一返回值写 actuators.torque,angle/普通 PID 的第二返回值写 steeringAngleDeg,curvature 型写 actuators.curvature,扭矩控制的第二返回值为零。LatControlAngle 与 LatControlCurvature 都返回 steer=0,有效输出分别是角度与曲率字段;LatControlPID 在施力/低速/安全限幅时冻结积分;LatControlTorque 把目标曲率换横向加速度后返回时会翻转符号,不能把 pid_log.output 与 actuators.torque 直接同号比较。

    state_control 对 Actuators 的数值字段逐个检查非有限值并清零,但这只是末端值清理,不是上游模型输入、车辆参数或中间 PID 状态的完整非有限传播防护。publish() 在 selfdriveState.active 时比较本次请求与已收到的 carOutput,把差异留给后续循环的控制器饱和判断,这不是同步的本帧硬件确认。

    控制器 有效输出字段 边界
    LatControlAngle steeringAngleDeg(steer=0) 曲率反算目标方向盘角并加角偏置。
    LatControlCurvature actuators.curvature(steer=0) 曲率误差 PID 或前馈,驾驶员施力时用前馈。
    LatControlPID steeringAngleDeg 或扭矩 施力/低速/安全限幅时冻结积分。
    LatControlTorque actuators.torque 返回时翻转符号;不能把 pid_log.output 与扭矩直接同号比较。

    自制硬件与插件契约

    源文档给出的约束是:候选规划插件应输出带来源、时间戳、年龄上限、曲率单位 1/m、车辆坐标/正方向、适用车速范围与显式失效状态的目标;仲裁层独占来源选择与 TTL,失效时回退模型或安全降级。车辆学习参数、转角编码与 carOutput 的实际执行反馈也应按车型/硬件版本记录。

    不能让普通插件修改 latActive、车型极限或 panda 固件;CarControl 及其 valid 位也不能替代板级安全门。现有横向饱和测试覆盖部分车型与控制器,扭矩缓冲测试覆盖停用后缓冲清零,但未证明四类控制器全部车型、计划抢占超时、模型丢帧、车况异步或实车符号正确,仍需补齐来源有效→停发/无效切换的回放测试与 NaN/Inf 注入。

    常见问题

    实验计划停了不发,为什么车还在用它?

    因为 controlsd 的来源选择只检查 sm.valid['lateralManeuverPlan'],未检查 updated、alive 或消息年龄。曾收到有效计划后,若发布者停发而进程不重启,最后一条计划仍可能被持续选为目标;但它仍经过限幅、latActive、车型与 panda 门。

    curvature_limited=False 就代表没有裁剪吗?

    不代表。这个标志只由横向加速度与绝对曲率两道限幅设置;变化率限幅本身不置位。探针显示变化率单独生效时输出已从 0.1 改成 0.0005,但标志仍是 False。

    三道限幅的阈值分别是多少?

    按顺序是:用至少 1 m/s 计算 5 m/s^3 的近似横向 jerk、±3 m/s^2 的横向加速度(含 roll 补偿)、±0.2 1/m 的绝对曲率。源码标注 jerk 公式为近似,不能直接当认证约束复用。

    扭矩控制器的输出符号可以直接对比吗?

    不能。LatControlTorque 返回时会翻转符号,不能把 pid_log.output 与 actuators.torque 直接同号比较;angle 与 curvature 两类控制器则返回 steer=0,有效输出在角度或曲率字段。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/LATERAL_TARGET_ACTUATION_DEEP_DIVE.md(两条来源仲裁、controlsd 逐层处理、三道限幅与四类控制器)。
    • 项目 02 —— openpilot-research/ 同目录探针记录:五项 clip_curvature() NumPy 数值契约隔离探针结论。

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

    更新记录

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