按「性能 / BUG / 错误 / 过期注释 / 命名与实际含义不符」四项对全仓做了一次审查, 四个只读子审查交叉核对后逐条回到源码确认再改。 一、数据层真 BUG - MatH5:U8/U16/U32/U64 用逐字节 Cached() 拼装,而 Cached 只服务「完全落在同一个 64KB 块内」的请求 => 跨块时静默读成 0(U32 整段为 0、U64 高 32 位为 0),分块 B-tree 的 chunk 偏移/子节点地址被破坏且无任何报错。新增 ScalarLE():命中缓存用缓存, 否则退回一次 ReadAt;U64 顺带从 8 次缓存查找降到 1 次。 - Mf4Loader::IsBlock 未校验链接表是否落在块内 => DataOffset 越界,随后 block_len - (data - offset) 在 uint64 上下溢,得到一个约 2^64 的"可用字节数", 下游 vector(usable+1) 直接 bad_alloc。同时把 offset + bytes > size 改成不产生回绕的写法。 - DecodeRawValue 的整数路径:bit_offset%8 != 0 且位宽 64 时需 9 个字节,先拼成 64 位 再整体右移会把最高字节静默丢弃 => 改为逐字节带位偏移累加。 - cg_cycle_count 加「数据段总字节 / 记录字节」上限(压缩段按 deflate 最大膨胀比估算)。 - CsvLoader 表头分支同样跳过 #/% 注释行(否则整份文件按 1 列解析并被丢弃)。 - MatLoader:double -> int 的越界转换加了范围检查;普通对象递归加深度上限与成环检测。 - Utf8Util::BuildTicks:double -> long long 越界转换前先判商是否落在 ±9e18 内。 二、界面 BUG - 工具栏按钮永远不显示悬停高亮:Paint 读的 Button::hover 全仓从未赋值,改为按 m_hoverIndex 判断。 - 菜单打开时"取消底层拖拽"对每个消息都执行,把菜单自己的 Target::EDIT_FIELD 也清掉 => 菜单内联输入框无法拖拽框选。排除 EDIT_FIELD。 - GdiplusShutdown 之前未释放 PlotView 的 GDI+ 位图(Application 是进程单例,成员析构在 WinMain 之后)=> 新增 PlotView::ReleaseGraphicsResources() 并在 Shutdown 里调用。 - WM_KILLFOCUS 现在会提交标签页/绘图区编辑并复位捕获与拖拽状态。 - WM_LBUTTONUP 不再抢走中键平移的鼠标捕获。 - TextEdit:被过滤器拒绝的字符返回 false;无选区时 Ctrl+X 只复制、不再清空整段文本。 - SignalPanel::InvalidateRows 作废编辑/待确认/改名计时等行下标状态。 - EndFilterEdit 改走 ApplyFilterText(否则输入纯空格后回车,列表仍停在筛选结果)。 - 新增 SignalBadgeVisible 作为唯一判据(原来命中只看"可见",绘制还要求"已分配网格")。 - 折起动画结束后夹取滚动量;双击分组名改名时清掉该组正在播放的折起动画。 - ScrollToSignal 复用 SignalScrollTarget(含悬浮组名避让)。 - RowNameRect 与绘制使用同一最小宽度(原来退化时返回空矩形)。 - 分隔条拖拽上限改用与 Relayout 同一公式;UpdateDragCursor 补 SPLITTER/EDIT_FIELD。 - SignalPanel 里几个守卫改为判断真正被解引用的指针;分组行 ✕ 也返回手型光标。 三、PlotView - FindStyleOnlyLanes 改用完整的 SceneStyleEntry::operator==(原来只比 4 个字段, 另一个网格同时改"调整"时带着旧像素被当成最新)。 - 宽基底图层不再烘焙拖拽光晕(LaneLayerHash 不含它),改由 PaintHoverHighlight 每帧叠加。 - PaintLaneLegend 提前返回前清掉本网格的命中矩形(否则能点中看不见的图例条目)。 - 面板主体改为完全不透明(消除平移后 4% 透出的旧波形,零额外代价)。 - PAN / Shift+滚轮改走 SetTimeView,使"自动点线"判据随平移重算。 - 内联输入框命中字体与绘制统一为 monoSmall。 - 派生曲线取值与窗口按 SourceIndexFor 映射(水平镜像时不再读到镜像另一端)。 - EdgePanelWidth 补 style/axis 空指针守卫;二分结果夹到 count。 四、清理 - 删除 Toolbar::ActionLabel、Theme::statusBg、PopupMenu 的 editResult 死机制、 Toolbar::Layout 的重复 clear()、HitRow 的恒假分支、OnDoubleClick 的重复判定、 PaintUtil 里孤立的注释块。 - 修正一批过期/错误注释(包络带"不做加宽"、LaneLayerHash 的图例说法、 均值滤波曲线的窗口说法、样式区行数与「调整」按钮顺序、FIT_ALL 退出缩放模式、 RemoveSignal 的 ✕ 位置、.mat/.dat 格式文档、MatH5 的"映射视图"、DataModel 的 "点线/被移除文件"、SignalPanel 的"行高连续缩小"、TextEdit 的 PaintUtil 引用等)。 五、二进制体积评估(按要求只评估,未做压缩修改) - release 809,472 字节;.text 652KB、.xdata+.pdata 约 75KB、.rdata 42KB、 .rsrc 20KB、.idata 11.5KB。 - 去掉 -ld3d11 -ldwmapi -ld3dcompiler(仓内无任何调用)实测 0 字节收益 (静态库只拉取被引用的对象)=> 删链接库不是压缩手段,只是配置整洁。 - 仓内已无 <iostream>/<sstream>/<filesystem>/<regex>/<locale> 等体积炸弹。 - 结论:已无明显的浪费可捡;要显著变小只能改变取舍(动态 CRT / UPX / 砍功能), 需要用户决策,本轮未动。 验证 - editcheck 20/20、round3check 31/31、round4check 34/34、round5check 5/5、convcheck 18/18。 - check 136/137:唯一失败项经 git stash 回到改动前复现同一数值,属该探针自身的既有 不稳定项(取决于前置用例留下的缓存/时序),与本轮改动无关。 - 真实数据:4 个 MF4 => 283/8/18/24 个信号;.mat 样例 8 信号、 带无障碍对照-防夹力曲线数据 8 信号、BIG_MAT.mat 5 信号(私有内存 +741MB)。 - make release / make debug 零警告。 文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 171-180 条硬约束 与文末本轮小节(含体积评估数据)。
2079 lines
70 KiB
C++
2079 lines
70 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 <utility>
|
||
#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 /**< 值区间 → 文本(`cc_val` 交错 `(下限,上限)` ↔ `cc_ref[i]` 文本)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief `##CA` 的数组类型(`ca_type`)。
|
||
*
|
||
* 三者**物理布局相同**(`ca_storage == 0` 时元素在本记录内按位宽连续排布,
|
||
* 实测 CANape 写出的 25 个 `##CA` 块全部如此),差别只在语义:
|
||
* `0` 普通数组 / `1` 缩放轴(特性曲线的断点) / `2` 区间轴(map 的一维断点)。
|
||
*/
|
||
enum class ArrayKind : int
|
||
{
|
||
ARRAY = 0, /**< 普通数值数组。 */
|
||
SCALING_AXIS = 1, /**< 缩放轴(断点表)。 */
|
||
INTERVAL_AXIS = 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 已从文件实际读取的字节数(用于「已读 / 总大小」进度)。
|
||
*
|
||
* 随机访问会重复读取元数据,因此这个值可能超过文件大小;
|
||
* 上报时必须自行夹到 `Size()`。
|
||
*/
|
||
std::uint64_t BytesRead() const { return m_bytesRead; }
|
||
|
||
/** @brief 读取一段原始字节。 */
|
||
bool ReadAt(std::uint64_t offset, void* dst, std::size_t bytes) const
|
||
{
|
||
// ⚠️ 不能写 `offset + bytes > m_size`:损坏文件里 offset 接近 2^64 时会回绕,
|
||
// 校验因此“通过”然后去读一个不存在的偏移。
|
||
if (m_file == nullptr || offset > m_size || bytes > m_size - offset)
|
||
{
|
||
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 判断某个偏移是否为一个有效块的开头(且标识匹配)。
|
||
*
|
||
* ⚠️ **必须同时校验链接表落在块内**(`24 + 8*link_count <= block_len`):
|
||
* 否则 `DataOffset()` 会算到块尾之外,随后 `block_len - (data - offset)` 在 `uint64`
|
||
* 上发生**下溢**,得到一个约 2^64 的“可用字节数”,下游 `vector(usable+1)` 直接抛
|
||
* `bad_alloc` —— 一个损坏的链接数就能让加载崩溃。
|
||
*
|
||
* @param [IN] offset 候选偏移。
|
||
* @param [IN] id 期望的 4 字节标识(如 "##CN");空字符串表示只校验偏移合法性。
|
||
*/
|
||
bool IsBlock(std::uint64_t offset, const char* id) const
|
||
{
|
||
if (offset == 0 || offset > m_size || kBlockHeaderBytes > m_size - offset)
|
||
{
|
||
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;
|
||
}
|
||
const std::uint64_t length = BlockLength(offset);
|
||
if (length < kBlockHeaderBytes || length > m_size - offset)
|
||
{
|
||
return false;
|
||
}
|
||
const std::uint64_t links = LinkCount(offset);
|
||
// 链接表必须真的落在块内(并限制在合理上限内,防止 length 被算成天文数字)。
|
||
return links <= (length - kBlockHeaderBytes) / 8u;
|
||
}
|
||
|
||
/**
|
||
* @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,3]`(1 时间 / 2 角度 / 3 距离):本组的主通道。 */
|
||
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;
|
||
/** 值 → 文本枚举表(`cc_type == 7` 精确值 / `cc_type == 8` 值区间,可能为空)。 */
|
||
SignalEnumText enumText;
|
||
/** @brief `##CA` 数组元素总数(0 = 普通标量通道)。 */
|
||
std::size_t arrayElements = 0;
|
||
/** @brief `##CA` 存储方式(0 = 元素连续存放在本记录内)。 */
|
||
int arrayStorage = 0;
|
||
/** @brief `##CA` 数组类型(`ca_type`,见 `ArrayKind`)。 */
|
||
int arrayType = 0;
|
||
/** @brief `##CA` 各维长度(仅用于生成 `name[i][j]` 这样的名字)。 */
|
||
std::vector<std::size_t> arrayDims;
|
||
/**
|
||
* @brief `##CA` 各维的周期数(`ca_cycle_count`);空 = 文件没写这个字段。
|
||
*
|
||
* 规范里 `ca_cycle_count[i] > 1` 表示该维的元素被**拆到多条记录**里,
|
||
* 这时单个记录内并不含全部元素 ⇒ 不能按「记录内连续」展开(见 `ExpandArrayChannel`)。
|
||
* ⚠️ 实测文件里这个字段**不存在**(`##CA` 块长度恰好到 `dim_size[]` 就结束),
|
||
* 因此读取前必须先按块长度做边界检查,不能盲读(盲读会拿到下一个块头当作数字)。
|
||
*/
|
||
std::vector<std::size_t> arrayCycles;
|
||
/** @brief `##CA` 的元素起始字节基址(`ca_byte_offset_base`,仅用于一致性校验与诊断)。 */
|
||
std::size_t arrayByteOffsetBase = 0;
|
||
/** @brief `##CA` 的无效位位置(`ca_inval_bit_pos`,仅记录用于诊断)。 */
|
||
std::size_t arrayInvalidBitPos = 0;
|
||
/** 数值有效性:只有整数与浮点通道可以绘制。 */
|
||
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 位整数再整体右移:`bit_offset % 8 != 0` 且位宽为 64 时
|
||
// 需要 9 个字节(65–71 位),先拼装会让最高字节整体左移出 64 位而被**静默丢弃**,
|
||
// 于是把一个 64 位计数读成“高位少 4~7 位”的错误值。这里改成
|
||
// 「每个字节按 `8*i - shift` 直接落到目标位上」,最右端不足一个字节的部分右移对齐,
|
||
// 因此任何位宽/位偏移组合都不会丢数据。
|
||
const std::size_t shift = spec.bitOffset % 8u;
|
||
const bool littleEndian = (spec.dataType == DataType::UINT_LE || spec.dataType == DataType::INT_LE);
|
||
std::uint64_t raw = 0;
|
||
for (std::size_t i = 0; i < byteCount; ++i)
|
||
{
|
||
// 该字节的第一位在“整体位流”里的位置(大端时字节序反向)。
|
||
const std::size_t position = littleEndian ? (8u * i) : (8u * (byteCount - 1u - i));
|
||
const std::uint64_t bits = buffer[i];
|
||
if (position >= shift)
|
||
{
|
||
const std::size_t left = position - shift;
|
||
if (left < 64u)
|
||
{
|
||
raw |= bits << left;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
raw |= bits >> (shift - position);
|
||
}
|
||
}
|
||
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;
|
||
case ConversionType::VALUE_RANGE_TO_TEXT:
|
||
// 值区间 → 文本:`cc_val[]` 交错存放 `(下限, 上限)`,`cc_ref[4 + i]` 是第 i 段的文本
|
||
//(最后一个引用是「默认文本」,这里不使用)。
|
||
// ⚠️ 区间**成对**出现在一个 1-D 表里:周期描述的是**一维断点表**的相邻输出值
|
||
//(例如 0–10 → 'A'、10–20 → 'B'),故取相交的第一个区间。
|
||
// ⚠️ 段数必须是 `cc_val_count / 2`(奇数个参数说明表是残缺的,整张表丢弃)。
|
||
if (valCount >= 2 && (valCount % 2) == 0)
|
||
{
|
||
const std::uint64_t ccLinkCount = file.LinkCount(cc);
|
||
const std::size_t ranges = static_cast<std::size_t>(valCount / 2);
|
||
for (std::size_t index = 0; index < ranges; ++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; // 没有文本的区间不进入枚举表
|
||
}
|
||
double lower = file.F64(ccData + 24u + 16u * index);
|
||
double upper = file.F64(ccData + 24u + 16u * index + 8u);
|
||
if (!std::isfinite(lower) || !std::isfinite(upper))
|
||
{
|
||
continue;
|
||
}
|
||
if (lower > upper)
|
||
{
|
||
std::swap(lower, upper);
|
||
}
|
||
spec.enumText.rangeMin.push_back(lower);
|
||
spec.enumText.rangeMax.push_back(upper);
|
||
spec.enumText.rangeTexts.push_back(std::move(text));
|
||
}
|
||
if (!spec.enumText.HasRanges())
|
||
{
|
||
spec.enumText = SignalEnumText();
|
||
}
|
||
}
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
// `##CA`(通道数组):一个通道装一整条曲线/矩阵(标定的特性曲线、map)。
|
||
// 数据段布局(实测 CANape 写出的文件):
|
||
// +0 ca_type(U8) / +1 ca_storage(U8) / +2 ndim(U16) / +4 flags(U32)
|
||
// +8 ca_byte_offset_base(U16) / +10 ca_inval_bit_pos(U16)
|
||
// +16 dim_size[](U64 × ndim) 然后紧接 cycle_count[](U32 × ndim,**可缺省**)
|
||
// ⚠️ `cycle_count` 在实测文件里**不存在**(块长度到 `dim_size[]` 就结束)。
|
||
// 必须按块长度做边界检查,否则会把下一个块头当成数字读进来(实测得到 1481909027)。
|
||
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);
|
||
const std::uint64_t caBlockLength = file.BlockLength(ca);
|
||
const std::uint64_t caUsable =
|
||
(caBlockLength > (caData - ca)) ? (caBlockLength - (caData - ca)) : 0;
|
||
spec.arrayType = (caUsable >= 1) ? static_cast<int>(file.U8(caData + 0)) : 0;
|
||
spec.arrayStorage = (caUsable >= 2) ? static_cast<int>(file.U8(caData + 1)) : 0;
|
||
const std::uint16_t rank = (caUsable >= 4) ? file.U16(caData + 2) : 0;
|
||
if (caUsable >= 12)
|
||
{
|
||
spec.arrayByteOffsetBase = file.U16(caData + 8);
|
||
spec.arrayInvalidBitPos = file.U16(caData + 10);
|
||
}
|
||
|
||
std::size_t total = (rank > 0) ? 1 : 0;
|
||
bool dimsOk = (rank > 0);
|
||
spec.arrayDims.clear();
|
||
spec.arrayCycles.clear();
|
||
for (std::uint16_t axis = 0; axis < rank && axis < 8; ++axis)
|
||
{
|
||
if (16u + 8u * (static_cast<std::uint64_t>(axis) + 1u) > caUsable)
|
||
{
|
||
dimsOk = false; // 维度表被截断:不做任何猜测
|
||
break;
|
||
}
|
||
const std::size_t dim =
|
||
static_cast<std::size_t>(file.U64(caData + 16u + 8u * axis));
|
||
if (dim == 0)
|
||
{
|
||
dimsOk = false;
|
||
break;
|
||
}
|
||
spec.arrayDims.push_back(dim);
|
||
// 元素总数太大时不再累乘(只用它判断「是否可展开」)。
|
||
if (total <= (1u << 24))
|
||
{
|
||
total *= dim;
|
||
if (total > (1u << 24))
|
||
{
|
||
total = (1u << 24) + 1u;
|
||
}
|
||
}
|
||
}
|
||
|
||
// `ca_cycle_count[]`(可缺省):只有真的读得到才记录。
|
||
bool cyclesPresent = (rank > 0);
|
||
for (std::uint16_t axis = 0; axis < rank && axis < 8; ++axis)
|
||
{
|
||
const std::uint64_t cycleOffset =
|
||
16u + 8u * static_cast<std::uint64_t>(rank) + 4u * static_cast<std::uint64_t>(axis);
|
||
if (cycleOffset + 4u > caUsable)
|
||
{
|
||
cyclesPresent = false;
|
||
break;
|
||
}
|
||
spec.arrayCycles.push_back(static_cast<std::size_t>(file.U32(caData + cycleOffset)));
|
||
}
|
||
if (!cyclesPresent || spec.arrayCycles.size() != spec.arrayDims.size())
|
||
{
|
||
spec.arrayCycles.clear(); // 缺省 = 每维 1 个周期(整个数组在本记录内)
|
||
}
|
||
|
||
spec.arrayElements = dimsOk ? total : 0;
|
||
}
|
||
|
||
// 通道类型 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 字节)。
|
||
* 展开后每个元素都是普通标量信号,因此自动获得光标取值、枚举、图例等全部能力。
|
||
*
|
||
* 展开的**必要条件**(任何一条不满足就原样保留,只显示元素 0,绝不猜):
|
||
* 1. `ca_storage == 0`:元素在本记录内连续存放;
|
||
* 2. `ca_type` ∈ {0 数组, 1 缩放轴, 2 区间轴}:三者物理布局相同(实测 25 个 `##CA` 块全部如此),
|
||
* 未知类型(> 2)不展开;
|
||
* 3. `cn_bit_count` ∈ [1, 64]:位宽是算元素步进的**唯一依据**,为 0 时不知道元素之间隔多少位
|
||
* (VLSD 通道的 `bit_count` 常写 0,那类数据在 `##SD` 里,与"记录内连续"完全不是一套逻辑);
|
||
* 4. `ca_cycle_count` 缺失或每一维都为 1:否则元素被拆到**多条记录**里,单个记录内不含全部元素;
|
||
* 5. 元素能放进本记录:`bit_offset + 元素数 × 位宽 ≤ 记录位数`(这是真正的完整性校验,
|
||
* 替代了「猜一个元素数上限」);
|
||
* 6. 规模护栏(防止一个巨型数组把信号列表/网格撑爆):
|
||
* 元素数 ≤ `kMaxArrayElements`,且 `元素数 × 记录数` ≤ `kMaxArrayCells`。
|
||
*
|
||
* @param [IN,OUT] spec 已解析的通道方案(不展开时原样平移进 `out`)。
|
||
* @param [IN] cycles 该通道组的记录条数(`cg_cycle_count`,用于规模护栏)。
|
||
* @param [IN] recordBytes 单条记录字节数(`cg_data_bytes`,0 = 未知则跳过第 5 条校验)。
|
||
* @param [OUT] out 展开后的通道列表。
|
||
*/
|
||
void ExpandArrayChannel(ChannelSpec&& spec, std::uint64_t cycles, std::size_t recordBytes,
|
||
std::vector<ChannelSpec>& out)
|
||
{
|
||
/** 单个通道最多展开成多少条信号。 */
|
||
constexpr std::size_t kMaxArrayElements = 16384;
|
||
/** 元素总数 × 记录数的上限(≈ 生成的数据点数 × 8 字节的内存/绘制代价)。 */
|
||
constexpr std::size_t kMaxArrayCells = 32u << 20;
|
||
|
||
const std::size_t count = spec.arrayElements;
|
||
bool expandable = (count > 1);
|
||
if (expandable && spec.arrayStorage != 0)
|
||
{
|
||
expandable = false; // 元素不在本记录内(跨通道/跨通道组)
|
||
}
|
||
if (expandable && (spec.arrayType < 0 || spec.arrayType > 2))
|
||
{
|
||
expandable = false; // 未知 `ca_type`
|
||
}
|
||
if (expandable && (spec.bitCount == 0 || spec.bitCount > 64))
|
||
{
|
||
expandable = false; // 位宽未知
|
||
}
|
||
if (expandable)
|
||
{
|
||
for (const std::size_t perAxis : spec.arrayCycles)
|
||
{
|
||
if (perAxis > 1)
|
||
{
|
||
expandable = false; // 元素被拆到多条记录
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (expandable && count > kMaxArrayElements)
|
||
{
|
||
expandable = false;
|
||
}
|
||
if (expandable && cycles > 1 && count > kMaxArrayCells / static_cast<std::size_t>(cycles))
|
||
{
|
||
expandable = false;
|
||
}
|
||
if (expandable && recordBytes > 0)
|
||
{
|
||
const std::uint64_t needBits = static_cast<std::uint64_t>(spec.bitOffset) +
|
||
static_cast<std::uint64_t>(count) *
|
||
static_cast<std::uint64_t>(spec.bitCount);
|
||
if (needBits > static_cast<std::uint64_t>(recordBytes) * 8u)
|
||
{
|
||
expandable = false; // 放不进本记录 ⇒ 元素位置不符合「记录内连续」
|
||
}
|
||
}
|
||
if (!expandable)
|
||
{
|
||
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;
|
||
// 维度后缀:按 `dim_size[]` 从末维向前取余,因此 1/2/3/… 维都得到 `[i][j][k]`。
|
||
std::wstring suffix;
|
||
if (dims.size() >= 2)
|
||
{
|
||
std::vector<std::size_t> axisIndex(dims.size(), 0);
|
||
std::size_t remainder = index;
|
||
for (std::size_t axis = dims.size(); axis-- > 0;)
|
||
{
|
||
const std::size_t size = dims[axis];
|
||
axisIndex[axis] = (size > 0) ? (remainder % size) : 0;
|
||
remainder = (size > 0) ? (remainder / size) : 0;
|
||
}
|
||
for (const std::size_t value : axisIndex)
|
||
{
|
||
suffix += L"[";
|
||
suffix += std::to_wstring(value);
|
||
suffix += 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);
|
||
|
||
// 解析通道链。
|
||
// `##CA` 数组通道就地展开:需要 `cg_data_bytes`(单个记录容量)与 `cycles`(规模护栏)。
|
||
std::vector<ChannelSpec> channels;
|
||
WalkChain(file, file.Link(cg, 1), "##CN", kMaxChannelsPerGroup,
|
||
[&](std::uint64_t cn)
|
||
{
|
||
ChannelSpec spec;
|
||
if (ParseChannel(file, cn, spec))
|
||
{
|
||
// 一个通道装整条曲线时,旧实现只能看到元素 0。
|
||
ExpandArrayChannel(std::move(spec), cycles, static_cast<std::size_t>(cgDataBytes),
|
||
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;
|
||
}
|
||
|
||
// ⚠️ `cg_cycle_count` 是文件里的数字,**不能无条件相信**:损坏或伪造的文件可以声明
|
||
// 几十亿条记录,而 `ReadChannelGroupRecords` 会按它一次性
|
||
// `assign(cycles × 通道数)` 个 double(几十 GB)⇒ 直接 bad_alloc 或把机器拖死。
|
||
// 这里用「记录数据总字节数」做上限:非压缩段的总载荷是精确值;
|
||
// 压缩段的解压上限按 deflate 的理论最大膨胀比(≈1032:1)估算。
|
||
{
|
||
const std::uint64_t payloadBound = anyCompressed ? payload * 1032ull : payload;
|
||
const std::uint64_t recordBound = payloadBound / static_cast<std::uint64_t>(recordBytes) + 2ull;
|
||
if (cycles > recordBound)
|
||
{
|
||
++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
|