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
+477 -47

No files matched your search

+386 -47
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,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<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;
}
}
else if (ccType == static_cast<std::uint8_t>(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<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 的时间轴),同样可以取值。
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<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;
});