保留 MoTeC C125 硬件,用开源工具链补上 i2 Pro 缺的那一半:
原生 .ld 解析(不经过 i2 Pro)→ Parquet 列式存储 → 自动切圈 · 赛道区段 · 数学通道 →
免安装浏览器工作台:多通道同步分析、双圈对比、直方图与频谱、时间/通道报表,全在一个单文件 HTML 里。
数据源不止 .ld——i2 Pro 导出的 CSV、别人的 Excel、以及车队自己录的原始 CAN 帧表(按 DBC 解码)
读进来都是同一种场次。
不依赖任何商业软件、不需要 npm、不需要 Rust、不需要构建。
运行期只用 Python 标准库 + 三个包(numpy / pandas / pyarrow),读 Excel 另加一个
openpyxl(见 docs/adr/0003)。
git clone 下来先装一次那四个包(唯一需要联网的一步),之后全程离线。
三步走见下面从零开始。
i2pro_data/*.ld ──► 原生解析(mmap) ──► Parquet + 元数据 ──► SQL / 单通道秒级裁剪
│
├──► 自动切圈 · GPS 校正 · 赛道区段 · 数学通道 · 注释 ──┐
│ ├──► 自包含 HTML 工作台
└──► 本地服务(局域网 + 可分享链接)────────┘ 图表 / 散点 / 直方图 / 频谱 / 报表
双击即开、无 CDN、无 npm
目录 · 一键启动 · 已验证的能力 · 架构与目录 · 命令 · 快捷键 · 数据里必须知道的坑 · 开发 · 常见问题
双击 启动.bat 就行——它会起本地服务并自动打开浏览器。(第一次运行 Windows 防火墙可能弹窗,选“允许访问”。)
新机器上先双击一次 安装依赖.bat,见下面三步;缺包时 启动.bat 也会直接告诉你该敲哪条命令。
| 双击这个 | 干什么 | 什么时候用 |
|---|---|---|
安装依赖.bat |
装那四个运行期包,装完念一遍版本号 | 每台新机器只做一次(唯一要联网的一步) |
启动.bat |
起服务 + 开浏览器,可任意缩放,控制台会打印一个局域网地址 | 日常分析、在 P 房几个人一起看 |
导出快照.bat |
把每个场次导出成 out\<场次>.html,再打开索引页 |
要把某一场发给没装 Python 的队友 |
导入数据.bat |
把 .ld/.ldx/.csv/.csv.gz/.xlsx/.txt/.tsv(或整个文件夹)拖上去,拷进 i2pro_data/ |
拿到新日志的时候 |
命令行等价写法:
cd E:\桌面\i3pro
.\i3pro.cmd serve --data i2pro_data --open # 等价于 启动.bat
.\i3pro.cmd snapshot --data i2pro_data --out out # 等价于 导出快照.bat
.\i3pro.cmd info i2pro_data\*.ld # 场次概览i3pro.cmd 会自动探测可用的 Python(python / py -3 / python3,以及
%LOCALAPPDATA%\Programs\Python\Python3*),把 src/ 加进 PYTHONPATH 再调
python -m i3pro——不需要安装这个项目本身(不 pip install i3pro、不编译、
不需要 node),但它会先确认那四个运行期包在不在;不在就直接把该敲的
pip install -r requirements.txt 打出来,而不是含糊地说"启动失败"。
-
装 Python 3.10 或更新——python.org, 安装时勾上
Add python.exe to PATH。 -
装四个运行期包——双击
安装依赖.bat,或者命令行一行:cd <clone 下来的目录> python -m pip install -r requirements.txt
装完它会自己把四个包的版本念一遍,念不出来就是没装好。
-
双击
启动.bat。
第 2 步只在每台机器上做一次,也是唯一需要联网的一步;之后 启动.bat /
导出快照.bat / 导入数据.bat 全程离线。requirements.txt 与 pyproject.toml
是同一份清单(有单测盯着,漂移就红),所以 pip install -e . 是等效的另一条路——
那条路装完连 PYTHONPATH 都不用设,直接 i3pro serve --data i2pro_data。
刚 clone 下来一个场次都没有,这是正常的。 车队日志(.ld / .csv / .xlsx)
不进仓库,跟着仓库走的只有 i2pro_data/dbc/ 那套 DBC
(ADR-0005:换台机器没有它就解不出 CAN
报文)。所以第一次打开侧边栏是空的,界面会写明"没有找到任何日志文件"——把你的日志
放进 i2pro_data/,或者直接拖到 导入数据.bat 上,刷新即可。
没有随仓库发布的示例数据是有意的(那是车队数据),所以四道回归里有三道在没有 数据时会自动跳过——跳过不算通过,要在有数据的机器上跑,见 AGENTS.md 第 7 条。
那是模板,本身不含数据——数据由 i3pro 在生成页面时注入。直接打开它只会看到一段说明 (以前是空白页 + 一个控制台报错,现在会明确告诉你去点哪个 bat)。
启动.bat(serve):每次缩放按可见区间重新取全分辨率数据 —— 1 秒窗口 = 100 个原始样本, 单次请求 3 ms。想看细节用这个。导出快照.bat(snapshot):波形是提前抽稀好的,放大到很细的时间段会看到折线, 但 HTML 自包含,发给谁都能双击打开。
全部结论来自 i2pro_data/ 里的真实 C125 日志和 MoTeC 自己导出的 CSV。口径跟着数据走,
本机 2026-10-19 这一份是 17 份 .ld(20.1–122.7 MB、259–451 通道,全部解析成功),
加上 tools\verify_ld_vs_csv.py 认的那两对 .ld + .csv(173.7 MB / 406.1 MB)。
| 能力 | 证据 |
|---|---|
原生解析 .ld(不需要 i2 Pro,也不需要任何转换工具) |
17 份日志全部解析成功,259–451 通道;int16/int32 与多档采样率混采(高避那份实测 100/50/25/20/10/5/2/1 Hz,共 437 条通道) |
| 数值正确性 | 与 MoTeC 导出的 CSV 逐样本比对:两个场次各 213/213 个可比通道全部落在显示精度内,最大偏差 0.005 G / 0.4 deg/s(都是半个显示位)。见 tools/verify_ld_vs_csv.py |
| 独立实现交叉验证 | 与另一套独立逆向的解析器 gotzl/ldparser 对比:通道数、通道名、单位、采样点数完全一致,抽样通道数值 rtol=1e-9 |
.ld → Parquet |
耐久正赛 118.8 MB → 19.0 MB(压掉 84%),1.3 s;高避5圈 36.6 MB → 5.3 MB,0.4 s |
| 单通道秒级抽取 | 19.4 万点抽稀成一个通道 → 7 ms;整套元数据+切圈+对比+轨迹 payload:高避5圈 0.037 s、耐久正赛(23 圈)0.200 s(其中区段自动切分 0.143 s、GPS 校正 0.002 s——真要提速先看这里) |
| GPS 自动切圈 | C125 的 beacon 没接线、.ldx 里 Total Laps = 1,i2 Pro 切不出圈。i3pro 用起终点门 + 航向判据切:高避5圈 → 5 个完整圈(40.4–51.2 s,806–816 m),耐久正赛 → 23 个完整圈(最快 54.900 s) |
| 距离轴双圈对比 | 两圈按 1 m 步长插值到同一距离轴,输出 Δ 曲线、最大损失点;Δ 终点与圈速差一致(< 0.6 s) |
| 浏览器工作台 | 单文件 HTML(高避 1.27 MB / 耐久 1.59 MB,数据与报表全内嵌),多通道同步光标 + 缩放平移 + 时间轴/距离轴/双圈三模式 + 赛道速度着色 + 全通道搜索 + PNG 导出 + 分享链接。无服务器、无 CDN、双击即开 |
| i2 Pro 级交互 | 双击拖拽框选缩放(Alt 纵向 / Ctrl 框选)· 概览条与滚动条 · 基准光标与 Δ 测量 · 可见区间 min/max/avg · 光标处全通道数值 · 点/线样式 · 按同单位分组的共享纵轴 · 状态故障带 · 散点组件(X×Y、第三通道着色、跟随缩放、光标联动) |
| 工作表 / 组件 | 12 列网格,拖标题栏移动、拖右下角同时改宽高、边缘吸附到相邻组件;组件类型:时间/距离图 · 散点图 · 赛道轨迹 · 直方图(柱状/折线/文字、按第三通道着色、门槛可写通道名或数学表达式)· 热力图(两个通道分箱、第三通道着色)· 仪表(数值/列表/条形/表盘/方向盘)· 姿态(立体车体 / PFD)· 时间差 Δ · 状态与故障 · 时间报告 · 通道报告;每个图有自己的通道列表;整张工作表连同位置尺寸能编码进分享链接 |
| 工作表就是一个文件 | 顶上那排按钮 = 仓库里 worksheets/*.json,一文件一套(随版本发布的是 分析 / 对比 / 动力 / 底盘 / 车手 / 仪表台 / 报表):加一套不用改代码,能 diff、能发给队友、换台机器就有,格式见 worksheets/README.md。组件靠 pick 按提示词挑本场次的通道(找不到就留空,不会乱指一条);文件读坏了、或写了这一版不认识的组件类型,那一个被跳过并在工作表那一栏说清是哪个文件、下一步做什么。布局记忆按场次分(i3pro.worksheet.v2:<场次>),不再带着上一场的布局串场 |
| 工作表增删改与进出 | 那一栏下面多一排:保存 / 另存为 / 新建 / 重命名 / 删除 / 导出 / 导入,改完立刻落到 worksheets/*.json。同名永不覆盖(文件加 -1、显示名加 2),改名连文件名一起改,删除要点两次且说清切回哪一套,导出的那份能原样导入回来。写回文件的是这一屏的形状:规则(pick)当场还算得出屏幕上那个值的键不写死(换场次照样自己挑通道),你在左边栏勾过的通道照实写回——不写你勾的保存一次就丢(A58 / A74)。快照(离线 HTML)只读:点了会提示改用 serve 模式 |
| 本场次没有的通道 | 一份工作表换到别的场次时,本场没有的那几条不再静默消失(以前是服务端跳过 + 前端再滤一遍 + 零文案,所以这条先是修 bug)。通道列表里它们灰着、写着"本场次没有",组件标题写 缺 N 条、图上单占一行 本场次没有(不画):…,页面抬头也写;绝不自动剔除,切回原场次勾选原样还在。三种状态分得开(present / empty 通道在但整段没有一个有效样本 / missing),判定只有一处。出口也写明:导出的元数据带 excluded_missing(空也在),界面导出带 skip_missing=1,命令行默认仍是硬报错(见 A59) |
| 通道别名(#36) | i2 Pro 的 Channel Aliases:一条别名 = 有序候选通道名,解析取第一条本场次存在的。组件里写 @左后轮速,同一套工作表因此能跨场次用——实测这条别名在耐久那台车落到 SpeedRL、在高避那台落到 Gear Speed1。别名存在工作表文件里(不进 .ld),左边那一栏是状态视图:每条别名这一场落到了哪条通道、哪几条一条都没落地,候选可增删可调序(顺序就是优先级)。落不到的引用留着,交给上面那条"本场次没有"显示(见 A61) |
| 通道处理(A75) | i2 Pro 的 Filter 与 Scale/Offset:通道列表每行一个 ⚙,四栏(低通含零相位 / 移动平均 · 增益 · 偏置 · 用光标处归零)拼成一条同名定义覆盖原通道——图、散点、报表、切圈、导出看到的都是处理后的值,.ld 一个字节不动。快捷键 Ctrl+Shift+F / O / Z;处理过的通道在列表与图上带 ƒ 角标,导出元数据里记一条 transformed。默认存本场次侧车,可勾"存成全局";一起去看原值点「删除处理」(见 A75) |
| 曲线连接 / i2 Pro 的 Up-sampling(A79) | 慢通道看起来"够不够丝滑",取决于点与点之间怎么取值——i2 Pro 的 Channel Up-sampling 四选一(Linear / Closest / Previous / Next,帮助原文:"used when displaying a graph to join the dots")。我们画图这条路以前一律按"保持前值"重复到 100 Hz 主时间基(高避金标准 437 条通道里 147 条比 50 Hz 慢,10 Hz 的通道放大就是 0.1 s 一个平顶),现在画图默认线性,组件栏「显示 → 曲线连接」可一键换回阶梯保持,标题写明「曲线:阶梯保持」。分析路径(距离轴 / 切圈 / 报告 / 直方图 / 导出)一个字节都没改,仍然是保持前值。已知边界:CSV / CAN 场次的列在导入时就零阶保持到主时间基了,换不回斜线(见 A79) |
| 侧边栏快速跳转(A78) | 左边那一栏十个模块竖着排,一屏只看得见两三块。顶上钉一排按钮(JUMP_TARGETS 一份清单同时喂按钮、落点、滚动高亮):点一下滚到那一栏、并把该聚焦的控件聚焦(点「通道」直接落在搜索框上);Ctrl+K 把光标放到这一排、左右方向键换、回车跳;「光标值」默认收着,跳过去会顺手打开它。实测跳完目标那一栏的标题落在按钮排下沿 6.1 px(Edge 1600×1000,见 A78) |
| 姿态图(#43) | 把车"摆"出来看:roll / pitch / yaw 三条通道,立体车体(线框盒子 + 地面网格,拖拽转视角、双击或 R 复位)与 PFD(天地线 + 俯仰刻度 + 滚转弧 + 航向带)两个视图,跟着光标走;基准光标处叠一层 30% 透明的"幽灵"跟当前姿态对比(Marple 的交互)。三条通道按实测名字自动挑(Aceinna Roll / Aceinna Pitch / AngleLocalYaw),挑不到就留空并说明白——"还没选" / "本场次没有这条通道" / "这一份快照没带它的数据"三态分开、标题写"缺 N 条",绝不静默少画一轴;三条这一屏完全没变化时还会写一句提醒(实测两台车的 IMU 不同:高避那份的 Aceinna Roll/Pitch 是空模板,有数的是 Roll (2)/Pitch (2),耐久正好反过来,下拉换一下即可);方向不对就给那条轴填个负倍数。算术在后端(attitude.py),前端那份 3-2-1 矩阵与挑通道规则由无头断言拿后端算出来的数对拍(差 < 1e-12),拖得动 / 复位 / 跟光标由真鼠标验收(见 A88) |
| 图表抬头 | i2 Pro 的 Measurements 列:通道名 | 光标值 | Δ | Min | Max | Avg,光标值随鼠标实时跳动,不用低头看左栏 |
| 热力图(#45) | 两个通道分箱、把样本丢进网格再上色(i2 Pro 的 Aggregate 类):着色两种问法——每格多少个样本,或格子里第三通道的均值(连同 min / max 一起给出来,只看均值会把"这里一会儿 0 一会儿 100"画成一片中间色)。三个下拉选 X / Y / 色(按单位分组),格数 8–200(默认 60×40),范围当前窗口 / 选中圈 / 整场;色标与格子同一段归一化,所以颜色不骗人;鼠标悬停圈出那一格并写"这一箱 N 点 · X / Y · 色通道均值(min…max)"。和直方图同一条纪律:数的是原始样本(不是画图那份抽稀),所以只在 serve 模式算——快照里没有它(直方图是"一条通道一份"可以内嵌,热力图是"两个通道的组合",十来条通道就是上百对),快照打开时组件会说明白并指路。见 A90 |
| 光标 A–B 区间统计(#44) | 抬头那三项 Min / Max / Avg 现在有两种范围:可见区间(默认,跟着缩放走)与 光标 A–B(由基准光标与主光标钉死),在组件栏「显示 → 统计范围」切换、跟着工作表存。区间左闭右闭(与导出那条范围同一个口径)、A 在 B 右边自动交换、只数有效样本(空区间印 -- 不是 0);选 A–B 而没放基准光标时抬头不印数字,改说那句现成的"按 D 打开基准光标…"——这句话只有一处实现,导出面板与抬头共用。Marple 实测 cursor/stats 只有三个数,所以我们不加 σ / 斜率 / 积分。算术在后端 cursorstats.py,前端那份镜像由无头断言拿同一组冻结常量对拍(20.00|40.00|30.00,B 挪一格变 20.00|50.00|35.00);真点菜单、真按空格放 A、B 一动数就变由真鼠标验收(见 A89) |
| 配色与抬头(#35) | 三套调色板(默认 / 色盲友好 / 高对比)+ 一个**"缺失"语义灰**(不属于任何一套)。可辨性是算出来的:每套前八色两两 CIE76 ΔE ≥ 25(实测最紧 26.43)、相邻 ≥ 30、对深色底 WCAG 对比度 ≥ 3.0(实测最紧 3.36)。点图例上的色块就给这条通道手选色(优先于调色板,换调色板也不动它);组件栏的「显示」管这一件组件的调色板与抬头字段(光标值 / Δ / Min·Max·Avg / 单位)。三样都存进工作表,改完点「保存」,切走再切回还在(见 A60) |
| 圈速交互 | 单击某圈 = 设成基准圈并跳到该圈;Ctrl+点击 = 对比圈;Shift+点击 = 只跳转 |
| 信标编辑 | 点信标名就地改名(回车保存 / Esc 取消,重名自动加后缀,可信标记跟着迁移);「+ 穿越」在光标处插入一次漏掉的穿越(i2 Pro 的 Missed Beacon),它只加边界、不会把已有圈次清空 |
| 切分方式 | 自动挑门 / 按运行分段(八字、直线加速、skidpad 的正确单位)/ 八字按环;一刀切不出圈的场次也能手工给信标 |
| 缩放不丢细节(serve 模式) | 快照是预先抽稀的;serve 模式每次缩放按可见区间重新取样:1 秒窗口返回 100 个原始样本,单次请求 3 ms |
| 数学通道 | 白名单表达式(56 个函数,不用 eval)造派生通道,效果与原生通道一样:可画图、可进散点、可切圈、可进报表。两种作用域——本地跟着场次(<场次>.maths.json),全局在仓库里(maths/global.json),同名时本地赢,界面上用角标写明谁生效。通道名直接打即可:空格/短横线/下划线可以省掉或互换、大小写不计较(FSD13Distance1 与 FSD13 Distance1 是同一条),认不准时报错会列出"以你打的字开头的那些通道",也可以从「插入通道」下拉直接选;试算结果会念出认到的通道名。本地坏式子不进侧车,一条坏了不拖累其它条 |
| 赛道区段 | 按曲率(GPS 轨迹算的,与速度无关)或横向加速度把一圈自动切成弯道与直道,灵敏度可调、单调可预测;区段沿距离定义、套在每条圈上,时间轴上画成弯/直带子。边界与名字都能手改,改过之后自动重切不会悄悄覆盖(先拦一次并说清后果,确认了才覆盖)。存在 <场次>.sections.json 侧车,.ld 永远只读 |
| 报表(i2 Pro 的时间报告 / 通道报告) | 时间报告:区段 × 圈的分段计时矩阵,按"离该段最快多远"分档着色,并给出理论最快圈(各段最快相加,只取完整圈)与连续最快圈(连续数据上滑一个一圈长度的窗口,两端都按首次到达取时刻)。通道报告:按圈或按区段列出最小/最大/绝对最大/均值/起值/终值/变化量/标准差,窗口是半开区间、取那条圈自己的边界时刻。两张表都是 DOM 表格,可按弯/直过滤、导出 CSV(表头即列标签,与 Python 端逐字节一致);快照离线也能看。同一个数字在快照、服务、i3pro report 三个入口是同一份(report.py 纯函数) |
| 注释(i2 Pro 的 Notes) | 在光标处放一条带文字的标记("这里换了刹车点"):时间轴上是琥珀色虚线 + 那行字,轨迹图上是一个点 + 那行字;点文字就地改(回车保存 / Esc 取消)、✕ 删除。存 <场次>.notes.json 侧车,不参与切圈 / 比圈 / 报表(实测加注释前后圈速表 9 行不变),随快照与分享链接一起走 |
| GPS 校正 | 把坏定位标出来再决定要不要修:掉星给的 (0,0)(高避5圈 638 点、TV0 32256 点)、卫星数不足、以及最阴的一类——定位整体跳到几百米外并留在那(黄金数据 耐久正赛 里就有一次 214.5 m / 0.05 s,隐含 15443 km/h;16 个场次 12 个有)。16 个场次里一个孤立毛刺都没有,所以不删点(删哪边都是猜),而是断开连线 + 标红 + 计数:轨迹图不再跨着空档画一条不存在的直线。校正本身(时间偏移按秒或按更新周期、插值到主采样率)默认关,开了之后可选择只作用于轨迹 / 切圈 / 距离轴;关掉时数值与旧实现逐点 array_equal。参数存 <场次>.gps.json。卫星数那条判据只在它真的报过数(某处 ≥ 4)时才算:本车那份 GPS Sats Used 整份只有 0 与 −1,是条死通道,拿它当判据会把定位全否掉(见 A80) |
| 数据导出(CSV / Excel) | 把数据带走去 Excel / Python / MATLAB 接着算:范围可选全部日志 / 当前视图 / 选中圈 / 光标 A–B / 指定时间段(12.5s,或 2026-09-14 12:34:56.789,也可以只写 12:35:10.123)/ 指定距离段,边界左闭右闭;通道分三档——本工作表所有组件的通道(并集,默认)/ 当前组件的通道(左栏正在编辑的那一件)/ 全部通道(本场次所有列),每档的名字里都写着有几条(数学通道单独开关);采样可选 Auto(各通道保留原始采样点,宽表以并集为索引、缺失留空)或统一采样率(1/5/10/20/50/100/200/500 与自定义,linear / hold / nearest / mean);主索引可选 time_s(相对秒)、timestamp(绝对时间戳=场次起点 + 相对秒)或 distance_m——范围自己决定轴(选距离段就是米、选时间段就是秒,主索引下拉跟着锁,免得把 1200–1850 当成"秒"导出去)。CSV 是 UTF-8 带 BOM 的宽表或长表(可选打包 metadata.json),Excel 是「元数据」sheet + 「数据」sheet、超 104 万行自动分 sheet——.xlsx 用标准库写(zipfile + 最小 OOXML),没有为它多一个运行期依赖。面板先给预计行数与体积,进度条按 Content-Length 走、再点一次就是取消。实测两份金标准 --rate 10:高避 4,641 行 × 442 列 / 6.7 MiB、耐久 19,431 行 × 347 列 / 22.6 MiB,末行时刻 463.99 / 1942.99。命令行同一个出口:i3pro export <文件> --rate 10 --out x.csv |
| CAN 原始帧 → 场次 | 车队自己录的原始帧表(can_data/*.csv,GBK、x| 5A 64 …)直接读成与 .ld 同形状的场次:DBC 取并集解码,含子目录(2026-10-05 实测:18 份 DBC / 137 条报文 / 124 条通道 / 覆盖 3,013,146 帧 = 81.77%;只挑一份最多只有 46 条 / 24.6%),并写明每条通道来自哪份 DBC、每份的 sha256 与贡献。除了 Vx_KF(车速 → 距离轴,实测 0.1–5546.8 m),dbc/261004/ 那批还带来 S-Motion 的光学地面速度、Xsens MTi 的姿态/加速度/经纬度与 sw260425 的方向盘转角;多路复用报文(M / m<n>,如 IMU.dbc 的双帧 IMU:byte0 选帧类型、后 6 字节分时是加速度或角速度)按选择子分路解成各自的通道,两路的真实更新率分开记(实测各 47.67 Hz)。解析器是标准库手写的(cantools 只在测试里当裁判逐信号对拍),用日志里实际的字节长度、不用 DBC 的 DLC;没有 DBC 覆盖的 ID 留在导入报告里(帧数 / 帧率 / 前 8 字节),连续记录自动并场(9 份 → 7 次记录) |
| 导入别人的表(CSV / Excel / TXT) | 队友发来的不是 .ld 也能用:.csv(i2 Pro 导出、别的队/别的设备的表)与 .xlsx 都读成与 .ld 同一种场次——进侧边栏、能画图/切圈/比圈/报表/导出。列名走 原名 → 别名 → 手工覆盖 三级回落,单位参与判断与补齐,选择存在 <场次>.map.json 侧车(.ld 永远只读)。Excel 多张 sheet 时默认挑第一张能当通道表读的(自己的导出把「元数据」放第一张),--sheet 可以指定;日期单元格 / ISO 文本的时间列折成相对秒;宏 / 图表 / 外部链接 / 公式没有缓存值这四种读不了的形态会明确报错并说下一步。分隔文本(.txt / .tsv / 分隔符不是逗号的 .csv)在导入前给预览:前 20 行 + 认出来的分隔符与编码,分隔符 / 编码 / 表头行 / 单位行都能在下拉框里改,"没有时间列"默认拒绝、可以显式选按固定采样率生成时间列——预览与导入是同一条代码路径,改了下拉框立刻看得见结果。硬判据是 round-trip:同一次导出的 CSV / xlsx / TXT 读回来逐点相同 |
逆变器日志(Ephorus .mat) |
德雷斯顿控制器导出的日志直接读成一场,进侧边栏就能画图 / 用数学通道 / 导出。它是 MATLAB v5(TinyMAT 写,不是 v7.3 HDF5),而且两处不按规格:MI 元素不补齐 8 字节、部分变量尾部多 8 字节零——读取器两种都认(numpy + 标准库 zlib,不引入 scipy / h5py,见 ADR-0013)。每份 388 个变量里 5 条重名(Reserved_1..3、Use_*_Ethernet)加 _2 后缀保留 → 387 条通道 + 一条真实时间轴 t;默认按整数倍抽到 ~100 Hz(不插值,留下的都是真实采样时刻)。10.22 那 5 份实测 128.0 / 119.0 / 316.6 / 58.1 / 1800.2 s,原生 499–500 Hz。单位先按通道名后缀猜(_RPM / _Nm / _degC / _A_rms …),猜不到留空 |
.csv.gz:压过的 CSV(只读) |
同一个 CSV 存成 gzip:合并产物实测 2724.1 MB -> 339.2 MB(12%),解压 453 MB/s(比解析这张表本身还快)。身份是主名——X.csv.gz 就是 X 那一场,与 X.csv 共用侧车,两个容器并存时侧边栏显示 X 与 X (csv.gz)。i3pro 只读不写:压缩用开发工具 python tools\make_csv_gz.py <文件或目录> [--delete](mtime=0、不写原文件名,同一份内容压两次逐字节相同)。.ld / .xlsx 不参与——二进制与已压缩容器再套 gzip 只会更慢更大。Excel 要先解压才能打开;帧表(can_data/ 那些原始帧日志)也不压,原因见 ADR-0012 |
CAN 记录仪 ↔ .ld 对齐与合并 |
C125 与记录仪记的东西不在同一份文件里:.ld 有电机 / AMK / MTI / GPS 共 459–474 条通道但没有 IVT,帧表有 I_Data / U1_Data 但没有电机通道,所以母线电流与电机电流从来不同框。i3pro pair 按墙钟挑候选(.ld 头的 log_time 是结束时刻;而头里的绝对时间不可靠,实测一天里差 19–505 s 还会跳,所以候选按钟差上限放宽)、用两边都有的速度互相关求偏移、再用加速度把偏移钉准并复核(三条轴取中位数;速度太平,短窗口上会在错偏移上给出 r=1.0000,实测多例),然后把两边的通道合到一个场次:时间轴以 .ld 为准、CAN 通道零阶保持、同名只留一份、C125 里那些整段为 0 的 IVT 模板通道会被真值替换;一个 .ld 对上不止一次记录时全部并进同一场(各自只补自己的那一段),而同一场里几次记录推出的钟差差出好几圈的当重复动作撞出来的剔掉。实测 10.5湾区 配出 9 对、10.4湾区测试 3 对(其中一场并进两次记录)、10.7湾区 15 对(另有两场各并进两次记录)、10.6中庭 2 对,合并后的 底马八字 是 127,600 行 × 555 列(其中 287 条真的有数,其余是 .ld 里整段为 0 的空模板),I Data(−84.1 A)与 MCU1 FR TorqueCurrent 同框(见 A76) |
| 接续:接连录下来的几场接成一场(A123) | 耐久照例是两次记录(换车手时记录仪停了再起),两段各是一个场次,分开看圈速接不上、整场电耗各算一半。i3pro stitch <前半> <后半> --out <场次>.csv 按给定顺序接在一条时间轴上:后一场的首点落在前一场末点加一个采样间隔上(--gap 可以留出真实停顿,默认 0 = 首尾相接——10.18 的 20261018-耐久合并 就是这么做的,两段 1457.99 + 1023.99 s、中间 140 s 换人停顿不在产物里)。列取并集(第一场的名字与顺序在前,新名字按名字排后;同名两种写法只留一条,判据与 pair 那条 pairing.name_key 同一处),某一场没有的列留空;比主时间基慢的通道(.ld 的 Gear 是 1 Hz)按零阶保持搬过去。实测 10.25 两段耐久:143,500 + 106,000 行 -> 249,500 行 × 515 列 / 491.4 MB,末点 2494.99 s,接缝处 515 条通道逐点与两份原件相同(0 条不一致),latitude MTI / longitude MTI 各 249,496 个有效样本;距离轴 22,721.9 m(= 两段 11,359.37 + 11,362.55);打开就是 50 圈 / 24 完整圈(= 两段各自单看的 12 + 12);压成 .csv.gz 后 79.0 MB。不给 --out 只打印方案、一个字节都不写;目标已存在先拒写(要 --overwrite) |
| 由电机电流推算母线电流 | 合并场次让这两个量第一次同框之后量出来的换算关系:母线电流量的是功率(I = P/U),所以公式必须带转速——I_bus = -(1/η)·Σ(T·ω)/U + b,实测 1/η = 1.312(η = 0.762)、R² = 0.9813、RMS = 2.25 A(17 场 / 139 万样本);只有电机电流那条退化成 I_bus = -0.5521·Σi_q - 0.523(R² 0.8673、RMS 6.01 A)。电机常数实测 kt = 0.2416 Nm/A(四个轮一致,R² ≥ 0.99);I Data 的符号约定是放电为负、回充为正。两条式子都能直接贴进「数学通道」(见 docs/bus-current.md) |
| 电池能量通道(Drive / Regen / Net Energy) | 三条全局数学通道把电池功率累计成 kWh:Drive Energy 是驱动电耗(正=放电)、Regen Energy 是回收能量(正=回充)、Net Energy = Drive Energy - Regen Energy 是净耗电。battery_power() 优先用 Battery Power,缺了自动回退到 -I Data×U1 Data(母线电压 >100 V);两套都没有时明确报错。10.18 耐久合并实测 6.351 / 1.025 / 5.326 kWh,IVT 对拍 6.355 / 1.023 / 5.332 kWh;另外四份耐久数据也在 docs/energy.md 里列了整场数字。累计曲线可按现有通道报告的变化量读每圈电耗,不需要新组件 |
| 工程化 | 692 项单测(真实数据回归 + HTTP 端到端 + 无头驱动前端 935 项检查),另有 真浏览器真鼠标验收 444 项全过(tools\verify_clicks.py,Edge 的 DevTools 协议发真事件,跑在两份金标准上);四条回归走一遍约 5.4 分钟(单测 113 s + 真鼠标 178 s + 两次 smoke 22 s + 解析对照 8 s,AGENTS.md 第 7 条有分档的短回路);侧边栏那份"每场圈数/最快圈"一场一把钥匙地缓存:导入一份日志后的第一次首屏 4.25 s(110 场),不再是整份重算的 69 s(A87);运行期依赖只有 numpy/pandas/pyarrow,唯一的例外是 openpyxl(只用于读 Excel,见 ADR-0003),清单钉在 requirements.txt 上(与 pyproject.toml 同一份,有单测盯着别漂移);改这个仓库的硬性规则见 AGENTS.md |
完整验收清单与复现命令见 docs/ACCEPTANCE.md;
规划、里程碑与风险见 docs/PLAN.md。
| 命令 | 作用 |
|---|---|
info <files...> |
场次概览:设备、日期、时长、采样率、通道数 |
channels <file> [--filter <子串>] |
列出通道(名称/单位/采样率/缩放/小数位) |
laps <file> [--json <path>] |
圈速表(含相对最快圈的 Δ 与异常段标记) |
track <file> |
圈速柱状速览(终端里快速判断切圈对不对) |
report <file> [--table time|channels] [--filter corner] [--by section] [--csv x.csv] |
时间报告 / 通道报告(分段计时、理论最快圈、连续最快圈、通道统计量) |
delta <file> [--ref 2] [--cmp 5] |
距离轴双圈对比,输出 Δ 曲线 JSON 与最大损失点 |
convert <files...> --out out |
.ld → Parquet + meta.json |
series <parquet> --channels a,b --from 400 --to 405 |
按列 + 按时间窗裁剪(Parquet 列式存储的真正用处) |
query "<SQL>" --parquet <files...> |
对已转换数据集跑 SQL |
export <file> --out x.csv |
导出选中通道为 CSV |
import <文件或目录> --data i2pro_data [--sheet 名字] [--delimiter ";"] [--rate 100] [--map 原始列=通道名] |
把 .ld/.ldx/.csv/.csv.gz/.xlsx/.txt/.tsv 拷进数据目录(--preview 先只看读法、不复制;拖到 导入数据.bat 上是同一件事) |
pair <数据目录> [--ld 名字…] [--list|--apply] [--copy --source 原始目录] [--out 目录] |
把 CAN 记录仪帧表对到对应的 .ld 场次:按墙钟挑候选与搜索中心(记录仪的钟实测能差几分钟,所以搜索给到 ±900 s)、按相对时钟用速度互相关求偏移、再用三条加速度轴的中位数复核(互相关至少要有 60 个比较点),然后把两边的通道合成一个场次(对上不止一次记录时全部并进同一场)。--list 只打印配对表、不写文件;--copy 先把缺的文件从共享拷进来 |
stitch <前半> <后半> […] --out x.csv [--gap 秒] [--overwrite] |
把接连录下来的几场接成一场(耐久里换车手):后一场首点落在前一场末点 + 一个采样间隔上,列取并集、某一场没有的列留空;不给 --out 只打印方案、一个字节都不写 |
render <file> [--channels a,b] [--ref 2] [--cmp 5] |
生成自包含 HTML 工作台 |
serve [--data dir] [--host 0.0.0.0] [--port 8731] [--open] |
本地/局域网 Web 工作台 |
snapshot [--data dir] [--out out] [--open] |
批量导出每个场次的离线 HTML + 索引页 |
| 类别 | 键 | 作用 |
|---|---|---|
| 缩放 | 双击拖拽 | 横向框选放大(Alt 纵向 · Ctrl 框选) |
| 双击 | 以点击处为中心放大 2× | |
| 滚轮 | 以指针处为中心缩放 | |
| ↑ ↓ / Alt+上下 | 横向 / 纵向缩放 | |
| F2 / W / Z | 全出 / 默认一圈 / 缩到两光标之间 | |
| Esc | 取消正在框选 | |
| 平移 | 拖拽图内或坐标轴 · 双击滚动条 | 平移 / 全出 |
| Shift+左右 · F B · H | 平移 · 前/后翻页 · 以光标居中 | |
| 光标 | 鼠标移动 · ← → · Ctrl+左右 | 移动 / 步进 0.01 s / 步进 1 s |
| D · 空格 · X | 基准光标开关 · 放置 · 与主光标交换 | |
| 显示 | S | 线样式 ⇄ 点样式 |
| G | 分栏 ⇄ 重叠 | |
| M | 可见区间 min/max/avg | |
| L / V / E | 图例 / Values 窗口 / 增删状态与故障组件 | |
| 圈 | 单击某圈 | 设成基准圈并跳到该圈 |
| Ctrl+点击 · Shift+点击 | 设成对比圈(切双圈对比)· 只跳转 | |
| N P · Ctrl+F · Q | 上/下一圈 · 最快圈 · 交换主/对比圈 | |
| 工作表 | 组件标题栏 | 拖拽移动(吸附)· ↑ ↓ 换序 · ✕ 移除 · 右下角拖拽同时改宽高 |
| 左上 | 下拉选组件类型 → 「+ 添加组件」;上面那排按钮一键换整张表 | |
| 左上(第二排) | 保存 · 另存为 · 新建 · 重命名 · 删除(点两次)· 导出 · 导入——直接改 worksheets/*.json,快照模式只提示 |
在浏览器控制台里可以用 i3pro.state / i3pro.zoomTo(...) 直接调试视图。
层号是仓库里现成的说法(TestStructureOfTheSplit 那组守卫用的就是它),不是文档里新造的:
| 层 | 职责 | 住哪 |
|---|---|---|
| L1 读取层 | 六种输入格式读成同一种场次:一个时间基、通道、单位、采样率 | ld · motec_csv · csvlog · txtlog · xlslog · canlog + dbc |
| L2 领域层 | 算的都在这里,基本是不依赖框架的纯函数 | derive · energy · laps · sections · maths · gpsfix · report · cursorstats · attitude · heatmap · histogram · spectrum · pairing · sidecar · timebase · cache |
| L3 载荷层 | 组装"要发出去的东西" | render(快照 payload)· tracewire(/trace 二进制载荷)· library(场次库 + 缓存)· api(动作)· server(HTTP)· indexpage · export · xlsx · store |
| 前端 | 手写 Canvas 的工作台 | web/viewer.html——一个文件、双击即开、无 CDN、无 npm(ADR-0002) |
三条硬边界,每条都有单测盯着:
- L1 不许 import L3。
csvlog当年为了三张"哪几类通道名"的表 import 了整个render—— 全包唯一一处反向依赖;表搬进channelpick之后两边都只 import 它 (test_读取层不再反向依赖载荷层)。 library不许 importapi/server。 那两个都 import 它,反过来就是环 (test_库不反向依赖服务)。好处是 HTTP 那一层可以不起 socket 直接调Api.handle(parts, query, method, body)验动作——"这个动作算什么"与"HTTP 怎么回"因此能分开测。server.py只收字节、发字节。 动作分派与请求体都在api.py(test_server_只剩_HTTP_管道:拆之前 1274 行,拆完 367 行,今天 419 行、守卫卡在 420)。
数据怎么流:
L1 解析 ──► L2 领域(切圈 / 区段 / 数学通道 / GPS / 报表 / 聚合)──► L3 组装
├──► render.py ──► 自包含 HTML 快照
└──► server.py + api.py ──► 浏览器工作台
侧车(信标 / 区段 / GPS / 注释 / 数学通道 / CSV 列映射)只有 sidecar.py 一个读写口;
指纹与磁盘缓存只有 cache.py 一处实现——"同一个概念只留一个答案"是这批守卫的共同口径,
四条回归也是为了钉它:解析正确性只认 MoTeC 自己导出的 CSV,交互只认真浏览器真鼠标。
下面这份不是全部(src/i3pro/ 现在 45 个 .py),是"你大概率会去改的那些":
启动.bat 一键起服务 + 开浏览器(给队友用这个)
安装依赖.bat 一键装四个运行期包(新机器上先跑这个)
导出快照.bat 一键导出所有离线 HTML 快照
导入数据.bat 拖文件上去拷进 i2pro_data/(同名永不覆盖)
i3pro.cmd 命令行入口(探测 Python + 查依赖),上面几个 bat 都调它
requirements.txt 运行期依赖清单(与 pyproject.toml 同一份,有单测盯着)
docs/ PLAN.md 规划 · ACCEPTANCE.md 验收清单 · ld-format.md 格式逆向记录
tools/ verify_ld_vs_csv.py 解析对照 · smoke_viewer.js 无头驱动前端
tests/ 692 项单测
maths/ 全局数学通道定义(global.json,跨场次复用)
out/ 生成物(快照 HTML / Parquet),已在 .gitignore 里
i2pro_data/ 试车数据(.ld/.ldx/.csv/.csv.gz/.txt/.tsv),不进仓库;只有 dbc/ 跟着仓库走
src/i3pro/
├─ ld.py .ld 原生解析(mmap,不复制数据)
├─ motec_csv.py i2 Pro CSV 导出读取(用于对照与兜底)
├─ csvlog.py CSV / 分隔文本读成场次:列名匹配 / 单位 / 报告(与 .ld 同一种形状)
├─ txtlog.py 分隔符与编码的嗅探、导入预览、按固定采样率生成时间列
├─ derive.py 速度源选择 / 距离轴积分 / GPS 局部投影
├─ energy.py 电池功率来源判定与累计 kWh(Drive / Regen / Net Energy)
├─ laps.py GPS 切圈 / 距离轴重叠 / Δ时间
├─ store.py Parquet + 元数据落盘、列式裁剪、SQL
├─ render.py 工作台 payload 构建 / 通道分组 / Min-Max 降采样 / 自包含 HTML
├─ maths.py 数学通道:白名单表达式编译求值 / 作用域 / 派生列缓存
├─ server.py 标准库 HTTP 服务(只收字节、发字节)
├─ indexpage.py 场次列表页 + 导入块(前 20 行预览那张卡片)
├─ api.py 路由与动作:每个请求的判断都在这一层
├─ sidecar.py 六种侧车文件(信标/区段/GPS/注释/数学通道/CSV 列映射)的唯一读写
├─ timebase.py 主时间基:一个场次的时间轴只有一处答案
├─ pairing.py CAN 帧表 ↔ .ld 对齐(墙钟挑候选 + 速度互相关 + 三条加速度轴的中位数复核)与通道合并
├─ stitch.py 接续:接连录下来的几场接成一场(耐久换车手那种)
├─ cli.py 命令行入口
└─ web/viewer.html 前端(手写 Canvas,无框架、无构建)
以下都是在这批真实日志上量出来的,不是猜测:
- MoTeC 的
Distance通道恒为 0,C125 的 beacon 输入没有接线,.ldx里Total Laps永远是 1。距离轴和圈次都得自己算—— 这正是 i3pro 存在的理由。 - 默认速度源是
Vx KF(记录仪自己的卡尔曼滤波纵向速度,100 Hz)。 轮速通道SpeedFR/FL/...在部分场次不存在,GPS Speed在部分场次恒为 0。 decimals是有符号 16 位,0xffff表示 −1(即 ×10)。 按无符号读会让Timestamp MTI之类通道整整差 100 倍。- CSV 导出会把慢通道重采样到 100 Hz 并去掉
(LoRes)后缀, 所以拿 CSV 校验时只能比对与导出采样率相同的通道。 - 格式细节与未解析部分见
docs/ld-format.md。
python -m unittest discover -s tests -v # 692 项,无数据文件时自动 skip
python tools\verify_ld_vs_csv.py # 与 i2 Pro CSV 逐通道对照
node tools\smoke_viewer.js out\demo.html # 无头驱动前端:缩放/光标/分组/信标编辑/数学通道/区段/报表/直方图/频谱/热力图/注释/GPS 校正/导出/工作表/CAN 报告/快速跳转/曲线连接/姿态等 935 项检查
python tools\verify_clicks.py # 真 Edge 发真鼠标/键盘:444 项交互验收(需要 Edge + 金标准数据)这四条里有三条会在缺东西时自动跳过,而跳过不算通过(AGENTS.md 第 7 条)。 想一次装齐:
| 要什么 | 谁需要它 | 装它 |
|---|---|---|
| Python 3.10+ 与那四个运行期包 | 全部 | 双击 安装依赖.bat,或 python -m pip install -r requirements.txt |
车队日志放 i2pro_data/(两份金标准 .ld + 它们的 i2 Pro CSV) |
单测的真实数据用例、verify_ld_vs_csv.py、verify_clicks.py |
车队数据不进仓库,自己放 |
| Node.js | tools\smoke_viewer.js(无头驱动前端) |
nodejs.org |
| Microsoft Edge | tools\verify_clicks.py(真鼠标真键盘) |
Windows 自带 |
cantools(测试期裁判) |
TestDbc / TestCanLog 跟手写的 dbc.py 逐信号对拍 |
python -m pip install -e ".[dev]" |
算法层(derive / laps / render)是不依赖框架的纯函数,改动请优先补单测——
这支车队最现实的风险是「写代码的人毕业了」。
双击 src\i3pro\web\viewer.html 一片空白 / 没有数据
那是模板文件,不含数据。双击 启动.bat,或者双击 out\<场次名>.html。
双击 启动.bat 提示 "Could not find a working Python 3"
装一个 Python 3.10+(python.org),安装时勾上
Add python.exe to PATH。注意本机如果装过 py 启动器但没有注册解释器,脚本会自动跳过它。
双击 启动.bat 提示 "the runtime packages are missing"
Python 有了,缺那四个包。i3pro.cmd 会把该敲的命令原样打出来(
<python> -m pip install -r requirements.txt),照着敲或者双击 安装依赖.bat 都行。
这条提示和上面"没有 Python"是两句不同的话:以前两种故障共用一句,缺 numpy 会被
报成"找不到 Python",害人去重装 Python。
安装依赖.bat 装到一半失败(UnicodeDecodeError / 下载超时)
中文 Windows 上 pip 用 cp936 解码 requirements.txt,所以那份清单必须全是
ASCII(有单测盯着);下载超时多半是代理,见下一条。想离线装:在能联网的机器上
pip download -r requirements.txt -d wheels,把 wheels/ 拷过去,然后
python -m pip install --no-index --find-links wheels -r requirements.txt。
端口 8731 被占用
启动脚本会自动往后找 10 个端口,控制台会打印实际用的那个。也可以手动指定
--port 9000。
git push 或 pip install 连不上外网(超时 / connection reset)
浏览器能开 GitHub、git 却不行,通常是系统走本地代理而 git 没走。查一下
HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings 里的 ProxyServer
(这台开发机是 127.0.0.1:7897),然后让 git 也用它:
git config --global http.proxy http://127.0.0.1:7897
git config --global https.proxy http://127.0.0.1:7897某个场次切不出圈:看 laps 的 complete 列。直线/单圈测试本来就只有 1 段,
首尾的进出场段会被标记成「进出场/泊车/异常段」,不计入最快圈统计。
通道值看起来差 100 倍:检查是不是把 decimals 当无符号读了(见上文第 3 条)。
GPL-3.0。格式逆向的原始工作来自 gotzl/ldparser,
本项目保留其源码在 vendor/ldparser/(含 GPL-3.0 全文)用于测试交叉验证,
运行期不依赖它。若希望以 MIT 发布,需要移除 vendor/ 与相关引用。