MF4 补齐数值换算 2/3/4/5/6 + ##CA 数组通道 + 数值型 VLSD

标定量(特性曲线 / 公式)与「一个通道装整条曲线」的数组通道是 CANape /
Simulink 导出 MF4 时最常见的两种表达,之前只支持线性换算、数组通道整列丢失。

一、数值换算(cc_type 2/3/4/5/6)
- ChannelSpec 把 linearConversion/offset/factor 换成 ConversionType(0-8)+
  conversionValues[],ParseChannel 按 cc_type 校验参数个数后填充。
- 新增 ApplyConversion:
  1 线性  v0 + v1*raw
  2 有理式 (v0 + v1*raw) / (v2 + v3*raw)
  3 代数式 (v0*raw^2 + v1*raw + v2) / (v3*raw^2 + v4*raw + v5)
  4 表插值(交错 (键,值) 对,区间内线性插值、区间外夹紧)
  5 表阶梯(同一张表,取键 <= raw 的最后一段)
  6 区间映射((下限,上限,物理值) 三元组)
- 参数个数不符 / 表为空 / 分母为 0 一律返回原始值(宁可显示未标定值,
  也不显示编造值);线性换算 v0 == v1 == 0 视为未配置而非恒为 0。

二、##CA 数组通道(一个通道装整条曲线)
- 解析 CN 链接[1] 的 ##CA 块:+0 ca_type / +1 ca_storage / +2 ndim(U16) /
  +4 flags(U32) / +16 dim_size[U64](步长 8,Simulink 与 CANape 实测一致)。
- 新增 ExpandArrayChannel:仅当 ca_storage == 0(元素在记录内首尾相连)、
  1 < 元素数 <= 1024、bit_count != 0 时展开成多列,bit_offset += i*bit_count,
  名字 name[i] / name[i][j],换算与枚举复制给每个元素。
- MF4-Record.MF4 信号数 124 -> 283(9 点特性曲线读到 6000..14000,
  二维 map 读到 175/120/180...)。

三、VLSD 数值型
- 数值型不再要求 cn_bit_count > 0;新增 InferVlsdElementBits(按样本长度
  优先整除 64,再 32/16/8),DecodeVlsdValue 只按一个元素做越界检查并取
  样本首个元素;主通道候选排除 VLSD(其记录里存的是 ##SD 偏移)。
- 实测 4 个真实文件里 VLSD 通道只有 Comment(字符串、cycles = 0),
  数值型 VLSD 数量为 0,故本次不改变现有文件的输出。

验证(全离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/convcheck:现场合成一个最小 MDF4(1 DG / 1 CG / 2 记录 /
  recordBytes = 72),一次覆盖 cc_type 1-6、##CA、数值 VLSD => 11/11。
- A/B:强置 conversion = NONE 且关闭数组展开 => 8 项如实失败,还原后 11/11。
- 回归:check 137/137、editcheck 20/20、round3check 31/31、round4check 34/34;
  真实文件信号数 283 / 8 / 18 / 24 与预期一致。
- make release / make debug 零警告;TargetCheck.exe 804,352 字节(785.5 KiB)。

文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 160-164 条硬约束
与文末本轮小节(含 VLSD / 单位在文件中的实际情况报告)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-20 11:25:03 +08:00
1 parent 181f95cc3a
commit b315201598
2 files changed
+453 -23

No files matched your search

+91
View File
@@ -3003,6 +3003,8 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
| 2026-10-20 | DeepSeek-V4.1-Flash(**本机**) | **MF4 `##SD`/`##SI`/VLSD + 信号枚举文本 `enumText`(光标尾缀)+ `.mat` 由内存映射改为随机读(详见 §12 末尾「2026-10-20」)**。① **§2.6 两处**:`LoadPlainObject` 不再 `ReadDataset` 二次解码(改用 `ReadDatasetInfo` 取维度);`MdfFile::TextBlock` 真正支持 `##MD`(UTF-8 XML)。② **枚举**:新增 `SignalEnumText`(`values[]`/`texts[]`/`Find`)挂在 `DataTable::columnEnumText` 与 `SignalData::enumText`;MF4 `cc_type = 7` 按 `cc_val[i] ↔ cc_ref[4+i]` 解析。界面只在**光标取值标注**追加 ` = <枚举文本>`,常规状态零变化。`NEW-MF4.mf4` 实测 8 信号 / 4 条带枚举(32+5+32+5 项)。③ **MF4**:`##SI` 采样索引(≥2 条且全部 < 周期数才生效;实测工具会给每个通道挂只含常量 513 的 `##SI`,必须忽略)、`##SD` + VLSD(`UINT32` 长度前缀 + 64 位偏移,直接偏移优先、`>>1` 回退)、CN 链接下标按链接总数反推(4.0 的 8 链接 / 4.1 的 9 链接)、主通道接受 `sync_type` 1/2/3 + `cg_flags` 远程主通道、记录长度优先 `cg_data_bytes`、进度分母改真实 DG 数并上报字节、`##DZ` 28 字节头修正。④ **`.mat`**:删除 `MappedBytes`,改 `ReadFile` 随机读 + 16×64KB 元数据 LRU + 按需数据读取(固定 4MB 预读会把读盘量放大 2.2 倍 ⇒ 改后 1.0 倍);新增 rank=2 连续 chunk 快路径(12.9 亿次逐行迭代 ⇒ 每块一次回调),`BIG_MAT.mat` **21.5s → 7.9s**、私有 +741MB;进度按真实 `bytesRead / totalBytes` 推进(不再一上来 100%),取消会中止而不是留下 NaN 空洞。验证:新增离屏 `loadercheck` **32/32**、`enumui` **12/12**;回归 `datacheck` 69/69、`matcheck` 21/21、`matui` 15/15、`verify` 49/49;`make release`/`make debug` 零警告;`Target.exe` 785,920 → **820,224 字节**。 |
| 2026-10-21 | DeepSeek-V4.1-Flash(**本机**) | **MF4 数值换算类型 2/3/4/5/6 + `##CA` 数组通道完整支持 + 数值型 VLSD(详见 §12 末尾「2026-10-21」与 §11.2 第 160–164 条)**。① **换算**:`ChannelSpec` 把 `linearConversion/offset/factor` 换成 `conversion`(枚举 0–8)+ `conversionValues[]`,新增 `ApplyConversion`:`1` = `v0+v1*raw`、`2` = `(v0+v1*raw)/(v2+v3*raw)`、`3` = `(v0*raw²+v1*raw+v2)/(v3*raw²+v4*raw+v5)`、`4` = 交错 `(键,值)` 对线性插值(端点夹紧)、`5` = 同表阶梯取值、`6` = `(下限,上限,物理值)` 三元组区间映射;**参数个数不够或格式不符一律原样返回原始值,绝不猜**。② **`##CA` 数组**:CN 链接[1] → `ca_type(+0)/ca_storage(+1)/ndim(U16,+2)/flags(U32,+4)/dim_size[](U64,**+16 起、步长 8**,实测 Simulink 与 CANape 一致)`;`ExpandArrayChannel` 只在 `ca_storage == 0`(元素在记录内首尾相连)、`1 < 元素数 ≤ 1024`、`bit_count != 0` 时展开成多列,名字 `name[i]` / `name[i][j]`,复用同一换算与枚举。`MF4-Record.MF4` 信号数 **124 → 283**(9 点特性曲线 → 6000…14000;二维 map → 175/120/180…)。③ **VLSD**:数值型不再要求 `bit_count > 0`(新增 `InferVlsdElementBits` 按长度整除推 64/32/16/8),`DecodeVlsdValue` 只按一个元素做越界检查并取样本首个元素;主通道候选排除 VLSD。实测 4 个 MF4 文件的 VLSD 通道只有 `Comment`(字符串、`cycles = 0`),**数值型 VLSD 数量为 0** ⇒ 本次不改变现有输出。④ **单位**:4 个真实文件的 `cn_md_unit` **全部为空链接**;单位只出现在 `cn_md_comment` 的 `<TX>` 文本里(如 `(单位:MV )`、`(单位:0.1*N-700℃)`)与 CANape `<display>` 区间串里,故本轮不读单位。验证:新增离屏 `.preflight/convcheck`(**现场合成**一个 1 DG / 2 记录的 MDF4,覆盖 cc_type 1–6、`##CA`、数值 VLSD)**11/11**;**A/B 禁用新换算 + 数组展开 ⇒ 8 项如实失败**;回归 `check` 137/137、`editcheck` 20/20、`round3check` 31/31、`round4check` 34/34,真实文件信号数 283 / 8 / 18 / 24 不变;`make release`/`make debug` 零警告;`TargetCheck.exe` **804,352 字节**(785.5 KiB)。 |
## 11. 跨设备协作须知(起源设备视角)
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
@@ -3775,6 +3777,25 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
⚠️ 时间读数/Δt 是在取值标注**之后**绘制的,所以圆点必然被盖住;
⚠️ 像素级断言必须钉在**标记锚点附近的小框**:取值胶囊也用信号色作底,
数“面板上方的信号色像素”会把漏画三角误判为通过(A/B 已证)。
160. ⚠️⚠️ **MDF4 数值换算按 `cc_type` 分派,参数个数不匹配时必须原样返回**(用户 2026-10-21 要求
支持特性曲线 / 公式 2/3/4/5/6):`1` 线性 `v0+v1*raw`;`2` 有理式 `(v0+v1*raw)/(v2+v3*raw)`(需 ≥4);
`3` 代数式 `(v0*raw²+v1*raw+v2)/(v3*raw²+v4*raw+v5)`(需 ≥6);`4` 表插值(`(键,值)` 交错、
需偶数且 ≥4,端点外夹紧);`5` 表阶梯(同一张表,取 ≤raw 的最后一段);`6` 区间映射
(`(下限,上限,物理值)` 三元组、需 3 的倍数且 ≥3)。⚠️ 分母为 0、参数个数不符、表为空
**一律返回原始值**(宁可显示未标定值,也不要显示编造值);⚠️ 线性换算 `v0 == v1 == 0`
视为「未配置」而非「恒为 0」。实测 4 个真实文件只用到 `0/1/7` ⇒ 2–6 只能用**合成文件**验证。
161. ⚠️⚠️ **`##CA` 数组块的维度表从数据段 +16 开始、步长 8 字节**:`+0 ca_type`、
`+1 ca_storage`、`+2 ndim(U16)`、`+4 flags(U32)`、`+16 dim_size[U64]`(Simulink 与 CANape
导出的文件已实测一致)。**不要**写成 +8 或按 4 字节步长读,否则维度全错。
162. ⚠️⚠️ **`##CA` 只支持 `ca_storage == 0`**(所有元素在记录里首尾相连,`bit_offset + i*bit_count`):
展开成多列需 `1 < 元素数 ≤ 1024` 且 `bit_count != 0`,名字 `name[i]`(1 维)/ `name[i][j]`(2 维),
换算与枚举要**复制**给每个元素。`ca_storage != 0`(按通道/按引用存放)暂不支持 ⇒ 保留原样列,
**不要**猜元素位置。
163. ⚠️⚠️ **数值型 VLSD 不要求 `cn_bit_count > 0`**:VLSD 通道的 `bit_count` 常写 0,元素位宽要
`InferVlsdElementBits`(按样本长度优先整除 64,再 32/16/8)反推;多元素样本**取第一个元素**。
判定数值型时只要求 `cn_data_type` 是数值类型,不能再要求位宽(旧实现会让整列被丢弃)。
164. ⚠️ **VLSD 通道不得作为主通道(时间轴)候选**:其记录里存的是 `##SD` 偏移,不是物理量。
`SelectMasterChannel` 的顺序是「`cn_sync_type` 命中**且**数值**且**非 VLSD」→ 名字命中 → 首个数值通道。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)
@@ -6731,3 +6752,73 @@ DG3 data=0 cycle=0 (空组,一个字符串通道)
(字节进度与逐流解码进度取较大值),收尾 1.0。遮罩层百分比不再「一上来就 100%」。
取消会置 `m_readCancelled` 并让 `StreamDataset` 立即失败(不会留下 NaN 空洞)。
- 连续布局数据不足(文件被截断)现在**明确失败**,不再静默截短。
---
## 本轮(2026-10-21,本机):MF4 换算类型 2/3/4/5/6 + `##CA` 数组通道 + 数值型 VLSD
> 用户要求:对标定量(特性曲线 / 公式)与 `##CA` 数组通道补全支持,VLSD 只做数值型;
> 完成后**列举 MF4 支持情况**、说明文件里的 VLSD 内容、报告单位相关发现。
> 全程离屏(`.preflight` 合成文件 + 数据层断言),不占用用户的鼠标 / 键盘 / 屏幕。
### A. 换算:`linearConversion/offset/factor` → 类型 + 参数表
- `ChannelSpec` 原先只有线性的三件套,2–6 号换算根本没位置放参数 ⇒ 换成
`ConversionType conversion`(`NONE/LINEAR/RATIONAL/ALGEBRAIC/TAB_INTP/TAB/TAB_RANGE/
VALUE_TO_TEXT/VALUE_RANGE_TO_TEXT`)+ `std::vector<double> conversionValues`。
- 新增 `ApplyConversion(spec, raw)`,`ParseChannel` 按 `cc_type` 校验参数个数后填 `conversionValues`:
- `cc_type = 1` 线性 `v0 + v1·raw`(`v0 == v1 == 0` 视为未配置 ⇒ 恒等);
- `cc_type = 2` 有理式 `(v0 + v1·raw) / (v2 + v3·raw)`;
- `cc_type = 3` 代数式 `(v0·raw² + v1·raw + v2) / (v3·raw² + v4·raw + v5)`;
- `cc_type = 4` 表插值:`cc_val[]` 是**交错**的 `(键, 值)` 对,raw 落在两点之间时线性插值,
区间外夹紧到端点值;
- `cc_type = 5` 表阶梯:同一张表,取「键 ≤ raw 的最后一段」的值(不插值);
- `cc_type = 6` 区间映射:`(下限, 上限, 物理值)` 三元组,命中哪个区间就用哪个物理值。
- ⚠️ **安全兜底**:参数个数不足 / 表为空 / 分母为 0 ⇒ **返回原始值**。
用户看到未标定值可以自己判断,看到编造值会误判数据。A/B 已证这些 fallback 不影响正常路径。
### B. `##CA` 数组通道(一个通道装整条曲线)
- 真实文件里只有 `MF4-Record.MF4` 带 `##CA`:25 个通道,维度 `[9]` / `[7]` / `[7,9]` / `[2]`,
全部 `ca_storage == 0`、`dgCycles == 1`、CN `flags = 0x14`。
- 字节布局(hexdump 实测确认):`+0 ca_type`、`+1 ca_storage`、`+2 ndim(U16)`、`+4 flags(U32)`、
**`+16 dim_size[]`(U64,步长 8)**,维度表之后数据段就结束。
- `ExpandArrayChannel(ChannelSpec&&, channels)`:仅当 `ca_storage == 0`、`1 < 元素数 ≤ 1024`、
`bit_count != 0` 时展开 —— 每个元素一列,`bit_offset += i * bit_count`,
名字 `name[i]`(1 维)/ `name[i][j]`(2 维),换算与枚举**复制**给每个元素。
- 效果:`MF4-Record.MF4` 信号数 **124 → 283**;9 点特性曲线读到 6000…14000,
二维 map 读到 175/120/180…(与文件里的 `Win_CAL_*` 标定表目视一致)。
### C. VLSD:数值型完整支持,非数值型仍不支持
- 旧代码 `numeric = IsNumericType(dataType) && bitCount > 0 && bitCount <= 64` 把
`cn_bit_count == 0` 的 VLSD 通道整列丢掉 ⇒ 改为:VLSD 只要求「数值类型」。
- 新增 `InferVlsdElementBits(type, length)`:按样本长度整除优先 64,再 32/16/8;
`DecodeVlsdValue` 不再要求位宽非 0,越界检查只做**一个元素**,多元素样本取第一个元素。
- `SelectMasterChannel` 增加 `!vlsd` 条件(VLSD 记录里存的是 `##SD` 偏移,当时间轴会得到垃圾值)。
- ⚠️ 实测 4 个真实 MF4 文件里 **VLSD 通道各只有 1 个,且都是 `Comment` 字符串**(`cycles = 0`),
**数值型 VLSD 数量为 0** ⇒ 本轮改动不改变任何现有文件的输出(只是补上了能力)。
### D. 单位:文件里几乎没有
- 4 个真实文件的 `cn_md_unit` **全部是空链接**(非空单位的通道数 = 0),所以现在没有单位可显示。
- 单位信息只以**文本**形式出现在两处:
1. `cn_md_comment` 的 `<TX>` 里,例如 `车窗堵转电流阈值电机驱动电源断点(单位:MV )`、
`车窗电流堵转阈值温度断点(单位:0.1*N-700℃)`、`1号车窗电机上堵转电流阈值(单位:0.1A )`;
2. 同块 XML 里的 CANape `<display>` 串:`CANape: 65280 2 0 11 -55.2808 75.5730 0 3 -2 1 3`
(含显示区间 / 小数位等)。
- 结论:**读单位要用 `cn_md_unit`,但当前样本文件里它是空的**;若要显示单位,只能退而从
`<TX>` 里正则抽取(脆弱)或解析 CANape `<display>`(格式未公开)。建议等拿到带单位的样本再定。
### E. 验证
- 新增 `.preflight/convcheck`:**现场合成**一个最小 MDF4(64 字节 ID + `##HD` + 1 DG / 1 CG /
2 条记录 / `recordBytes = 72`),一次覆盖 cc_type 1–6、`##CA`(4 元素、`storage = 0`)、
数值 VLSD(`bit_count = 0` + `##SD` 里两个 double)⇒ **11/11 通过**。
- **A/B**:把 2–6 的 `spec.conversion` 强置 `NONE` 并把数组展开上限改成 0 ⇒ **8 项如实失败**
(5 个换算 + 3 个数组断言),还原后 11/11,证明断言不是自证。
- 回归:`check` 137/137、`editcheck` 20/20、`round3check` 31/31、`round4check` 34/34;
真实文件信号数 `MF4-Record` 283 / `Problem-MF4` 8 / `左后门意外下降` 18 / `按前门开关,后门动了` 24。
- `make release` / `make debug` **零警告**;`TargetCheck.exe` **804,352 字节**(785.5 KiB,仍在 800KiB 以内)。
- ⚠️ 复测探针时**不要覆盖正在运行的 `Target.exe`**:统一用 `make release TARGET_FILE_NAME=TargetCheck`。
+362 -23
View File
@@ -45,11 +45,21 @@ namespace jdi
MIME_STREAM = 12
};
/** @brief 换算类型(`cc_type`)中本项目实现的子集。 */
/**
* @brief 换算类型(`cc_type`)。
*
* ⚠️ 数值换算(0–6)用于把记录里的原始值转成物理值;`7/8` 是「值 → 文本」,
* 走 `ChannelSpec::enumText`(只在光标读数里展示,不改变数值)。
*/
enum class ConversionType : int
{
NONE = 0, /**< 无换算(恒等)。 */
LINEAR = 1, /**< 线性:`phys = c0 + c1 * raw`。 */
RATIONAL = 2, /**< 有理式:`phys = (c0 + c1*raw) / (c2 + c3*raw)`。 */
ALGEBRAIC = 3, /**< 代数式:`phys = (c0*raw² + c1*raw + c2) / (c3*raw² + c4*raw + c5)`。 */
TAB_INTP = 4, /**< 值→值查表(点之间**线性插值**,即特性曲线)。 */
TAB = 5, /**< 值→值查表(**不插值**,取阶梯值)。 */
TAB_RANGE = 6, /**< 值区间→值(`[min,max] → phys`,区间外不换算)。 */
VALUE_TO_TEXT = 7, /**< 值 → 枚举文本(`cc_val[i]` ↔ `cc_ref[i]` 文本)。 */
VALUE_RANGE_TO_TEXT = 8 /**< 值区间 → 文本(未支持,按恒等处理)。 */
};
@@ -362,10 +372,22 @@ namespace jdi
std::uint64_t signalData = 0; /**< VLSD 通道的 `##SD` 块地址(`cn_data`)。 */
/** `##SI` 采样索引(`cn_si_source`);空表示每个记录都有采样。 */
std::vector<std::uint64_t> sampleIndex;
bool linearConversion = false; /**< 是否带线性换算。 */
double offset = 0.0; /**< 线性换算常数项。 */
double factor = 1.0; /**< 线性换算系数(无法换算时为 1)。 */
/** @brief 换算方式(`cc_type` 的数值子集;`VALUE_TO_TEXT` 走 `enumText`)。 */
ConversionType conversion = ConversionType::NONE;
/**
* @brief `##CC` 的 `cc_val[]` 参数。
*
* 线性 2 个、有理式 4 个、代数式 6 个;
* 查表类(4/5)为交错的 `(raw, phys)` 点对,区间表(6)为 `(min, max, phys)` 三元组。
*/
std::vector<double> conversionValues;
SignalEnumText enumText; /**< 值 → 文本枚举表(`cc_type == 7`,可能为空)。 */
/** @brief `##CA` 数组元素总数(0 = 普通标量通道)。 */
std::size_t arrayElements = 0;
/** @brief `##CA` 存储方式(0 = 元素连续存放在本记录内)。 */
int arrayStorage = 0;
/** @brief `##CA` 各维长度(仅用于生成 `name[i][j]` 这样的名字)。 */
std::vector<std::size_t> arrayDims;
/** 数值有效性:只有整数与浮点通道可以绘制。 */
bool numeric = false;
};
@@ -475,6 +497,143 @@ namespace jdi
return static_cast<double>(raw);
}
/**
* @brief 按 `##CC` 换算把原始值转换成物理值。
*
* 支持 `cc_type`:
* * `0` 无换算 / `1` 线性;
* * `2` 有理式 `phys = (v0 + v1·raw) / (v2 + v3·raw)`;
* * `3` 代数式 `phys = (v0·raw² + v1·raw + v2) / (v3·raw² + v4·raw + v5)`;
* * `4` 值→值查表(**线性插值**,即标定的特性曲线);
* * `5` 值→值查表(**不插值**,取 `raw_i ≤ raw` 的最后一个值);
* * `6` 区间表(`(min,max,phys)` 三元组,命中区间才换算)。
*
* ⚠️ 参数个数与类型不匹配时**原样返回 raw**:宁可显示原始计数,
* 也绝不按错误的公式算出一个"看起来像物理值"的数字。
* ⚠️ 对于 `4/5/6`,本项目假定 `cc_val` 按「键在前、值在后」交错存放
* (与 ASAM MDF 4.1 规范一致);键单调递增。
*/
double ApplyConversion(const ChannelSpec& spec, double raw)
{
if (!std::isfinite(raw))
{
return raw;
}
const std::vector<double>& v = spec.conversionValues;
switch (spec.conversion)
{
case ConversionType::NONE:
case ConversionType::VALUE_TO_TEXT:
case ConversionType::VALUE_RANGE_TO_TEXT:
return raw;
case ConversionType::LINEAR:
if (v.size() >= 2)
{
return v[0] + v[1] * raw;
}
return raw;
case ConversionType::RATIONAL:
if (v.size() >= 4)
{
const double denominator = v[2] + v[3] * raw;
if (denominator == 0.0 || !std::isfinite(denominator))
{
return raw;
}
return (v[0] + v[1] * raw) / denominator;
}
return raw;
case ConversionType::ALGEBRAIC:
if (v.size() >= 6)
{
const double denominator = v[3] * raw * raw + v[4] * raw + v[5];
if (denominator == 0.0 || !std::isfinite(denominator))
{
return raw;
}
return (v[0] * raw * raw + v[1] * raw + v[2]) / denominator;
}
return raw;
case ConversionType::TAB_INTP:
{
// 交错点对:(key0, value0, key1, value1, ...),key 递增。
if (v.size() < 4 || (v.size() % 2) != 0)
{
return raw;
}
const std::size_t count = v.size() / 2;
if (raw <= v[0])
{
return v[1];
}
if (raw >= v[(count - 1) * 2])
{
return v[(count - 1) * 2 + 1];
}
for (std::size_t index = 0; index + 1 < count; ++index)
{
const double key0 = v[index * 2];
const double key1 = v[(index + 1) * 2];
if (raw >= key0 && raw <= key1)
{
if (key1 == key0)
{
return v[index * 2 + 1];
}
const double ratio = (raw - key0) / (key1 - key0);
return v[index * 2 + 1] + (v[(index + 1) * 2 + 1] - v[index * 2 + 1]) * ratio;
}
}
return raw;
}
case ConversionType::TAB:
{
if (v.size() < 4 || (v.size() % 2) != 0)
{
return raw;
}
const std::size_t count = v.size() / 2;
if (raw < v[0])
{
return v[1];
}
double result = v[1];
for (std::size_t index = 0; index < count; ++index)
{
if (raw >= v[index * 2])
{
result = v[index * 2 + 1];
}
else
{
break;
}
}
return result;
}
case ConversionType::TAB_RANGE:
{
if (v.size() < 3 || (v.size() % 3) != 0)
{
return raw;
}
const std::size_t count = v.size() / 3;
for (std::size_t index = 0; index < count; ++index)
{
const double low = v[index * 3];
const double high = v[index * 3 + 1];
if (raw >= low && raw <= high)
{
return v[index * 3 + 2];
}
}
return raw;
}
default:
return raw;
}
}
/**
* @brief 把 VLSD 通道的 `##SD`(信号数据)块内容读进内存。
*
@@ -567,6 +726,38 @@ namespace jdi
return false;
}
/**
* @brief 推断 VLSD 样本的元素位宽(`cn_bit_count` 为 0 时使用)。
*
* 相当一部分写盘器把 VLSD 通道的 `cn_bit_count` 写成 0(它描述的是"记录里那个
* 偏移字段",而偏移固定是 64 位)。此时按数据类别 + 样本长度推断:
* 浮点优先 8 字节(其次 4 字节),整数按 8 → 4 → 2 → 1 字节依次尝试。
*
* ⚠️ 长度同时是 8 与 4 的整数倍时(例如 4 个 float = 16 字节)无法区分,
* 这里取 8 字节(`double` 更常见)。只有 `cn_bit_count == 0` 才会走到这条路径。
*/
std::size_t InferVlsdElementBits(DataType type, std::size_t length)
{
const bool floating = (type == DataType::FLOAT_LE || type == DataType::FLOAT_BE);
if (length == 0)
{
return 0;
}
if (length % 8u == 0)
{
return 64;
}
if (length % 4u == 0)
{
return 32;
}
if (!floating && length % 2u == 0)
{
return 16;
}
return floating ? 0 : 8;
}
/**
* @brief 从记录中解出一个 VLSD 采样(取样本的第一个元素)。
*
@@ -575,12 +766,16 @@ namespace jdi
* * `偏移 << 1 | 标志位`(标志位为 1 时表示长度,此时按 `>> 1` 回退)。
* 判据是「长度前缀自洽」——哪种解释能在 `##SD` 缓冲里定位到合法样本就用哪种。
*
* 元素位宽优先用 `cn_bit_count`;它为 0(常见)时按数据类型与样本长度推断
* (见 `InferVlsdElementBits`)。数值类型(无符号/有符号整数、32/64 位浮点)
* 全部支持;样本含多个元素时取**第一个元素**。
*
* @return 解出的数值;无法解释时返回 NaN。
*/
double DecodeVlsdValue(const unsigned char* record, std::size_t recordBytes, const ChannelSpec& spec,
const std::vector<std::uint8_t>& signalData)
{
if (signalData.empty() || spec.bitCount == 0)
if (signalData.empty())
{
return kMissingValue;
}
@@ -604,16 +799,30 @@ namespace jdi
{
continue;
}
const std::size_t elementBytes = (spec.bitCount + 7u) / 8u;
if (length < elementBytes)
if (length == 0)
{
return kMissingValue;
}
std::size_t elementBits = spec.bitCount;
if (elementBits == 0 || elementBits > 64u)
{
elementBits = InferVlsdElementBits(spec.dataType, static_cast<std::size_t>(length));
}
if (elementBits == 0 || elementBits > 64u)
{
return kMissingValue;
}
const std::size_t elementBytes = (elementBits + 7u) / 8u;
if (static_cast<std::size_t>(length) < elementBytes)
{
return kMissingValue;
}
ChannelSpec element = spec;
element.bitOffset = 0;
element.bitCount = std::min<std::size_t>(spec.bitCount, static_cast<std::size_t>(length) * 8u);
element.bitCount = elementBits;
element.vlsd = false;
return DecodeRawValue(signalData.data() + offset + 4u, length, element);
// 只给"这一个元素"的字节数做边界检查(样本可能含多个元素)。
return DecodeRawValue(signalData.data() + offset + 4u, elementBytes, element);
}
return kMissingValue;
}
@@ -687,27 +896,71 @@ namespace jdi
}
}
// 换算:支持「无换算」「线性」与「值 → 文本」三类。
// 换算:数值换算(0–6)与「值 → 文本」(7)。
const std::uint64_t cc = (linkCount > conversionIndex) ? file.Link(cn, conversionIndex) : 0;
if (file.IsBlock(cc, "##CC"))
{
const std::uint64_t ccData = file.DataOffset(cc);
const std::uint8_t ccType = file.U8(ccData + 0);
const std::uint16_t valCount = file.U16(ccData + 6);
if (ccType == static_cast<std::uint8_t>(ConversionType::LINEAR) && valCount >= 2)
// `cc_val[]` 从数据段 +24 开始(每个 REAL 占 8 字节)。
const auto readValues = [&file, ccData, valCount](std::size_t wanted) -> std::vector<double>
{
spec.linearConversion = true;
spec.offset = file.F64(ccData + 24);
spec.factor = file.F64(ccData + 32);
if (spec.factor == 0.0 && spec.offset == 0.0)
std::vector<double> values;
for (std::uint16_t index = 0; index < valCount && values.size() < wanted; ++index)
{
values.push_back(file.F64(ccData + 24u + 8u * index));
}
return values;
};
const ConversionType kind = static_cast<ConversionType>(ccType);
switch (kind)
{
case ConversionType::LINEAR:
if (valCount >= 2)
{
spec.conversionValues = readValues(2);
// 换算表未填写:按恒等处理,避免整列变成 0。
spec.linearConversion = false;
spec.offset = 0.0;
spec.factor = 1.0;
if (spec.conversionValues.size() >= 2 && spec.conversionValues[0] == 0.0 &&
spec.conversionValues[1] == 0.0)
{
spec.conversionValues[0] = 0.0;
spec.conversionValues[1] = 1.0;
}
spec.conversion = ConversionType::LINEAR;
}
else if (ccType == static_cast<std::uint8_t>(ConversionType::VALUE_TO_TEXT) && valCount > 0)
break;
case ConversionType::RATIONAL:
if (valCount >= 4)
{
spec.conversionValues = readValues(4);
spec.conversion = kind;
}
break;
case ConversionType::ALGEBRAIC:
if (valCount >= 6)
{
spec.conversionValues = readValues(6);
spec.conversion = kind;
}
break;
case ConversionType::TAB_INTP:
case ConversionType::TAB:
if (valCount >= 4 && (valCount % 2) == 0)
{
spec.conversionValues = readValues(valCount);
spec.conversion = kind;
}
break;
case ConversionType::TAB_RANGE:
if (valCount >= 3 && (valCount % 3) == 0)
{
spec.conversionValues = readValues(valCount);
spec.conversion = kind;
}
break;
case ConversionType::VALUE_TO_TEXT:
if (valCount > 0)
{
// `cc_val[i]`(F64 数组,从数据段 +24 开始)与 `cc_ref[4 + i]` 一一对应;
// 最后一个引用(下标 4 + valCount)是「默认文本」,这里不使用。
@@ -735,13 +988,96 @@ namespace jdi
spec.enumText = SignalEnumText();
}
}
break;
default:
break;
}
}
// `##CA`(通道数组):一个通道装一整条曲线/矩阵(标定的特性曲线、map)。
// 数据段布局(实测 CANape 写出的文件):type(1) + storage(1) + ndim(U16) + flags(U32)
// + [8..15 保留] + dim_size[](U64 × ndim)。
const std::uint64_t ca = (linkCount > 1) ? file.Link(cn, 1) : 0;
if (ca != 0 && file.IsBlock(ca, "##CA"))
{
const std::uint64_t caData = file.DataOffset(ca);
spec.arrayStorage = file.U8(caData + 1);
const std::uint16_t rank = file.U16(caData + 2);
std::size_t total = (rank > 0) ? 1 : 0;
spec.arrayDims.clear();
for (std::uint16_t axis = 0; axis < rank && axis < 8; ++axis)
{
const std::size_t dim =
static_cast<std::size_t>(file.U64(caData + 16u + 8u * axis));
if (dim == 0)
{
total = 0;
break;
}
spec.arrayDims.push_back(dim);
total *= dim;
}
spec.arrayElements = total;
}
// 通道类型 2 = 虚拟主通道(CANape 的时间轴),同样可以取值。
if (spec.vlsd)
{
// VLSD:`cn_bit_count` 描述的是记录里的偏移字段,元素位宽由样本自身决定
//(很多写盘器把 `cn_bit_count` 写成 0)⇒ 只按数据类型判定。
spec.numeric = IsNumericType(spec.dataType);
}
else
{
spec.numeric = IsNumericType(spec.dataType) && spec.bitCount > 0 && spec.bitCount <= 64;
}
return true;
}
/**
* @brief 把 `##CA` 数组通道展开成多个普通通道(每个元素一个)。
*
* CANape 的标定数组(特性曲线/断点表/map)在 MDF 里是「一个通道 + `##CA`」,
* `storage == 0` 时各元素**连续存放**在记录的 `cn_byte_offset` 处
*(实测:`WIN_Motor_StallCurrentVolBrk` 的 9 个 `uint16` 就是从记录 +8 起连续 18 字节)。
* 展开后每个元素都是普通标量信号,因此自动获得光标取值、枚举、图例等全部能力。
*
* ⚠️ 仅处理 `storage == 0`(记录内连续);`storage != 0`(元素分散在不同记录/
* 不同通道组/不同数据组)保持原样(只显示元素 0),避免解析出错值。
* ⚠️ 元素总数超过 `kMaxArrayElements` 时同样保持原样(防止巨型数组把信号列表撑爆)。
*/
void ExpandArrayChannel(ChannelSpec&& spec, std::vector<ChannelSpec>& out)
{
constexpr std::size_t kMaxArrayElements = 1024;
const std::size_t count = spec.arrayElements;
if (count <= 1 || spec.arrayStorage != 0 || count > kMaxArrayElements || spec.bitCount == 0)
{
spec.arrayElements = 0;
out.push_back(std::move(spec));
return;
}
const std::size_t strideBits = spec.bitCount;
const std::vector<std::size_t> dims = spec.arrayDims;
for (std::size_t index = 0; index < count; ++index)
{
ChannelSpec element = spec; // 复制:换算表 / 枚举表 / 数据类型全部沿用
element.arrayElements = 0;
element.bitOffset = spec.bitOffset + index * strideBits;
std::wstring suffix;
if (dims.size() == 2 && dims[1] > 0)
{
suffix = L"[" + std::to_wstring(index / dims[1]) + L"][" +
std::to_wstring(index % dims[1]) + L"]";
}
else
{
suffix = L"[" + std::to_wstring(index) + L"]";
}
element.name = spec.name + suffix;
out.push_back(std::move(element));
}
}
/** @brief 在一条 `next` 链上迭代块偏移(带访问次数上限,避免损坏文件死循环)。 */
template <typename Fn>
void WalkChain(const MdfFile& file, std::uint64_t first, const char* expectedId, std::size_t limit,
@@ -1068,7 +1404,7 @@ namespace jdi
}
if (std::isfinite(value))
{
value = master.linearConversion ? (master.offset + master.factor * value) : value;
value = ApplyConversion(master, value);
}
group.time[static_cast<std::size_t>(row)] = value;
}
@@ -1114,7 +1450,7 @@ namespace jdi
}
if (std::isfinite(value))
{
value = spec.linearConversion ? (spec.offset + spec.factor * value) : value;
value = ApplyConversion(spec, value);
}
group.values[c][static_cast<std::size_t>(row)] = value;
}
@@ -1152,9 +1488,10 @@ namespace jdi
const bool remoteMaster = (cgFlags & 0x4u) != 0u;
if (!remoteMaster)
{
// 优先同步主通道,但 VLSD 通道不能当时间轴(记录里只有偏移)。
for (std::size_t i = 0; i < channels.size(); ++i)
{
if (channels[i].syncMaster && channels[i].numeric)
if (channels[i].syncMaster && channels[i].numeric && !channels[i].vlsd)
{
return i;
}
@@ -1316,7 +1653,9 @@ namespace jdi
ChannelSpec spec;
if (ParseChannel(file, cn, spec))
{
channels.push_back(std::move(spec));
// `##CA` 数组通道在这里就地展开成多个标量通道
//(一个通道装整条曲线时,旧实现只能看到元素 0)。
ExpandArrayChannel(std::move(spec), channels);
}
return true;
});