diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index a0f326e..3e04a27 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -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` 的 `` 文本里(如 `(单位:MV )`、`(单位:0.1*N-700℃)`)与 CANape `` 区间串里,故本轮不读单位。验证:新增离屏 `.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 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` 的 `` 里,例如 `车窗堵转电流阈值电机驱动电源断点(单位:MV )`、 + `车窗电流堵转阈值温度断点(单位:0.1*N-700℃)`、`1号车窗电机上堵转电流阈值(单位:0.1A )`; + 2. 同块 XML 里的 CANape `` 串:`CANape: 65280 2 0 11 -55.2808 75.5730 0 3 -2 1 3` + (含显示区间 / 小数位等)。 +- 结论:**读单位要用 `cn_md_unit`,但当前样本文件里它是空的**;若要显示单位,只能退而从 + `` 里正则抽取(脆弱)或解析 CANape ``(格式未公开)。建议等拿到带单位的样本再定。 + +### 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`。 + diff --git a/Utilities/Src/Mf4Loader.cpp b/Utilities/Src/Mf4Loader.cpp index 2d084b7..1d99b90 100644 --- a/Utilities/Src/Mf4Loader.cpp +++ b/Utilities/Src/Mf4Loader.cpp @@ -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 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 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 arrayDims; /** 数值有效性:只有整数与浮点通道可以绘制。 */ bool numeric = false; }; @@ -475,6 +497,143 @@ namespace jdi return static_cast(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& 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& 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(length)); + } + if (elementBits == 0 || elementBits > 64u) + { + return kMissingValue; + } + const std::size_t elementBytes = (elementBits + 7u) / 8u; + if (static_cast(length) < elementBytes) { return kMissingValue; } ChannelSpec element = spec; element.bitOffset = 0; - element.bitCount = std::min(spec.bitCount, static_cast(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,61 +896,188 @@ 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(ConversionType::LINEAR) && valCount >= 2) + // `cc_val[]` 从数据段 +24 开始(每个 REAL 占 8 字节)。 + const auto readValues = [&file, ccData, valCount](std::size_t wanted) -> std::vector { - 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 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(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; - } - } - else if (ccType == static_cast(ConversionType::VALUE_TO_TEXT) && valCount > 0) - { - // `cc_val[i]`(F64 数组,从数据段 +24 开始)与 `cc_ref[4 + i]` 一一对应; - // 最后一个引用(下标 4 + valCount)是「默认文本」,这里不使用。 - const std::uint64_t ccLinkCount = file.LinkCount(cc); - spec.enumText.values.reserve(valCount); - spec.enumText.texts.reserve(valCount); - for (std::uint16_t index = 0; index < valCount; ++index) - { - const std::uint64_t textLink = 4u + index; - if (textLink >= ccLinkCount) + if (spec.conversionValues.size() >= 2 && spec.conversionValues[0] == 0.0 && + spec.conversionValues[1] == 0.0) { - break; + spec.conversionValues[0] = 0.0; + spec.conversionValues[1] = 1.0; } - std::wstring text = file.TextBlock(file.Link(cc, textLink)); - if (text.empty()) - { - continue; // 没有文本的取值不进入枚举表 - } - spec.enumText.values.push_back(file.F64(ccData + 24u + 8u * index)); - spec.enumText.texts.push_back(std::move(text)); + spec.conversion = ConversionType::LINEAR; } - if (spec.enumText.texts.size() != spec.enumText.values.size() || - spec.enumText.texts.empty()) + break; + case ConversionType::RATIONAL: + if (valCount >= 4) { - spec.enumText = SignalEnumText(); + 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)是「默认文本」,这里不使用。 + const std::uint64_t ccLinkCount = file.LinkCount(cc); + spec.enumText.values.reserve(valCount); + spec.enumText.texts.reserve(valCount); + for (std::uint16_t index = 0; index < valCount; ++index) + { + const std::uint64_t textLink = 4u + index; + if (textLink >= ccLinkCount) + { + break; + } + std::wstring text = file.TextBlock(file.Link(cc, textLink)); + if (text.empty()) + { + continue; // 没有文本的取值不进入枚举表 + } + spec.enumText.values.push_back(file.F64(ccData + 24u + 8u * index)); + spec.enumText.texts.push_back(std::move(text)); + } + if (spec.enumText.texts.size() != spec.enumText.values.size() || + spec.enumText.texts.empty()) + { + 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(file.U64(caData + 16u + 8u * axis)); + if (dim == 0) + { + total = 0; + break; + } + spec.arrayDims.push_back(dim); + total *= dim; + } + spec.arrayElements = total; + } + // 通道类型 2 = 虚拟主通道(CANape 的时间轴),同样可以取值。 - spec.numeric = IsNumericType(spec.dataType) && spec.bitCount > 0 && spec.bitCount <= 64; + 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& 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 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 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(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(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; });