Files
Just-DataInspector/Utilities/Src/Mf4Loader.cpp
T
DeepSeek V4.1 Flash 6eedbe3c5d 数据层大重构:文件→分组 / 时间轴一等公民 / 加载器抽象 + CSV 多时间轴 + MF4 支持
一、数据模型解耦(详见 Documents/refactor_report_2026-10-15.md 与 agent_mem §9.27)
1. DataFile → SignalGroup:删除 filePath / fileName / time / timeColumnName / timeMonotonic /
   minTime / maxTime —— 「分组」是应用内的逻辑容器,加载完成后与磁盘文件再无关系。
2. 新增 TimeAxis(name / time / minTime / maxTime / monotonic / finiteCount),一个分组可含 1..N 条;
   SignalData 新增 axisIndex 指向所属时间轴,采样值与时间的对应关系自包含。
3. SignalData 删除冗余的 rawHeader(name 的副本)与 unit(由列名猜出、全仓无人使用);
   名字只保留 name(源名称)+ displayName(用户重命名),Title() 统一展示。
4. SignalRef::fileIndex → groupIndex;DataSet:AddGroup/RemoveGroup/GroupAt/GroupCount/Groups()/
   SetGroupDisplayName/RemapSelectionAfterGroupRemoval + 新增 Axis(ref);
   TabConfig:SetGroupVisible/GroupHasVisible/GroupAllVisible/RemapAfterGroupRemoval +
   RowKind::GROUP / CMD_GROUP_*。
5. 采样与统计 API 首参由 const DataFile& 改为 const TimeAxis&:
   SampleSignalAt / SampleDisplayedSignalAt / FindNearestSample(Utilities)与
   SampleSpacingPixels / VisibleIndexRange / EdgeIndexRange / MeanFilterValueAt / WeakPeakValueAt /
   EdgeCountsFor / EdgeCenterCountsFor / EdgeScanFor(PlotView)。
6. 视图层:ResolvedSignal = { data, style, group, axis, visible };PolylineCacheEntry::file→group;
   EdgeScanState::file→axis(统计身份 = 时间轴 + 信号);
   ComputeVisibleTimeBounds 只统计可见信号真正用到的时间轴。

二、加载层抽象(格式与模型彻底解耦)
- 新增 Utilities/Inc/DataTable.hpp:DataTable(通用列优先表格)+ LoadProgress/Info +
  TableBlock(一个时间轴块)+ LoadedSource(源文件全部块)。
- 新增 Utilities/Inc|Src/DataLoader.*:唯一入口 LoadSignalSource 按扩展名分发 +
  IsSupportedSignalFile + SignalFileDialogFilter。
- 新增 Utilities/Inc|Src/CsvLoader.*:CSV 解析 + 多时间轴切分 + 各轴按自己的时间列裁剪。
- 新增 Utilities/Inc|Src/Mf4Loader.*:ASAM MDF v4 解析。
- 删除 Utilities/Inc/CsvTable.hpp 与 Utilities/Src/CsvTable.cpp。
- 唯一装配点 BuildSignalGroup(LoadedSource&&, paletteBase, group, error, progress):
  时间轴去重合并 + 信号命名 + 调色板取色 + 统计与粗粒度包络;新增格式只需写一个加载器。

三、CSV 多时间轴(修正用户报告的缺陷)
- 只有名为 t/time/timestamp/simulation time/时间/时间轴 的列是时间轴(判据收紧,
  不再用「包含 time」以免把 timeout 误判成时间轴)。
- 每个时间列开启一个新块,其后直到下一个时间列之前的列都属于该块;各块按自己的时间列裁剪
  ⇒ 支持各轴长度不同(Multi_Timeline.CSV 实测切出 3 条轴,长度 7/4/7);
  **不再把其它 time 列当成信号显示**。
- 完全没有时间列时保留旧行为(第一列当时间轴,单轴)。

四、MF4 支持(新功能)
- 加载后新建一个分组、组名 = 文件名,文件内全部信号归入该分组。
- 每个通道组(CG)是一条时间轴,内容相同的时间轴自动合并(实测 27 个 CG → 3 条轴 / 124 条信号)。
- 实现 MDF v4 块解析(HD/DG/CG/CN/CC/DT)、1–64 位整数(任意位偏移、含符号扩展)与
  32/64 位 IEEE 浮点解码、无效位(inval bit)处理、线性换算 phys = cc_val[0] + cc_val[1]*raw。
- TX 文本块同时兼容规范写法(U32 长度 + UTF-16/UTF-8)与 CANape 的「无长度前缀 NUL 结尾裸文本」。
- 未实现(跳过该组):压缩块 ##DZ/##HL/##SD、##DL 数据列表、字符串/MIME 通道。

五、顺带修复
- Application::Initialize:窗口类已注册(ERROR_CLASS_ALREADY_EXISTS)时复用而不是弹模态错误框
  (旧实现让同一进程第二次构造 Application 永久阻塞);Shutdown 注销窗口类并销毁窗口。
- UI 文案统一到「分组」:选中该分组全部信号 / 取消该分组全部信号 / 关闭该分组(从工作区移除)/
  请关闭部分分组或信号后重试。/ 尚未加载数据,请打开数据文件 / 打开数据文件以查看波形 /
  (可同时加载多个文件,各自成为一个分组);对话框过滤器增加 *.mf4/*.mdf。

六、验证(全部离屏:BELOW_NORMAL 优先级 + SW_HIDE 窗口 + 只注入合成消息)
- 新增 .preflight/perf/datacheck_main.cpp:69/69 通过(单轴 CSV 回归、多时间轴 CSV、MF4 组名/信号数/
  时间轴/主时间轴 ns→s/setSpeed 首样本与原始字节人工核对、DataSet 不变量、派生信号沿用源轴、
  MF4 集成 124 信号并一格 60~68ms/帧)。
- 新增 .preflight/perf/edgecheck_main.cpp(ASCII-only,替代被编码破坏的 panelcheck):46/46 通过
  (「调整」符号规则走真实菜单路径、边沿统计按帧推进/不饿死/逐位一致/无鼠标输入也完成、
  面板两列等宽与信号名限宽、弱峰复位控件)。
- 既有 .preflight/perf/verify_main.cpp(已适配新 API):47/47,另有 2 条本机未同步的旧规则断言
  (derived offsetTime kept / mouse in lane1 lower half),与本轮无关。
- make clean && make all 零警告。
2026-10-15 03:48:58 +08:00

821 lines
25 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 "Utf8Util.hpp"
#include <windows.h>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#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`。 */
RATIONAL = 2 /**< 有理式(未支持,按恒等处理)。 */
};
/**
* @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 读取一段原始字节。 */
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;
}
return std::fread(dst, 1, bytes, m_file) == bytes;
}
/** @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` 块里的文本(MDF 的 TEXT 块)。 */
std::wstring TextBlock(std::uint64_t offset) const
{
std::wstring text;
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;
};
/** @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; /**< 无效位的绝对位偏移。 */
bool syncMaster = false; /**< `cn_sync_type == 1`:时间主通道。 */
bool linearConversion = false; /**< 是否带线性换算。 */
double offset = 0.0; /**< 线性换算常数项。 */
double factor = 1.0; /**< 线性换算系数(无法换算时为 1)。 */
/** 数值有效性:只有整数与浮点通道可以绘制。 */
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 解析一个 CN 块为读取方案。 */
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);
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.syncMaster = (sync == 1);
// 换算:只支持“无换算”与“线性”。
const std::uint64_t cc = file.Link(cn, 4);
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;
}
}
}
// 通道类型 2 = 虚拟主通道(CANape 的时间轴),同样可以取值。
spec.numeric = IsNumericType(spec.dataType) && spec.bitCount > 0 && spec.bitCount <= 64;
(void)type;
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 一个通道组(CG)解析出的时间轴 + 信号列。 */
struct ChannelGroupData
{
std::wstring timeName; /**< 时间列名(主通道名或 `time`)。 */
std::vector<ChannelSpec> channels; /**< 非时间通道。 */
std::vector<double> time; /**< 时间轴数值。 */
std::vector<std::vector<double>> values; /**< 每个非时间通道的采样值。 */
};
/**
* @brief 读取一个通道组的全部记录(流式,按 4MB 分块)。
*
* @param [IN] file 文件访问器。
* @param [IN] dt 数据块偏移(`##DT`),记录从 `DataOffset(dt)` 开始连续存放。
* @param [IN] cycles 记录条数。
* @param [IN] recordBytes 单条记录字节数。
* @param [IN,OUT] group 输出(`timeName`/`channels` 已填好,函数填充 `time`/`values`)。
*/
bool ReadChannelGroupRecords(const MdfFile& file, std::uint64_t dt, std::uint64_t cycles,
std::size_t recordBytes, std::size_t masterIndex,
ChannelGroupData& group)
{
if (cycles == 0 || recordBytes == 0)
{
return false;
}
const std::uint64_t dataStart = file.DataOffset(dt);
const std::uint64_t payload = file.BlockLength(dt) - (dataStart - dt);
if (payload < cycles * recordBytes)
{
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));
std::vector<unsigned char> buffer(kReadChunkBytes);
std::uint64_t done = 0;
while (done < cycles)
{
const std::uint64_t remainingRecords = cycles - done;
const std::uint64_t wantBytes = std::min<std::uint64_t>(
remainingRecords * recordBytes, static_cast<std::uint64_t>(buffer.size()));
const std::uint64_t wantRecords = wantBytes / recordBytes;
if (wantRecords == 0)
{
break;
}
const std::size_t bytes = static_cast<std::size_t>(wantRecords * recordBytes);
if (!file.ReadAt(dataStart + done * recordBytes, buffer.data(), bytes))
{
return false;
}
for (std::uint64_t r = 0; r < wantRecords; ++r)
{
const unsigned char* record = buffer.data() + r * recordBytes;
const std::size_t row = static_cast<std::size_t>(done + r);
// 时间主通道。
{
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[row] = value;
}
// 各信号通道。
for (std::size_t c = 0; c < group.channels.size(); ++c)
{
if (c == masterIndex)
{
continue;
}
const ChannelSpec& spec = group.channels[c];
double 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][row] = value;
}
}
done += wantRecords;
}
return true;
}
/**
* @brief 从通道组里选出时间主通道。
* @return 主通道下标;找不到时返回 `channels.size()`(表示没有可用时间轴)。
*/
std::size_t SelectMasterChannel(const std::vector<ChannelSpec>& channels)
{
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;
}
std::wstring lower;
lower.reserve(channels[i].name.size());
for (const wchar_t c : channels[i].name)
{
lower.push_back((c >= L'A' && c <= L'Z') ? static_cast<wchar_t>(c - L'A' + L'a') : c);
}
if (lower == L"time" || lower == L"t" || lower == L"timestamp")
{
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);
std::size_t groupCount = 0;
WalkChain(file, firstDg, "##DG", kMaxGroups,
[&](std::uint64_t dg)
{
++groupCount;
const std::uint64_t cg = file.Link(dg, 1);
const std::uint64_t dt = file.Link(dg, 2);
if (!file.IsBlock(cg, "##CG") || !file.IsBlock(dt, "##DT"))
{
return true; // 跳过不完整的数据组
}
const std::uint64_t cgData = file.DataOffset(cg);
const std::uint64_t cycles = file.U64(cgData + 8);
if (cycles == 0)
{
return true;
}
const std::uint64_t payload = file.BlockLength(dt) - (file.DataOffset(dt) - dt);
// 解析通道链。
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())
{
return true;
}
// 记录长度:优先用「数据块大小 / 记录数」反推(最可靠),
// 否则按通道覆盖的位宽向上取整到 4 字节。
std::size_t recordBytes = 0;
if (payload % cycles == 0 && payload / cycles > 0)
{
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)
{
return true;
}
const std::size_t masterIndex = SelectMasterChannel(channels);
if (masterIndex >= channels.size())
{
return true; // 没有可用时间轴通道
}
ChannelGroupData group;
group.timeName = channels[masterIndex].name;
if (group.timeName.empty())
{
group.timeName = L"time";
}
group.channels = std::move(channels);
if (!ReadChannelGroupRecords(file, dt, cycles, recordBytes, masterIndex, group))
{
return true;
}
// 组织成一个时间轴块:时间列 + 其余数值通道。
TableBlock block;
block.timeHeader = group.timeName;
block.table.headers.push_back(group.timeName);
block.table.columns.push_back(std::move(group.time));
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]));
}
if (block.table.columns.size() < 2)
{
return true; // 只有时间列:没有可绘制信号
}
block.table.rowCount = static_cast<std::size_t>(cycles);
source.blocks.push_back(std::move(block));
if (progress)
{
LoadProgressInfo info;
info.ratio = (groupCount > 0) ? std::min(1.0, static_cast<double>(groupCount) / 64.0)
: 0.0;
info.hasBytes = false;
if (!progress(info))
{
return false;
}
}
return true;
});
if (source.blocks.empty())
{
error = L"未在文件中找到可绘制的数值通道。";
return false;
}
if (progress)
{
LoadProgressInfo info;
info.ratio = 1.0;
progress(info);
}
return true;
}
} // namespace jdi