标签: 测试验证

  • VTRUX 与 Coda 的 CAN 解码笔记:信号、电芯索引与验证方法

    结论:这份笔记覆盖九份 DBC、合计 238 条报文与 1,066 条信号,分属 Coda 的 B/C/D 三总线、VTRUX 的 OBD/PT 两总线、Bel 逆变器的 J1939 扩展帧,以及一辆 1975 Porsche 914 改装车的自定义 VCU 遥测。它的价值不在清单本身,而在把「反编译出来的标签」和「验证过的信号」分成五档,并如实记录了一次被纠正三次的电芯索引映射与八条被推翻的假设。

    九份 DBC 的量级与定义来源

    仓库统计明确说明:下面的报文 / 信号数来自拷进来的 DBC 文件本身,而不是上游 README 的表格——后者已经过期。Coda 的 B、D 两总线在上游 README 里写的是 35 和 22,实际文件是 34 和 23;914 的 README 还在描述一份 75 报文 / 496 信号的 schema 3.0,而文件已经是 schema 4.2、hash 0x09EB175A。

    DBC 文件 报文 / 信号 总线 定义来源 证据边界
    CODA-B-CAN.dbc 34 / 156 B CAN 原厂 CODACAN LabVIEW 诊断应用 + 实车抓包 上游 README 的 35 为过期值,以文件为准
    CODA-C-CAN-cells.dbc 26 / 104 C CAN 26 帧 0x000–0x019,每帧 4 个 16 位 Motorola 电芯,1 mV/bit 电芯编号自 2026-07-22 起改为 1-based
    CODA-D-CAN.dbc 23 / 44 D CAN LabVIEW 反编译 + 日志 上游 README 的 22 为过期值
    epri-obd-bus.dbc 12 / 18 OBD EPRI Logger iPhone 应用的 Ghidra ARMv7 反编译,再用车辆日志比对修正 字节位置与刻度并非一律正确
    epri-pt-bus.dbc 26 / 101 PT 同上 同上
    vtrux-obd-experimental.dbc 3 / 5 OBD 作者由日志推导的实验性定义 含未解析报文桩
    vtrux-powertrain-experimental.dbc 19 / 26 PT 同上 同上
    BelInverter-v2.dbc 8 / 73 J1939 扩展 ID EPRI 结论 + 一份更早的 DBC 合并 原始作者与部分字节布局未确认
    914-telemetry.dbc 87 / 539 自定义 VCU 遥测 改装 914 的 VCU 映射,非原厂 Porsche 协议 标准 11 位 ID 为真实车辆帧;29 位扩展 ID 为 VCU 串行遥测里的内部合成值

    确认档位:怎么区分「反编译出来的」和「验证过的」

    这份仓库最值得抄的一点是它不给信号打勾,而是打档。反编译只能告诉你应用把哪个字节当什么读,不能告诉你它读对了;日志相关只能告诉你两件事一起动,不能告诉你它们是不是一件事。于是每条信号都落在下面五档之一,档位直接写在验证表里。

    档位 含义
    CONFIRMED 编码与角色都被独立验证过:物理测试、行为日志证据、与已知参考相关、或跨 DBC 比对一致
    CONFIRMED-SYMM 靠与某个 CONFIRMED 信号的结构对称性确认,没有独立测试
    CONFIRMED-DECOMPILE 编码由 Ghidra 确认,角色只是应用自己的标签,没有独立物理或行为检查
    PARTIAL 编码确认,角色或刻度还有一条具体问题挂着
    REMOVED 按 DBC 信号策略删掉 SG_ 条目,原因记在 CM_ 注释里

    档位不是形式主义

    PT 总线上 0x0054 的 GENE_EmergcyStp_flg 是 CONFIRMED-DECOMPILE:Ghidra 里写着 (pbVar15[6] 与 8) 不等于 0,但所有日志里它一次都没触发过,没有行为测试。同理 0x0580 的 tcm_CAN_ERR。这类条目可以读,但不能当成「已经证明这个位是这个意思」来用。

    0x381 与 C CAN 电芯索引:一条被纠正了三次的映射

    C CAN 只在少数几次采集里被同时录下(需要第三个适配器),所以能回答这个问题的只有三份日志。总线识别靠内容而不是总线号:0x000–0x019 的电芯帧是 C CAN,0x381/0x382/0x383/0x390/0x439 是 B CAN,0x308/0x312/0x325/0x330/0x55C 是 D CAN——两次会话给同一条物理总线编的总线号并不一致。

    会话 日志 配对样本 偏移 +1 严格命中率
    2026-07-21 Android 三适配器 coda_20260721_192435_T0..T2、_201109_T0/T1、_202031_T0 660 82–87%
    2026-02 Bonnydoon 行驶 33 min projects/coda/logs/frombonnydoon.log 1,972 80–84%
    2026-07-22 充至 100% coda_20260722_173203_T0..T21 979 100%

    合计 43 万帧以上的 C CAN 电芯帧、8,300 帧以上 0x381、3,600 对以上时间配对样本,且配对时刻 C CAN 数组完整、没有帧旧于 2 秒。充电会话最干净:负荷低而稳,电芯在 B 与 C 两轮扫掠之间几乎不重排,严格测试在 VmaxId(591 样本)与 VminId(706 样本)上都命中 100.0%。

    会话 信号 严格样本数 命中偏移 +1 不同 ID 数 ID 范围
    Android VmaxId 135 82.2% 11 6–104
    Android VminId 146 87.0% 26 1–101
    Bonnydoon VmaxId 225 79.6% 44 6–104
    Bonnydoon VminId 376 84.3% 33 1–104
    充电 07-22 VmaxId 591 100.0% 2 82–95
    充电 07-22 VminId 706 100.0% 1 96

    「严格」的口径是:只保留 C CAN 数组自身的极值恰好等于所报极值、该极值由唯一一只电芯持有、且领先第二名至少 3 mV 的样本。行驶会话里没有任何竞争偏移超过 4%;剩下约 15% 也不是反例——两条总线不是同时采样的,C CAN 扫完 26 帧要约 800 ms,而 0x381 只有约 1.1 Hz,行驶中负荷波动会在两个时刻之间把几乎并列的电芯重排。签名也说明这是偏斜而不是置换:未命中的配对集中在少数几个本来就常年处于极值的 C CAN 索引上(max 侧 81/83/94/102,min 侧 76/80/95),真置换会把它们撒满整个数组。

    三次纠正各纠正了什么

    第一次是编号口径:映射被发现时 C CAN DBC 还是 0-based,关系是「上报 id 等于旧索引加 1」;2026-07-22 改为 1-based 后加 1 消失,两套编号重合。这也推翻了更早那句「id 用的是与 C CAN 位置无关的 BMS 内部编号」——BMS 编号就是 C CAN 的发送顺序(1-based)。

    第二次是字节序。2026-07-15 之前 0x381 的归属是 B0-1=MinCellVoltage、B2-3=MaxCellVoltage、B4-5=AvgCellVoltage、B6=VminId、B7=VmaxId;修正后是 B0-1=Avg、B2-3=Max、B4-5=Min、B6=VmaxId、B7=VminId。当年的错误来自照着 LabVIEW cluster 的名字「VminVmax」排,而不是按 LabVIEW 字段顺序 Vavg、Vmax、Vmin、VmaxId、VminId。重跑严格测试可以量化这两种读法的差距。

    归属方式 Android 样本 / 命中 +1 Bonnydoon 样本 / 命中 +1
    修正后 max — 值 B2-3,id B6 135 / 111(82.2%) 225 / 179(79.6%)
    修正后 min — 值 B4-5,id B7 146 / 127(87.0%) 376 / 317(84.3%)
    旧 max — 值 B2-3,id B7 135 / 0(0.0%) 225 / 1(0.4%)
    旧 min — 值 B0-1,id B6 0 / 0 0 / 0

    两端的失效并不对称。min 侧当时根本没有信号可用:旧的 MinCellVoltage 读 B0-B1,而那其实是包平均值;平均值几乎不可能唯一且带余量地等于某一只电芯,所以两次会话里都是零样本存活,没有东西可相关。min 侧还有第二个错:id 也取自错误的字节(B6 其实是真正的 VmaxId)。max 侧则是「值对、id 错」:B2-B3 在两种读法下都真是最大值,但索引来自 B7(真正的 VminId),于是正确的最大电压配上了最小电芯的编号,噪声里最好的偏移只有 21%——读起来很像「另一套编号约定」,非常容易被误判。

    第三次是对比方式。frombonnydoon.log 同时带 C CAN 与 0x381,它自己的 0x381 报 VminId 95 / VmaxId 84,正是那次笔记已经点名的两只电芯。也就是说这条映射在单份日志内部就可验,而当时的笔记却拿它们去和另一次(充电)会话的 96/76 比,把差距读成了编码差异。结论成了方法:只要两条总线同时在场,就在同一会话内部比。

    还有一条适用范围限制:本节的「+1」只适用于经 DBC 从原始字节解出来的值。从 CODACAN 界面截图上读到的数字(如充电分析里的 VminId=96 / VmaxId=76)来自 LabVIEW cluster,它一直是对的,不受 DBC 错误影响。哨兵值 255 也要单独处理:104 只电芯的包里 255 不可能是索引,它是无效 / 无数据哨兵,不要当成「第 254 只电芯」。

    证据边界:部分解码、未解与已推翻

    「已确认且完整」的信号有其编码摘要,例如 0x275 BMSDriveLimits 的 B0 = 放电限值(uint8 乘 2 得 A)、B1 = 充电限值(乘 2 得 A)、B2-B3 = 包电流(BE uint16 乘 0.1 A,零点 0x8000)、B4-B5 = 包电压(BE uint16,1 V/bit)、B6 = HVAC 限值(乘 2 得 A)、B7 未知(基线 0xDD)。下面列的是还没收口的。

    ID 名称 已知 还缺什么
    0x050 BMSChargerCmd B0-B1 = HVCurrentCmd(BE uint16 乘 0.1 A,R2 = 0.992;基线 0x00DC = 22 A);B2-B3 = MaxVoltage(乘 0.1 V,R2 = 0.605;基线 0x0FF7 = 408.7 V);中位帧 00 dc 0f f7 00 01 3e 01 B4-B5 取非基线值会把 Main Current 压到约 6 A,疑似帧校验或滚动计数;B6-B7 未知;需注入测试
    0x270 BMS_FaultStatus B0 为故障字节(0x00 无故障,0x12 = 故障码 18,bit 1 = 0x02 与 bit 4 = 0x10 置位);比 0x271 早一帧;drop 后 CODACAN 的 BMS Fault Severity 掉下去 B0 内部故障码与严重度的分界未扫;B6-B7 未知
    0x2C1 BMSDrivingRange B0 状态字节(raw = 0x70 时翻转);B1 / B3 / B5 分别为 Exp / Max / Min,R2 约 0.96–0.97;比例暂定约 1.1 乘 raw 减 19 刻度 / 偏移未精确确认;B2、B4、B6-B7 未扫;单位可能是英里
    0x330 Long_Accel 被抑制时会掉,但与噪声边界模糊;VICD 刻度为有符号字节乘 0.08 m/s² 未确认为模拟量;需要一次含急刹的行驶事件做时序提取
    0x383 BMSBattStatus SoC 在 B0-B1(BE uint16 除以 10 得 %)已确认;同时带 HVBattCharging、ChrgPortOpen、ChargeCableConnected 三个布尔位的确切位号未记录
    0x410 AC_Compressor_Command B2 = compressor_on_off / CASS_on_off / warmup_cycle_cntr_incr;B6 = 目标转速(乘 60 rpm/bit);B7 = 最大功耗(乘 0.1 kW/bit) B0 的三个布尔位(HVAC FaultRed、FaultYellow、KeepWakeNetworkHVAC)位号未解;也可能是扫描引起的 ECU 响应假象
    0x241 MCM_Thermal 角色已确认:drop-ID 后 Rotor / Stator / IGBT 三个字段归零;DLC = 8 已由日志确认 逐字节归属未确认;偏移假定为减 40 摄氏度;B4 恒为 0x00,疑似哨兵或非温度字段
    0x491 / 0x273 IPK 帧 两者都带 Primary ODO(km)、Interior Illum、Side Light Active、Low Beam Active;0x491 还会点亮 Overview 的 IPK 指示,0x273 不会 ODO 的字节位置与编码(可能是 BE uint24 或 uint32);灯光的位号

    八条被推翻的早期假设

    这些条目都曾写进笔记或报告,后来被数据推翻。留着不是自曝,而是让下一个人不用再踩一遍。

    早期假设 更正为
    0x212 = EPS_SAM_1(转向角,0.25 deg/count) 0x212 = DLCM Torque Request;转向角在 0x080
    0x210 = Vehicle_Mode 含 VBatt(乘 0.4 V/count) 0x210 B2 = ThrottlePedalPos(乘 0.4 %/count)
    0x271 = Death Counter 的一部分(整数字节计数) 0x271 = GWM 心跳 + VehFaults 的源
    0x55C = VehFaults(3 字节:灯 / 严重度 / 故障字) 0x55C = TCM 心跳
    0x381 = BMSDriveLimits 候选(哨兵 1000.0 A) 0x381 = BMSVminVmax;BMSDriveLimits 在 0x275
    0x382 B0 = 充电阶段计数器(0x8B 到 0x8E 步进) B0 = Cell_T_avg;充电过程中包升温时为 11–14 摄氏度
    0x275 B4 = HV Active 布尔标志 B4-B5 = PackVoltage(BE uint16,1 V/bit);104 串 LFP 包始终使 uint16 的最高位置位
    BattSentDischargeLimit / ChargeLimit 刻度 = 1 刻度 = 2(uint8 乘 2 得 A)

    常见问题

    反编译出来的 DBC 能直接拿来解日志吗?

    不能直接。反编译只证明应用把某个字节按某种方式读,不证明它读对了。所以这里把信号分成 CONFIRMED、CONFIRMED-SYMM、CONFIRMED-DECOMPILE、PARTIAL、REMOVED 五档,只有前两档带独立验证,第三档只是「应用自己这么命名」。

    电芯编号到底是 0 起还是 1 起?

    C CAN DBC 自 2026-07-22 起改为 1-based(Cell1 到 Cell104),与 BMS 自己的 VmaxId / VminId 直接对应,VmaxId 82 就是 Cell82。此前是 0-based,老 CSV、老图、老导出都要加 1 才能对上,例如旧 Cell94 等于现 Cell95。MinTempId / MaxTempId 是另一套索引,不受影响。

    为什么 0x381 的映射要改字节序?

    因为最初的归属照着 LabVIEW cluster 的名字「VminVmax」排,把 Min 与 Avg、VminId 与 VmaxId 都放反了;按 LabVIEW 字段顺序应是 Vavg、Vmax、Vmin、VmaxId、VminId。重跑严格测试后,旧的 max 读法命中 0.0%,旧的 min 读法零样本存活,修正后分别是 82.2% 与 87.0%。

    偏航率刻度为什么判不出来?

    两种说法分别是 0.0045(转向角 / 轴距运动学模型)与 0.003924(后轮差速几何法),但它们不是两条独立标定:同一几何法随速度窗口与轮距取值会给出 0.00347 到 0.00488。系统误差预算合计 43.4%(左右不对称 34.1%、速度窗口 21.5%、旅程间 16.0%),远大于两者相差的 14.7%,因此只能记为「判不出」。

    这份素材的许可证状况如何?

    被检查的上游快照里没有许可证文件,本文所依据的是一份离线私有研究副本。公开仓库可访问并不等于取得再分发或制作衍生作品的权利,对那些从第三方应用推断出来的定义尤其如此。共享或商用之前应与相关权利人确认。

    延伸阅读

    参考资料

    • 项目 01 —— public-can-dbc-datasets/additional/nickyivyca-vtrux-coda/SOURCE.md(九份 DBC 的报文 / 信号数与权利边界)
    • 项目 01 —— public-can-dbc-datasets/additional/nickyivyca-vtrux-coda/projects/coda/notes/codacan-signal-decode-notes.md(CODACAN 台架方法、分阶段完成表、模块在位映射与已推翻假设)
    • 项目 01 —— public-can-dbc-datasets/additional/nickyivyca-vtrux-coda/projects/coda/notes/cell-index-mapping.md(0x381 VmaxId/VminId 与 C CAN 电芯数组的三次纠正与严格测试口径)
    • 项目 01 —— public-can-dbc-datasets/additional/nickyivyca-vtrux-coda/projects/vtrux/notes/epri-verification.md(PT 与 OBD 总线的逐信号确认档位与 Gap)

    免责声明

    本文所述信号定义全部来自社区逆向工程,不代表车辆制造商的官方数据。Coda 与 VTRUX 两家整车企业均已退出市场,无原厂文档可核对;VTRUX 的 EPRI 定义最初由一款 iPhone 日志应用的二进制反编译重建,再用车辆日志比对修正,字节位置与比例并非一律正确。上游快照未包含许可证文件,公开仓库可访问不构成再分发或衍生权利。涉及安全相关用途时请仅作参考,并以实际测量为准。

    更新记录

    • 2026-10-15 初稿

  • 改装 ECU 的 CAN 仿真器:8/90 与 38/159 两份样例 DBC 怎么验

    结论:用桌面 CAN 仿真器验证改装 ECU 的 DBC,最省事的路径是先跑 virtual 接口做单进程自检,再用 vcan 或真实 SocketCAN 做端到端收發比对。这份归档里的两份样例分别是 8 报文 90 信号与 38 报文 159 信号,文件名指向两款常见改装 ECU,但仓库没有给出固件版本、装车适配和物理标定记录——它们能验证的是编码/解码链路本身,不能验证信号物理含义。

    两份样例 DBC 的实际量级

    仿真器自带两个样例库,归档时按 Git blob 与 SHA-256 做了固化。两者的量级差了一个数量级:小的一份 8 条报文却塞了 90 个信号,说明单帧内信号排布非常密;大的一份 38 条报文 159 个信号,更接近一台完整发动机控制器的输出规模。

    文件 报文数 信号数 SHA-256(前 16 位) 推断的 ECU 证据状态
    databases/emu_black.dbc 8 90 61caa4ea47dc0be3 ECUMaster EMU Black 名称推断,固件与装车未记录
    databases/haltech_elite.dbc 38 159 cbe0b8c73c842df1 Haltech Elite 名称推断,固件与装车未记录

    其中 emu_black.dbc 含有遗留的非 UTF-8 单位字节,归档时保持原样未做转换。这一点对工具链有实际影响:按严格 UTF-8 读取单位字段的解析器会报错或产生乱码,需要在加载前明确编码容错策略。另外,这两份文件是部件级协议样例,不是整车网络地图——把它们当成某台车的完整定义来用,从一开始就错了。

    三种接口的适用边界

    工具支持三类接口,选择依据不是「哪个更好」,而是当前验证处在哪个阶段。virtual 接口不需要硬件也不需要 root,但只在同一个 Python 进程内有效;socketcan 能跨进程、可配波特率,但改波特率需要 CAP_NET_ADMIN;slcan 面向 USB 串口适配器,适合接真实硬件。

    接口类型 硬件需求 权限需求 能否跨进程 波特率可配 适用阶段
    virtual 无 无 否,仅同进程 不适用 UI 与编码逻辑快速自检
    socketcan 真实或虚拟 CAN 接口 改波特率需 CAP_NET_ADMIN 是 是 与第三方工具联调
    slcan USB/串口适配器 无特殊权限 是 是 接真实改装 ECU

    SocketCAN 的波特率是网络链路自身的属性,不是应用每次连接时设定的。工具里的「Apply Bitrate」按钮实际执行的是把接口 down 掉、用 ip link 设定位速率、再 up 起来的三步操作,因此必然需要管理员权限;如果失败,可以事先自行配置好接口再启动工具。「Create vcan」按钮同理,它加载 vcan 内核模块并起一个虚拟链路,默认名 vcan0,之后另一个终端用 candump 就能看到流量。

    一条可复用的 DBC 验证流程

    把仿真器当验证工具而不是发帧玩具,核心是让「发什么」和「收回来解出什么」形成闭环。工具本身已经提供了闭环所需的两个能力:展开报文后按 DBC 定义生成控件,以及把总线上收到的每一帧连同解码结果打进日志。

    步骤 操作 判据
    1 加载 载入目标 DBC,观察是否有解析告警 无告警再继续;非 UTF-8 字节需先定编码策略
    2 展开 展开报文,数值信号看滑块量程,枚举信号看下拉项 量程与下拉项应与 DBC 的 range/resolution 和值表一致
    3 编码 拖动滑块,观察实时生成的原始字节 字节随取值单调变化,边界值不溢出
    4 发送 打开 TX 开关按周期发,或用 Send once 单发 周期默认 100 ms;DBC 未指定时以此为准
    5 回收 在日志里核对 RX 帧与解码结果 ID 在 DBC 内则应解码成功;回环数值应与发送值一致
    6 压力 在高负载总线上观察日志 出现 RX overflow 说明日志丢弃,不能据此判定丢帧

    第 5 步是这个流程的关键:只有当发出的值在接收侧被同一个 DBC 解回相同数值时,编码与解码的一致性才算被验证。它验证的是链路,不是物理含义——滑块拉到某个位置对应的字节是否正确,与这个字节在真实 ECU 上代表多少 kPa,是两个问题。

    日志里 RX overflow 不是丢帧证据

    日志面板会打印总线上收到的每一帧,格式形如 RX 0x360 [8] 后面跟数据。在非常繁忙的总线上,多余的帧会被从日志中丢弃并报告为 RX overflow。这是日志侧的丢弃,不是总线侧的丢帧,两者不能混为一谈;要测丢包率应当用独立的计数器,而不是看日志有没有 overflow。

    常见问题

    这两份样例 DBC 能当作某台改装车的完整网络定义吗?

    不能。它们是部件级协议样例,文件名指向两款改装 ECU,但仓库没有记录具体固件版本、装车适配、信号原作者或物理标定。能验证的是编码与解码链路,信号物理含义需要另行标定。

    没有硬件能不能先把验证跑起来?

    可以。virtual 接口是 python-can 的进程内总线,不需要硬件也不需要 root,适合做 UI 与编码逻辑的快速自检;若需要跨进程联调,则用 socketcan 加 vcan 虚拟链路,两个应用打开同一个接口即可互通。

    为什么点了 Apply Bitrate 会失败?

    因为 SocketCAN 的波特率属于网络链路属性,按钮实际执行的是 down、设定位速率、再 up 三步,需要 CAP_NET_ADMIN。权限不足时可以自行提前配置接口,或以足够权限运行工具。

    周期发送的默认周期是多少?

    打开报文的 TX 开关会按该报文的周期时间发送,周期可在界面上编辑;如果 DBC 没有指定周期,则默认取 100 ms。单帧发送则用 Send once。

    日志里的 RX overflow 意味着总线丢帧吗?

    不意味着。它表示繁忙总线上多余的帧被从日志面板丢弃,是日志侧的限流行为,不是总线侧丢帧。判定丢包率要用独立计数器,也可以取消勾选日志头的 Show received frames 来静音接收帧显示。

    延伸阅读

    参考资料

    • 项目 01 —— public-can-dbc-datasets/additional/candbcemu-aftermarket-ecu/SOURCE.md(两份样例 DBC 的报文/信号数、Git blob 与 SHA-256、非 UTF-8 字节说明与权利状态)
    • 项目 01 —— public-can-dbc-datasets/additional/candbcemu-aftermarket-ecu/README.md(三种接口类型、波特率配置、vcan 创建、TX/RX 行为与项目结构)

    免责声明

    本文所述功能与参数均来自公开仓库的说明文件,未经在真实改装 ECU 上的独立验证。两份样例 DBC 的车型归属为名称推断,固件版本、装车适配与信号物理标定均未由仓库给出。涉及安全相关用途时请仅作参考,并以实际测量与官方资料为准。

    更新记录

    • 2026-10-14 初稿

  • 学生方程式车队的 CAN 调试工具链:10 ms–10 s 周期与 63 报文夹具

    结论:这套面向学生方程式车队的 CAN 调试工具,价值不在界面而在「逐报文可控」:发送侧支持单发与周期发送,周期可在 10 ms 到 10 s 之间逐条设定,每条报文既能单独停止也能整体停止;接收侧按 DBC 自动抽取并解释信号。但它的硬前提是 SocketCAN——必须运行在 Linux 或等效模拟器(如 WSL)上,Windows 虽有预编译包,却绕不过这一层。

    两种模式的能力边界

    工具以 Go 的 Bubble Tea 构建终端界面(TUI),核心是加载并解析 DBC 后同时管理多条报文。发送侧与接收侧的能力并不对称:发送侧提供单发与连续发送两类选项,连续发送按自定义周期运行,周期范围的默认值在源码 internal/ui/types.go 的 rangeMs 中定义,可按需修改;接收侧则由使用者挑选要监听的报文,并依据 DBC 定义自动完成信号抽取与数值解释。

    模式 能力 参数 约束
    发送 单发(single-shot) 单次触发 须先建立 SocketCAN 接口
    发送 连续发送 逐报文独立频率,范围 10 ms – 10 s 范围由 rangeMs 决定,改源码可调
    发送 停止控制 单条停止或全部停止 多条并发时逐条管理
    接收 选择性实时监听 指定要监控的报文 未选中的报文不展开
    接收 信号自动解释 依赖已加载的 DBC 定义 DBC 未覆盖的报文只能看原始字节

    平台与分发物:能装上不等于能跑通

    发布页提供四个预编译包,覆盖 Linux x86_64、macOS Intel、macOS Apple Silicon 与 Windows x86_64。但 README 在需求一节写得很直接:需要 SocketCAN 接口(vcan0 或真实 CAN 接口),因此要有 Linux 或类似 WSL 的模拟器。换句话说,Windows 包只是把二进制带过去,真正的总线层仍然要落在 Linux 内核的 SocketCAN 上。

    平台 分发文件名 说明
    Linux x86_64 can-debug_Linux_x86_64.tar.gz 解压后 chmod +x 即可运行,原生支持 SocketCAN
    macOS Intel can-debug_Darwin_x86_64.tar.gz macOS 无 SocketCAN,需借助 Linux 环境
    macOS Apple Silicon can-debug_Darwin_arm64.tar.gz 同上,总线层依赖外部 Linux 环境
    Windows x86_64 can-debug_Windows_x86_64.zip 需配合 WSL 等模拟器使用
    任意(源码构建) Go 1.19 及以上 go build -o can-debug,可按需改周期范围

    命令行只有三种形态:带接口名启动后用文件选择器挑 DBC、带接口名与 DBC 路径直接加载、以及 -h 或 --help 查看完整帮助。启动前必须先建好 can 或 vcan 网络,这是文档里反复强调的一条。

    测试夹具 MCB.dbc 的量级与证据边界

    随工具提供的样例 DBC 位于 internal/test/MCB.dbc,包含 63 条报文与 300 条信号定义,覆盖电池、显示、IMU 与牵引系统等车队自研部件节点。它足够用来验证工具本身,但不足以当作车辆信号研究成果:上游并未确立这份额外夹具对应的具体车辆、赛季、总线,也没有实车验证记录。

    文件 报文数 信号数 证据边界
    internal/test/MCB.dbc 63 300 样例测试文件,未确立车辆、赛季、总线与实车验证
    同车队另一份已归档 Main_CAN_Bus.dbc 61 293 相关车队数据,但报文与信号定义不同,不可互相替代
    归档库中的既有 DBC — — SHA-256 与已归档的 2258 份 DBC 均不匹配,属新增条目

    这张表给出一个实用的判断方法:两份报文数相近(63 与 61)、信号数只差 7 的 DBC,并不构成互为佐证的关系。源说明特别提示,不能把这份夹具当作独立的车辆信号研究来引用;仓库为 MIT 许可,但其中任何厂商部件信号的原始权利并未被独立确认。

    从 vcan 到实车接口的启动顺序

    快速路径是运行仓库内的 TestVcanSetUp.sh 脚本,一键创建 vcan 接口并启动程序,然后在另一个终端用 candump vcan0 观察程序发出的帧、用 cansend vcan0 123#DEADBEEF 向程序注入一帧;Ctrl+C 结束抓包。手动路径则依次加载 can、can-raw、vcan 模块,安装 can-utils,创建并拉起 vcan0,再用 ip a show vcan0 确认。

    接真实接口时把创建命令换成设置波特率的形式:先用 ip link set 把目标接口设为 CAN 类型并指定 bitrate,再拉起接口,然后用 ip a show 确认状态,最后以接口名(可选带 DBC 路径)启动程序。文档命令中的接口名与波特率均为占位符,需按实际硬件替换。

    常见问题

    这套工具能在 Windows 上直接连实车 CAN 吗?

    不能。README 明确要求 SocketCAN 接口(vcan0 或真实 CAN 接口),也就是需要 Linux 或类似 WSL 的模拟器。Windows 的预编译包只是可执行文件的分发形态,总线层仍需落在 Linux 环境的 SocketCAN 上。

    每条报文的发送周期能设到多少?

    连续发送支持逐报文独立设定频率,默认范围是 10 ms 到 10 s;这个范围由源码 internal/ui/types.go 中的 rangeMs 决定,需要更长或更短周期时修改该值即可。每条报文既能单独停止,也能一次性全部停止。

    随包的 MCB.dbc 能不能当作车队的正式总线定义?

    不能。它是随调试工具提供的样例测试文件,含 63 条报文、300 条信号,覆盖电池、显示、IMU 与牵引系统等节点,但上游未确立其对应的具体车辆、赛季、总线,也没有实车验证记录;同车队的另一份 Main_CAN_Bus.dbc 有 61 条报文、293 条信号,定义并不相同,两者不可互相替代。

    为什么它的 SHA-256 要单独登记?

    因为去重口径依赖哈希:这份 MCB.dbc 的 SHA-256 与已归档的 2258 份 DBC 都不相同,才被判定为新增条目。若只比对文件名或报文数,很容易把 63 条报文的夹具与 61 条报文的同车队文件误判为同一份。

    延伸阅读

    参考资料

    • 项目 01 —— public-can-dbc-datasets/additional/squadracorsepolito-can-debug/README.md(TUI 能力、发送/接收模式、平台分发物与 vcan/实车接口启动步骤)
    • 项目 01 —— public-can-dbc-datasets/additional/squadracorsepolito-can-debug/SOURCE.md(MCB.dbc 的 63 报文/300 信号、哈希登记与证据边界)

    免责声明

    本文所述内容来自公开开源仓库的说明文件,涉及的报文与信号数量仅为该测试夹具自身的规模,不代表具体车辆、赛季或总线的定义,也未经实车验证。仓库为 MIT 许可,但夹具中任何厂商部件信号的原始权利并未被独立确认。发送类工具一旦接入真实总线即具备主动写入能力,请在隔离环境或虚拟总线上完成验证,并遵守所在赛事与场地的安全规定。

    更新记录

    • 2026-10-14 初稿

  • 东风 E70:23 个已验证信号与 7 报文种子 DBC

    结论:东风 E70 这个项目的核心落差是数量级的:作为唯一真源的 production/e70_current.dbc 只有 7 条报文、7 行信号定义,而由它生成、也是唯一部署物的 RealDash XML 有 21 个 frame 元素与 46 个 value 元素。46 个值里只有 23 个是作者认证过的 validated,其余 20 个为 plausible、另有 partial、suspect、unverified 各一。把 XML 当作 46 个已验证信号来用,是最容易踩的坑。

    采集条件与产物分工

    所有数据的来源是 ESP8266 加 MCP2515 组成的 SLCAN 桥,在 500 kbit/s 上做只听抓取。这个硬件前提决定了它的能力边界:MCP2515 是经典 CAN 控制器,这套装置看不到 FD 帧,也不具备任何主动请求能力,因此 UDS 一类的按需数据完全不在范围内。

    产物 路径 规模 用途
    唯一部署的 RealDash 配置 production/e70_all_signals_merged2.xml 21 个 frame、46 个 value 以 CAN/Custom XML 输入方式加入 RealDash
    信号定义的唯一真源 production/e70_current.dbc 7 条报文、7 行信号 每个信号的 bit、scale、offset 以此为准
    验证报告 reports/validation_report.txt 对 46 个值逐条判定 23 validated / 20 plausible / 各 1 的 partial、suspect、unverified
    抓包与工具 capture/、analysis/ 含 SLCAN 回放模拟器 生成新日志、跑事后分析与报告

    production/ 目录的原则是「每种只有一个」:当前配置与当前 DBC 各一份,其余全部进 archive/ 且永不删除。这条约定比它看起来重要——看到 archive/xml/ 里还有老的 merged 与 probe XML、archive/dbc/ 里还有自动解码的临时 DBC 时,如果不看这组约定,很容易把过期文件当成候选版本。

    而 XML 与 DBC 的关系是单向的:XML 由 DBC 生成,改信号只能改 DBC,绝不能手改 XML。这条规则一旦破掉,DBC 就不再是唯一真源,两份文件会各自漂移。

    23 个 validated 与 20 个 plausible 的分界线

    作者给出的分级标准非常明确,这也是这份资料最值得学习的地方。

    等级 判据 数量 能不能用于判断
    VALIDATED 场景已确认,且在全部 167 份日志中都保持一致 23 可以
    PLAUSIBLE 存在且落在物理量程内,但含义与缩放仍需实车确认 20 只能观察趋势
    PARTIAL / SUSPECT 多为位运算符定义的枚举,观测分布不全 各 1 不能,需补抓
    UNVERIFIED 未在作者抓包中验证 1 不能

    已验证的 23 个信号覆盖面其实相当完整:整车与轮速、制动与加速踏板、挡位、里程、车门与灯光及远近光、转向灯、电子驻车(EPB)、SOC(0x397 与 0x380)、充电状态与握手信号。它们的验证强度来源于「场景确认 + 全部 167 份日志一致性」双重条件,合计约 2400 万条匹配帧(原文记为 ~24M matched frames)。

    仍在 plausible 里的则肯定是那些该小心的人:电池包电压(0x0D3)、高压电流(0x0EF)、转向角、若干温度、扭矩与功率。它们的共同特点是「读数落在合理区间」,但没人证明这个换算系数是对的。

    而电池相关的锚定之所以卡住,原因写得一清二楚:完整的包电压、SOC、电流缩放系数标定,需要一份从接近空到充满的真实充电抓包,让 SOC 沿单调充电曲线变化才能把 scale 与 offset 钉住。现存的充电日志跨越的 SOC 区间不够宽,钉不住。

    自动化评分会误报,这是已知缺陷

    这一条必须单独说,否则看到报告里的 PARTIAL 会做出错误判断。凡 RealDash 的 conversion 里用到了位移或按位与的枚举/位域信号,自动评分器可能把它报成 PARTIAL 或 SUSPECT,即便它在 RealDash 里渲染完全正常。

    原因是评分器看的是原始字段的分布:只要某一次挡位或充电状态在抓包里从未出现,它就会认为这个枚举缺状态而报警。正确的读法是把 >> 或 & 定义出来的枚举上的 PARTIAL/SUSPECT 理解为「该事件没有被抓到」,而不是「解码坏了」;确认方式是在车上把缺失的状态实际执行一次再抓。

    这两条命令的实际用法也值得抄:validate_profile.py 针对单份日志打印每个信号的 min/max/mean 与可用性/量程状态,配合 --expect expects.json 可以做物理量断言,越界即 FAIL;analysis/validate_xml.py 则把整份 XML 对全部日志跑一遍,把逐信号判据表写回 reports/。前者适合改完 DBC 立刻自检,后者适合入库前的整体复核。

    权利状态

    最后一个必须写在使用之前的约束:归档方在这个修订版上没有找到仓库总体许可证;仓库里那份 .claude/skills/LICENSE 只覆盖随仓库携带的一个 skill,并没有明确延伸到整车研究数据。归档说明因此要求把它作为私有离线研究副本保留,发布或再分发之前先确认权利。

    常见问题

    那份 RealDash XML 里的 46 个值都是已验证信号吗?

    不是。归档保留的验证报告把这 46 个值划分为 23 个 validated、20 个 plausible,以及 partial、suspect、unverified 各 1 个。只有 validated 那 23 个通过了「场景确认且在全部 167 份日志中保持一致」的双重条件,其余不能用于判断。

    为什么当前 DBC 只有 7 条报文却有 46 个 XML 值?

    因为 DBC 与 XML 承担的角色不同。production/e70_current.dbc 是一份小而规范的种子文件,只有 7 条报文与 7 行信号定义,是所有位、缩放与偏移的唯一真源;XML 是由它生成并扩展配置后的部署物,含 21 个 frame 与 46 个 value。二者不可混为一谈,且改信号只能改 DBC。

    报告里的 PARTIAL 一定说明解码有问题吗?

    不一定。凡是 conversion 里用位移或与运算定义的枚举或位域,评分器可能报 PARTIAL 或 SUSPECT,即使它在 RealDash 中渲染正常;因为评分器按原始字段分布判断,某个状态在抓包中未出现就会被认为缺状态。应理解为事件未被抓到,补抓确认即可。

    电池 SOC 和包电压的缩放系数为什么一直没有定下来?

    作者说明:完整的包电压、SOC 与电流缩放标定需要一份从接近空到充满的真实充电抓包,让 SOC 沿单调充电曲线变化才能锚定 scale 与 offset。现有充电日志跨越的 SOC 区间不够宽,因此这三项仍停留在候选状态,不能当作已标定量使用。

    延伸阅读

    参考资料

    • 项目 01 —— public-can-dbc-datasets/additional/ali-roohy-dongfeng-e70/UPSTREAM_README.md(ESP8266 加 MCP2515 的 SLCAN 桥与 500 kbit/s 只听抓取、production 目录原则与产物分工、RealDash 的 CAN/Custom XML 加载方式、XML 由 DBC 生成且不可手改、23 个 validated 信号清单与 167 份日志约 2400 万条匹配帧、约 20 个 plausible 候选与 SOC 标定等待充电抓包的原因、验证 commands 与 –expect 断言、枚举位运算被误报为 PARTIAL/SUSPECT 的已知缺陷)
    • 项目 01 —— public-can-dbc-datasets/additional/ali-roohy-dongfeng-e70/SOURCE.md(production/e70_current.dbc 的 7 报文 7 信号行、XML 的 21 个 frame 与 46 个 value、46 个值的 23/20/1/1/1 分级、三个文件的 blob 与 SHA-256、未找到仓库许可且 skill 许可不覆盖整车数据的说明)

    免责声明

    本文所述报文 ID、信号数量与分级结果均来自公开开源仓库的说明与归档文件,属第三方逆向工程产物,未经独立实车核验,也不代表车辆制造商的官方数据。不同车型年款、市场与 ECU 版本可能存在差异。该数据当前缺少明确的仓库许可,不宜直接集成或再分发。涉及行车安全的判断请以实际测量与官方资料为准。

    更新记录

    • 2026-10-14 初稿

  • DBC 到固件的版本校验:T26 用 64 位 SHA-256 心跳

    结论:T26 的 DBC 工作流把版本一致性做成了一条 CAN 心跳:cantools 生成 C 代码的同时,脚本把每个 DBC 的 SHA-256 截断为前 8 字节(64 bit)写成宏,各 ECU 用专用 ID 0x055、DLC 8 周期广播并互相比对。截断到 64 bit 不是为了省事,而是为了塞进一帧经典 CAN 的 8 字节载荷——代价是碰撞概率从 2⁻²⁵⁶ 升到约 2⁻⁶⁴,对这个用途完全够用。

    DBC 字段与固件解码的一一对应

    DBC 的价值在于把十六进制报文翻译成带名字和单位的信号。T26 的流程从 Kvaser Database Editor 建库开始:先建节点(如 ECU_Principal),再建报文(Name、ID、DLC、Transmitter),最后逐条加信号。每个字段的填写错误都会直接变成固件里的读数错误。

    DBC 字段 要填什么 填错后的固件表现
    ID 与 DLC CAN 标识符与字节长度(如 0x101、8) 收不到该帧,或按错误长度解包直接越界
    Start bit 与 Length 起始位与位长度(如 0、16) 取到相邻信号的位,数值整体偏移
    Byte order Intel(小端)或 Motorola(大端) 高低字节互换,数值随量级剧烈跳变
    Value type Signed / Unsigned 负值变成极大正数,温度、转矩类信号尤其明显
    Factor 与 Offset 缩放因子与偏移(如 factor = 0.1,则 100 表示 10.0 km/h) 量程整体缩放错误,趋势对但绝对值全错
    Unit 单位(如 km/h) 不影响解码,但跨团队协作时是主要误读源

    这张表也是版本校验为何必要的直接解释:上述任何一格被改动,固件都不会报错,只会安静地输出错误数据。

    自动生成:DBC 到 C 代码与 Excel

    仓库配了一个 GitHub Actions 工作流,每当 DBC 变更被推送,就用 python3 -m cantools generate_c_source Autonomous.dbc --output autonomous_c_output 重新生成 C 代码,产物落在 generated/。同一个工作流还会导出 xls,每个 DBC 一个页面、每个 ID 一张表,方便非固件成员审阅。

    把生成环节放进 CI 的意义在于消除「本地改了 DBC、忘了重新生成」这类偏差。手工维护的解码头文件几乎必然与 DBC 漂移,而漂移本身不可见。

    版本校验怎么做

    工作流同时执行 embed_dbc_sha256.py,计算每个 .dbc 的 SHA-256,并把结果作为宏写进生成的头文件:

    #define DATA_T26_DBC_SHA256 0x3ee5339f1dc658c6ULL

    为塞进经典 CAN 单帧最大 8 字节的限制,哈希被截断为 SHA-256 的前 8 字节(64 bit)。文档给出的取舍依据是:两个不同版本碰撞的概率约 2⁻⁶⁴;要累积到约 43 亿个版本才有 50% 的碰撞概率;而加密哈希的前 64 位仍保留完整的雪崩效应,改一个字节就会改变该值。

    方案 承载报文 载荷长度 碰撞概率 适用网络
    截断 64 bit 经典 CAN 2.0,DLC 8 8 字节 约 2⁻⁶⁴,约 43 亿版本才有 50% 概率 CAN 2.0 网络
    完整 256 bit CAN FD 单帧 32 字节 2⁻²⁵⁶ 需 CAN FD(单帧至多 64 字节)

    文档给的实现示例是 STM32 HAL + CAN 2.0:每个 ECU 用专用 ID 0x055(文中示例为每 1 秒一次)发送 8 字节版本帧,其余 ECU 收到后与自身宏比对,不一致即判定为版本落后并告警或拒绝该节点。拷贝时所有 ECU 必须统一使用 Intel 小端字节序,否则比对本身会失效。

    不做校验会发生什么

    文档把风险写得很直白:运行旧版 DBC 的 ECU 会按错误的 bit order、factor、offset 或信号位置去解释报文,可能导致传感器读数错误、发动机与制动等系统误动作、以及难以诊断的安全故障——而这些症状在运行时没有任何可见错误。加上版本常量比对后,版本分歧在第一帧收到时就能被发现。

    在 Teensy 4.1 上的落地形态

    仓库给出的 MCU 示例用 FlexCAN_T4,CAN.setBaudRate(500000),接收队列 RX_SIZE_256、发送队列 TX_SIZE_16。解码时直接包含生成的头文件,例如 autonomous_dv_driving_dynamics_1.h,调用 autonomous_dv_driving_dynamics_1_unpack 读入 speed_actual、speed_target、steering_angle_actual;回包时用 _pack 打包,示例把 speed_target 改为 150 后写回同一 ID。

    这条链路把「DBC 是唯一事实源」落到了实处:固件里不应再出现手写的位运算,所有位定义都来自生成产物,而生成产物与 DBC 哈希绑定。

    常见问题

    为什么 SHA-256 要截断到 64 位?

    为了塞进一帧经典 CAN。CAN 2.0 单帧最多 8 字节,因此只取 SHA-256 的前 8 字节(64 bit)作为宏。文档给出的依据是碰撞概率约 2⁻⁶⁴、需约 43 亿个版本才有 50% 碰撞概率,且前 64 位保留完整雪崩效应,改一个字节即变化。

    DBC 版本不一致在实车上会表现出什么症状?

    不会报错,只会输出错误数据。运行旧版 DBC 的 ECU 会按错误的 bit order、factor、offset 或信号位置解包,导致传感器读数错误、执行系统误动作以及难以诊断的安全故障,运行期没有可见错误提示。因此需要版本心跳在第一帧就比对出来。

    版本心跳必须用 0x055 吗?

    不必。0x055 只是文档示例里为版本心跳专门分配的 ID,任何未被占用的标准 ID 都可以,关键是全网络约定一致、DLC 固定为 8,且所有 ECU 拷贝常量时使用相同的 Intel 小端字节序。

    没有 CAN FD 的网络还能做版本校验吗?

    能。经典 CAN 用截断到 64 bit 的方案即可,把 8 字节常量放进 DLC 8 的帧里周期广播。若网络支持 CAN FD(单帧至多 64 字节),则可以一帧发送完整的 32 字节 SHA-256,无需截断。

    延伸阅读

    参考资料

    • 项目 01 —— public-can-dbc-datasets/additional/fslart-t26-dbc/README.md(DBC 字段定义、cantools 自动生成、SHA-256 截断与 0x055 心跳示例、Teensy 4.1 用例)

    免责声明

    本文内容整理自公开开源仓库的自述文档,其中的宏值、ID、波特率与概率估算均为该仓库给出的示例或说明,未经独立实测验证。不同工具链、编译器与网络拓扑下的实现细节可能不同,工程落地请以实际测试为准。

    更新记录

    • 2026-10-14 初稿

  • feberkeley-sn4 数据集:99 报文、1305 信号与 11 段帧 ID 分区

    结论:feberkeley-sn4 数据集的核心是一份由 Python 定义生成的 DBC,含 99 条 BO_ 报文与 1,305 条 SG_ 信号。它的可复用之处在于一套清晰的帧 ID 分区策略:11 段地址区间按板卡/子系统划分并预留扩展空间。但有两个必须记住的边界——README 自称 SN4 库却描述为 SN5 车辆,且快照中未发现仓库许可证。

    数据集的构成:从 Python 定义到 DBC 与 C 代码

    这个库的工作方式不是手写 DBC,而是用 Python 的 cantools 对象描述报文,再由脚本生成 DBC 与 C 编解码代码。生成脚本 generate_can.sh 会自动创建虚拟环境、安装指定版本 cantools(39.4.13)、校验定义、生成 DBC、再由 DBC 生成 C 源文件,并归一化时间戳以保证 diff 稳定。

    产物 路径 作用
    DBC 数据库 gen/FEB_CAN.dbc 99 条 BO_ 报文、1,305 条 SG_ 信号
    C 头文件 gen/feb_can_db.h 编解码函数与最新值数据库
    C 实现 gen/feb_can_db.c 上述两者的实现

    external_dbc/ 目录里放供应商 DBC,其中的 inverter.dbc(Cascadia PM100 逆变器)与 elcon.dbc(Elcon/HK 充电机)会被自动合并进生成结果。这意味着 99 条报文里包含第三方设备的定义,使用时需要知道哪些信号来自自有板卡、哪些来自外部设备。

    帧 ID 分区:这套数据集最值得抄的部分

    帧 ID 按板卡划段,每段都预留了扩展余量。这种分配方式让「新增一条报文」变成一个可判定的问题——先看所属板卡区间还有没有空位,而不是全局找一个没用过的 ID。

    区间 归属 当前报文数 用途
    0x00-0x08 BMS 5 电池管理
    0x09-0x0F PCU 2 安全关键(制动、BSPD)
    0x10-0x15 DASH 1 仪表盘 I/O
    0x16-0x1D LVPDB 3 配电
    0x1E-0x2C Sensors 8 胎温、轮速、线性电位计
    0x2D-0x33 DART 2 风扇控制
    0x34-0x3F TPS 4 电源监控芯片
    0xC0-0xCF RMS 2 逆变器通信
    0xD0-0xDF Heartbeats 6 板卡健康监测
    0xE0-0xEF Debug 4 测试与调试报文
    0xFF PCU 1 原始 ADC 值

    注意地址空间并非连续填满:0x40 到 0xBF 之间是一大片未分配区域,Heartbeats 与 Debug 被放到了高位。这种「功能段上移、控制段下沉」的排布,让低地址区留给数量可能增长的子系统。

    新增一条报文要改三处

    流程是固定的:先在对应板卡的 *_messages.py 里写一个返回 cantools.db.Message 的函数,再在 generate.py 的 MESSAGE_REGISTRY 字典里用可用帧 ID 注册,最后重新运行 generate_can.sh 并用 --list 与 --dump 核验。校验阶段会拦两类错误:同一函数被注册到多个帧 ID(报 Duplicate function),以及帧 ID 超出标准 CAN 的 0x000-0x7FF 范围(报 Frame ID out of range)。

    信号定义的统一约定

    整个库统一使用小端字节序,这一点在排障提示里被单独强调:解包后数值不对,第一件事就是检查字节序与有无符号。位位置按小端排列,8 字节报文的位编号从 0 到 63,与字节序一一对应。

    信号类型 典型长度 有无符号 用途
    16 位电压/电流 16 无符号 以 mV、mA 为单位传输
    16 位温度 16 有符号 摄氏度
    单比特故障标志 1 无符号 故障位
    8 位状态枚举 8 无符号 状态机取值
    32 位计数器 32 无符号 计数与心跳

    心跳报文有一套标准模式:64 个独立的错误标志位,实现见 lvpdb_messages.py 中的 get_lvpdb_heartbeat()。把板卡健康状态压成 64 个比特放进周期性心跳,是这套库里最通用的设计模式之一。

    CI 里的 check 模式

    GitHub Actions 工作流用 ./generate_can.sh --check 验证生成文件是否最新,确保 Python 定义与提交进仓库的 C 代码一致。这条检查把「改了定义忘了重新生成」变成一次 CI 失败,而不是运行期的错位解码。本地若遇到该检查失败,重新生成并提交 gen/ 目录即可。

    使用边界:车款归属与权利状态

    数据集有两处明确的边界,引用前必须确认。

    边界项 实际情况 影响
    车辆归属 仓库名为 SN4,README 却描述为 FEB SN5 方程式电动车 确切车款与赛季未定,复用需自行确认
    许可证 快照中未发现仓库许可证 仅可作为私有离线研究副本
    第三方定义 合并了 Cascadia PM100 与 Elcon/HK 的供应商 DBC 供应商定义的权利未单独声明,需另行确认
    归档范围 仅归档生成的 DBC 与 README 两个供应商源文件未单独拷贝,但定义可能已在生成的 DBC 内

    综合来看,这份数据集适合作为「如何组织一个多板卡 CAN 库」的参考实现——分区策略、生成流水线、心跳模式、CI 校验都可直接借鉴;但作为某辆具体赛车的权威信号定义来用则证据不足。

    常见问题

    这份 DBC 有多少报文和信号?

    生成的 DBC 有 99 条 BO_ 报文与 1,305 条 SG_ 信号行。其中包含自动合并进来的第三方设备定义:external_dbc/ 目录下的 Cascadia PM100 逆变器 DBC 与 Elcon/HK 充电机 DBC 会被自动并入生成结果。

    新增一条 CAN 报文的正确步骤是什么?

    三步:先在对应板卡的 *_messages.py 里定义返回 cantools.db.Message 的函数;再在 generate.py 的 MESSAGE_REGISTRY 里用一个可用帧 ID 注册;最后重新运行 generate_can.sh,并用 –list 与 –dump 核验。校验会拦截同函数重复注册与帧 ID 超出 0x000-0x7FF 两类错误。

    怎么知道哪些帧 ID 还没被占用?

    运行 ./generate_can.sh –ids 查看帧 ID 分配图。帧 ID 按板卡分区并预留扩展空间,例如 BMS 为 0x00-0x08(已用 5 条)、Sensors 为 0x1E-0x2C(已用 8 条)、Heartbeats 为 0xD0-0xDF(已用 6 条)。

    解包出来的数值不对,先查什么?

    先查字节序与有无符号。这个库统一使用小端字节序,若定义与实际不符,解包结果就会错。其次确认 C 侧的 pack 函数名——报文名会被规范化成 C 标识符,实际函数名需在 gen/feb_can_db.h 中确认(小写、下划线)。

    这份数据集能用于我自己的赛车吗?

    作为组织方式的参考可以,作为权威信号定义则证据不足。仓库名为 SN4 但 README 描述为 FEB SN5 方程式电动车,确切车款与赛季未定;快照中未发现仓库许可证;合并的供应商定义权利也未单独声明。建议仅作私有离线研究副本。

    延伸阅读

    参考资料

    • 项目 07 —— public-can-dbc-datasets/additional/feberkeley-sn4/SOURCE.md(固定提交、99 报文 1,305 信号规模、车款归属疑问与权利状态)
    • 项目 07 —— public-can-dbc-datasets/additional/feberkeley-sn4/README.md(生成流水线、帧 ID 分区表、信号定义模式与 CI 校验)

    免责声明

    本文内容来自公开仓库的说明文件与生成产物统计,未对信号定义做逐条验证。数据集的车款归属与赛季在来源中存在不一致,且未发现仓库许可证,供应商定义的权利亦未单独声明。请仅作为私有离线研究副本使用,实际车辆应用前请以原始来源的权利确认与自有验证为准。

    更新记录

    • 2026-10-14 初稿

  • IFS08 uDV 节点:6 报文 DBC 与预编译 libmicroros 子模块

    结论:IFS08 的 uDV 节点是一套跑在 STM32H7 上的 micro-ROS 固件,随仓库发布的 IFS08_FSG26_datalogger.dbc(标注为 IFS08_FSG26_DL_v1.0)只有 6 条报文、45 条信号,用途单一——把行驶动力学、自动驾驶系统状态与安全遥测发给数采。它是一份部分节点地图,不是整车总线定义;真正决定这套固件能否顺利构建的,是那个预编译了 libmicroros.a 的子模块。

    DBC 的规模与覆盖范围

    这份 DBC 位于 docs/IFS08_FSG26_datalogger.dbc,是六报文、45 信号的规模。内容按三类组织:行驶动力学、自动驾驶系统状态、安全遥测,目标接收方是数采设备。

    维度 内容 边界
    规模 6 条报文、45 条信号 仅覆盖发给数采的部分
    版本标识 IFS08_FSG26_DL_v1.0 对应 FSG26 赛季数采配置
    内容分类 行驶动力学、自动驾驶系统状态、安全遥测 不含其他节点间通信
    验证状态 未独立核查哪些报文实际在车上有过流量 需实车核对后使用

    「未核查哪些报文实际在车上有过流量」这一条很关键。DBC 里写了不等于车上发了,尤其在赛季前配置频繁变动的阶段。用这份文件做离线解码是安全的,但拿它推断整车的总线负载就会出错。

    micro-ROS 子模块为什么是构建的关键

    固件依赖一个名为 micro_ros_stm32cubemx_utils 的子模块,固定在 isc-fs 自己的 humble-isc 分支上,而非上游 micro-ROS 组织的主线。这个 fork 相对上游有两处差异,其中第一处直接决定了开发体验。

    差异项 上游 micro-ROS 做法 本 fork 做法 对开发的影响
    预编译库 libmicroros.a 与 microros_include/ 被 gitignore 在工作树中跟踪这两个产物 克隆带 –recurse-submodules 即可构建,无需 Docker 步骤
    自定义接口 仅标准接口 把自定义 ROS 接口包编译进 libmicroros 当前固件只用 std_srvs/std_msgs,用标准重建也可以

    把预编译库纳入版本跟踪,本质上是把「构建环境」变成「仓库内容」。代价是仓库体积,收益是任何装了 ARM 工具链的机器都能直接 make。对一支成员轮换频繁的学生车队来说,这个取舍是合理的——它消除了最常见的「在我机器上能编」类问题。

    当前固件的 ROS 接口面有多窄

    仓库注明固件目前只用到 std_srvs 与 std_msgs,因此即使用上游标准方式重建 libmicroros 也能工作。这一点写在 Core/Src/ros_task.c 里。反过来说,一旦引入自定义消息类型,就必须按 firststeps.md 里记录的流程重建库,而不能沿用现成的预编译产物。

    持续集成:双平台门控

    仓库的 .github/workflows/build.yml 在每次推送到 main、dev、feat/**、fix/** 以及每个 PR 时运行一次固件构建。

    检查项 配置 作用
    工具链 arm-none-eabi-gcc 14.3.Rel1 锁定编译器版本,避免工具链漂移
    运行平台 ubuntu-latest 与 macos-latest 双跑 排除只在单一平台能编的偶发通过
    构建方式 make -j 并行构建,缩短反馈时间
    产物断言 检查 uDV.elf / uDV.bin / uDV.hex 存在 防止「编译无错但没出产物」
    产出 上传为 uDV-firmware 构件 每次提交都有可下载固件

    dev 分支设有保护规则:两个 runner 都必须为绿才允许合并 PR,因此 CI 红了合并按钮直接变灰。这条规则把「CI 通过」从建议变成了硬门槛。

    分支模型:把分支历史挪进 Issue

    这套流程的设计目标很明确:让仓库保持干净,同时不丢失任何一次改动的历史。仓库只有 main 与 dev 两条常驻分支,且都不允许直接提交。

    分支 定位 写入方式
    main 生产分支,只含可刷车的已验证代码 由 dev 经完整固件验证(HIL/台架)后 PR 合入
    dev 集成分支 只接受来自 feature 分支的 PR
    feat/编号-短标题 新功能 从 dev 创建,合并后删除
    fix/编号-短标题 缺陷修复 从 dev 创建,合并后删除

    feature 分支合并后即被删除,历史靠 GitHub Issue 保留:每个分支对应一个带 feat 或 fix 标签的 Issue,标题里写完整分支名。要查当前在做什么就按标签过滤「open」,要查全部历史就过滤「closed」;下一个分支编号取同类最后一个已关闭 Issue 的编号加一。

    三个自动化细节

    推送分支时工作流会自动开 Issue,标题镜像分支名并打上正确标签,还带一个填写说明的模板与创建者名字。如果编号不是预期的下一个,Issue 里会显示警告并提示删除重建。开发者第一次提交并推送后,Issue 的描述区会被该提交信息自动填充一次——若开发者事先手动编辑过则不会被覆盖,且后续提交不再修改。这几处设计把「编号靠自觉」变成了「编号错了会被提示」。

    常见问题

    这份 DBC 能代表 IFS08 整车的 CAN 总线吗?

    不能。它是六报文、45 信号的数采专用定义,只覆盖行驶动力学、自动驾驶系统状态与安全遥测这三类发给数采的内容,属于部分节点地图而非整车总线定义。上游也未独立核查哪些报文实际在车上有过流量。

    为什么克隆时必须带 –recurse-submodules?

    因为 micro-ROS 支持库在子模块里,且该 fork 把预编译的 libmicroros.a 与 microros_include/ 直接跟踪在工作树中。带上这个参数后,任何装了 ARM 工具链的机器都能直接构建,不需要额外的 Docker 步骤。若忘了带,可在仓库内执行 git submodule update –init –recursive 补救。

    CI 为什么要同时在 Ubuntu 和 macOS 上跑?

    为了排除只在单一平台能编译通过的偶发情况。dev 分支保护规则要求两个 runner 都为绿才允许合并 PR,因此任一平台失败都会让合并按钮变灰,把 CI 通过从建议变成硬门槛。

    固件现在用了自定义 ROS 消息类型吗?

    没有。仓库注明固件目前只使用 std_srvs 与 std_msgs,因此用上游标准方式重建 libmicroros 也能工作,相关信息写在 Core/Src/ros_task.c 里。一旦引入自定义消息类型,就必须按 firststeps.md 记录的流程重建库。

    如何知道下一个 feature 分支该用几号?

    取同类最后一个已关闭 Issue 的编号加一。在 GitHub Issues 里按 feat 或 fix 标签过滤并按最新排序,读出最后一个编号即可;feat 与 fix 两个计数器互相独立。编号不对时自动创建的 Issue 会显示警告并提示删除重建。

    延伸阅读

    参考资料

    • 项目 06 —— public-can-dbc-datasets/additional/isc-ifs08-udv/SOURCE.md(DBC 路径、六报文 45 信号规模、版本标识与权利状态)
    • 项目 06 —— public-can-dbc-datasets/additional/isc-ifs08-udv/README.md(子模块 fork 差异、CI 配置、分支模型与自动化流程)

    免责声明

    本文内容来自公开仓库的说明文件,描述的是一支学生方程式车队在特定赛季的开发配置,其工具链版本、分支策略与 CI 配置随时间可能变动。DBC 规模与覆盖范围均依据该仓库说明,未做实车核查。文中方法可直接参考,但具体参数请以实际项目状态为准。

    更新记录

    • 2026-10-14 初稿

  • process_replay 的配置、触发器与同步:14 个进程怎么被回放

    结论:process_replay 的 ProcessConfig 里 pubs 是注入给进程的主题,subs 是采集进程输出的主题——与被测进程自己的发布 / 订阅视角相反。回放的「首帧」「双摄齐备」「输出主题周期」都是测试调度逻辑,给出的是功能回归证据,不等于实时车载时序:processing_time 只用于给输出合成 logMonoTime,不是测量到的运行时延。固定快照基线为 c8fb906,本文对原版三个回调做了 AST 隔离探针,不启动任何受管进程。

    配置字段与调用顺序

    ignore 和 tolerance 由测试比较器消费;processing_time 仅给输出合成 logMonoTime;timeout 控制单个 run_step() 周期。vision_pubs 把相机状态与额外 VisionIPC 帧发送关联起来。注意 ignore_alive_pubs 在两个模型配置中赋值,但该字段在本回放模块没有读路径,不能把它当作已实现的存活校验豁免。

    get_process_config() 从表中深拷贝配置;ProcessContainer.__init__() 再深拷贝配置及受管进程,故有状态触发器通常每次回放独立。start() 依次建立隔离前缀、Params / 环境、config_callback、fake 事件、Pub / Sub、可选 VisionIPC、进程,再执行 init_callback。ProcessConfig.__post_init__() 对普通 MessageBasedRcvCallback 自动把触发主题设为 main_pub,模型配置则显式指定 VisionIPC 端点——后者的事件同步点是图像流端点,不能简单等同相机状态消息自身。

    14 个进程配置速查

    下表是回放触发逻辑,不是车型支持或测试实际覆盖矩阵;默认测试会排除两个模型进程。各行 pubs / subs 的完整清单以 CONFIGS 定义为准。

    进程 回放触发 / 同步入口 主要输出
    selfdrived carState,并设置 first_frame=True selfdriveState、onroadEvents
    controlsd selfdriveState carControl、controlsState
    card can 输出频率与 sendcan 启动门,main_pub_drained=True sendcan、carState、carParams、carOutput、radarTracks
    radard modelV2 radarState
    plannerd modelV2 longitudinalPlan、driverAssistance
    calibrationd cameraOdometry,first_frame=True extrinsicsCalibration
    dmonitoringd driverStateV2 driverMonitoringState
    locationd cameraOdometry deviceMotion(合成处理时间 10 ms)
    paramsd deviceMotion vehicleParameters
    lagd deviceMotion lateralDelay
    ubloxd 没有回调:每条被注入消息都可结束一轮 ubloxGnss、gpsLocationExternal
    torqued deviceMotion,first_frame=True lateralTorqueParameters
    modeld 双路道路相机状态标志;主入口为窄路 VisionIPC 端点 modelV2、drivingModelData、cameraOdometry
    dmonitoringmodeld cabinCameraState;主入口为座舱 VisionIPC 端点 driverStateV2

    除 locationd 外,显式 processing_time 是 selfdrived / card / paramsd 的 4 ms 和两个模型的 20 ms,其余默认 1 ms。这些值用于回放中不同进程输出的排序,不能拿来证明对应进程在硬件上达到该延迟。

    ignore / tolerance 由测试比较器消费,属于输出比对边界,而不是进程行为的一部分;调整它们改变的是「通过」的定义,而非被测逻辑本身。ubloxd 没有回调,每条被注入消息都可结束一轮——这类「无触发门」的进程在回放里最容易产生与真实节拍不同的批次划分。模型配置的 fake 事件主入口是图像流端点(窄路或座舱 VisionIPC),而不是相机状态消息本身。

    触发器逐函数行为

    MessageBasedRcvCallback.__call__() 返回「当前主题是触发主题」或「first_frame=True 且 frame==1」。这里的 frame 实际是 run_step() 完成周期后递增的 self.cnt。对非触发主题做隔离探针,计数为 0、1、2 时结果依次为 False, True, False;因此「首帧」并不意味着第一个到达的输入无条件结束周期。

    触发器 探针结果与含义
    ModeldCameraSyncRcvCallback 双摄时两路出现标志齐备才返回真并复位;单摄时窄路出现即可。输入 wide, wide, narrow 结果为 False, False, True——只追踪出现标志,不匹配 frameId,也不验证宽 / 窄帧同一时刻。
    card_rcv_callback 只对 can 计算输出主题周期 frame % int(can频率 / 输出主题频率),且 sendcan 在 (frame - 1) < 2000 时从候选主题删掉。频率相同且仅 sendcan 为输出时,周期计数 0、1、2000、2001 返回 False, False, False, True。
    selfdrived_config_callback 把 cfg.pubs 由列表改为集合差值,删除 GPS 二选一的一侧。
    card_fingerprint_callback 初始化时喂入最多 300 条 CAN,等待 CarParams 出现;get_car_params_callback() 优先按指定指纹构造参数,否则用 CAN、固件查询跳过标志和缓存去识别车型。

    某一路相机缺失时消息会在 msg_queue 中继续累积,不能把「已等齐两路」解释为视觉同步已验证。card_rcv_callback 是测试初始化阶段的输出采集门,不是车端允许发送 CAN 的 safety 决策;配置的频率比还需保持可除且 int 后非零,源码没有在此函数显式校验非法配置。

    对插件化与自制硬件的含义

    新增插件不能只注册一个进程名:应同时声明输入 / 输出主题、schema、触发帧或时间门、初始 Params、相机帧关联键、输出完整性和故障策略。回放配置要能区分「收到任意消息」「收到触发主题」「同一 frameId 的双摄齐备」和真实 deadline;当前回放的标志同步与合成时间戳只给出功能回归证据。自制相机 / 计算板还须独立验证时间戳域、丢帧 / 重复帧、IPC 缓冲寿命、端到端帧年龄、超时回退,以及 panda / opendbc 的发送限制。本文未运行目标硬件或完整回放。

    常见问题

    pubs 和 subs 到底哪个是给被测进程的?

    pubs 是注入给进程的主题,subs 是采集进程输出的主题,与被测进程自己的发布 / 订阅视角相反。混淆两者会读错回放配置。

    回放的 processing_time 是真实运行时延吗?

    不是。processing_time 只给输出合成 logMonoTime,用于回放中不同进程输出的排序。显式值有 selfdrived / card / paramsd 的 4 ms 与两个模型的 20 ms,其余默认 1 ms;不能拿来证明硬件达到该延迟。

    双摄对齐在回放里有校验吗?

    没有校验同一时刻。ModeldCameraSyncRcvCallback 只追踪宽 / 窄两路「出现」标志,标志齐备即返回真,不匹配 frameId。探针输入 wide, wide, narrow 得到 False, False, True。

    「已等齐两路」能说明视觉同步已验证吗?

    不能。回放只检查出现标志;某一路缺失时消息会在 msg_queue 中继续累积。「首帧」也不意味着第一个到达的输入无条件结束周期——它依赖周期计数在 1 时被其他条件允许。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PROCESS_REPLAY_CONFIG_TRIGGER_SYNC_CONTRACT_DEEP_DIVE.md(取用了 ProcessConfig 字段、14 个 CONFIGS、触发器探针行为与插件化含义分析)。

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

    更新记录

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

  • onroad 验收测试:25 秒日志、相机 SOF/EOF 与模型耗时各断言了什么

    结论:test_onroad.py 是需要 comma 硬件的设备测试,不是实车驾驶测试:它启动 manager、跑 25 秒、读首段 rlog.zst,再对服务频率、日志大小、相机时序、模型耗时和资源占用做断言。它的边界很明确——只分析短时日志、不测驾驶动态与安全限幅,且部分断言因算式问题无法触发、部分后备分支不可达。本文基于固定源码快照的静态分析,没有运行 manager、相机、panda、模型或车辆 I/O。

    启动链与副作用

    TestOnroad 设置 COMMA_HARDWARE_TEST=True;继承的 OpenpilotTestCase.setUpClass() 在非 comma 硬件跳过,然后调用小写的 setup_class()。普通路径先删 CurrentRoute、开启 RecordFront、写启用/条款/标定参数并设置 REPLAY、MSGQ_PREALLOC、TESTING_CLOSET,随后 Paths.log_root() 指向的目录若存在就被递归删除。在 AGNOS 设备上该路径位于设备本地介质目录,不随 per-test OPENPILOT_PREFIX 隔离。

    类级准备通过 subprocess.Popen() 启动 manager,最多等 30 秒收到 carState,要求 CurrentRoute 存在且日志目录中恰有一个匹配路段,然后睡眠 25 秒;终止 manager 后读取首段 rlog.zst。TestOnroad.setUp() 直接调用 unittest.TestCase.setUp(),没有调用每测试硬件初始化的那层,但继承的 run() 仍对各测试包裹环境快照、Prefix 和 manager 清理——类级准备发生在 per-test Prefix 之前,不能把它的 Params 和日志删除视为测试命名空间内的动作。

    并行运行时有隐患:通用 test_runner.make_batches() 只按类自身的大写 fixture 合批,而该类只定义小写 setup_class。若用多 worker 运行该文件,类级 manager 启动与日志目录删除可能在多个批次重复甚至并发;发布脚本和 Jenkins 直接调用本文件的 unittest.main(),不走这条并行分组路径。另外 finally 中判断 proc.wait(60) is None,而 Popen.wait() 正常返回退出码、超时抛 TimeoutExpired、不会返回 None,故这条 proc.kill() 后备分支不可达。

    主要验收断言与实际覆盖

    测试函数 有效检查 覆盖边界
    test_service_frequencies() 对已在日志中出现的非 GPS/init/sentinel 主题要求至少约 frequency * 20 * 0.8 条 未出现的主题不进循环;不是全服务存在性证明
    test_manager_starting_time() manager 启动到首条 managerState 小于 15 秒 只约束首消息,不证明所有进程就绪
    test_cloudlog_size() logMessage 总字节数小于 350 kB spam 判据 n / sum > 30 左侧至多 1,额外断言无法触发
    test_log_sizes() 文件落在 60 秒量按 25 秒缩放后的 0.5~1.5 倍 未知文件名访问会报错;不验证内容可解码
    test_ui_timings() 跳过约前 8 秒;最大绘制 CPU <250 ms、均值 <20 ms、>40 ms 帧少于 5 用 uiDebug.cpuTimeMillis,不是屏幕端到端延迟
    test_cpu_usage() 每进程上限 max(expected*1.8, expected+5) 总预算只求静态表之和;缺样本时上限可能未赋值
    test_memory_usage() 跳过前 8 秒,检查 deviceState 内存均值与增量 25 秒窗口不是长期泄漏证明

    相机与模型相关断言如下,它们共同的特点是只检验录入日志的元数据,不解码视频像素、不判断预测准确率:

    测试函数 有效检查 覆盖边界
    test_camera_frame_timings() 三路相机相邻 SOF 与 50 ms 的最大偏差 <5 ms 只检验日志帧时间戳
    test_camera_sync() 帧号连续、EOF 在 SOF 后 50 ms 内;道路/宽角同帧差 <2 ms、座舱错开 20~30 ms、编码器起始帧一致 相机状态只要求最早帧号 <100;仅对三路共同帧号比较
    test_camera_encoder_matches() 编码索引帧号对应相机状态的 SOF/EOF 完全相等 只核对元数据,不解码视频像素
    test_model_execution_timings() 8 秒后道路模型最大/均值 <60/40 ms,驾驶员模型 <300/50 ms 比较执行时间,非模型输出数值或最坏闭环时延
    test_timings() 预设服务消息间隔均值在目标频率 3% 内 独立于实际执行器安全约束
    test_startup()/test_engagable() RELEASE 存在时比较启动告警;8 秒后 engageable 全为真 release_only 只检查变量存在;可接管不等于真正 engage

    DEBUG 分支仅初始化 lr/ts,未设置 manager_st、msgs、log_sizes,只能支持使用到这些字段的定向用例,不能当作完整 suite 的等价运行模式。

    资源类断言也各有算式边界。test_cpu_usage() 用 procLog 首末 CPU 时间差与日志时长计算各进程使用率,每进程上限为 max(expected*1.8, expected+5),而总预算断言只求静态 PROCS 表的和,不约束实测使用率总和;缺样本时 max_allowed 可能未赋值或沿用上一进程值。test_memory_usage() 跳过前 8 秒,检查 deviceState 内存占用均值与相邻增量并打印 procLog 报告,但 25 秒窗口与百分比变化不是长期泄漏证明,共享内存区用量也只打印。test_log_sizes() 要求文件落在预设 60 秒量按 25 秒缩放后的 0.5~1.5 倍,未知文件名访问 LOGS_SIZE[f.name] 会报错,且它不验证文件内容可解码或跨段完整性。

    Chestnut 独立测试

    TestChestnutOnroad 还要求设备类型为 mici,并检查 Chestnut 硬件/编译产物。它写入演示 CarParams,用 mock deviceMotion 和合成 deviceState 驱动 camerad、calibrationd、modeld、dmonitoringmodeld,等到模型有效再收集 25 秒消息。断言包括:五种服务数量在目标频率约 5% 内、Event valid、帧号单调上升、至少 90% 道路模型帧与窄路相机交集、全部 modelV2.big 为真、以及 position.x 有限。

    这能证明短时大模型路径没有回落到小模型的可见标志,但不证明预测准确率、整帧像素对齐、车辆控制或长期热稳定性。发布脚本在强推前直接运行此文件的指定 unittest 入口,Jenkins 另把 Chestnut 测试与模型回放放在设备阶段;两者属于不同调用场景,且有分支/环境/硬件门,不能用任意一次绿色结果宣称全部平台通过。

    验证范围与插件化含义

    已静态核对完整测试文件、相邻 OpenpilotTestCase/Paths/helper 和 CI 入口,但未执行任何测试,未验证真实日志格式、采样分布、模型权重或目标设备。文中不可达/无效断言是直接源码控制流与算式结论,非已在设备上复现的故障。硬件插件验收应补固定日志的数值基准、CAN safety 测试、故障注入与独立台架时序,再进入实车驾驶员监督验证。

    常见问题

    test_onroad.py 是用来验证实车驾驶表现的吗?

    不是。它是需要 comma 硬件的设备测试,只启动 manager、跑 25 秒、读首段 rlog.zst,对服务频率、日志大小、相机时序、模型耗时和资源占用做断言。源文档明确它既不测驾驶动态与安全限幅,也不验证真实驾驶表现;本文也未运行 manager、相机、panda、模型或车辆 I/O。

    为什么说 proc.kill() 后备分支不可达?

    因为源码判断的是 proc.wait(60) is None:Popen.wait() 正常返回退出码,超时抛 TimeoutExpired,不会返回 None,所以基于该条件的后备分支逻辑上不可达。这是直接源码控制流结论,不是设备上复现的故障。

    相机同步测试通过,是否说明视频像素也对齐?

    不代表。test_camera_sync() 只比较录入日志的帧号与 SOF/EOF 时间戳,test_camera_encoder_matches() 只核对编码索引帧号与相机状态元数据,都不解码视频像素;且仅对三路共同帧号比较,不证明未记录帧的同步。

    Chestnut 测试通过能说明什么?

    它能证明短时大模型路径没有回落到小模型的可见标志(如 modelV2.big 全为真、帧号单调上升、服务数量符合目标频率)。但它不证明预测准确率、整帧像素对齐、车辆控制或长期热稳定性,也不能用绿测结果宣称全部平台通过。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/ONROAD_TEST_ACCEPTANCE_LOG_TIMING_CAMERA_MODEL_CONTRACT_DEEP_DIVE.md(取用了启动链与副作用、验收断言覆盖边界、Chestnut 专项与验证范围)。
    • 项目 02 —— openpilot-research/TEST_RUNNER_PARALLEL_FIXTURE_CONTRACT_DEEP_DIVE.md(仅用于确认测试合批与 fixture 命名的边界)。

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

    更新记录

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

  • process_replay 容器:初始 Params、消息合流与合成时钟

    结论:回放器的关键是分清「测试初始条件」与「真实运行」,以及分清「合成时钟」与「真实时延」。replay_process()` 先用 migrate_all() 迁移历史消息,再交给多进程调度;return_all_logs=True 时一旦某主题产生至少一条新输出,该主题全部旧消息被替换。「完整日志」不是原始日志加增量。同时 get_output_msgs() 把输出时间改成 start_time + processing_time,这是调度模型的合成值,不是进程真实运行时间、相机曝光时间或 CAN 发送时间。

    一、入口和数据所有权

    入口/对象 行为 重要边界
    replay_process_with_name() 名称经 get_process_config() 转成独立配置,再委托 replay_process()。 名称必须存在于 CONFIGS,不是 manager 的 44 个进程均可回放。
    replay_process() migrate_all() 先做历史 schema/manager/panda/相机状态准备,再调用多进程调度;return_all_logs=True 时删除迁移日志中所有与新输出同主题的旧消息,合并后按 logMonoTime 排序。 迁移会改变输入和时间顺序;同主题旧消息被整体替换,而非增量叠加。
    ProcessContainer.__init__() 创建随机 OpenpilotPrefix,深拷贝回放配置和受管进程对象,维护输入队列/计数/最近输入时间。 前缀隔离 Params、msgq、部分日志路径,不隔离 CPU、设备或权限;同一解释器内环境变量仍是进程级共享状态。
    ProcessContainer.start() 显式创建前缀目录,写 Params/环境,运行配置回调,开启 fake 事件,创建 Pub/Sub 和可选 VisionIPC,然后启动进程并运行初始化回调。 先启动后回调意味着初始化回调必须按 fake 事件握手;启动中途异常可能留下部分资源。
    ProcessContainer.run_step() 先用触发器决定周期是否结束,当前消息始终入 msg_queue;周期结束才收上周期输出、依次发布队列消息与相机帧、解锁 fake 事件。 输出采集发生在本周期输入发送之前,不能按单次调用直接对齐输入输出。
    ProcessContainer.stop() 对进程发 SIGKILL、关闭 fake 事件、删除前缀目录及回放设置的环境变量。 不恢复回放前相同变量的旧值;失败清理可能再抛异常并掩盖最初故障。

    二、初始 Params、指纹与环境

    生成函数 关键写入 语义
    generate_params_config() 默认开启 OpenpilotEnabledToggle/DisengageOnAccelerator,默认不禁用日志;含 ubloxGnss 写 UbloxAvailable,含 driverMonitoringState 写 IsRhdDetected;固件来源保存 CarParamsCache,纵向权限与 notCar 决定 AlphaLongitudinalEnabled、JoystickDebugMode。 这些是测试初始条件,不是重新判断车辆安全许可。
    generate_environ_config() 设置 PARAMS_ROOT 和 REPLAY=1,可选 LOG_ROOT;强制指纹时设 SKIP_FW_QUERY=1。 原函数的 AST 隔离探针分别确认三种字典输出,未创建任何目录或执行指纹查询。
    _setup_env() / _clean_env() 对空字符串执行「从 os.environ 删除该键」,非空值覆盖;设置 PROC_NAME;SIMULATION=1。 _clean_env() 不会恢复之前的值,故回放应运行于独立子进程或自行保留环境快照。

    环境隔离还有一层容易被忽略:_setup_env() 对配置中的空字符串执行「从 os.environ 删除该键」,非空值直接覆盖,并设置 PROC_NAME;_clean_env() 删除配置中所有环境键及 PROC_NAME/SIMULATION,不会恢复之前的值。而 OpenpilotPrefix 自己仅保存和恢复 OPENPILOT_PREFIX。因此在已有环境变量的测试宿主中,回放应运行于独立子进程或自行保留环境快照,不能把容器当成可嵌套的完整环境事务。

    OpenpilotPrefix 自己仅保存和恢复 OPENPILOT_PREFIX;因此在已有环境变量的测试宿主中,容器不是可嵌套的完整环境事务。

    三、消息合流和时钟

    _replay_multi_process() 先创建所有容器,再计算 all_pubs(注入主题)与 all_subs(输出主题)。仅把原路段中属于 all_pubs - all_subs 的消息放入外部队列;与某个受测进程输出同主题的原日志不会进入外部队列,必须依靠新的内部输出驱动下游。每次被测进程产出的消息都加入最终 log_msgs;若其主题又属于 all_pubs,则加入内部队列和按 logMonoTime 排序的小根堆。外部和内部候选比较时,时间相等优先内部消息。

    时间上,get_output_msgs() 等进程下一次 recv_called,排空输出 socket,并把所有采到的输出时间改成 start_time + processing_time;start_time 是此前收到的输入最大 logMonoTime。run_step() 内的 Timeout 只覆盖已结束的周期;没有触发周期的积压输入,以及尾部 get_output_msgs() 冲刷,不共享这个超时上下文。

    两个收尾条件值得单独验证:主循环在外部队列耗尽、所有容器的 msg_queue 都为空时会停止,即使内部堆仍有已生成消息——这些消息可能已进入最终输出,却未再次送给下游容器;结束后逐容器冲刷输出也只追加到最终结果,不重新进入内部堆。这是源码控制流可见的覆盖边界,尚无实际多进程路段复现,不能把多进程回放等同完整闭环执行。

    四、故障与硬件插件验收

    失效路径也需注意:_replay_multi_process() 先将容器加入 containers 再调用 start(),若在 self.rc 或 capture 初始化之前抛错,finally 中的 stop() 及捕获读取可能因部分初始化而再次异常。尾部冲刷的 ReplayContext.wait_for_recv_called() 最终使用默认无限等待,需另设总 deadline 才能证明崩溃/失联路径有界。以上为静态失效路径,未执行故障注入。

    要把新的传感器、计算或车辆接口做成插件,需给回放器明确的输入/输出主题、触发条件、初始 Params/环境、时钟域、消息新鲜度、最大积压和异常清理契约。验收应分开记录功能输出、端到端时延、处理器资源、断流/进程退出和 panda 安全测试;processing_time 或对齐后的参考日志不构成实车时限证据。当前缺 capnp、参考路段和目标硬件,本专题没有运行完整回放。此外迁移本身会改变输入与时间顺序,因此比较两次回放前必须固定同一迁移版本与路段快照,否则结果不可比。

    常见问题

    所有 manager 进程都能被回放吗?

    不是。replay_process_with_name() 要求名称存在于 CONFIGS,源文档明确不是 manager 的 44 个进程均可回放。名称只是转成独立配置的入口,不代表该进程具备完整的回放输入/输出契约。

    return_all_logs=True 时日志是原日志加增量吗?

    不是。一旦某主题产生至少一条新输出,该主题全部旧消息被替换,而不是叠加。源文档强调「完整日志」不是原始日志加增量,且迁移会改变输入和时间顺序。

    processing_time 是进程真实运行时间吗?

    不是。get_output_msgs() 把输出时间改成 start_time + processing_time,start_time 是此前输入最大 logMonoTime,因此它是调度模型的合成值,不是进程真实运行时间、相机曝光时间或 CAN 发送时间。

    多进程回放等于完整闭环吗?

    不等于。主循环在外部队列耗尽且所有容器 msg_queue 为空时会停止,即使内部堆仍有已生成消息;尾部冲刷也不重新进入内部堆。源文档称这是控制流可见的覆盖边界,尚无实际多进程路段复现。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PROCESS_REPLAY_CONTAINER_SCHEDULER_PARAM_LIFECYCLE_DEEP_DIVE.md(取用了容器入口与数据所有权、初始 Params/环境生成、消息合流与合成时钟、故障与插件验收)。

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

    更新记录

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