Files
Just-DataInspector/Utilities/Src/Mf4Loader.cpp
T
DeepSeek V4.1 Flash b315201598 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 / 单位在文件中的实际情况报告)。
2026-10-20 11:25:03 +08:00

1839 lines
59 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Mf4Loader.hpp"
#include "Deflate.hpp"
#include "Utf8Util.hpp"
#include <windows.h>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <functional>
#include <limits>
#include <string>
#include <vector>
namespace jdi
{
namespace
{
const double kMissingValue = std::numeric_limits<double>::quiet_NaN(); /**< 缺失值占位。 */
const std::size_t kReadChunkBytes = 4u * 1024u * 1024u; /**< 数据区流式读取块大小。 */
const std::size_t kMaxChannelsPerGroup = 4096; /**< 单个通道组的通道数上限(防损坏文件死循环)。 */
const std::size_t kMaxGroups = 65536; /**< 数据组数量上限。 */
/** MDF 块头固定长度(id + reserved + length + link_count)。 */
const std::size_t kBlockHeaderBytes = 24;
/** @brief 通道数据类型(`cn_data_type`)。 */
enum class DataType : int
{
UINT_LE = 0,
UINT_BE = 1,
INT_LE = 2,
INT_BE = 3,
FLOAT_LE = 4,
FLOAT_BE = 5,
STRING_LATIN1 = 6,
STRING_UTF8 = 7,
STRING_UTF16_LE = 8,
STRING_UTF16_BE = 9,
BYTE_ARRAY = 10,
MIME_SAMPLE = 11,
MIME_STREAM = 12
};
/**
* @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 /**< 值区间 → 文本(未支持,按恒等处理)。 */
};
/**
* @brief 只读的 MDF 文件访问器(按需 seek + read,不把整个文件读进内存)。
*/
class MdfFile
{
public:
~MdfFile() { Close(); }
bool Open(const std::wstring& path, std::wstring& error)
{
m_file = _wfopen(path.c_str(), L"rb");
if (m_file == nullptr)
{
error = L"无法打开文件,请确认路径与访问权限。";
return false;
}
if (_fseeki64(m_file, 0, SEEK_END) != 0)
{
error = L"无法定位文件末尾。";
return false;
}
const long long size = _ftelli64(m_file);
if (size <= 0)
{
error = L"文件内容为空。";
return false;
}
m_size = static_cast<std::uint64_t>(size);
// ID 块:0..7 = "MDF ",8..15 = 版本号,例如 "4.10 "。
char id[8] = {};
char version[8] = {};
if (!ReadAt(0, id, sizeof(id)) || std::memcmp(id, "MDF", 3) != 0)
{
error = L"不是有效的 MDF 文件(缺少 MDF 标识)。";
return false;
}
if (!ReadAt(8, version, sizeof(version)) || version[0] != '4')
{
error = L"仅支持 MDF v4 格式(当前文件不是 v4)。";
return false;
}
return true;
}
void Close()
{
if (m_file != nullptr)
{
std::fclose(m_file);
m_file = nullptr;
}
}
std::uint64_t Size() const { return m_size; }
/**
* @brief 已从文件实际读取的字节数(用于「已读 / 总大小」进度)。
*
* 随机访问会重复读取元数据,因此这个值可能超过文件大小;
* 上报时必须自行夹到 `Size()`。
*/
std::uint64_t BytesRead() const { return m_bytesRead; }
/** @brief 读取一段原始字节。 */
bool ReadAt(std::uint64_t offset, void* dst, std::size_t bytes) const
{
if (m_file == nullptr || offset + bytes > m_size)
{
return false;
}
if (_fseeki64(m_file, static_cast<long long>(offset), SEEK_SET) != 0)
{
return false;
}
const bool ok = std::fread(dst, 1, bytes, m_file) == bytes;
if (ok)
{
m_bytesRead += bytes;
}
return ok;
}
/** @brief 读取一个小端标量(最多 8 字节)。越界返回 0。 */
std::uint64_t Scalar(std::uint64_t offset, std::size_t bytes) const
{
unsigned char buffer[8] = {};
if (bytes == 0 || bytes > 8 || !ReadAt(offset, buffer, bytes))
{
return 0;
}
std::uint64_t value = 0;
for (std::size_t i = 0; i < bytes; ++i)
{
value |= static_cast<std::uint64_t>(buffer[i]) << (8u * i);
}
return value;
}
std::uint32_t U32(std::uint64_t offset) const
{
return static_cast<std::uint32_t>(Scalar(offset, 4));
}
std::uint16_t U16(std::uint64_t offset) const
{
return static_cast<std::uint16_t>(Scalar(offset, 2));
}
std::uint8_t U8(std::uint64_t offset) const
{
return static_cast<std::uint8_t>(Scalar(offset, 1));
}
std::uint64_t U64(std::uint64_t offset) const { return Scalar(offset, 8); }
double F64(std::uint64_t offset) const
{
const std::uint64_t bits = U64(offset);
double value = 0.0;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
/** @brief 读取块标识(4 字节 ASCII)。 */
bool BlockId(std::uint64_t offset, char out[5]) const
{
out[0] = out[1] = out[2] = out[3] = '?';
out[4] = '\0';
return ReadAt(offset, out, 4);
}
/** @brief 块总长度(含块头)。 */
std::uint64_t BlockLength(std::uint64_t offset) const { return U64(offset + 8); }
/** @brief 块内链接数量。 */
std::uint64_t LinkCount(std::uint64_t offset) const { return U64(offset + 16); }
/** @brief 第 `index` 个链接(绝对文件偏移,0 表示空)。 */
std::uint64_t Link(std::uint64_t offset, std::uint64_t index) const
{
if (index >= LinkCount(offset))
{
return 0;
}
return U64(offset + kBlockHeaderBytes + 8u * index);
}
/** @brief 块的数据段起始偏移。 */
std::uint64_t DataOffset(std::uint64_t offset) const
{
return offset + kBlockHeaderBytes + 8u * LinkCount(offset);
}
/**
* @brief 判断某个偏移是否为一个有效块的开头(且标识匹配)。
* @param [IN] offset 候选偏移。
* @param [IN] id 期望的 4 字节标识(如 "##CN");空字符串表示只校验偏移合法性。
*/
bool IsBlock(std::uint64_t offset, const char* id) const
{
if (offset == 0 || offset + kBlockHeaderBytes > m_size)
{
return false;
}
char actual[5] = {};
if (!BlockId(offset, actual))
{
return false;
}
if (id != nullptr && id[0] != '\0' && std::memcmp(actual, id, 4) != 0)
{
return false;
}
return BlockLength(offset) >= kBlockHeaderBytes && offset + BlockLength(offset) <= m_size;
}
/**
* @brief 读取 `##TX` / `##MD` 块里的文本。
*
* ⚠️ 此前注释写的是「`##TX` / `##MD`」但实现只接受 `##TX`,于是 `##MD`
*(XML 元数据:单位、注释、显示名等)一律返回空。现在两种块都读:
* * `##TX`:兼容规范的「长度 + UTF-16 / UTF-8」与 CANape 的「NUL 结尾裸文本」;
* * `##MD`:数据段是 XML(UTF-8),没有长度前缀 ⇒ 直接转成宽字符串。
*/
std::wstring TextBlock(std::uint64_t offset) const
{
std::wstring text;
if (IsBlock(offset, "##MD"))
{
const std::uint64_t data = DataOffset(offset);
const std::uint64_t usable = BlockLength(offset) - (data - offset);
if (usable == 0)
{
return text;
}
std::vector<char> buffer(static_cast<std::size_t>(usable) + 1u, '\0');
if (!ReadAt(data, buffer.data(), static_cast<std::size_t>(usable)))
{
return text;
}
std::size_t length = static_cast<std::size_t>(usable);
while (length > 0 && buffer[length - 1] == '\0')
{
--length;
}
return Utf8ToWide(buffer.data(), static_cast<int>(length));
}
if (!IsBlock(offset, "##TX"))
{
return text;
}
const std::uint64_t data = DataOffset(offset);
const std::uint64_t usable = BlockLength(offset) - (data - offset);
if (usable < 2)
{
return text;
}
// MDF 规范里 TX 的数据段是「长度 + 文本」,长度可能是字符数(UTF-16)或字节数
//(UTF-8,MDF 4.1 起)。但部分工具(如 CANape 16)会**直接写 NUL 结尾的
// 原始文本、不带长度前缀**,因此这里按「先按规范、再按裸文本回退」的顺序解析。
if (usable >= 4)
{
const std::uint64_t declared = U32(data);
if (declared > 0 && 4u + 2u * declared <= usable)
{
// 长度前缀 + UTF-16 代码单元。
std::vector<char> buffer(static_cast<std::size_t>(2u * declared));
if (!ReadAt(data + 4, buffer.data(), buffer.size()))
{
return text;
}
text.reserve(static_cast<std::size_t>(declared));
for (std::uint64_t i = 0; i < declared; ++i)
{
const wchar_t c =
static_cast<wchar_t>(static_cast<unsigned char>(buffer[2 * i]) |
(static_cast<unsigned char>(buffer[2 * i + 1]) << 8));
if (c == L'\0')
{
break;
}
text.push_back(c);
}
return text;
}
if (declared > 0 && 4u + declared <= usable)
{
// 长度前缀 + UTF-8 字节。
std::vector<char> buffer(static_cast<std::size_t>(declared) + 1u, '\0');
if (!ReadAt(data + 4, buffer.data(), static_cast<std::size_t>(declared)))
{
return text;
}
return Utf8ToWide(buffer.data(), static_cast<int>(declared));
}
}
// 回退:数据段直接是 NUL 结尾的文本(UTF-16 或 UTF-8)。
std::vector<unsigned char> buffer(static_cast<std::size_t>(usable));
if (!ReadAt(data, buffer.data(), buffer.size()))
{
return text;
}
const bool utf16 = (buffer.size() >= 2 && buffer[1] == 0 && buffer[0] != 0);
if (utf16)
{
for (std::size_t i = 0; i + 1 < buffer.size(); i += 2)
{
const wchar_t c = static_cast<wchar_t>(buffer[i] | (buffer[i + 1] << 8));
if (c == L'\0')
{
break;
}
text.push_back(c);
}
return text;
}
std::string raw;
for (const unsigned char c : buffer)
{
if (c == '\0')
{
break;
}
raw += static_cast<char>(c);
}
return Utf8ToWide(raw.data(), static_cast<int>(raw.size()));
}
private:
FILE* m_file = nullptr;
std::uint64_t m_size = 0;
mutable std::uint64_t m_bytesRead = 0; /**< 已实际读取的字节数(进度上报用)。 */
};
/** @brief 一个通道的读取方案(已从 CN 块解析出来)。 */
struct ChannelSpec
{
std::wstring name; /**< 通道名(`cn_tx_name`)。 */
std::size_t bitOffset = 0; /**< 记录内的起始位偏移(= byte_offset*8 + bit_offset)。 */
std::size_t bitCount = 0; /**< 位宽。 */
DataType dataType = DataType::UINT_LE; /**< 原始数据类型。 */
bool allInvalid = false; /**< `cn_flags & 1`:整列无效。 */
bool hasInvalidBit = false; /**< `cn_flags & 2`:有无效位。 */
std::size_t invalidBitPos = 0; /**< 无效位的绝对位偏移。 */
int type = 0; /**< `cn_type`:0 定长 / 1 VLSD / 2 虚拟主通道 / 3 同步通道。 */
int syncType = 0; /**< `cn_sync_type`:1 时间 / 2 角度 / 3 距离。 */
bool syncMaster = false; /**< `cn_sync_type >= 1`:本组的主通道。 */
bool vlsd = false; /**< `cn_type == 1`:值存放在 `##SD` 里(记录内是偏移)。 */
std::uint64_t signalData = 0; /**< VLSD 通道的 `##SD` 块地址(`cn_data`)。 */
/** `##SI` 采样索引(`cn_si_source`);空表示每个记录都有采样。 */
std::vector<std::uint64_t> sampleIndex;
/** @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;
};
/** @brief 判断数据类型是否为可绘制的数值类型。 */
bool IsNumericType(DataType type)
{
return type == DataType::UINT_LE || type == DataType::UINT_BE || type == DataType::INT_LE ||
type == DataType::INT_BE || type == DataType::FLOAT_LE || type == DataType::FLOAT_BE;
}
/**
* @brief 按位从记录缓冲中取出一个数值。
*
* 支持 1–64 位整数(任意位偏移)与 32/64 位浮点(字节对齐)。
*
* @param [IN] record 记录起始指针。
* @param [IN] recordBytes 记录长度(字节),用于边界检查。
* @param [IN] spec 通道读取方案。
* @return 原始数值;越界或类型不支持时返回 NaN。
*/
double DecodeRawValue(const unsigned char* record, std::size_t recordBytes, const ChannelSpec& spec)
{
const std::size_t bitEnd = spec.bitOffset + spec.bitCount;
if (spec.bitCount == 0 || spec.bitCount > 64 || bitEnd > recordBytes * 8u)
{
return kMissingValue;
}
const std::size_t byteStart = spec.bitOffset / 8u;
const std::size_t byteCount = (bitEnd - byteStart * 8u + 7u) / 8u;
unsigned char buffer[9] = {};
std::memcpy(buffer, record + byteStart, byteCount);
if (spec.dataType == DataType::FLOAT_LE || spec.dataType == DataType::FLOAT_BE)
{
if (spec.bitOffset % 8u != 0u || (spec.bitCount != 32u && spec.bitCount != 64u))
{
return kMissingValue;
}
if (spec.dataType == DataType::FLOAT_LE)
{
if (spec.bitCount == 32u)
{
std::uint32_t bits = 0;
std::memcpy(&bits, buffer, 4);
float value = 0.0f;
std::memcpy(&value, &bits, 4);
return static_cast<double>(value);
}
double value = 0.0;
std::memcpy(&value, buffer, 8);
return value;
}
if (spec.bitCount == 32u)
{
std::uint32_t bits = (static_cast<std::uint32_t>(buffer[0]) << 24) |
(static_cast<std::uint32_t>(buffer[1]) << 16) |
(static_cast<std::uint32_t>(buffer[2]) << 8) |
static_cast<std::uint32_t>(buffer[3]);
float value = 0.0f;
std::memcpy(&value, &bits, 4);
return static_cast<double>(value);
}
std::uint64_t bits = 0;
for (int i = 0; i < 8; ++i)
{
bits = (bits << 8) | static_cast<std::uint64_t>(buffer[i]);
}
double value = 0.0;
std::memcpy(&value, &bits, 8);
return value;
}
// 整数:先按字节序拼成一个 64 位无符号数,再按位偏移右移并解释符号。
std::uint64_t raw = 0;
if (spec.dataType == DataType::UINT_LE || spec.dataType == DataType::INT_LE)
{
for (std::size_t i = byteCount; i-- > 0;)
{
raw = (raw << 8) | static_cast<std::uint64_t>(buffer[i]);
}
}
else
{
for (std::size_t i = 0; i < byteCount; ++i)
{
raw = (raw << 8) | static_cast<std::uint64_t>(buffer[i]);
}
}
const std::size_t shift = spec.bitOffset % 8u;
raw >>= shift;
if (spec.bitCount < 64u)
{
raw &= (1ull << spec.bitCount) - 1ull;
}
const bool signedType = (spec.dataType == DataType::INT_LE || spec.dataType == DataType::INT_BE);
if (signedType && spec.bitCount < 64u && (raw & (1ull << (spec.bitCount - 1u))) != 0u)
{
// 符号扩展。
raw |= ~((1ull << spec.bitCount) - 1ull);
}
if (signedType)
{
return static_cast<double>(static_cast<std::int64_t>(raw));
}
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`(信号数据)块内容读进内存。
*
* MDF4 的 VLSD 数据格式(实测 + 参考实现一致):
* * `##SD` 数据段是一串「`UINT32` 长度 + 该长度的字节」的样本;
* * 记录(`##DT`)在该通道的 `cn_byte_offset` 处放一个 **64 位偏移**,
* 指向样本长度前缀在 `##SD` 数据里的位置(偏移按各 `##SD` 数据段顺序累加)。
*
* 只有 VLSD 通道才需要这块数据,因此按通道按需加载,不做全局缓存。
*
* @param [IN] block 起始块(`##SD` / `##DZ` / `##DL` / `##HL`)。
* @param [IN,OUT] out 追加写入的逻辑字节流。
* @return 读到至少一段数据返回 true。
*/
bool AppendSignalData(const MdfFile& file, std::uint64_t block, std::vector<std::uint8_t>& out,
int depth)
{
if (block == 0 || depth > 8 || !file.IsBlock(block, ""))
{
return false;
}
// 防御:单个通道的 VLSD 数据超过 512MB 视为异常,避免损坏文件吃光内存。
constexpr std::size_t kMaxSignalDataBytes = 512u * 1024u * 1024u;
char id[5] = {};
file.BlockId(block, id);
const std::uint64_t data = file.DataOffset(block);
const std::uint64_t payload = file.BlockLength(block) - (data - block);
if (payload == 0)
{
return false;
}
if (std::memcmp(id, "##SD", 4) == 0)
{
if (out.size() + payload > kMaxSignalDataBytes)
{
return false;
}
const std::size_t base = out.size();
out.resize(base + static_cast<std::size_t>(payload));
if (!file.ReadAt(data, out.data() + base, static_cast<std::size_t>(payload)))
{
out.resize(base);
return false;
}
// `##SD` 可能还有同类型的后继块(链接 0)。
if (file.LinkCount(block) > 0)
{
AppendSignalData(file, file.Link(block, 0), out, depth + 1);
}
return true;
}
if (std::memcmp(id, "##DZ", 4) == 0)
{
// `##DZ` 数据段布局:original_type(4) + zip_type(1) + reserved(3) +
// param(4) + original_size(8) + zip_size(8) + 压缩数据。
const std::uint8_t zipType = file.U8(data + 4);
const std::uint64_t zipSize = file.U64(data + 20);
if (zipType != 0 || payload <= 28u) // 只支持纯 deflate(不支持转置/LZ4/ZSTD)
{
return false;
}
const std::uint64_t usable = std::min(payload - 28u, zipSize);
if (usable == 0 || out.size() + usable > kMaxSignalDataBytes)
{
return false;
}
std::vector<std::uint8_t> compressed(static_cast<std::size_t>(usable));
if (!file.ReadAt(data + 28u, compressed.data(), compressed.size()))
{
return false;
}
std::vector<std::uint8_t> inflated;
if (!InflateZlib(compressed.data(), compressed.size(), inflated))
{
return false;
}
out.insert(out.end(), inflated.begin(), inflated.end());
return true;
}
if (std::memcmp(id, "##DL", 4) == 0 || std::memcmp(id, "##HL", 4) == 0)
{
const std::uint64_t links = file.LinkCount(block);
bool any = false;
for (std::uint64_t index = 0; index < links && index < 1000000; ++index)
{
any = AppendSignalData(file, file.Link(block, index), out, depth + 1) || any;
}
return any;
}
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 采样(取样本的第一个元素)。
*
* 记录里放的是 64 位偏移。两种写法都接受:
* * 直接偏移(部分写盘器);
* * `偏移 << 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())
{
return kMissingValue;
}
const std::size_t byteStart = spec.bitOffset / 8u;
if (byteStart + 8u > recordBytes)
{
return kMissingValue;
}
std::uint64_t raw = 0;
std::memcpy(&raw, record + byteStart, sizeof(raw));
for (int attempt = 0; attempt < 2; ++attempt)
{
const std::uint64_t offset = (attempt == 0) ? raw : (raw >> 1);
if (offset + 4u > signalData.size())
{
continue;
}
std::uint32_t length = 0;
std::memcpy(&length, signalData.data() + offset, sizeof(length));
if (static_cast<std::uint64_t>(length) > signalData.size() - offset - 4u)
{
continue;
}
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 = elementBits;
element.vlsd = false;
// 只给"这一个元素"的字节数做边界检查(样本可能含多个元素)。
return DecodeRawValue(signalData.data() + offset + 4u, elementBytes, element);
}
return kMissingValue;
}
/**
* @brief 解析一个 CN 块为读取方案。
*
* ⚠️ **链接布局有两种写法**:MDF 4.0 写 8 个链接(没有 `cn_sr_source`),
* 4.1 写 9 个。二者共有的尾部 4 个链接是
* `cn_cc_conversion` / `cn_data` / `cn_md_unit` / `cn_md_comment`,
* 因此这里按**链接总数**反推换算/数据块下标,而不是写死 4/5。
*(实测 Simulink 导出的「4.10」文件仍使用 8 链接布局。)
*/
bool ParseChannel(const MdfFile& file, std::uint64_t cn, ChannelSpec& spec)
{
if (!file.IsBlock(cn, "##CN"))
{
return false;
}
const std::uint64_t data = file.DataOffset(cn);
const std::uint8_t type = file.U8(data + 0);
const std::uint8_t sync = file.U8(data + 1);
const std::uint8_t dataType = file.U8(data + 2);
const std::uint8_t bitOffset = file.U8(data + 3);
const std::uint32_t byteOffset = file.U32(data + 4);
const std::uint32_t bitCount = file.U32(data + 8);
const std::uint32_t flags = file.U32(data + 12);
const std::uint32_t invalidBit = file.U32(data + 16);
const std::uint64_t linkCount = file.LinkCount(cn);
// 8 链接布局:[4] = 换算、[5] = 数据块、[6] = 单位、[7] = 注释。
// 9 链接布局:[5] = 换算、[6] = 数据块、[7] = 单位、[8] = 注释。
const std::uint64_t conversionIndex = (linkCount >= 9) ? 5 : 4;
const std::uint64_t signalDataIndex = conversionIndex + 1;
spec.name = file.TextBlock(file.Link(cn, 2));
spec.dataType = static_cast<DataType>(dataType);
spec.bitCount = bitCount;
spec.bitOffset = static_cast<std::size_t>(byteOffset) * 8u + bitOffset;
spec.allInvalid = (flags & 0x1u) != 0u;
spec.hasInvalidBit = (flags & 0x2u) != 0u;
spec.invalidBitPos = invalidBit;
spec.type = static_cast<int>(type);
spec.syncType = static_cast<int>(sync);
spec.syncMaster = (sync >= 1 && sync <= 3);
spec.vlsd = (type == 1);
if (spec.vlsd && linkCount > signalDataIndex)
{
spec.signalData = file.Link(cn, signalDataIndex);
}
// 采样索引(`##SI`):只有当它确实是「多条记录下标」时才生效。
// ⚠️ 实测部分工具会给每个通道挂一个只含 1 个 8 字节值的 `##SI`(如 513),
// 那显然不是采样下标(远大于周期数)⇒ 必须忽略,否则会把整列清成 NaN。
{
const std::uint64_t si = (linkCount > 3) ? file.Link(cn, 3) : 0;
if (file.IsBlock(si, "##SI"))
{
const std::uint64_t siData = file.DataOffset(si);
const std::uint64_t usable = file.BlockLength(si) - (siData - si);
const std::size_t count = static_cast<std::size_t>(usable / 8u);
if (count >= 2 && count <= (1u << 24))
{
spec.sampleIndex.resize(count);
bool ok = file.ReadAt(siData, spec.sampleIndex.data(), count * 8u);
if (!ok)
{
spec.sampleIndex.clear();
}
}
}
}
// 换算:数值换算(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);
// `cc_val[]` 从数据段 +24 开始(每个 REAL 占 8 字节)。
const auto readValues = [&file, ccData, valCount](std::size_t wanted) -> std::vector<double>
{
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。
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;
}
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)是「默认文本」,这里不使用。
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 的时间轴),同样可以取值。
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,
Fn&& visit)
{
std::uint64_t current = first;
std::size_t guard = 0;
while (current != 0 && guard++ < limit)
{
if (!file.IsBlock(current, expectedId))
{
break;
}
if (!visit(current))
{
break;
}
current = file.Link(current, 0);
}
}
/**
* @brief 一段原始记录数据(MDF4 的 `##DT` / `##DZ` / `##DV` 块的数据区)。
*
* MDF4 允许把记录拆到多段里:
* * `##DT` 普通数据块(记录连续存放);
* * `##DZ` 压缩数据块(zlib 流,解压后是普通记录);
* * `##DV` 数据值块(`##DL` 列表指向的原始值);
* * `##DL` 数据列表(链接到多个 `##DT` / `##DZ`);
* * `##HL` 头部列表(链接到多个数据块,含压缩的)。
*/
struct DataSegment
{
std::uint64_t offset = 0; /**< 数据区起始绝对偏移。 */
std::uint64_t bytes = 0; /**< 数据区字节数(压缩段为压缩后长度)。 */
bool compressed = false; /**< 是否 need inflate。 */
};
/**
* @brief 递归展开数据块,收集所有数据段。
*
* ⚠️ 用户 2026-10-19 报告:Simulink SDI 导出的 MF4 把记录写在 `##DL`(数据列表)
* 里,而旧实现只认 `##DT` ⇒ 整个数据组被跳过(8 个信号只能读出 4 个)。
*/
bool CollectDataSegments(const MdfFile& file, std::uint64_t block,
std::vector<DataSegment>& out, int depth)
{
if (block == 0 || depth > 4 || !file.IsBlock(block, ""))
{
return false;
}
char id[5] = {};
file.BlockId(block, id);
const std::uint64_t data = file.DataOffset(block);
const std::uint64_t payload = file.BlockLength(block) - (data - block);
if (payload == 0)
{
return false;
}
if (std::memcmp(id, "##DT", 4) == 0 || std::memcmp(id, "##DV", 4) == 0)
{
out.push_back(DataSegment{data, payload, false});
return true;
}
if (std::memcmp(id, "##DZ", 4) == 0)
{
// `##DZ` 数据段布局:original_type(4) + zip_type(1) + reserved(3) +
// param(4) + original_size(8) + zip_size(8) + 压缩数据。
// ⚠️ 载荷里含 28 字节头,不能把整段当 zlib 流(旧实现在这里错了)。
char originalType[5] = {};
file.BlockId(data, originalType); // 读 4 字节原始类型("DT"/"SD"/...)
const std::uint8_t zipType = file.U8(data + 4);
const std::uint64_t zipSize = file.U64(data + 20);
if (zipType != 0 || payload <= 28u)
{
return false; // 只支持纯 deflate
}
if (std::memcmp(originalType, "SD", 2) == 0)
{
return false; // 压缩的 VLSD 数据由 AppendSignalData 负责
}
const std::uint64_t usable = std::min(payload - 28u, zipSize);
out.push_back(DataSegment{data + 28u, usable, true});
return true;
}
if (std::memcmp(id, "##DL", 4) == 0 || std::memcmp(id, "##HL", 4) == 0)
{
const std::uint64_t links = file.LinkCount(block);
bool any = false;
for (std::uint64_t index = 0; index < links && index < 1000000; ++index)
{
any = CollectDataSegments(file, file.Link(block, index), out, depth + 1) || any;
}
return any;
}
// 到这里说明不是记录数据块:
// * `##SD`(Signal Data,VLSD 通道的样本)由 `AppendSignalData` 按通道读取;
// * `##SI` 是采样索引,不是数据块。
return false;
}
/**
* @brief 依次从若干数据段中读取连续记录,并对每条完整记录调用 `visit`。
*
* * 记录可能**跨段**(`##DT` 载荷不是记录长度的整数倍时)⇒ 用 `carry` 拼接;
* * 压缩段先用内置 inflate 展开(`##DZ`),再按普通字节流消费;
* * 每读一块调用 `cancelled`(可为空),返回 true 表示请求取消。
*
* @return 成功读取到至少 `cycles` 条记录返回 true。
*/
bool ReadRecordSegments(
const MdfFile& file, const std::vector<DataSegment>& segments, std::uint64_t cycles,
std::size_t recordBytes,
const std::function<void(const unsigned char* record, std::uint64_t row)>& visit,
const std::function<bool()>& cancelled = std::function<bool()>())
{
if (recordBytes == 0 || cycles == 0)
{
return false;
}
std::vector<unsigned char> carry(recordBytes);
std::size_t carryBytes = 0;
std::uint64_t row = 0;
std::vector<std::uint8_t> inflated;
for (const DataSegment& segment : segments)
{
if (row >= cycles)
{
break;
}
if (cancelled && cancelled())
{
return false;
}
const unsigned char* data = nullptr;
std::size_t bytes = 0;
if (segment.compressed)
{
std::vector<std::uint8_t> compressed(static_cast<std::size_t>(segment.bytes));
if (!file.ReadAt(segment.offset, compressed.data(), compressed.size()))
{
return false;
}
if (!InflateZlib(compressed.data(), compressed.size(), inflated))
{
return false;
}
data = inflated.data();
bytes = inflated.size();
}
else
{
if (segment.offset + segment.bytes > file.Size())
{
return false;
}
// 非压缩段同样分块读,避免一次分配整段(巨型数据组可能是 GB 级)。
std::vector<unsigned char> buffer;
std::uint64_t consumed = 0;
while (consumed < segment.bytes && row < cycles)
{
const std::size_t want = static_cast<std::size_t>(
std::min<std::uint64_t>(kReadChunkBytes, segment.bytes - consumed));
buffer.resize(std::max<std::size_t>(want, recordBytes));
if (!file.ReadAt(segment.offset + consumed, buffer.data(), want))
{
return false;
}
data = buffer.data();
bytes = want;
std::size_t position = 0;
while (position < bytes && row < cycles)
{
const std::size_t need = recordBytes - carryBytes;
const std::size_t take = std::min(need, bytes - position);
std::memcpy(carry.data() + carryBytes, data + position, take);
carryBytes += take;
position += take;
if (carryBytes == recordBytes)
{
visit(carry.data(), row++);
carryBytes = 0;
}
}
consumed += want;
if (cancelled && cancelled())
{
return false;
}
}
continue;
}
// 压缩段:整段已在内存里,直接消费。
std::size_t position = 0;
while (position < bytes && row < cycles)
{
const std::size_t need = recordBytes - carryBytes;
const std::size_t take = std::min(need, bytes - position);
std::memcpy(carry.data() + carryBytes, data + position, take);
carryBytes += take;
position += take;
if (carryBytes == recordBytes)
{
visit(carry.data(), row++);
carryBytes = 0;
}
}
}
return row > 0;
}
/** @brief 一个通道组(CG)解析出的时间轴 + 信号列。 */
struct ChannelGroupData
{
std::wstring timeName; /**< 时间列名(主通道名或 `time`)。 */
std::vector<ChannelSpec> channels; /**< 非时间通道。 */
std::vector<double> time; /**< 时间轴数值。 */
std::vector<std::vector<double>> values; /**< 每个非时间通道的采样值。 */
};
/**
* @brief 把 `##SI` 采样索引转换成「逐记录有效位图」。
*
* 只有当索引表**至少两条**且**全部落在记录范围内**时才生效;
* 否则(例如某些工具给每个通道挂一个只含 `513` 的 `##SI`)返回空向量表示「全有效」,
* 与不支持 `##SI` 时的行为完全一致。
*/
std::vector<std::uint8_t> BuildSampleMask(const ChannelSpec& spec, std::uint64_t cycles)
{
std::vector<std::uint8_t> mask;
if (spec.sampleIndex.size() < 2 || cycles == 0)
{
return mask;
}
for (const std::uint64_t index : spec.sampleIndex)
{
if (index >= cycles)
{
return mask; // 不像采样索引:忽略
}
}
mask.assign(static_cast<std::size_t>(cycles), 0);
for (const std::uint64_t index : spec.sampleIndex)
{
mask[static_cast<std::size_t>(index)] = 1;
}
return mask;
}
/**
* @brief 读取一个通道组的全部记录(流式,按 4MB 分块)。
*
* @param [IN] file 文件访问器。
* @param [IN] segments 数据段列表(`##DT` / `##DL` / `##HL` / `##DZ`)。
* @param [IN] cycles 记录条数。
* @param [IN] recordBytes 单条记录字节数。
* @param [IN] masterIndex 时间主通道下标。
* @param [IN,OUT] group 输出(`timeName`/`channels` 已填好,函数填充 `time`/`values`)。
* @param [IN] cancelled 取消检查(可为空)。
*/
bool ReadChannelGroupRecords(const MdfFile& file, const std::vector<DataSegment>& segments,
std::uint64_t cycles, std::size_t recordBytes,
std::size_t masterIndex, ChannelGroupData& group,
const std::function<bool()>& cancelled = std::function<bool()>())
{
if (cycles == 0 || recordBytes == 0 || segments.empty())
{
return false;
}
group.time.assign(static_cast<std::size_t>(cycles), kMissingValue);
group.values.assign(group.channels.size(),
std::vector<double>(static_cast<std::size_t>(cycles), kMissingValue));
// `##SI`:每个通道各自的「有效记录」位图(空 = 每个记录都有效)。
std::vector<std::vector<std::uint8_t>> masks(group.channels.size());
for (std::size_t index = 0; index < group.channels.size(); ++index)
{
masks[index] = BuildSampleMask(group.channels[index], cycles);
}
// VLSD 通道的 `##SD` 数据:按通道加载(只有 VLSD 通道才需要)。
std::vector<std::vector<std::uint8_t>> signalData(group.channels.size());
for (std::size_t index = 0; index < group.channels.size(); ++index)
{
const ChannelSpec& spec = group.channels[index];
if (spec.vlsd && spec.signalData != 0 && spec.numeric)
{
AppendSignalData(file, spec.signalData, signalData[index], 0);
}
}
std::vector<const std::vector<std::uint8_t>*> signalDataPtr(group.channels.size(), nullptr);
for (std::size_t index = 0; index < group.channels.size(); ++index)
{
signalDataPtr[index] = signalData[index].empty() ? nullptr : &signalData[index];
}
// ⚠️ 记录可能分散在多个 `##DT` / `##DZ`(经 `##DL` / `##HL` 列表)中,
// 由 `ReadRecordSegments` 负责跨段拼接与解压(见该函数的说明)。
return ReadRecordSegments(
file, segments, cycles, recordBytes,
[&group, masterIndex, recordBytes, &masks, &signalDataPtr](const unsigned char* record,
std::uint64_t row)
{
// 时间主通道。
{
const ChannelSpec& master = group.channels[masterIndex];
double value = DecodeRawValue(record, recordBytes, master);
if (master.allInvalid)
{
value = kMissingValue;
}
else if (master.hasInvalidBit)
{
const std::size_t byteIndex = master.invalidBitPos / 8u;
if (byteIndex < recordBytes &&
((record[byteIndex] >> (master.invalidBitPos % 8u)) & 1u) != 0u)
{
value = kMissingValue;
}
}
if (std::isfinite(value))
{
value = ApplyConversion(master, value);
}
group.time[static_cast<std::size_t>(row)] = value;
}
// 各信号通道。
for (std::size_t c = 0; c < group.channels.size(); ++c)
{
if (c == masterIndex)
{
continue;
}
const ChannelSpec& spec = group.channels[c];
// `##SI` 采样索引:只有表中列出的记录才有采样,其余保持 NaN。
if (!masks[c].empty() && masks[c][static_cast<std::size_t>(row)] == 0)
{
continue;
}
double value = kMissingValue;
if (spec.vlsd)
{
// VLSD:记录里是 64 位偏移,真正的取值在 `##SD` 里。
if (signalDataPtr[c] != nullptr)
{
value = DecodeVlsdValue(record, recordBytes, spec, *signalDataPtr[c]);
}
}
else
{
value = DecodeRawValue(record, recordBytes, spec);
}
if (spec.allInvalid)
{
value = kMissingValue;
}
else if (spec.hasInvalidBit)
{
const std::size_t byteIndex = spec.invalidBitPos / 8u;
if (byteIndex < recordBytes &&
((record[byteIndex] >> (spec.invalidBitPos % 8u)) & 1u) != 0u)
{
value = kMissingValue;
}
}
if (std::isfinite(value))
{
value = ApplyConversion(spec, value);
}
group.values[c][static_cast<std::size_t>(row)] = value;
}
},
cancelled);
}
/** @brief 主通道名字判定用的 ASCII 小写化。 */
std::wstring LowerAscii(const std::wstring& text)
{
std::wstring lower;
lower.reserve(text.size());
for (const wchar_t c : text)
{
lower.push_back((c >= L'A' && c <= L'Z') ? static_cast<wchar_t>(c - L'A' + L'a') : c);
}
return lower;
}
/**
* @brief 从通道组里选出时间主通道。
*
* 判定顺序(越靠前越可信):
* 1. `cn_sync_type` = 1(时间)/ 2(角度)/ 3(距离)且为数值通道;
* 2. 名字为 `t` / `time` / `timestamp` / `zeit` / `时间`(含 `t[s]` 这类带单位后缀);
* 3. 第一个数值通道(兜底:至少让数据能画出来,而不是整组丢弃)。
*
* ⚠️ `cg_flags` 的第 2 位表示「远程主通道」(主通道不在本 CG 里)。此时跳过第 1 步,
* 直接按名字 / 首个数值通道兜底,避免把某个普通信号误当成时间轴。
*
* @return 主通道下标;一个数值通道都没有时返回 `channels.size()`。
*/
std::size_t SelectMasterChannel(const std::vector<ChannelSpec>& channels, std::uint16_t cgFlags)
{
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 && !channels[i].vlsd)
{
return i;
}
}
}
for (std::size_t i = 0; i < channels.size(); ++i)
{
if (!channels[i].numeric)
{
continue;
}
const std::wstring lower = LowerAscii(channels[i].name);
if (lower == L"time" || lower == L"t" || lower == L"timestamp" || lower == L"zeit" ||
lower == L"时间" || lower.rfind(L"t[", 0) == 0 || lower.rfind(L"time[", 0) == 0)
{
return i;
}
}
for (std::size_t i = 0; i < channels.size(); ++i)
{
if (channels[i].numeric && !channels[i].vlsd)
{
return i;
}
}
return channels.size();
}
} // namespace
bool LoadMf4Source(const std::wstring& filePath, LoadedSource& source, std::wstring& error,
const LoadProgress& progress)
{
source = LoadedSource();
error.clear();
MdfFile file;
if (!file.Open(filePath, error))
{
return false;
}
const std::size_t slashPos = filePath.find_last_of(L"\\/");
source.sourceName = (slashPos == std::wstring::npos) ? filePath : filePath.substr(slashPos + 1);
// 头块 → 第一个数据组。
std::uint64_t hd = 0;
{
char id[5] = {};
// 头块通常紧跟在 64 字节 ID 块之后;用一个小的扫描窗口定位,兼容前置填充。
for (std::uint64_t offset = 24; offset + 64 <= file.Size() && offset < 4096; offset += 8)
{
if (file.BlockId(offset, id) && std::memcmp(id, "##HD", 4) == 0)
{
hd = offset;
break;
}
}
}
if (hd == 0)
{
error = L"未找到 MDF 头块(##HD)。";
return false;
}
const std::uint64_t firstDg = file.Link(hd, 0);
// 先走一遍 DG 链只收集偏移(只读块头,代价极小):
// 进度分母必须是**真实数据组数量**(旧实现写死 64,组数少的文件会「停在 44% 再跳 100%」)。
std::vector<std::uint64_t> dataGroups;
WalkChain(file, firstDg, "##DG", kMaxGroups,
[&](std::uint64_t dg)
{
dataGroups.push_back(dg);
return true;
});
std::size_t processedGroups = 0;
double lastRatio = -1.0;
const auto emitProgress = [&]() -> bool
{
if (!progress)
{
return true;
}
LoadProgressInfo info;
const double byGroup =
dataGroups.empty() ? 1.0
: static_cast<double>(processedGroups) /
static_cast<double>(dataGroups.size());
double ratio = 0.98 * byGroup;
ratio = std::max(ratio, lastRatio); // 保证单调
lastRatio = ratio;
info.ratio = ratio;
// 同时上报真实字节数:`MdfFile::ReadAt` 逐次累计(随机访问可能重复读元数据,需夹住)。
info.hasBytes = true;
info.bytesRead = std::min<std::uint64_t>(file.BytesRead(), file.Size());
info.totalBytes = file.Size();
return progress(info);
};
std::size_t skippedNoLayout = 0;
std::size_t skippedEmpty = 0;
std::size_t skippedNoAxis = 0;
bool cancelRequested = false;
for (const std::uint64_t dg : dataGroups)
{
if (cancelRequested)
{
break;
}
++processedGroups;
const std::uint64_t cg = file.Link(dg, 1);
const std::uint64_t dataBlock = file.Link(dg, 2);
if (!file.IsBlock(cg, "##CG") || dataBlock == 0)
{
++skippedNoLayout; // 不完整的数据组(没有 CG 或没有数据块)
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
// 记录可能写在 `##DT` / `##DL`(数据列表)/ `##HL`(头部列表)/ `##DZ`(压缩)里:
// 旧实现只认 `##DT`,于是用 `##DL` 存放记录的文件会**整组被跳过**
//(用户 2026-10-19 报告:Simulink SDI 导出的 8 个信号只读出 4 个)。
std::vector<DataSegment> segments;
if (!CollectDataSegments(file, dataBlock, segments, 0) || segments.empty())
{
++skippedNoLayout;
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
const std::uint64_t cgData = file.DataOffset(cg);
const std::uint16_t cgFlags = file.U16(cgData + 0);
const std::uint64_t cycles = file.U64(cgData + 8);
if (cycles == 0)
{
++skippedEmpty; // 空数据组(例如只有注释通道的组)
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
// `cg_data_bytes`(+24)是单条记录字节数,4.1 起由文件直接给出,最可靠。
const std::uint32_t cgDataBytes = file.U32(cgData + 24);
// 解析通道链。
std::vector<ChannelSpec> channels;
WalkChain(file, file.Link(cg, 1), "##CN", kMaxChannelsPerGroup,
[&](std::uint64_t cn)
{
ChannelSpec spec;
if (ParseChannel(file, cn, spec))
{
// `##CA` 数组通道在这里就地展开成多个标量通道
//(一个通道装整条曲线时,旧实现只能看到元素 0)。
ExpandArrayChannel(std::move(spec), channels);
}
return true;
});
if (channels.empty())
{
++skippedNoLayout;
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
// 记录长度:优先用 `cg_data_bytes`;否则用「数据块大小 / 记录数」反推;
// ⚠️ 压缩段(`##DZ`)的载荷是压缩后长度,不能用来反推 ⇒ 退回按通道位宽推算。
std::uint64_t payload = 0;
bool anyCompressed = false;
for (const DataSegment& segment : segments)
{
payload += segment.bytes;
anyCompressed = anyCompressed || segment.compressed;
}
std::size_t recordBytes = 0;
if (cgDataBytes > 0 && cgDataBytes <= (1u << 20))
{
recordBytes = static_cast<std::size_t>(cgDataBytes);
}
else if (!anyCompressed && payload % cycles == 0 && payload / cycles > 0 &&
payload / cycles <= (1u << 20))
{
recordBytes = static_cast<std::size_t>(payload / cycles);
}
else
{
std::size_t bits = 0;
for (const ChannelSpec& spec : channels)
{
bits = std::max(bits, spec.bitOffset + spec.bitCount);
}
recordBytes = (bits + 31u) / 32u * 4u;
}
if (recordBytes == 0)
{
++skippedNoLayout;
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
const std::size_t masterIndex = SelectMasterChannel(channels, cgFlags);
if (masterIndex >= channels.size())
{
++skippedNoAxis; // 没有任何数值通道,无法建立时间轴
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
// 主通道自身若是 VLSD/字符串,取值无法作为时间轴 ⇒ 直接跳过该组。
if (channels[masterIndex].vlsd)
{
++skippedNoAxis;
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
ChannelGroupData group;
group.timeName = channels[masterIndex].name;
if (group.timeName.empty())
{
group.timeName = L"time";
}
group.channels = std::move(channels);
bool readCancelled = false;
const auto cancelled = [&]() -> bool
{
if (emitProgress())
{
return false;
}
readCancelled = true;
return true;
};
const bool readOk =
ReadChannelGroupRecords(file, segments, cycles, recordBytes, masterIndex, group, cancelled);
if (readCancelled)
{
cancelRequested = true;
break;
}
if (!readOk)
{
++skippedNoLayout;
if (!emitProgress())
{
cancelRequested = true;
break;
}
continue;
}
// 组织成一个时间轴块:时间列 + 其余数值通道。
TableBlock block;
block.timeHeader = group.timeName;
block.table.headers.push_back(group.timeName);
block.table.columns.push_back(std::move(group.time));
block.table.columnEnumText.emplace_back(); // 时间列没有枚举信息
for (std::size_t c = 0; c < group.channels.size(); ++c)
{
if (c == masterIndex || !group.channels[c].numeric)
{
continue;
}
std::wstring name = group.channels[c].name;
if (name.empty())
{
name = L"Channel" + std::to_wstring(c + 1);
}
block.table.headers.push_back(name);
block.table.columns.push_back(std::move(group.values[c]));
block.table.columnEnumText.push_back(std::move(group.channels[c].enumText));
}
if (block.table.columns.size() < 2)
{
++skippedEmpty; // 只有时间列:没有可绘制信号
continue;
}
block.table.rowCount = static_cast<std::size_t>(cycles);
source.blocks.push_back(std::move(block));
}
if (cancelRequested)
{
error.clear(); // 用户取消:不显示错误
return false;
}
if (source.blocks.empty())
{
// 说明「为什么一个信号都没有」,而不是笼统地说找不到数值通道。
std::wstring detail;
if (skippedNoLayout > 0)
{
detail += L"数据块布局无法解析 " + std::to_wstring(skippedNoLayout) + L" 组;";
}
if (skippedNoAxis > 0)
{
detail += L"缺少可用时间通道 " + std::to_wstring(skippedNoAxis) + L" 组;";
}
if (skippedEmpty > 0)
{
detail += L"空数据组 " + std::to_wstring(skippedEmpty) + L" 组;";
}
error = L"未在文件中找到可绘制的数值通道。";
if (!detail.empty())
{
error += L"(" + detail + L")";
}
return false;
}
if (progress)
{
LoadProgressInfo info;
info.ratio = 1.0;
info.hasBytes = true;
info.bytesRead = std::min<std::uint64_t>(file.BytesRead(), file.Size());
info.totalBytes = file.Size();
progress(info);
}
return true;
}
} // namespace jdi