标签: HMI

  • VisionIPC 缓冲契约:FD 握手、帧通知与复用覆盖窗口

    结论:VisionIPC 用两条独立通道搬运图像:Unix socket 用 SCM_RIGHTS 传 FD 与缓冲描述,msgq 只发 {server_id, idx, extra} 小包。图像字节在共享缓冲里循环复用,因此「msgq 收到帧通知」不等于「当前像素通过完整性与时序验证」——包顺序并不保证旧包对应的像素仍是旧帧。

    双通道结构:FD 握手与帧通知

    相机采集器创建固定数量的 VisionBuf(NV12 共享内存或 ION),客户端通过 Unix socket 的 SCM_RIGHTS 拿到 FD 并导入内存;随后每次发布只发一个含 server_id、缓冲索引与 extra 的小包。通知通道与描述/FD 通道是不同的故障面:前者决定图像何时被认为可读,后者决定缓冲能否安全导入。

    VisionIpcServer.listener() 在 Unix socket 接受流类型请求,可返回流列表,或把该流的元数据与所有缓冲 FD 一起传给客户端,返回描述里附上服务端随机 server_id 并清掉私有 ION handle 字段。VisionIpcClient.connect() 先释放旧映射,再接收 FD、导入内存并初始化 NV12 指针;源码明确写明该操作非线程安全,调用期间不能使用旧缓冲。

    缓冲创建与 NV12 布局

    默认 create_buffers() 用 width*height*3/2、stride=width、uv_offset=width*height,不能直接套在有 ISP 行对齐的硬件上。相机侧使用带实际尺寸的接口创建 18 个 YUV 缓冲。num_buffers 只要求小于 VISIONIPC_MAX_FDS=128,没有在此处证明任意数量足以避免覆盖。

    缓冲分配还涉及平台差异:通用平台用共享文件 FD 加 mmap,图像区尾部再留 8 字节存 frame_id;通用实现的 sync() 是空操作,不代表所有 GPU/ISP 一致性已保证。ION 分支申请共享句柄并 mmap,sync() 通过 ION ioctl 做缓存操作;新硬件的 DMA-BUF 分配和 cache API 不能仅替换文件名或跳过同步。

    函数与数据契约

    组件/入口 行为 不能假定
    VisionBuf::allocate() 通用平台用共享文件 FD 加 mmap,图像区尾部再留 8 字节存 frame_id。 通用实现的 sync() 是空操作,不代表所有 GPU/ISP 一致性已保证。
    VisionBuf::allocate() ION 版 Qualcomm/ION 分支申请共享句柄并 mmap,sync() 通过 ION ioctl 做缓存操作。 新硬件的 DMA-BUF 分配和 cache API 不能仅替换文件名或跳过同步。
    VisionIpcServer::get_buffer() 默认按索引循环取缓冲,或由摄像头指定索引。 无消费者释放/引用计数等待;源码此处还有「是否检查已发送」的待办。
    VisionIpcServer::send() 可选执行缓存同步,然后发 {server_id, idx, extra} 小包。 msgq 包不是图像快照;发送后对应共享缓冲仍可被生产者重用。
    VisionIpcClient::recv() 按 msgq 包索引找已导入缓冲;越界或 server_id 不同会断开,成功时缓存同步并返回指针。 不比对共享缓冲尾部 frame_id 与 packet extra.frame_id,也不验证像素读取期间未被再次覆盖。
    VisionIpcBufExtra 通知含 frame_id、SOF/EOF 时间戳和 valid。 Python 发送包装固定写 valid=False;本快照原生相机的三字段聚合初始化也令其默认为 false,不能把该位作为普遍帧健康门。
    send() Python 包装 复制输入字节进下一个缓冲、写尾部帧号并发通知。 这是一次 CPU 拷贝路径,不等于通用零拷贝采集。

    复用覆盖窗口:包顺序不等于像素帧

    缓冲与通知异步推进,包顺序不保证旧包对应的像素仍是旧帧。例如只创建一个缓冲,连续发送帧 1、帧 2 后再让非 conflate 客户端读取:通知可能先返回 extra.frame_id=1,但同一缓冲已被帧 2 改写。现有非合并测试只检查两次通知的 frame_id,未在这种情况下比较两次读取的像素内容。这是代码结构推得的覆盖风险,源文档未声称已在本机实际复现。

    conflate=True 只跳过旧通知,不能独立证明最新缓冲在 GPU/CPU 读取完成前不被下一轮覆盖。因此需要在目标板上记录 packet.frame_id、共享尾部 frame_id、像素哈希、SOF/EOF 与推理/显示使用时刻,注入断连、重启、丢帧和消费者长尾耗时,才能验证这一窗口。

    重连与插件契约

    VisionIpcClient::recv() 的越界/server_id 检查能识别服务端重建、索引集合变化,但只在收到新通知时触发;没有通知时返回 nullptr,并不会自动重新握手。UI 的 CameraView 有连接重试/切流逻辑;道路 modeld 和 dmonitoringmodeld 在启动时循环连接,主循环遇到 recv() is None 时继续等待,源码未在循环里显式重新 connect()。不能从 UI 的恢复路径推断模型进程也能无重启恢复。

    消费者 连接与重连行为 conflate
    UI CameraView 有连接重试/切流逻辑 —
    modeld 主相机 启动时循环连接,主循环 recv() is None 只继续等待 True
    modeld 广角附加流 同上,并以 SOF 时间戳对齐 False
    dmonitoringmodeld 启动时循环连接,源码未见循环内显式重连 —

    modeld 主相机使用 conflate=True,广角附加流使用 False 并以 SOF 时间戳对齐;即使两路通知的时间相差超过 10 ms,代码记录错误后仍向后执行。它依赖同一时钟域和合理缓冲复用窗口。自制相机插件必须说明:SOF/EOF 来自何处、时钟域/单位、帧号如何跨 VisionBuf 尾部与通知保持一致、行步长/UV 偏移、缓冲数和最大持有时长、缓存同步方向、服务端重启后的 FD 重新导入。

    常见问题

    msgq 收到帧通知,是否代表像素已经就绪?

    不代表。msgq 只发 {server_id, idx, extra} 小包,图像字节在共享缓冲里;缓冲与通知异步推进,包顺序不保证旧包对应的像素仍是旧帧。recv() 也不比对共享缓冲尾部 frame_id 与 packet 的 extra.frame_id。

    为什么只创建一个缓冲特别危险?

    因为连续发送帧 1、帧 2 后,通知可能先返回 extra.frame_id=1,但同一缓冲已被帧 2 改写,客户端读到的像素已不是帧 1。现有非合并测试只检查两次通知的 frame_id,未比较像素内容。

    conflate=True 能保证最新缓冲不被覆盖吗?

    不能。conflate=True 只跳过旧通知,不能独立证明最新缓冲在 GPU/CPU 读取完成前不被下一轮覆盖;仍需在目标板上用像素哈希与时间戳验证覆盖窗口。

    模型进程会像 UI 一样自动重连吗?

    源文档未给出该结论。UI 的 CameraView 有连接重试/切流逻辑,但道路 modeld 与 dmonitoringmodeld 在启动时循环连接,主循环遇到 recv() is None 只继续等待,源码未在循环里显式重新 connect()。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/VISIONIPC_BUFFER_CONTRACT_DEEP_DIVE.md(取用了双通道结构、缓冲创建与 NV12 布局、函数与数据契约及复用覆盖窗口)。
    • 项目 02 —— openpilot-research/VISIONIPC_UNIX_FD_HANDSHAKE_ERROR_CONTRACT_DEEP_DIVE.md(仅用于确认 FD 握手与错误码传播的边界)。

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

    更新记录

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

  • Fisker Ocean 2023-24 的 ADASBUS:单总线取数与 92 个地址

    结论:这台 Fisker Ocean 2023-24 的 CAN 数据特征不是「OBD 口后面有没有网关」,而是整车信号被网关镜像到一条 ADASBUS 上:只需要接一条 CAN-FD 总线就能读到车身与 HMI 信号。源文档记录,panda 以摄像头转接线束插在原车网关侧(bus 0)与 OEM ADAS 模块侧(bus 2)之间,bus 1 未使用;bus 0 一次实车指纹抓取到 92 个地址(网关镜像的车身/HMI 加上 ESP、EPS、YRS、BCM、VCU、MFS、ICC),bus 2 约 65 个地址且全部由 ADAS 发出。下述结论仅限源文档记录的这一台车与 2023-24 年款,不代表同型号全系。

    车辆与适用范围

    源文档是一份开源 ADAS 项目里的车型平台定义,属于单车型移植(single-model port),不是车厂或第三方机构的批量采集。文档明确说明固件版本指纹被故意留空,识别以 CAN 指纹为准,因此「适配到哪一台车」由这一次实车抓包决定。

    项 内容
    车型 Fisker Ocean 2023-24(源文档记为 Ocean Extreme 整备质量口径)
    年款 / 市场版本 2023-24;市场版本源文档未确认
    ECU / 改装状态 摄像头转接线束(camera-splice harness);panda 接在车辆侧网关与 OEM ADAS 模块之间;bus 1 未使用
    采集跨度 源文档只写「来自一次实车指纹抓取(live fingerprint capture)」,未给出日志数或帧数
    适用范围 仅限该次抓取的一台 2023-24 Ocean;轴距、方向盘比、轮胎刚度等车辆模型参数仍标注为待阶跃响应日志复核

    总线与寻址

    源文档把总线分成两侧:车辆侧(bus 0)与 OEM ADAS 模块侧(bus 2)。因为网关把车身/HMI 信号镜像到同一条总线上,移植只需要接一条总线;bus 1 在代码里被标为未使用。报文 ID 都是 11 位,ADAS 侧除固定报文外还有一段雷达目标列表。

    项 内容
    物理层 CAN-FD 双绞线;源文档注明 ADASBUS 为 CAN-FD
    链路层 / 比特率 源文档未给出比特率数值
    寻址方式 11 位 ID,例如 0x117、0x118、0x121、0x1C0、0x1D0、0x313、0x314、0x315、0x316、0x317、0x31A、0x31C
    总线划分 bus 0 = 车辆侧(网关镜像);bus 2 = OEM ADAS 模块侧;bus 1 未使用
    CAN FD 是;源文档按 CAN-FD 处理
    SecOC 0x1D0 与 0x121 受 SecOC 保护;同步报文 0x20 GW_Syn_All,50 ms,线上 40 位 freshness

    已解码信号

    下表只登记源文档里带证据或明确标注状态的条目。挡位枚举与按键映射直接来自平台定义中的 DBC 值表与按键映射表;转向与加速度指令给出缩放与周期,但周期条目原文标注为「需从日志确认」。雷达目标只给 ID 区间,未给字段定义。

    信号 / 功能 位置(ID / 来源) 解码 证据
    挡位 VCU_GearSig 网关镜像到 bus 0 的 VCU 报文 枚举 1=P、2=N、3=R、4=D、6=E、7=S 平台定义 DBC VAL_ 注释
    转向角指令 ADAS_LatCtrl_SteerAnReq 0x1D0(ADAS 侧) 0.0625°/LSB,量程 ±780°;周期 10 ms(≈100 Hz),原文标注需从日志确认 平台定义注释
    加速度指令 ADAS_LgtCtrl_AccelReq 0x121 0.0004882 m/s²/LSB,偏移 −16 平台定义注释
    驾驶员转向力矩 EPS_DrvrSteerTq 网关镜像的 EPS 报文 0.01 Nm/LSB;驾驶员接管阈值 0.5 Nm 平台定义注释
    巡航主开关 MFS_RiBtnNorth MFS(方向盘多功能开关) 映射为 mainCruise(ADAS 总开关) 按键映射表
    巡航设定 MFS_LeRollPress MFS 映射为 setCruise(接合巡航) 按键映射表
    加速 / 减速 MFS_LeRollUp、MFS_LeRollDwn MFS 映射为 accelCruise / decelCruise 按键映射表
    LKAS 开关 MFS_RiBtnSouth MFS 映射为 lkas 按键映射表
    取消 / 恢复 / 车距调节按键 MFS 源文档标注为 TBD(待台架测试确定) 按键映射表注记
    雷达目标列表 0x2C7…0x2E9(ADAS 侧) 源文档只给 ID 区间,未给字段定义 总线说明
    网关镜像的车身/HMI 信号 bus 0 挡位、踏板、车门、安全带、转向灯、MFS 按键 总线说明

    读取方法与注意事项

    第一条是取数路径:因为网关把车身/HMI 信号镜像到 ADASBUS,移植只接一条总线,摄像头转接线束让 panda 落在车辆侧网关与 OEM ADAS 模块之间。源文档强调 bus 1 未使用,因此不要把抓不到流量归因于「少接了一条线」。

    第二条是 SecOC:转向角指令与加速度指令在 ADAS 侧受 SecOC 保护,需要同步报文 0x20 GW_Syn_All 提供的 40 位 freshness,同步报文周期 50 ms。源文档说明这些数据 ID 在 AUTOSAR Short SecOC 口径下等于 CAN 报文 ID。任何只解析载荷、不做 freshness 校验的读取方式,都拿不到可信的指令值。

    第三条是周期:0x1D0 每个控制帧都发送,源文档把周期记为 10 ms(≈100 Hz),但同一行原文写了「需从日志确认周期」,因此本条保留不确定性,不作为实测结论。第四是识别:固件版本指纹被故意留空,品牌不参与固件版本匹配,CAN 指纹是唯一依据。

    最后是车辆模型参数:轴距按公布数据从 2.99 改成 2.921(源文档给出理由:原值让横向模型预期更慢的入弯、导致 MPC 在弯道入口过度指令转角),steerRatio 15.0 记为「first-cut」,tireStiffnessFactor 0.85 记为「crossover-typical」。这些都属待复核值,不应作为 Fisker Ocean 的通用参数。

    证据等级与边界

    可视为实车观测的是总线地址数量(bus 0 约 92 个、bus 2 约 65 个)与两侧总线的分工,它们来自一次实车指纹抓取;挡位枚举和按键映射来自 DBC 值表与映射表,属文档级证据。属于待复核或推测的是转向角周期、steerRatio、tireStiffnessFactor,以及取消/恢复/车距调节三个按键。

    不能外推的地方有三处。第一,源文档是单车型移植,固件指纹为空,挡位与按键映射只在这台车上被记录,其他年款、市场版本的报文布局未验证。第二,雷达目标列表只给了 ID 区间,字段含义未知,不能据此宣称读到了目标距离或速度。第三,车辆模型参数是工程首值,不是实测标定,换车即失效。

    关于「安全访问」类问题:源文档未提供任何绕过方法,且不建议尝试;本文只整理公开文档登记的广播信号与指令定义,不涉及对车辆安全机制的规避。

    常见问题

    Fisker Ocean 是从 OBD 口取数,还是接别的总线?

    源文档没有走 OBD 诊断口,而是接 ADASBUS:panda 以摄像头转接线束落在车辆侧网关(bus 0)与 OEM ADAS 模块(bus 2)之间。OBD 口在本车是否可读这些报文,源文档未给出结论,因此不能据此推断。

    为什么只需要接一条 CAN 总线?

    因为网关把车身/HMI 信号(挡位、踏板、车门、安全带、转向灯、MFS 按键)镜像到 ADASBUS 上,源文档说明移植只接一条总线即可;bus 1 未使用。

    转向角和加速度指令为什么不只解载荷?

    因为 0x1D0 与 0x121 受 SecOC 保护。源文档说明需要同步报文 0x20 GW_Syn_All(50 ms、40 位 freshness)才能得到可信指令值,只解析载荷不做 freshness 校验拿不到可信值。

    挡位信号在哪条总线上?

    挡位来自网关镜像到 bus 0 的 VCU 报文,源文档的 DBC 值表给出 1=P、2=N、3=R、4=D、6=E、7=S 的枚举。

    延伸阅读

    参考资料

    • 项目 01 —— public-can-dbc-datasets/additional/jos-technion-fisker-ocean/README.md(主来源:该项目保留的开源 ADAS 项目根文档)。
    • 项目 01 —— public-can-dbc-datasets/additional/jos-technion-fisker-ocean/opendbc/car/fisker/values.py(平台常量、总线划分、挡位与按键映射、SecOC 说明)。

    说明:本文整理自第三方开源项目的公开文档,仅代表该文档所记录的单个车辆样本,不代表同型号全系车型。CAN 与诊断操作存在风险,任何实车操作前请自行复核并遵守当地法规。

    更新记录

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

  • 行车界面 GPU 渲染链:NV12 缓冲、投影裁剪与告警的受信任边界

    结论:行车画面是一条纯显示链:camerad → VisionIPC → CameraView → EGL 外部纹理或 PC Y/UV 纹理 → AugmentedRoadView → ModelRenderer/HUD/Alert → GuiApplication.render → 屏幕;模型叠加另走 modelV2 + extrinsicsCalibration + radarState → ModelRenderer,并非从当前显示的视频帧反推。这条链不是车辆控制或功能安全的执行链,主循环在顶层 Widget 绘制后才更新 UIState,因此订阅的新消息通常在下一帧绘制,不能仅凭帧率推断相机到显示或故障到告警的端到端时延。实机 GPU、摄像头时序、屏幕亮度和触摸延迟均未测试。

    一帧如何到达屏幕

    GuiApplication.init_window() 创建窗口和可选的离屏 render texture,render() 每帧收集触摸、绘制栈顶 Widget、结束绘制;关屏时仍 yield False,让外层更新状态。AugmentedRoadView._render() 做行车门控、选宽/窄相机、更新标定、scissor 限定内容区,并按相机、模型、HUD、告警、驾驶员状态的顺序绘制。CameraView._render() 用 recv(timeout_ms=0) 非阻塞取最新帧:无新帧沿用旧画面,连接丢失清帧并显示占位,座舱相机水平翻转。断连时 _ensure_connection() 按 0.2 秒间隔重试,_handle_switch() 在目标相机流有帧后才切换并重建纹理。

    组件 / 函数 静态职责
    GuiApplication.init_window() / render() 创建窗口与可选离屏纹理;每帧收集触摸、绘制栈顶 Widget、结束绘制;关屏仍 yield False
    AugmentedRoadView._render() 行车门控、选宽窄相机、更新标定、scissor 限定内容区,按固定顺序绘制
    CameraView._render() 非阻塞取最新帧;无新帧沿用旧画面,断连清帧显示占位;座舱相机水平翻转
    CameraView._ensure_connection() / _handle_switch() 断连按 0.2 秒重试;目标流有帧后切换并重建纹理
    AugmentedRoadView._update_calibration() / _calc_frame_matrix() 接受有效且 calibrated 的外参,按内参/宽窄镜头/消失点算视频变换,并设车体到屏幕矩阵
    ModelRenderer._render() modelV2 与外参需本次行车后至少到达一次;按脏位重算图形
    AlertRenderer.get_alert() 正常显示 selfdriveState 告警;首启等待与状态失联有单独降级提示

    图像缓冲与 GPU 资源

    comma 分支由 CameraView.__init__() 初始化 EGL 和占位纹理。每个 VisionIPC 缓冲的 idx 首次出现时,create_egl_image() 复制 fd,并用 NV12 两平面、stride、UV 偏移属性导入 DMA-BUF;_render_egl() 缓存 idx -> EGLImage,绑定 GL_TEXTURE_EXTERNAL_OES 后由 shader 绘制。切流或关闭时 _clear_textures() 销毁 EGL image,destroy_egl_image() 关闭复制的 fd。这里的「零拷贝」只表示 UI 不执行 CPU 端 YUV 上传,不保证摄像头至显示全链路没有拷贝。

    非 comma 分支由 _initialize_textures() 创建 Y 和 UV 纹理,_render_textures() 仅在帧变化时上传数据,fragment shader 把 YUV 转成 RGB。两个分支都依赖 producer 的 NV12 缓冲尺寸、stride 与生命周期;更换摄像头/ISP、显示驱动或 GPU 时应先验证缓冲复用、纹理方向、色彩和断流恢复。idx 缓存以 _initialize_textures() 为释放边界,非切流的异常重连是否会复用相同 idx 但更换底层 fd,需要实机注入断连测试,不能仅凭源码保证。

    投影、裁剪和告警

    ModelRenderer._update_raw_points() 提取路径、车道线、路缘和加速度,_update_model() 依据前车距离缩短路径,_map_line_to_polygon() 把车体坐标两侧采样点投影成屏幕上的带状多边形,并过滤投影奇点和区域外点。渲染层外侧还有 CLIP_MARGIN=500 的几何筛选与 AugmentedRoadView 的 scissor 裁剪,两者用途不同。shader_polygon.draw_polygon() 把两条边交织成 triangle strip 并懒加载 shader;Gradient 限制最多 20 个色标,但显式 stops 不校验与颜色等长。普通路径颜色跟 longitudinalPlan.allowThrottle 混合,实验模式路径颜色与加速度有关。

    需要明确的是:这些门控只保证某些消息在本次行车后到达过一次,并不构成「摄像头帧与模型预测严格同时间戳」的保证,也没有在 CameraView 每帧旧画面保留时自动隐藏模型。告警应作为独立于装饰性图层的受信任通道验证:摄像头占位、UI 消息断流、绘制遮挡、驾驶员接管提示和语音提示须分别测量。

    大屏与 mici 的实现差异

    mici 的 AugmentedRoadView._render() 为侧栏留宽、添加渐隐纹理、置信球与书签,告警先经 will_render() 决定 HUD 顶部图标和方向盘提示,再绘制告警/HUD;大屏则使用彩色行车边框,叠加顺序不同。mici 的 ModelRenderer._render() 在 disengaged 时隐藏车道/路径,大屏没有这一条件——两者不能视为同一 UI 的简单缩放。

    对比项 大屏 mici
    布局与边框 彩色行车边框 为侧栏留宽、渐隐纹理、置信球与书签
    告警协调 按固定叠加顺序绘制 先经 will_render() 决定 HUD 顶部图标与方向盘提示
    车道/路径显示 无 disengaged 隐藏条件 disengaged 时隐藏车道与路径
    离路切换 重新 onroad 且曾有帧时重建客户端 排空现有连接的队列并清帧
    着色器 基础 YUV 转换 另有 engaged 去饱和/对比度与座舱增强

    过渡逻辑也不同:mici 的 CameraView._offroad_transition() 排空现有连接的队列并清帧,大屏的 _offroad_transition() 在重新 onroad 且曾有帧时重建客户端;迁移时要为两种屏幕分别验证切换时不会闪出上一行程图像。mici 的着色器另有 engaged 去饱和/对比度和座舱增强。

    自制硬件与插件化结论

    硬件原型至少需要相机同步/时间戳、可稳定复用的 NV12 缓冲和 DMA-BUF/EGL 支持或经验证的纹理上传替代、可见且可测的显示路径、可靠触摸/背光/屏幕唤醒,以及独立于 UI 的车辆安全网关。台架验收项可列为:相机断连/重启/切宽窄流、GPU 上下文丢失、屏幕休眠唤醒、模型/状态消息失联、告警抢占、长时间热降频,并测量真实最坏时延。

    「万物即插件」适合导航、可视化指标和非关键叠加;摄像头底图生命周期、标定/投影契约、接管告警与失联降级、驾驶员状态显示应由受信任宿主掌控。插件只读受版本约束的消息快照,通过宿主分配的低优先级绘图区域输出,不能遮挡或替代核心告警,更不能发布 selfdriveState 或触发车辆控制。

    常见问题

    「零拷贝」是否意味着摄像头到屏幕全链路都没有拷贝?

    不是。源文档明确这里的「零拷贝」只表示 UI 不执行 CPU 端 YUV 上传,不保证摄像头至显示全链路没有拷贝。comma 分支用 DMA-BUF 导入 EGLImage,非 comma 分支则在帧变化时上传 Y/UV 纹理,两条路径的拷贝点不同。

    模型叠加会不会因为显示旧画面而与视频帧错位?

    存在这种可能。CameraView._render() 无新帧时沿用旧画面,而源码没有在每帧旧画面保留时自动隐藏模型;门控只保证 modelV2 与外参在本次行车后至少到达一次,并不构成摄像头帧与模型预测严格同时间戳的保证。

    大屏和 mici 两套界面能不能当作同一 UI 的缩放?

    不能。mici 为侧栏留宽、加渐隐纹理与置信球,告警先经 will_render() 协调 HUD,且 ModelRenderer 在 disengaged 时隐藏车道/路径;大屏使用彩色行车边框、叠加顺序不同,也没有 disengaged 隐藏条件。

    第三方插件能否自行绘制告警或相机底图?

    不建议,也不符合源文档的边界。摄像头底图生命周期、标定/投影契约、接管告警与失联降级、驾驶员状态显示应由受信任宿主掌控;插件只读受版本约束的消息快照,在宿主分配的低优先级区域输出,不能遮挡核心告警或发布 selfdriveState。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/ONROAD_GPU_RENDER_DEEP_DIVE.md(取用了帧循环、图像缓冲与 GPU 资源、投影裁剪与告警、大屏与 mici 差异及硬件插件边界)。
    • 项目 02 —— openpilot-research/UI_SHADER_POLYGON_GRADIENT_CONTRACT_DEEP_DIVE.md(仅用于确认 shader_polygon 与 Gradient 的边界)。

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

    更新记录

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

  • 车载 UI 控件:可点击不等于授权,滑动确认不重检车况

    结论:界面控件「可点击、可见、已滑动确认」都不是车辆控制授权,也不构成功能安全保证。Toggle 先翻转内存状态再调用回调,set_state() 甚至不触发业务回调;MultiOptionDialog 的「显示层选择」与「持久化」是两步;滑动确认只减少偶发触碰,并不自动重检当前车辆状态。本文为固定源码快照的静态阅读,失败模式是源码边界推断,不是实车复现。

    基础输入与弹窗

    Button 复用基础 Widget 的点击回调和命中规则,_update_state() 根据按压与禁用选择背景/文字颜色,_render() 绘制按钮及文字。ButtonRadio 在释放后切换自身选中状态;IconButton 用纹理和透明度滤波表示状态。视觉上的 DANGER 风格只改变样式,不增加确认门槛。

    控件 关键行为 边界
    Toggle _handle_mouse_release() 先翻转内存状态、再调用可选回调 set_state() 不调用业务回调,也不代表 Params 已更新
    ConfirmDialog 确认或取消时先 pop_widget(),再以 DialogResult 调业务回调 接受 Enter/Escape 物理键;若当作不可绕过安全门需另作状态约束
    单按钮 alert_dialog() 隐藏取消按钮 底层仍支持 Escape 取消
    MultiOptionDialog 先在列表中暂存 selection,仅当当前选项与原值不同才启用 Select _set_result() 出栈后由调用方决定是否写参数;显示与持久化是两步

    两种屏幕的设置写入路径

    大屏与 mici 不是仅换了皮肤的同一套业务状态机,新增设置时必须逐屏追踪写入和刷新路径。

    路径/关键函数 参数与门控
    TogglesLayout._update_toggles() 大屏按 CarParams 的纵向控制能力显示/禁用实验模式和驾驶风格;从 Params 重新同步开关;需重启的开关在 engaged 时禁用
    TogglesLayout._handle_experimental_mode_toggle() 首次开启实验模式弹出富文本确认;确认后写 ExperimentalMode 与 ExperimentalModeConfirmed,取消时回滚 UI 开关
    TogglesLayout._toggle_callback() 非实验模式开关同步写布尔参数;需重启的选项追加 OnroadCycleRequested
    TogglesLayoutMici._on_experimental_mode() 首次开启跳到确认页,取消时不写开启参数;完成滑动确认后才写入确认与模式参数
    BigParamControl._handle_mouse_release() 先执行 BigToggle 的状态翻转/可选回调,再写对应 Params;restart_needed_callback() 只请求行车周期重启

    mici 多选项控件的回调与参数顺序

    BigMultiToggle._handle_mouse_release() 先调用 BigToggle 父类:最底层 Widget._handle_mouse_release() 执行可选通用点击回调,然后 BigToggle 翻转 _checked 并执行 toggle_callback(布尔值);返回后才用 options.index(value) 循环到下一选项,执行 select_callback(新字符串)。BigMultiParamToggle._handle_mouse_release() 再把新选项索引写入 Params。因此若调用方同时注册这些回调,观察顺序是点击 → 布尔翻转 → 选项变化 → 参数写入,前面的回调不能假定持久参数已更新。父类 BigToggle 的 _checked 在 BigMultiToggle._draw_content() 中不参与选项 pill 绘制,布尔回调值与屏幕当前选项语义无关。

    BigMultiParamToggle._load_value() 用 Params.get(param) or 0 直接索引选项列表。LongitudinalPersonality 在参数 schema 中是整数,log.capnp 枚举的 aggressive/standard/relaxed 顺序与当前 UI 选项一致。缺参回到 0;越界正值会在构造时抛 IndexError,负值按 Python 负索引显示非预期选项。本类不校验外部修改后的参数范围,也没有在写入失败时回滚已轮转的画面值。

    插件化时多选设置应显式建模为一个枚举状态和一次提交结果,不把内部隐藏布尔态暴露为第二个业务状态;宿主应在显示与提交两处检查选项范围、车辆状态和参数写入回执。硬件台架要测物理/触摸重复释放、滚动撤销、外部方向盘按钮并发改 personality、畸形持久值及写入失败后的 UI 一致性。

    滑动确认如何生效

    SliderBase._handle_mouse_event() 只在圆形手柄区域开始拖动,并根据释放时的位置设置已确认时间;_update_state() 等手柄动画到终点且至少经过 0.2 秒后仅调用一次确认回调。LargerSlider 用于 mici 安装流程,BigSlider/RedBigSlider 用于 BigConfirmationDialog,后者可在确认后先动画退出,再调用业务动作。滑动减少偶发触碰,但并不自动重检当前车辆状态。

    还需注意释放路径的守卫条件:基础 Widget._process_mouse_events() 在收到 release 时先调用子类 _handle_mouse_event(),再检查此前是否按下并在命中区内;而 SliderBase._handle_mouse_event() 的确认判断只看手柄位置,未显式要求本次仍在拖动手柄。若 UI 帧停顿、手势取消或其他释放事件与残留位置交错,需验证不会误确认——这是静态推断的手势竞争测试点,不是实测误触结论。

    硬件与插件化边界

    硬件验收应覆盖触摸抖动、按下后移出、长按、快速滑动、休眠首触、物理键 Enter/Escape、页面弹出时触点未释放,以及确认期间 engaged/车型能力变化。控制类写入应在最终业务回调再次核对当前状态,不能只靠按钮 enabled、警示颜色或滑块门槛。插件可提供非关键 UI 控件和设置说明,但宿主须掌握关键参数允许列表、写入权限、确认流程及行车冻结规则;涉及 OpenpilotEnabledToggle、实验模式或硬件/网络状态的写入不得由任意插件直接提交。

    常见问题

    把开关拨到「开」就代表参数已写入吗?

    不一定。Toggle._handle_mouse_release() 先翻转内存状态再调用可选回调,set_state() 甚至不调用业务回调;显示层的翻转与 Params 持久化是不同的两步。

    滑动确认能替代对车辆状态的检查吗?

    不能。滑动只在手柄到达终点且至少经过 0.2 秒后调用一次确认回调,用于减少偶发触碰,但不会自动重检当前车辆状态;控制类写入仍需在最终业务回调再次核对。

    mici 多选项控件的回调顺序是什么?

    若调用方同时注册回调,观察顺序是点击 → 布尔翻转 → 选项变化 → 参数写入,前面的回调不能假定持久参数已更新,因为参数写入发生在最后一步。

    为什么负值的持久化参数会显示意外选项?

    BigMultiParamToggle._load_value() 用 Params.get(param) or 0 直接索引选项列表,负值会按 Python 负索引取到非预期选项,越界正值则在构造时抛 IndexError,本类不校验外部修改后的参数范围。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/UI_CONTROLS_CONFIRMATION_DEEP_DIVE.md(取用了基础输入与弹窗、两屏设置写入路径、mici 多选项控件、滑动确认与硬件/插件边界)。
    • 项目 02 —— openpilot-research/DYNAMIC_ALERT_CALLBACKS_DEEP_DIVE.md(仅用于确认「可见状态不等于授权」的相邻表述)。

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

    更新记录

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

  • 原生 Git 安装器:分支迁移、缓存复制与断电非原子替换窗口

    结论:这个原生 Git 安装器把 git clone/fetch、git checkout/reset 和复制、删除、移动交给 shell,只用断言判断返回码,且在关键窗口先删缓存与旧安装目录、再把新目录移入,三步不是一次原子替换——断电或命令失败可能留下旧目录已删、新目录未就位的状态。它也没有内建的下载总超时、安装树签名/摘要验证或固定 commit,远端仓库与分支即安装内容的信任根。已有的继续启动脚本会让程序直接进入完成画面而不核对安装目录,因此进度 100% 或 60 秒完成屏幕都不是安装成功回执。本文为固定快照静态分析,未运行下载、Git 命令、文件替换或设备启动。

    构建、入口与板型分支

    构建脚本仅在 extras 开启且架构为 comma_arm64 时产出四个变体:openpilot、openpilot_test、openpilot_nightly、openpilot_internal,分支由 BRANCH 编译宏写入,internal 变体另定义 INTERNAL。继续脚本和字体通过 ld -r -b binary 嵌入 ELF。安装器全局的 tici_device 调 C++ 硬件类型判断,选择 2160×1080 或 536×240 窗口;新板型进入该路径前必须先满足跨语言硬件身份契约。

    函数/组件 实现链与限制
    get_str()、GIT_URL/BRANCH_STR 从带分隔与填充空格的编译字符串截取有效值;没有运行时 URL/分支白名单
    branchMigration() 按 TICI/TIZI/MICI 对 release3、staging、master 等分支作迁移;未知板型保留原分支
    doInstall() 无限等待系统时间有效,删除临时安装目录;只有旧安装目录与缓存目录同时存在才走缓存复制
    freshClone()/cachedFetch() 新安装用浅克隆与递归子模块;缓存分支先整目录复制、改 remote/branch,再 fetch
    executeGitCommand() popen() 读 Git 输出,以英文进度前缀与百分号更新界面;命令没有整体期限
    cloneFinished() 断言 Git 返回 0,checkout/reset/submodule update,删旧标记与安装目录,移动新目录
    main() 初始化界面与字体,迁移分支;已有继续启动脚本则直接显示完成画面,否则安装后收尾

    拉取路径上,doInstall() 没有内建超时或离线失败界面:代码不验证旧目录或缓存标记内容的真实性。executeGitCommand() 用 popen() 读 Git 输出并以英文进度前缀与百分号更新界面,std::stoi() 解析进度没有局部异常处理,命令也没有整体 deadline。

    落盘次序与非事务替换

    安装器把 git clone/fetch 的 shell 命令、git checkout/reset 和复制、删除、移动、改权限都交给系统调用;run() 只用 assert(err == 0) 报失败。在启用断言的构建中会中止;如果断言被编译掉,命令非零返回仍可能继续后续步骤。cloneFinished() 对 pclose 和 chdir 也采用断言,没有结构化错误恢复。不能把最终 100% 进度或 60 秒完成屏幕当作安装成功回执。

    关键窗口在 cloneFinished():先删缓存目录,再删旧安装目录,然后移动新目录;这些操作不是单个原子替换,断电或命令失败可能留下旧目录已删除而新目录未就位的状态。继续脚本先写新文件、再改权限与重命名;脚本内容仅进入安装目录并执行启动脚本。源码中未见安装树签名或摘要验证、Git commit 固定 pin,也没有对继续脚本执行成功的确认,因此远端仓库与分支就是安装内容的信任根。INTERNAL 分支还会写入 SSH 开关、前向摄像头锁和公钥参数,不能把该构建变体当作普通公开设备的等价安装包。

    步骤 动作 风险
    1 删除缓存目录 后续失败时缓存已丢失
    2 删除旧安装目录 旧版本在此刻起不可用
    3 移动新目录就位 断电可致新目录未就位
    4 写并重命名继续脚本 仅在收尾阶段执行

    继续启动链与两层信任

    已有继续启动脚本时,本程序不再核对安装目录是否存在、是否匹配标记,也不执行更新,只进入完成画面;旧安装树与标记的组合可能来自外部安装或恢复流程,本归档无法证明其完整性。若外部启动器执行该脚本,它会先进入安装目录,再执行启动脚本,而启动脚本立即转交另一个脚本。后者先加载环境脚本中的平台版本与 staging 根,再检查已定稿的 overlay 更新,并可能替换整个安装目录、重新执行启动脚本;之后建立子模块 import 链接,执行平台初始化与版本升级门,最后按 prebuilt 标志决定是否调用构建,再运行进程管理器。这进一步说明继续脚本不是「安装完成」的最终回执,后面仍可能发生 overlay 切换、系统镜像更新和构建失败。

    还需要区分两层:Python 下载界面只检查 ELF 魔数,这个原生 Git 安装器再拉取源码并准备启动脚本;中间的分发、执行与重启交接仍需在目标镜像与服务端验证,不能从这两份源码推导端到端可信链。平台初始化在版本比对和进程管理器启动之前把当前槽位标记为成功,它只表示该标记的时点,不证明服务已通过健康检查。

    自制硬件与插件化验收

    安装器应作为受信任维护服务,而不是任意插件执行 shell 的入口。新硬件方案需定义板型与目标分支的签名清单、固定 commit 或内容摘要、下载总超时和空间预算;先在新目录完整验证,再以可回滚的目录或槽位协议切换。宿主应记录安装事务 ID、来源、验证结果、旧版本与新版本,并在启动后回执确认;界面插件只显示状态。台架至少覆盖时钟长期无效、缓存损坏、Git 进度格式变化、fetch 与子模块失败、每一步断电、继续脚本已存在但安装目录缺失,以及不同板型窗口与字体。

    常见问题

    安装进度到 100% 就说明安装完成了吗?

    不能。进度来自解析 Git 的英文输出,cloneFinished() 之后还有 checkout、reset、子模块更新和目录替换,且这些操作不是原子的。已有继续脚本时程序还会跳过安装直接显示完成画面,因此 100% 与 60 秒完成屏幕都不是安装成功回执。

    为什么断电可能让设备处于中间状态?

    因为关键窗口先删缓存目录、再删旧安装目录、然后移动新目录,这三步不是一次原子替换。cloneFinished() 只用断言判断返回码,如果断电发生在移动之前,就可能出现旧目录已删除而新目录尚未就位的状态,后续还依赖外部启动器恢复。

    安装内容的信任根是什么?

    是远端仓库与分支。源码中未见安装树签名或摘要验证,也没有固定 commit pin;get_str() 只从编译进去的字符串截取 URL 与分支,没有运行时白名单。因此产物或分发端若改写这些字节,不能只靠界面文案判断目标值。

    缓存复制路径更安全吗?

    不必然。doInstall() 只在旧安装目录与缓存目录同时存在时才走缓存复制,cachedFetch() 会先整目录复制,再改 remote/branch 并 fetch;代码不验证旧目录或缓存标记内容的真实性,因此缓存路径的提升主要是速度,不是可信度。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/NATIVE_GIT_INSTALLER_BRANCH_CACHE_ACTIVATION_DEEP_DIVE.md(取用了构建变体、分支迁移、缓存复制、落盘次序、非事务替换窗口与继续启动链)。
    • 项目 02 —— openpilot-research/INITIAL_INSTALLER_DOWNLOAD_TRUST_BOUNDARY_DEEP_DIVE.md(仅用于确认下载侧只检查 ELF 魔数的信任边界)。

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

    更新记录

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

  • 硬件后端与 USB 快照:平台选择、热区与设备发现契约

    结论:openpilot 的硬件后端在模块导入时就以 /AGNOS 是否为文件确定平台,并实例化一个 HardwareComma 或 HardwarePc 单例;这不是按 USB、车型或插件清单动态完成的,运行中增删 /AGNOS 也不会切换已导入的单例。更关键的是,热区缺测会被解析为 0 摄氏度、USB 线缆 connected 只表示 Type-C 朝向非零,二者都不能当作传感器健康证据。本文为固定源码快照 c8fb906 的静态阅读与离线探针,未接触车端设备或真实热管理 sysfs。

    平台抽象与路径:能力不是「能实例化」就具备

    common.hardware.__init__ 在模块导入时以 /AGNOS 是否为文件确定 AGNOS、COMMA_HARDWARE、PC 三个标记,并实例化 HARDWARE:comma 设备用 HardwareComma,其他环境用 HardwarePc。cast(HardwareBase, ...) 只是类型提示,不提供运行时能力校验。HardwareBase 只把 get_device_type() 声明为抽象方法,其余多数接口有默认值、空操作或打印行为。

    接口 基类默认行为 含义
    电流/功率 默认 0 没有实现时返回 0,不等于实测为 0
    网络类型 默认 none HardwarePc 覆盖为 wifi
    shutdown() 仅打印 「能实例化」不等于具备车载关机能力
    get_thermal_config() PC 基类返回空配置 无热区配置时不应假定温度可用

    Paths 按已选平台给出日志、配置、持久化、共享内存和下载缓存路径。PC 的 comma_home() 带 OPENPILOT_PREFIX,设备侧 log_root() 固定在 /data/media/0/realdata/ 但可被 LOG_ROOT 覆盖,下载缓存可用 COMMA_CACHE 覆盖。这些路径不是同一个全局「沙箱根」,OPENPILOT_PREFIX 不具备权限隔离语义。

    热区读取的健康语义

    ThermalZone.read() 第一次在 /sys/devices/virtual/thermal 中按 type 查找编号并缓存,之后读取 temp 并按 scale 转为摄氏度。临时 sysfs 探针确认:未找到匹配项时编号仍为 -1,对应路径触发 FileNotFoundError 后返回 0;temp 文件消失同样返回 0,后续名称出现又可重新发现。但 os.listdir() 和 type 文件读取位于 try 之外,缺失 type 会传播异常,temp 非整数也会传播 ValueError。更需注意的是 zone_number 一经缓存不再重新按名称查找,探针确认热区编号对应的 type 改变后仍读原编号。

    ThermalConfig.get_msg() 遍历配置字段并把单项或列表输出为 cpuTempC、gpuTempC 等消息字段,未配置的类别被省略;HardwareComma.get_thermal_config() 给出对应平台的热区名称。结论很直接:自研主机不能用「0 °C」表示传感器失联,必须单独上报有效位或错误,否则后续热安全逻辑可能把缺测当低温。

    USB 发现到消息的逐层判据

    阶段 实现与确切含义
    拓扑变化 get_usb_topology() 返回 /sys/bus/usb/devices 中的名称集合;探针确认产品字符串改变但名称不变时集合不变。os.listdir() 抛 OSError 时返回空集合。
    设备枚举 usb_devices() 取有 idVendor 的 sysfs 节点并按名称排序;get_usb_state() 逐个读取 VID/PID、bus/dev 编号、速率、厂商和产品字符串。
    控制器与方向 controller() 从真实路径祖先中寻找 .ssusb 控制器;只有主控制器才把 Type-C 朝向 1/2 映射为 a/b。linkErrorCount 从 portli 读取并截 16 位,失败也变 0。
    线缆状态 cable_connected() 仅判断 Type-C 朝向整数是否非零;这是线缆/朝向指示,不是设备枚举成功、PCIe 就绪或 USB 传输健康的证明。
    消息发布 set_usb_state() 把传入列表写到 deviceState.usbState.devices;chestnutPresent 只根据运行态 VID/PID 判断,不核验产品字符串、数量或模型可用性。

    hw_state_thread() 约每 10 秒或名称集合变化时采集昂贵的硬件信息,并尝试投递到容量为 1 的队列;队列满则丢弃新快照。原函数探针确认它在采集和入队之前更新 prev_usb_topology:如果队列满、采集异常,而拓扑随后保持不变,下一轮不会仅因刚才未送达而重试,只能等周期触发或下一次拓扑变化。队列满时 prev_hw_state 仍更新,后续调制解调器温度为空时甚至可能继承一份从未交给主线程的温度。

    hardware_thread() 从初始空快照开始,拿不到新快照时沿用旧数据,随后调用 set_usb_state() 并驱动 Chestnut 维护和告警。因此 devices 与调用时重新读取的 connected 可能来自不同采样时刻,突然断线、重枚举或读取失败不能被简单解释为同一个原子 USB 状态。速率字段直接调用 read_int():探针确认 1.5 这类非整数文本会回退为 0 Mbps 而非保留小数或标记「未知」;同一测试中缺失制造商字段变空串、portli=0x10002 截为 2、主控制器朝向 1 映射为 lane a。所以 speedMbps == 0 不能直接解释为物理链速率为零。

    下游各自使用更严格或不同的判据:ChestnutStatus.update() 检查设备数量、精确产品字符串和速度;modeld.chestnut_present() 也要求产品字符串匹配;selfdrived 使用 chestnutPresent 与模型/Params 状态产生失败事件;UIState 还把线缆和编译产物纳入展示状态。不能把任意一个布尔值当作完整的外置计算就绪证明。

    面向自研硬件和插件的约束

    1. 平台后端应声明供电、热区、关机、屏幕、蜂窝、USB/PCIe 等能力及「不可用」状态;禁止用基类静默默认值伪装传感器健康。
    2. 设备发现事件与状态快照应带单调时钟采样时间、错误原因和新鲜度。VID/PID、固件版本、传输速率、线缆朝向、模型就绪必须分层校验。
    3. 数据总线可发布只读设备观测插件;固件刷写、特权 sysfs 写入、设备重置和关机属于受信任平台服务,只能在驻车维护流程中授权。
    4. 替换 /AGNOS 二选一选择前,先让原有 comma 与 PC 行为保持可回归,再对新平台测量热区缺测、USB 同名重枚举、队列积压和设备掉线后的退出时序。

    常见问题

    热区传感器缺失时读到 0 °C 意味着什么?

    意味着失联被当成低温。源码中未匹配到 type 或 temp 文件消失都会返回 0。自研主机必须另设有效位或错误上报,否则热安全逻辑可能误判。

    usbState.connected 为真代表设备已枚举成功吗?

    不代表。cable_connected() 仅判断 Type-C 朝向整数是否非零;它是线缆/朝向指示,不是枚举成功、PCIe 就绪或传输健康的证明,且发布时是另读的当前朝向,不必与设备快照同龄。

    为什么快照队列满之后可能不再重试?

    因为 hw_state_thread() 在采集和入队之前就更新了 prev_usb_topology。若队列满或采集异常而拓扑保持不变的,下一轮不会仅因未送达而重试,只能等周期触发或下一次拓扑变化。

    为什么 speedMbps == 0 不等于链路速率为零?

    因为速率字段直接调用 read_int(),1.5 这类非整数文本会回退为 0。读取或解析失败都归一为 0,因此不能把 0 解释为物理链速率为零。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/HARDWARE_BACKEND_USB_SNAPSHOT_CONTRACT_DEEP_DIVE.md(取用了平台抽象与路径、热区健康语义、USB 逐层判据、自研硬件约束与离线探针结论)。
    • 项目 02 —— openpilot-research/SYSTEM_FOUNDATION_DEEP_DIVE.md(仅用于确认 deviceState 与 hardware 线程的衔接位置)。

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

    更新记录

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

  • UIState.update() 在绘制之后:告警时延与 5 Hz 刷新

    结论:车载 UI 是呈现与交互层,不裁决自动驾驶使能,权威状态来自 hardwared、selfdrived、card 与模型进程。决定告警时延的是一条容易被忽略的顺序:GuiApplication.render() 的生成器在绘制顶层 Widget 之后才结束本帧绘制,本轮 UIState.update() 因此发生在绘制之后,新消息通常在下一帧才用于 Widget——所以 uiDebug.frameTimeMillis 不能当成「消息发布到屏幕显示」的端到端时延。本文为对本地固定源码快照的静态阅读,逐个像素、全部语言与 GPU 合成均未验证。

    入口、数据与帧顺序

    ui.main() 初始化窗口后,按屏幕尺寸创建 MainLayout 或 MiciMainLayout,每次从 GuiApplication.render() 的生成器取得帧状态再调用 UIState.update()。屏幕关闭时生成器仍周期性 yield False,让 UIState 有机会更新并重新唤醒屏幕。触摸事件经 MouseState 由顶层 Widget 分发到子 Widget 或导航栈。

    组件/关键函数 实现作用
    UIState._initialize() 建立单例与大量 cereal 主题订阅,初始化 started、status、车型、实验模式和 Chestnut 状态
    UIState.update() 非阻塞 SubMaster.update(0),刷新状态/告警枚举,再调用屏幕设备管理;约 5 Hz 后台线程读取较慢的 Params
    UIState._update_state() / _update_status() 从 pandaStates、deviceState、selfdriveState 派生点火/行车/使能状态并通知边沿回调;started 要求 deviceState.started 且自身缓存点火为真
    UIState._update_chestnut_state() 根据设备发现、模型编译产物、新鲜度与 Params 显示断开/加载/激活/失败,与固件刷写状态不是同一状态机
    Device._update_brightness() 行车时按相机曝光估算环境亮度并平滑,后台线程设置背光;触摸、点火、交互超时控制屏幕电源
    Widget.render() / _process_mouse_events() 先更新状态再绘制,然后处理触摸;只接受从自身命中区域开始的触摸,长按达 0.45 秒后清除点击跟踪

    需要单独记住的是:UIState.update() 里的非阻塞订阅只刷新枚举与屏幕设备管理,较慢的参数项由约 5 Hz 的后台线程承担,这两者的节奏并不相同。

    页面与关键交互

    MainLayout._set_mode_for_state() 管理首页、设置、行车三态;行车切换会自动进入摄像头画面,侧边栏可由行车画面触摸切换。SettingsLayout 分为 Device、Network、Toggles、Software、Firehose、Developer 六面板,其中 TogglesLayout._update_toggles() 会根据车型纵向能力、分支和 engaged 状态限制开关。

    页面/组件 关键入口 行为
    大屏主导航 MainLayout._set_mode_for_state() 首页、设置、行车三态;行车切换自动进入摄像头画面,body/notCar 特殊处理
    大屏首页/侧边栏 HomeLayout._update_state() / Sidebar._update_state() 显示更新/驻车告警、网络/温度/连接/设备状态,可进入设置或发布书签
    大屏设置 SettingsLayout Device、Network、Toggles、Software、Firehose、Developer 六面板,开关受车型与状态限制
    首次引导 OnboardingWindow 及其 mici 版本 条款版本与培训版本写 Params;mici 另延长交互超时并将亮度设为 100,退出后恢复
    mici 主导航 MiciMainLayout._handle_transitions() 横向滚动驻车告警、首页、行车/Body;点火后约 2.5 秒切换行车,静止时返回行车

    行车画面与告警

    AugmentedRoadView._render() 先绘制摄像头,再按顺序叠加模型路径、HUD、告警和驾驶员状态,且仅在 ui_state.started 时执行。CameraView._render() 非阻塞读取 VisionIPC:没有新帧时保留上一帧,连接丢失时清空并绘制占位;comma 硬件走 EGL 直接缓冲,PC 走 Y/UV 纹理拷贝。ModelRenderer._render() 要求模型和标定至少在本次行车之后收到过。

    相机画面的连接管理也不简单:_ensure_connection() 对重连做了节流,_handle_switch() 要等目标流有帧才完成宽/窄切换,而宽/窄流的选择取决于实验模式与速度门限;_update_calibration() 只消费有效标定矩阵。屏幕上的路径只是视觉呈现,不是控制输出的独立证明。

    告警侧的两个时间门值得单列:AlertRenderer.get_alert() 在 10 秒未收到首次行车 selfdriveState 时显示等待;在 comma 硬件上,已收过状态但断流超过 5 秒时先显示接管/系统失联,再显示重启提示;大屏实现还会拒绝前一行车周期的旧告警。mici 的 AlertRenderer.get_alert() 有相同超时分支,但正常告警路径没有大屏的 recv_frame < started_frame 过滤——这是一个需要回放验证的旧告警可见性风险,不是已实测问题。语音失联告警由 soundd 负责,不能由普通 UI 插件替代。

    硬件与插件化边界

    显示平台适配需提供可靠触摸、屏幕电源、背光、GPU/EGL 或纹理路径和相机图像零拷贝能力,并监测触摸到反馈、消息到危险告警、屏幕唤醒与画面丢帧的真实时延。装饰性页面与指标可以插件化,但接管告警、失联占位、条款/培训、驾驶员监测提示及设置权限应由受信任核心约束;插件不能覆盖 selfdriveState 权威主题,也不能在未完成车型和安全校验时直接开启控制选项。

    现有 test_raylib_ui 只检查 UI 进程初始化;截图差异回放覆盖若干页面/告警视觉状态,但本次未运行。两种屏幕、触摸竞争、前后帧时延、长期断流与各语言文本溢出均尚未实测。

    因此插件化要区分「装饰」与「权威」:装饰性页面与指标可自由替换,而告警呈现、失联占位、条款培训与设置权限必须留在受信任核心;插件也不得在未完成车型与安全校验时开启任何控制选项。

    常见问题

    uiDebug.frameTimeMillis 是端到端显示时延吗?

    不是。生成器在绘制顶层 Widget 之后才结束本帧,本轮 UIState.update() 发生在绘制之后,新消息通常在下一帧才用于 Widget;因此该指标只是帧数据,不等于从消息发布到屏幕显示的端到端时延。

    UIState 的状态刷新是实时的吗?

    不是。状态订阅用非阻塞 SubMaster.update(0),而较慢的 Params 项由约 5 Hz 的后台线程刷新;屏幕关闭时生成器仍周期性 yield False 让 UIState 有机会更新并重新唤醒屏幕。

    UI 的 started 状态是怎么定的?

    UIState._update_state() 要求 deviceState.started 为真且自身缓存的点火为真,同时从 pandaStates/deviceState/selfdriveState 派生状态并通知边沿回调。

    行车告警的等待与失联阈值是多少?

    AlertRenderer.get_alert() 在 10 秒未收到首次行车 selfdriveState 时显示等待;comma 硬件上已收过状态但断流超过 5 秒时先显示接管/系统失联,再显示重启提示。mici 缺少大屏那层旧告警过滤,尚需回放验证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/UI_RUNTIME_DEEP_DIVE.md(UI 入口、UIState 聚合、页面导航与行车告警时延)
    • 项目 02 —— openpilot-research/CHESTNUT_USB_FIRMWARE_DEEP_DIVE.md(源文档指向的 Chestnut 固件刷写状态专题)
    • 项目 02 —— openpilot-research/UI_SIDEBAR_STATUS_FRESHNESS_ACTION_CONTRACT_DEEP_DIVE.md(源文档指向的侧栏状态新鲜度与动作门专题)

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

    更新记录

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

  • 52 张预览自动校验:五屏 HUD 怎么把「文字出界」变成一条断言

    结论:当原型扩展到 5 块屏 × 5 套主题 × 全部副视图之后,人工目检一定会漏。capture.mjs 用 Playwright 遍历这个矩阵、输出 52 张预览,并把六类检查写成断言——其中最关键的一条是:用 getBBox() 逐项检测每个 <text> 是否越出画布,把「文字出界」从肉眼问题变成可回归的失败。

    为什么人工目检在 5×5 矩阵下必然漏

    5 块屏乘以 5 套主题,再乘上每块屏的副视图,需要核对的画面数量在几十到上百量级。人工看图的失效方式很规律:前十几张会认真比对,之后注意力衰减,最后只看「有没有明显崩掉」。而越界的文字往往只差几个像素——恰恰是最不容易被肉眼抓住的那类问题。

    更麻烦的是回归:一旦改了某个令牌或布局推导规则,理论上所有尺寸都要重看一遍。人工流程做不到这一点,于是问题会以「上个版本还好好的」的形式在下游暴露。

    遍历矩阵与产出

    脚本按「屏幕 × 主题 × 副视图」三重循环遍历,每张画面截图输出到 previews/,同时执行断言。最终产物是 52 张预览加一份断言结果——前者用于人工确认视觉意图,后者用于机器判定是否回归。

    运行方式上有一个实际操作细节:node 不在系统 PATH 上,必须用完整路径调用,因此脚本入口固定写明了解释器路径,避免在不同机器上以不同的 Node 版本跑出不同结果。

    六类自动断言

    断言 检查什么 为什么重要
    viewBox 原生像素 SVG viewBox 是否等于各板原生分辨率 防止某个尺寸悄悄套用了别的画布
    文字越界 所有 <text> 的 getBBox() 是否超出画布 最容易被肉眼漏掉、也最影响可读性
    undefined 泄漏 SVG 内容中是否出现字符串 undefined 数据绑定失败的典型症状
    默认主题 默认主题是否为 pro、主题切换是否生效 防止切换逻辑静默失效
    状态推进 OBD 回放推进、计时完成、断连状态、路书翻页 验证交互状态机而非静态版式
    移动端溢出 移动端是否出现横向溢出 预览页本身的可用性

    getBBox 为什么是关键一条

    浏览器渲染文本时,元素的实际包围盒由字体度量决定,而不是由设计稿里的数字决定。所以「设计上放得下」和「渲染后放得下」是两件事。getBBox() 返回的是渲染后的真实尺寸,用它逐项比对画布边界,才能把这类问题从主观判断变成布尔结果。

    这条断言的直接收益是:改了字号、字距或字体之后,脚本会立刻告诉你哪些画面出界,而不需要人再逐张看。它同时也解释了为什么「换字体」必须重跑校验——字体一换,字宽就变,原本贴边的文本可能立刻越界。

    字体是隐性风险

    原型使用的是系统字体(Bahnschrift / Microsoft YaHei / Impact / Georgia / Consolas)。这些字体在桌面浏览器上可用,但不一定允许嵌入式再分发。因此固件移植前必须重新选定字体,并重跑一次越界检测。

    这类风险的特点是「在原型的阶段看不出来,在移植阶段集中爆发」。把它写进校验清单,而不是留到移植时才发现,是这套流程设计上比较值的一点。

    自动校验证明不了什么

    边界必须写清楚。这套校验只覆盖原型层的布局与状态逻辑,不证明:ESP-IDF/LVGL 的点屏、SPI/DSI 实际帧率、实车 PID 支持,以及计时精度。所有画面数值均为模拟数据并永久标注 DEMO,档位与油压尚无车型适配,实机须接入受保护的 OBD 网关。

    另外两项待实机确认的项:3.5 寸现有固件 DEMO 已采用横屏坐标,但尚未移植这套 UI;1.64 寸的横屏旋转与触摸映射仍待实机验证。浏览器里的通过,不等于屏幕上的通过。

    实测之后的链路可以这样分工:赛道上的实时状态由 SR-Telemetry 实时遥测云 以低于 500 ms 的延迟推给场边教练;像 肇庆国际赛车场 这类高差明显的场地,更适合用来验证坡度相关信息的显示优先级。

    FAQ

    为什么不能靠设计师人工看预览图?

    因为 5 块屏 × 5 套主题 × 副视图是上百量级的画面,人工注意力会衰减,而文字越界往往只差几个像素。改一次规则就要全部重看,人工流程无法回归。

    getBBox 检测的是什么?

    检测渲染后的真实文本包围盒。字体度量与实际渲染尺寸只有在浏览器里才能确定,所以「设计上放得下」不等于「渲染后放得下」。

    为什么换字体要重跑校验?

    因为字宽会变。原本贴边的文本在换字体后可能立刻越界,必须重跑一次越界检测。

    自动校验通过了能直接上实机吗?

    不能。它只覆盖原型层的布局与状态逻辑,不证明 ESP-IDF/LVGL 点屏、SPI/DSI 帧率、实车 PID 支持与计时精度;1.64 的横屏旋转与触摸映射也仍需实机验证。

    参考资料

    • 项目 06 —— D:/AI/PROJECTS/kno/capture.mjs(Playwright 自动校验脚本)、previews/(52 张预览)
    • 项目 06 —— D:/AI/PROJECTS/kno/README.md(校验清单与已知限制)
    • 项目 04 —— research/instrument-matrix.md(风格覆盖规则与 52 张预览口径)

    更新记录

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

    {“@context”:”https://schema.org”,”@graph”:[{“@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”为什么 HMI 原型不能靠设计师人工看预览图?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”因为 5 块屏乘以 5 套主题再加副视图是上百量级的画面,人工注意力会衰减,而文字越界往往只差几个像素。改一次规则就要全部重看,人工流程无法做回归。”}},{“@type”:”Question”,”name”:”getBBox 越界检测到底检测什么?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”检测渲染后的真实文本包围盒。字体度量与实际渲染尺寸只有在浏览器里才能确定,所以设计上放得下不等于渲染后放得下。”}},{“@type”:”Question”,”name”:”为什么更换字体后要重跑越界校验?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”因为字宽会变。原型使用系统字体(Bahnschrift、Microsoft YaHei 等)且不一定允许嵌入式再分发,移植前必须重选字体并重跑文字边界检测。”}},{“@type”:”Question”,”name”:”自动视觉校验通过后能直接上实机吗?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”不能。这套校验只覆盖原型层的布局与状态逻辑,不证明 ESP-IDF/LVGL 点屏、SPI/DSI 实际帧率、实车 PID 支持与计时精度;1.64 的横屏旋转与触摸映射仍需实机验证。”}}]}]}

    延伸阅读

  • 一套令牌覆盖 5 种分辨率:车载 HUD 从 456×280 到 1280×720 的适配工程

    结论:一块屏扩成五块屏,成本取决于「一套规则能复用多少」,而不是画了多少张图。我们的赛道 HMI 原型用一套设计令牌加一组布局推导规则驱动 5 种画布,从 456×280 到 1280×720,自动生成 52 张预览。新增一块屏时改的是推导规则,不是重画一套视觉。

    五块画布的尺寸与旋转关系

    画布 UI 分辨率 物理面板 定位
    1.64 寸 456×280 280×456 竖屏,旋转使用 极简零百计时
    3.5 寸 480×320 320×480 竖屏,旋转使用 单表
    4.3 寸 800×480 480×800 IPS 横屏使用 赛道分区
    5.0 寸 1280×720 720×1280 IPS 横屏使用 宽屏全景
    3.97 寸电子纸 800×480 800×480 电子纸 离线路书

    这里有个容易沿用的错误:1.64 寸不是圆屏。它的原生面板是 280×456 的长方形 AMOLED,早期视觉稿里的「400×400 圆表」是过时概念稿。这个数字很重要——它决定了 UI 必须按 456×280 重新排,而不是把圆表缩放过来。

    令牌分两层:结构色与语义色

    以默认的「厂商(pro)」主题为例,令牌按职责分成两层,这个分层是跨尺寸复用的前提:

    层 令牌 色值 用途
    结构层 背景 #05080A 夜间主屏
    面板 #0D1417 卡片与次级底条
    抬升面 #131D21 最佳成绩与告警格底
    主读数 #EDF3F4 车速、转速、时间
    次级文字 #93A7AC 标签与单位
    三级文字 #5B6E75 指标名与弱化信息
    语义层 青色 #4FD1C5 最佳圈、速度弧
    琥珀 #F5B942 DELTA、预警、转速弧
    绿色 #7CD98A 低转换挡灯、正常状态
    红色 #FF5A4E 高转灯、报警

    结构层可以随主题变化,语义层不可以。红 / 琥珀 / 绿在五套主题中始终对应报警、预警与正常——这样驾驶员换主题后仍然能「不读数值就判断状态」。语义色不得用于装饰,只能承载状态。

    布局靠推导,不靠等比放大

    这是跨分辨率适配的核心规则:圆表的半径与圆心由可用区宽与高度共同推导,而不是从一个基准尺寸缩放。宽屏下如果直接放大,两个圆表会重叠;按可用区反推半径之后,4.3 寸与 5.0 寸都能保证不重叠。

    同理,信息密度也不是线性增长。5.0 寸的底栏比 4.3 寸多一格胎压,这是宽屏专属的密度提升,而不是把 4.3 的布局拉长。反过来,1.64 寸承担不了圈速栈——那个尺寸只做超大数字双格与底部进度条。

    两类硬约束:电子纸与浅底主题

    电子纸单独一套版式,白底黑字,不套用任何彩屏主题。这不是风格偏好,而是刷新特性决定的:它只用于静态路书与停车状态信息,不显示实时车速、转速或胎压告警。局刷、翻页残影与休眠电流目前仍是待实测项目,在确认波形与刷新能力之前,不能把部分刷新写成必然支持。

    复古主题是浅底设计,被明确排除在夜间驾驶首屏之外——浅底在夜间的反差不足。这条排除规则和电子纸一样,属于设计约束而非审美取舍。

    余光可读靠排版,不只靠字号

    标签统一加字距,数值走 Bahnschrift 类窄体无衬线。窄体能让同样的横向空间容纳更多字符,加字距则让标签在余光扫过时更容易被区分。这两个手段加起来的效果,比单纯放大字号更省空间。

    需要提醒的是,预览使用的是系统字体(Bahnschrift / Microsoft YaHei / Impact / Georgia / Consolas)。固件移植前必须选定允许嵌入式再分发的字体,并重新检查文字边界——换字体就会改变字宽,越界检测要重跑一遍。

    主题切换在设计上有一个可复现的前提:五套主题共享同一组模拟 OBD 数据、计时与告警状态,切换主题不重置回放。这让主题对比变成可比较的实验,而不是五次不同的演示。像 鄂尔多斯国际赛车场 这类高差明显的场地,坡度信息在底部辅助条里的价值更高;而赛道数据的赛后分段复盘可以交给 SR-Data 圈速分析套件,HMI 只负责当场能读懂的那一屏。

    FAQ

    一套令牌真的能覆盖 456×280 到 1280×720?

    能。令牌定义的是色板与字号层级,布局由可用区宽与高度共同推导。例如双圆表的半径与圆心会按可用区计算,以保证宽屏下不重叠。

    1.64 寸是圆屏吗?

    不是。它的原生面板是 280×456 长方形 AMOLED,UI 横屏使用为 456×280,早期视觉稿里的 400×400 圆表是过时概念稿。

    为什么切换主题不会重置回放?

    因为五套主题共享同一组模拟 OBD 数据、计时与告警状态,主题只改变渲染方式,不重置数据。

    语义色可以跟着主题改吗?

    不可以。红 / 琥珀 / 绿在五套主题中分别对应报警、预警与正常,主题只改变底色与版式。

    参考资料

    • 项目 06 —— D:/AI/PROJECTS/kno/README.md(五屏五主题、设计令牌与布局推导)
    • 项目 06 —— D:/AI/PROJECTS/kno/style-skill-research.md(原创风格与检索边界)
    • 项目 04 —— research/five-board-hardware-facts.md(各板原生分辨率与旋转关系)
    • 项目 04 —— research/instrument-matrix.md(风格覆盖规则与 52 张预览)

    更新记录

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

    {“@context”:”https://schema.org”,”@graph”:[{“@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”一套设计令牌真的能覆盖 456×280 到 1280×720 五种分辨率?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”能。令牌定义的是色板与字号层级,布局由可用区宽与高度共同推导。双圆表的半径与圆心会按可用区计算,以保证宽屏下不重叠。”}},{“@type”:”Question”,”name”:”1.64 寸车载屏是圆屏吗?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”不是。它的原生面板是 280×456 长方形 AMOLED,UI 横屏使用为 456×280。早期视觉稿里的 400×400 圆表是过时概念稿。”}},{“@type”:”Question”,”name”:”为什么切换主题不会重置 OBD 回放?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”五套主题共享同一组模拟 OBD 数据、计时与告警状态,主题只改变渲染方式,不重置数据,因此主题对比是可复现的实验。”}},{“@type”:”Question”,”name”:”HMI 的语义色可以跟着主题改变吗?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”不可以。红、琥珀、绿在五套主题中分别对应报警、预警与正常状态,主题只改变底色与版式,语义色不得用于装饰。”}}]}]}

    延伸阅读

  • 车电接入:为什么 ESP32 的 TWAI 不等于 OBD 接口

    结论:TWAI 只是 ESP32 片内的 CAN 控制器,负责按 CAN 协议收发帧;它既不含物理收发器,也不是 OBD 接口。从芯片引脚到车上那个 OBD 口,中间还缺收发器、隔离与车电保护、接插件、车型协议栈四层,任何一层没做,都不算「支持汽车 OBD」。

    TWAI 是控制器,不是收发器,也不是接口

    因为 TWAI 在芯片内部只做到协议层,它输出的是逻辑电平的 CAN 帧,而不是能挂到双绞线上抗干扰的差分信号。芯片文档把它列为 CAN 控制器外设;要真正接上总线,必须在外面再加一颗 CAN 收发器。也就是说,TWAI 解决的是「怎么组帧、怎么仲裁、怎么应答」,收发器解决的是「怎么把 0/1 变成 CAN_H/CAN_L 的电压差」——这是两块硅片干的活。内部调研文档对此的表述很直接:芯片的 TWAI 不是 OBD 线束或车电接口;也不能把 ESP 芯片的 TWAI 外设等同于「可以直接接车载 CAN」。

    这个区分不是抠字眼。很多演示能跑通,是因为桌面上用的是现成收发器模块加固定线束;一旦换成真车,变量立刻从「代码能不能跑」变成「电气和协议能不能活下来」。

    从 TWAI 到 OBD 口,中间缺的四层

    因为缺的是四层而不是一层,所以「加了收发器就完事」的判断往往是错的。把每一层单独拆开看,缺口会很清楚:

    层级 ESP32 芯片里有没有 还缺什么 判断依据
    协议控制器(帧收发与仲裁) 有,即 TWAI 外设 — 芯片内置 CAN 控制器
    物理层收发器 没有 外部 CAN 收发器(如 CA-IS3050G,官方标称最高 1 Mbps、1000V 隔离) 内部调研:TWAI 需外部 CAN 收发器
    电气隔离与车电保护 没有 隔离 / 保护设计;稳压输入范围不等于车规结论 6–36V 是稳态输入范围,不证明能抗负载突降、反接或冷启动
    接插件与线束 没有 OBD 接头、电源取电、终端电阻、总线接入方式 需单独确认连接器与总线接入兼容性
    车型协议栈 没有 OBD-II 诊断请求,或按 DBC 解码的原始 CAN 帧 两条协议路径不同,不能互相替代

    四层里最容易被低估的是最后一层。物理上接通、帧也收到了,并不代表理解了内容——这取决于车型,也取决于你走的是哪条协议路径。

    「能收到 CAN 帧」不等于「支持 OBD」

    因为原始 CAN 帧解码和 OBD-II 诊断请求是两条不同的协议路径,收得到帧只是拿到了开场。OBD-II 走的是标准化的诊断请求与响应:你按标准向 ECU 问一个 PID,比如车速 PID 0x0D,ECU 按约定格式回一个值——这个值以整数 km/h 报告。而原始 CAN 帧走的是另一条路:总线上本来就有的报文,你需要有一份 DBC 或等价描述,才知道某个字节的某几位代表什么。

    两者的覆盖范围不同。标准 OBD-II 只能拿到法定那一批通用 PID;档位、油温、油压、增压这类字段,只有对应车型或外接传感器被确认之后,才有有效值可显示。所以不能用一个「TWAI + DBC」的后端宣称覆盖所有 OBD 功能。如果做数据记录,合理的做法是同时保留三样东西:原始帧、解码值、时间戳——保留原帧是为了以后换一份 DBC 能重算,时间戳是为了还原时序。站内另一篇讲车速 PID 取值的文章对这件事有更细的展开,见 OBD-II PID 入门:车速 0x0D 为什么是整数。

    电这一层:6–36V 是稳态范围,不是装车结论

    因为汽车电环境在启动和负载切换时会剧烈波动,一个标着宽压输入的模块并不能证明它能上车。M5Dial 后端子标称 DC 6–36V,但这是稳态输入范围,它不证明该板能抗汽车负载突降、反接或冷启动时的电压跌落。这类验证需要在车上按实际工况测,而不是看参数区间推导。

    同样的道理适用于任何「能供电」的表象。供电接通、CAN 收发器也接上了,仍然可能因为一次冷启动、一次搭电、一次反接而失效。车电保护是要单独设计的部分,不是买板子时附带的赠品。

    引脚预算:1.75 上真正可用的通用 GPIO 只有 16/17/18

    因为外设要靠引脚接出来,而这块板的引脚早就被板载器件分掉了大半,所以「计划外接多少东西」必须在画线之前先算。以已购的 ESP32-S3-Touch-AMOLED-1.75 为例:8Pin 排针只有 VBUS/GND/3V3、UART0 和 GPIO16/17/18,其中 43/44 是 UART0,可能还要承担调试输出;-G GPS 版本还可能占用 17/18;I2C 14/15 是板载多器件共用的总线。

    这意味着如果走 MCU 的 TWAI 控制器外接 CAN 收发器,GPIO 分配要和 GPS、TPMS 接收器、串口日志同时规划——三者会抢同一批引脚。另外要注意,P4 那几块板的 Wiki 概览页既没有列板载 GPS,也没有列车载 CAN 收发器;不能因为屏幕大就默认它自带车源接口。显示链路本身的差异另见 车载 HMI 的 30 FPS:SPI 与 MIPI-DSI 显示链路差在哪。

    常见问题

    ESP32 自带的 TWAI 能直接接汽车 CAN 总线吗?

    不能直接接。TWAI 是片内 CAN 控制器,输出的是逻辑信号,必须外接 CAN 收发器才能变成总线上的差分电平;除此之外还要解决隔离、车电保护、接插件和车型协议。缺任何一层都不算可用,「芯片支持 CAN」和「产品支持 OBD」之间隔着整条链路。

    加一块 CAN 收发器模块,是不是就等于支持 OBD 了?

    不等于。收发器只解决物理收发的一部分。收得到帧之后,你要么发出标准 OBD-II 诊断请求并按 PID 解析响应,要么按 DBC 解原始帧——这是两条不同路径,覆盖的字段也不同。此外还要确认 OBD 接头、电源取电、终端电阻和总线接入方式,这些都不是收发器模块自带的。

    只有 OBD-II,没有 DBC,能读到油温、增压这些字段吗?

    多数情况下读不到有效值。标准 OBD-II 只覆盖法定那批通用 PID,档位、油温、油压、增压这类字段需要对应车型或外接传感器被确认之后才能显示。没有 DBC 或等价描述,总线上即便有这些报文,也无法知道某个字节代表什么。缺失时应写「待实测」,而不是用猜的数值填表。

    为什么说 6–36V 供电不代表能装车?

    因为 6–36V 描述的是稳态输入范围,而车上更危险的是瞬态:冷启动跌落、负载突降、反接。宽压输入模块通常不包含这些防护。要上车,得按实际车电工况单独设计和验证保护电路,不能靠参数区间推导结论。

    本文数据来自星核火花赛车科技内部「车载小仪表开发板调研」项目的两份核查文档:《小仪表开发板详细调研与路线建议》(调研日期 2026-10-03)与《五块赛道产品候选板:硬件事实表》(核查日期 2026-10-03)。文中引脚编号、SKU、标称参数均以微雪官方 Wiki 概览页与项目内核查记录为准;项目文档明确说明本阶段为桌面资料核查,未做实机测试或供应商询价。TWAI 与 OBD 的层级差异、DBC 解码与 OBD-II 诊断的协议路径区分、引脚占用与车电保护要求,均引自上述两份文档的原始表述。涉及车载电压环境的判断(如 6–36V 为稳态范围)为文档给出的限定说明,不作为实测结论。相关产品形态与显示链路的讨论,见 圆屏还是方屏:车载仪表的显示形态该怎么选 与 OBD AI 插件化平台:端侧解析为什么必须插件化;数据采集形态可参考 SR-RaceBox 车载数据采集器。