1) 修复上轮记录的两个已知问题
- MatLoader: LoadPlainObject 不再为拿 dims 调 ReadDataset(整块解码第二次),
改用 ReadDatasetInfo;普通 v7.3 大矩阵的时间/内存不再翻倍。
- MdfFile::TextBlock 注释与实现对齐:真的支持 ##MD(UTF-8 XML,无长度前缀)。
2) 信号枚举信息(以 TestData/NEW-MF4.mf4 为目标)
- 新增 SignalEnumText(DataTable.hpp):values[] + texts[] + Empty()/Find(),
命名用 enumText —— 它描述"取值代表哪个状态",不是单位。
- DataTable::columnEnumText 与 columns 同下标;SignalData::enumText 由
BuildSignalGroup 移动赋值;其它格式(CSV/.mat)保持为空。
- MF4 cc_type = 7:cc_val[i](F64,数据段 +24)↔ cc_ref[4 + i](##TX/##MD 文本),
最后一个引用是默认文本,不使用。
- 界面只在**光标取值标注**追加 " = <枚举文本>"(等号两侧各一个空格、无尖括号),
查表用采样原值(ApplyDrawTransform 之前);常规状态显示一个像素都不变。
- 实测:NEW-MF4.mf4 8 信号 / 4 条带枚举(32+5+32+5),MF4-Record.MF4 另有 2 种(12/32)。
3) MF4 支持范围
- ##SI 采样索引:只有索引 ≥2 条且全部 < 周期数才生效(逐记录有效性位图)。
实测部分工具给每个通道挂只含常量 513/4 的 ##SI,必须忽略,否则整列变 NaN。
- ##SD + VLSD(cn_type == 1):##SD 数据段是「UINT32 长度 + 字节」样本序列,
记录里放 64 位偏移;AppendSignalData 递归展开 SD/DZ/DL/HL(单通道上限 512MB),
DecodeVlsdValue 先按直接偏移、失败再按 >>1 回退,取样本第一个元素。
- CN 的 cn_data / md_unit / md_comment 下标按链接总数反推:8 链接(MDF 4.0 风格)
= 换算[4]/数据[5]/单位[6]/注释[7],9 链接(4.1)= [5]/[6]/[7]/[8]。
实测 Simulink 导出的“4.10”仍写 8 链接,不能写死 Link(cn,4/5)。
- 主通道:cn_sync_type 1/2/3(时间/角度/距离)→ 名字 t/time/timestamp/zeit/时间
(含 t[...])→ 第一个数值通道兜底;cg_flags 第 2 位(远程主通道)跳过 sync 判定。
- 记录长度优先 cg_data_bytes(CG 数据段 +24);旧实现把 U8(cgData) 当
cg_record_id_len,其实是 cg_flags 低字节(总线事件位)。
- 进度:先走 DG 链得到真实组数(不再写死 64),hasBytes = true,bytesRead 取
MdfFile::ReadAt 累计(夹到文件大小);4MB 分块读取检查取消。
- ##DZ:数据段前有 28 字节头(original_type/zip_type/param/original_size/zip_size),
旧实现整段当 zlib 流是错的;只支持纯 deflate(zip_type = 0)。
- 一个信号都没有时,错误信息附带「布局无法解析 / 缺少时间通道 / 空数据组」计数。
4) .mat:内存映射 → 随机读 + 元数据块缓存(消除 SEH 风险)
- MatHdf5::Open 改为 CreateFileW(FILE_FLAG_SEQUENTIAL_SCAN) + ReadFile;
删除 MappedBytes,改为 ReadAt / Cached(16 × 64KB LRU + 上次命中槽)/ Tag /
ReadData / ReadCString。I/O 失败只会变成 false,不再触发页访问异常。
- 数据读取:连续布局按 4MB 切片;分块布局按需 + 64KB 对齐。
固定 4MB 预读在交错存放的分块数据集上会把读盘量放大到 2.2 倍(2874MB/1293MB),
改后 1301MB。
- 关键提速:dims=[N]、chunk=[32768,1] 归一化后 rank=2、每行 1 字节,逐行算坐标
是 12.9 亿次迭代;新增「rank == 2 且 chunkDims[1] == dims[1]」快路径(整块一次
sink)。BIG_MAT.mat(1.29GB / 5 × 16,142,471):21.5s → 7.9s,私有内存 +741MB。
- 进度:Open 立刻上报「0 / 总大小」,之后每读一块回调 ReadProgress(bytesRead,total)
→ MatLoader 映射到 0.02–0.98 并单调夹紧(字节进度与逐流解码取较大值),收尾 1.0。
遮罩层百分比不再一上来就 100%;取消会中止(StreamDataset 返回 false),不留 NaN 空洞。
- 连续布局数据不足(文件被截断)现在明确失败,不再静默截短。
验证(全部离屏:无可见窗口、不注入鼠标键盘屏幕)
- 新增 .preflight/perf/loadercheck(数据层)32/32:
NEW-MF4 8 信号 / 4 条枚举 / 枚举文本逐条核对 / 9396 与 94 行;
MF4-Record 124 信号 / 3 轴 / 4 条枚举;data-sample.mat 4 组 8 信号 / 行数;
BIG_MAT.mat 5 信号 × 16,142,471 + 进度(total 已知、ratio 单调、bytes 递增、
起点 ≤0.05、终值 1.0、更新 ≥10 次)。
- 新增 .preflight/perf/enumui(真实 Application + SW_HIDE)12/12:
只显示 gVR_WinOpenReq[0],光标落在枚举采样点上 —— 标签宽度 265px;
临时清空 enumText 后 25px;差值 240px 与 " = WinOpenReqAtDrvr_Idle" 的
UiMeasureTextWidth 完全一致;恢复后宽度回到 265px。
- 回归:datacheck 69/69、matcheck 21/21、matui 15/15(loading bytes 达到总大小)、
verify 49/49。
- make release / make debug 零警告。Build/Release/TargetCheck.exe = 820,224 字节
(上轮 785,920,+34KB)。(Target.exe 正在被用户运行的程序占用,链接步骤未覆盖它。)
1500 lines
48 KiB
C++
1500 lines
48 KiB
C++
#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`)中本项目实现的子集。 */
|
||
enum class ConversionType : int
|
||
{
|
||
NONE = 0, /**< 无换算(恒等)。 */
|
||
LINEAR = 1, /**< 线性:`phys = c0 + c1 * raw`。 */
|
||
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;
|
||
bool linearConversion = false; /**< 是否带线性换算。 */
|
||
double offset = 0.0; /**< 线性换算常数项。 */
|
||
double factor = 1.0; /**< 线性换算系数(无法换算时为 1)。 */
|
||
SignalEnumText enumText; /**< 值 → 文本枚举表(`cc_type == 7`,可能为空)。 */
|
||
/** 数值有效性:只有整数与浮点通道可以绘制。 */
|
||
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 把 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 采样(取样本的第一个元素)。
|
||
*
|
||
* 记录里放的是 64 位偏移。两种写法都接受:
|
||
* * 直接偏移(部分写盘器);
|
||
* * `偏移 << 1 | 标志位`(标志位为 1 时表示长度,此时按 `>> 1` 回退)。
|
||
* 判据是「长度前缀自洽」——哪种解释能在 `##SD` 缓冲里定位到合法样本就用哪种。
|
||
*
|
||
* @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)
|
||
{
|
||
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;
|
||
}
|
||
const std::size_t elementBytes = (spec.bitCount + 7u) / 8u;
|
||
if (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.vlsd = false;
|
||
return DecodeRawValue(signalData.data() + offset + 4u, length, 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();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 换算:支持「无换算」「线性」与「值 → 文本」三类。
|
||
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)
|
||
{
|
||
spec.linearConversion = true;
|
||
spec.offset = file.F64(ccData + 24);
|
||
spec.factor = file.F64(ccData + 32);
|
||
if (spec.factor == 0.0 && spec.offset == 0.0)
|
||
{
|
||
// 换算表未填写:按恒等处理,避免整列变成 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)
|
||
{
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 通道类型 2 = 虚拟主通道(CANape 的时间轴),同样可以取值。
|
||
spec.numeric = IsNumericType(spec.dataType) && spec.bitCount > 0 && spec.bitCount <= 64;
|
||
return true;
|
||
}
|
||
|
||
/** @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 = master.linearConversion ? (master.offset + master.factor * value) : 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 = spec.linearConversion ? (spec.offset + spec.factor * value) : 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)
|
||
{
|
||
for (std::size_t i = 0; i < channels.size(); ++i)
|
||
{
|
||
if (channels[i].syncMaster && channels[i].numeric)
|
||
{
|
||
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))
|
||
{
|
||
channels.push_back(std::move(spec));
|
||
}
|
||
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
|