标定量(特性曲线 / 公式)与「一个通道装整条曲线」的数组通道是 CANape / Simulink 导出 MF4 时最常见的两种表达,之前只支持线性换算、数组通道整列丢失。 一、数值换算(cc_type 2/3/4/5/6) - ChannelSpec 把 linearConversion/offset/factor 换成 ConversionType(0-8)+ conversionValues[],ParseChannel 按 cc_type 校验参数个数后填充。 - 新增 ApplyConversion: 1 线性 v0 + v1*raw 2 有理式 (v0 + v1*raw) / (v2 + v3*raw) 3 代数式 (v0*raw^2 + v1*raw + v2) / (v3*raw^2 + v4*raw + v5) 4 表插值(交错 (键,值) 对,区间内线性插值、区间外夹紧) 5 表阶梯(同一张表,取键 <= raw 的最后一段) 6 区间映射((下限,上限,物理值) 三元组) - 参数个数不符 / 表为空 / 分母为 0 一律返回原始值(宁可显示未标定值, 也不显示编造值);线性换算 v0 == v1 == 0 视为未配置而非恒为 0。 二、##CA 数组通道(一个通道装整条曲线) - 解析 CN 链接[1] 的 ##CA 块:+0 ca_type / +1 ca_storage / +2 ndim(U16) / +4 flags(U32) / +16 dim_size[U64](步长 8,Simulink 与 CANape 实测一致)。 - 新增 ExpandArrayChannel:仅当 ca_storage == 0(元素在记录内首尾相连)、 1 < 元素数 <= 1024、bit_count != 0 时展开成多列,bit_offset += i*bit_count, 名字 name[i] / name[i][j],换算与枚举复制给每个元素。 - MF4-Record.MF4 信号数 124 -> 283(9 点特性曲线读到 6000..14000, 二维 map 读到 175/120/180...)。 三、VLSD 数值型 - 数值型不再要求 cn_bit_count > 0;新增 InferVlsdElementBits(按样本长度 优先整除 64,再 32/16/8),DecodeVlsdValue 只按一个元素做越界检查并取 样本首个元素;主通道候选排除 VLSD(其记录里存的是 ##SD 偏移)。 - 实测 4 个真实文件里 VLSD 通道只有 Comment(字符串、cycles = 0), 数值型 VLSD 数量为 0,故本次不改变现有文件的输出。 验证(全离屏,不占用鼠标/键盘/屏幕) - 新增 .preflight/convcheck:现场合成一个最小 MDF4(1 DG / 1 CG / 2 记录 / recordBytes = 72),一次覆盖 cc_type 1-6、##CA、数值 VLSD => 11/11。 - A/B:强置 conversion = NONE 且关闭数组展开 => 8 项如实失败,还原后 11/11。 - 回归:check 137/137、editcheck 20/20、round3check 31/31、round4check 34/34; 真实文件信号数 283 / 8 / 18 / 24 与预期一致。 - make release / make debug 零警告;TargetCheck.exe 804,352 字节(785.5 KiB)。 文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 160-164 条硬约束 与文末本轮小节(含 VLSD / 单位在文件中的实际情况报告)。
1839 lines
59 KiB
C++
1839 lines
59 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`)。
|
||
*
|
||
* ⚠️ 数值换算(0–6)用于把记录里的原始值转成物理值;`7/8` 是「值 → 文本」,
|
||
* 走 `ChannelSpec::enumText`(只在光标读数里展示,不改变数值)。
|
||
*/
|
||
enum class ConversionType : int
|
||
{
|
||
NONE = 0, /**< 无换算(恒等)。 */
|
||
LINEAR = 1, /**< 线性:`phys = c0 + c1 * raw`。 */
|
||
RATIONAL = 2, /**< 有理式:`phys = (c0 + c1*raw) / (c2 + c3*raw)`。 */
|
||
ALGEBRAIC = 3, /**< 代数式:`phys = (c0*raw² + c1*raw + c2) / (c3*raw² + c4*raw + c5)`。 */
|
||
TAB_INTP = 4, /**< 值→值查表(点之间**线性插值**,即特性曲线)。 */
|
||
TAB = 5, /**< 值→值查表(**不插值**,取阶梯值)。 */
|
||
TAB_RANGE = 6, /**< 值区间→值(`[min,max] → phys`,区间外不换算)。 */
|
||
VALUE_TO_TEXT = 7, /**< 值 → 枚举文本(`cc_val[i]` ↔ `cc_ref[i]` 文本)。 */
|
||
VALUE_RANGE_TO_TEXT = 8 /**< 值区间 → 文本(未支持,按恒等处理)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 只读的 MDF 文件访问器(按需 seek + read,不把整个文件读进内存)。
|
||
*/
|
||
class MdfFile
|
||
{
|
||
public:
|
||
~MdfFile() { Close(); }
|
||
|
||
bool Open(const std::wstring& path, std::wstring& error)
|
||
{
|
||
m_file = _wfopen(path.c_str(), L"rb");
|
||
if (m_file == nullptr)
|
||
{
|
||
error = L"无法打开文件,请确认路径与访问权限。";
|
||
return false;
|
||
}
|
||
if (_fseeki64(m_file, 0, SEEK_END) != 0)
|
||
{
|
||
error = L"无法定位文件末尾。";
|
||
return false;
|
||
}
|
||
const long long size = _ftelli64(m_file);
|
||
if (size <= 0)
|
||
{
|
||
error = L"文件内容为空。";
|
||
return false;
|
||
}
|
||
m_size = static_cast<std::uint64_t>(size);
|
||
|
||
// ID 块:0..7 = "MDF ",8..15 = 版本号,例如 "4.10 "。
|
||
char id[8] = {};
|
||
char version[8] = {};
|
||
if (!ReadAt(0, id, sizeof(id)) || std::memcmp(id, "MDF", 3) != 0)
|
||
{
|
||
error = L"不是有效的 MDF 文件(缺少 MDF 标识)。";
|
||
return false;
|
||
}
|
||
if (!ReadAt(8, version, sizeof(version)) || version[0] != '4')
|
||
{
|
||
error = L"仅支持 MDF v4 格式(当前文件不是 v4)。";
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
void Close()
|
||
{
|
||
if (m_file != nullptr)
|
||
{
|
||
std::fclose(m_file);
|
||
m_file = nullptr;
|
||
}
|
||
}
|
||
|
||
std::uint64_t Size() const { return m_size; }
|
||
|
||
/**
|
||
* @brief 已从文件实际读取的字节数(用于「已读 / 总大小」进度)。
|
||
*
|
||
* 随机访问会重复读取元数据,因此这个值可能超过文件大小;
|
||
* 上报时必须自行夹到 `Size()`。
|
||
*/
|
||
std::uint64_t BytesRead() const { return m_bytesRead; }
|
||
|
||
/** @brief 读取一段原始字节。 */
|
||
bool ReadAt(std::uint64_t offset, void* dst, std::size_t bytes) const
|
||
{
|
||
if (m_file == nullptr || offset + bytes > m_size)
|
||
{
|
||
return false;
|
||
}
|
||
if (_fseeki64(m_file, static_cast<long long>(offset), SEEK_SET) != 0)
|
||
{
|
||
return false;
|
||
}
|
||
const bool ok = std::fread(dst, 1, bytes, m_file) == bytes;
|
||
if (ok)
|
||
{
|
||
m_bytesRead += bytes;
|
||
}
|
||
return ok;
|
||
}
|
||
|
||
/** @brief 读取一个小端标量(最多 8 字节)。越界返回 0。 */
|
||
std::uint64_t Scalar(std::uint64_t offset, std::size_t bytes) const
|
||
{
|
||
unsigned char buffer[8] = {};
|
||
if (bytes == 0 || bytes > 8 || !ReadAt(offset, buffer, bytes))
|
||
{
|
||
return 0;
|
||
}
|
||
std::uint64_t value = 0;
|
||
for (std::size_t i = 0; i < bytes; ++i)
|
||
{
|
||
value |= static_cast<std::uint64_t>(buffer[i]) << (8u * i);
|
||
}
|
||
return value;
|
||
}
|
||
|
||
std::uint32_t U32(std::uint64_t offset) const
|
||
{
|
||
return static_cast<std::uint32_t>(Scalar(offset, 4));
|
||
}
|
||
std::uint16_t U16(std::uint64_t offset) const
|
||
{
|
||
return static_cast<std::uint16_t>(Scalar(offset, 2));
|
||
}
|
||
std::uint8_t U8(std::uint64_t offset) const
|
||
{
|
||
return static_cast<std::uint8_t>(Scalar(offset, 1));
|
||
}
|
||
std::uint64_t U64(std::uint64_t offset) const { return Scalar(offset, 8); }
|
||
|
||
double F64(std::uint64_t offset) const
|
||
{
|
||
const std::uint64_t bits = U64(offset);
|
||
double value = 0.0;
|
||
std::memcpy(&value, &bits, sizeof(value));
|
||
return value;
|
||
}
|
||
|
||
/** @brief 读取块标识(4 字节 ASCII)。 */
|
||
bool BlockId(std::uint64_t offset, char out[5]) const
|
||
{
|
||
out[0] = out[1] = out[2] = out[3] = '?';
|
||
out[4] = '\0';
|
||
return ReadAt(offset, out, 4);
|
||
}
|
||
|
||
/** @brief 块总长度(含块头)。 */
|
||
std::uint64_t BlockLength(std::uint64_t offset) const { return U64(offset + 8); }
|
||
/** @brief 块内链接数量。 */
|
||
std::uint64_t LinkCount(std::uint64_t offset) const { return U64(offset + 16); }
|
||
/** @brief 第 `index` 个链接(绝对文件偏移,0 表示空)。 */
|
||
std::uint64_t Link(std::uint64_t offset, std::uint64_t index) const
|
||
{
|
||
if (index >= LinkCount(offset))
|
||
{
|
||
return 0;
|
||
}
|
||
return U64(offset + kBlockHeaderBytes + 8u * index);
|
||
}
|
||
/** @brief 块的数据段起始偏移。 */
|
||
std::uint64_t DataOffset(std::uint64_t offset) const
|
||
{
|
||
return offset + kBlockHeaderBytes + 8u * LinkCount(offset);
|
||
}
|
||
|
||
/**
|
||
* @brief 判断某个偏移是否为一个有效块的开头(且标识匹配)。
|
||
* @param [IN] offset 候选偏移。
|
||
* @param [IN] id 期望的 4 字节标识(如 "##CN");空字符串表示只校验偏移合法性。
|
||
*/
|
||
bool IsBlock(std::uint64_t offset, const char* id) const
|
||
{
|
||
if (offset == 0 || offset + kBlockHeaderBytes > m_size)
|
||
{
|
||
return false;
|
||
}
|
||
char actual[5] = {};
|
||
if (!BlockId(offset, actual))
|
||
{
|
||
return false;
|
||
}
|
||
if (id != nullptr && id[0] != '\0' && std::memcmp(actual, id, 4) != 0)
|
||
{
|
||
return false;
|
||
}
|
||
return BlockLength(offset) >= kBlockHeaderBytes && offset + BlockLength(offset) <= m_size;
|
||
}
|
||
|
||
/**
|
||
* @brief 读取 `##TX` / `##MD` 块里的文本。
|
||
*
|
||
* ⚠️ 此前注释写的是「`##TX` / `##MD`」但实现只接受 `##TX`,于是 `##MD`
|
||
*(XML 元数据:单位、注释、显示名等)一律返回空。现在两种块都读:
|
||
* * `##TX`:兼容规范的「长度 + UTF-16 / UTF-8」与 CANape 的「NUL 结尾裸文本」;
|
||
* * `##MD`:数据段是 XML(UTF-8),没有长度前缀 ⇒ 直接转成宽字符串。
|
||
*/
|
||
std::wstring TextBlock(std::uint64_t offset) const
|
||
{
|
||
std::wstring text;
|
||
if (IsBlock(offset, "##MD"))
|
||
{
|
||
const std::uint64_t data = DataOffset(offset);
|
||
const std::uint64_t usable = BlockLength(offset) - (data - offset);
|
||
if (usable == 0)
|
||
{
|
||
return text;
|
||
}
|
||
std::vector<char> buffer(static_cast<std::size_t>(usable) + 1u, '\0');
|
||
if (!ReadAt(data, buffer.data(), static_cast<std::size_t>(usable)))
|
||
{
|
||
return text;
|
||
}
|
||
std::size_t length = static_cast<std::size_t>(usable);
|
||
while (length > 0 && buffer[length - 1] == '\0')
|
||
{
|
||
--length;
|
||
}
|
||
return Utf8ToWide(buffer.data(), static_cast<int>(length));
|
||
}
|
||
if (!IsBlock(offset, "##TX"))
|
||
{
|
||
return text;
|
||
}
|
||
const std::uint64_t data = DataOffset(offset);
|
||
const std::uint64_t usable = BlockLength(offset) - (data - offset);
|
||
if (usable < 2)
|
||
{
|
||
return text;
|
||
}
|
||
|
||
// MDF 规范里 TX 的数据段是「长度 + 文本」,长度可能是字符数(UTF-16)或字节数
|
||
//(UTF-8,MDF 4.1 起)。但部分工具(如 CANape 16)会**直接写 NUL 结尾的
|
||
// 原始文本、不带长度前缀**,因此这里按「先按规范、再按裸文本回退」的顺序解析。
|
||
if (usable >= 4)
|
||
{
|
||
const std::uint64_t declared = U32(data);
|
||
if (declared > 0 && 4u + 2u * declared <= usable)
|
||
{
|
||
// 长度前缀 + UTF-16 代码单元。
|
||
std::vector<char> buffer(static_cast<std::size_t>(2u * declared));
|
||
if (!ReadAt(data + 4, buffer.data(), buffer.size()))
|
||
{
|
||
return text;
|
||
}
|
||
text.reserve(static_cast<std::size_t>(declared));
|
||
for (std::uint64_t i = 0; i < declared; ++i)
|
||
{
|
||
const wchar_t c =
|
||
static_cast<wchar_t>(static_cast<unsigned char>(buffer[2 * i]) |
|
||
(static_cast<unsigned char>(buffer[2 * i + 1]) << 8));
|
||
if (c == L'\0')
|
||
{
|
||
break;
|
||
}
|
||
text.push_back(c);
|
||
}
|
||
return text;
|
||
}
|
||
if (declared > 0 && 4u + declared <= usable)
|
||
{
|
||
// 长度前缀 + UTF-8 字节。
|
||
std::vector<char> buffer(static_cast<std::size_t>(declared) + 1u, '\0');
|
||
if (!ReadAt(data + 4, buffer.data(), static_cast<std::size_t>(declared)))
|
||
{
|
||
return text;
|
||
}
|
||
return Utf8ToWide(buffer.data(), static_cast<int>(declared));
|
||
}
|
||
}
|
||
|
||
// 回退:数据段直接是 NUL 结尾的文本(UTF-16 或 UTF-8)。
|
||
std::vector<unsigned char> buffer(static_cast<std::size_t>(usable));
|
||
if (!ReadAt(data, buffer.data(), buffer.size()))
|
||
{
|
||
return text;
|
||
}
|
||
const bool utf16 = (buffer.size() >= 2 && buffer[1] == 0 && buffer[0] != 0);
|
||
if (utf16)
|
||
{
|
||
for (std::size_t i = 0; i + 1 < buffer.size(); i += 2)
|
||
{
|
||
const wchar_t c = static_cast<wchar_t>(buffer[i] | (buffer[i + 1] << 8));
|
||
if (c == L'\0')
|
||
{
|
||
break;
|
||
}
|
||
text.push_back(c);
|
||
}
|
||
return text;
|
||
}
|
||
std::string raw;
|
||
for (const unsigned char c : buffer)
|
||
{
|
||
if (c == '\0')
|
||
{
|
||
break;
|
||
}
|
||
raw += static_cast<char>(c);
|
||
}
|
||
return Utf8ToWide(raw.data(), static_cast<int>(raw.size()));
|
||
}
|
||
|
||
private:
|
||
FILE* m_file = nullptr;
|
||
std::uint64_t m_size = 0;
|
||
mutable std::uint64_t m_bytesRead = 0; /**< 已实际读取的字节数(进度上报用)。 */
|
||
};
|
||
|
||
/** @brief 一个通道的读取方案(已从 CN 块解析出来)。 */
|
||
struct ChannelSpec
|
||
{
|
||
std::wstring name; /**< 通道名(`cn_tx_name`)。 */
|
||
std::size_t bitOffset = 0; /**< 记录内的起始位偏移(= byte_offset*8 + bit_offset)。 */
|
||
std::size_t bitCount = 0; /**< 位宽。 */
|
||
DataType dataType = DataType::UINT_LE; /**< 原始数据类型。 */
|
||
bool allInvalid = false; /**< `cn_flags & 1`:整列无效。 */
|
||
bool hasInvalidBit = false; /**< `cn_flags & 2`:有无效位。 */
|
||
std::size_t invalidBitPos = 0; /**< 无效位的绝对位偏移。 */
|
||
int type = 0; /**< `cn_type`:0 定长 / 1 VLSD / 2 虚拟主通道 / 3 同步通道。 */
|
||
int syncType = 0; /**< `cn_sync_type`:1 时间 / 2 角度 / 3 距离。 */
|
||
bool syncMaster = false; /**< `cn_sync_type >= 1`:本组的主通道。 */
|
||
bool vlsd = false; /**< `cn_type == 1`:值存放在 `##SD` 里(记录内是偏移)。 */
|
||
std::uint64_t signalData = 0; /**< VLSD 通道的 `##SD` 块地址(`cn_data`)。 */
|
||
/** `##SI` 采样索引(`cn_si_source`);空表示每个记录都有采样。 */
|
||
std::vector<std::uint64_t> sampleIndex;
|
||
/** @brief 换算方式(`cc_type` 的数值子集;`VALUE_TO_TEXT` 走 `enumText`)。 */
|
||
ConversionType conversion = ConversionType::NONE;
|
||
/**
|
||
* @brief `##CC` 的 `cc_val[]` 参数。
|
||
*
|
||
* 线性 2 个、有理式 4 个、代数式 6 个;
|
||
* 查表类(4/5)为交错的 `(raw, phys)` 点对,区间表(6)为 `(min, max, phys)` 三元组。
|
||
*/
|
||
std::vector<double> conversionValues;
|
||
SignalEnumText enumText; /**< 值 → 文本枚举表(`cc_type == 7`,可能为空)。 */
|
||
/** @brief `##CA` 数组元素总数(0 = 普通标量通道)。 */
|
||
std::size_t arrayElements = 0;
|
||
/** @brief `##CA` 存储方式(0 = 元素连续存放在本记录内)。 */
|
||
int arrayStorage = 0;
|
||
/** @brief `##CA` 各维长度(仅用于生成 `name[i][j]` 这样的名字)。 */
|
||
std::vector<std::size_t> arrayDims;
|
||
/** 数值有效性:只有整数与浮点通道可以绘制。 */
|
||
bool numeric = false;
|
||
};
|
||
|
||
/** @brief 判断数据类型是否为可绘制的数值类型。 */
|
||
bool IsNumericType(DataType type)
|
||
{
|
||
return type == DataType::UINT_LE || type == DataType::UINT_BE || type == DataType::INT_LE ||
|
||
type == DataType::INT_BE || type == DataType::FLOAT_LE || type == DataType::FLOAT_BE;
|
||
}
|
||
|
||
/**
|
||
* @brief 按位从记录缓冲中取出一个数值。
|
||
*
|
||
* 支持 1–64 位整数(任意位偏移)与 32/64 位浮点(字节对齐)。
|
||
*
|
||
* @param [IN] record 记录起始指针。
|
||
* @param [IN] recordBytes 记录长度(字节),用于边界检查。
|
||
* @param [IN] spec 通道读取方案。
|
||
* @return 原始数值;越界或类型不支持时返回 NaN。
|
||
*/
|
||
double DecodeRawValue(const unsigned char* record, std::size_t recordBytes, const ChannelSpec& spec)
|
||
{
|
||
const std::size_t bitEnd = spec.bitOffset + spec.bitCount;
|
||
if (spec.bitCount == 0 || spec.bitCount > 64 || bitEnd > recordBytes * 8u)
|
||
{
|
||
return kMissingValue;
|
||
}
|
||
const std::size_t byteStart = spec.bitOffset / 8u;
|
||
const std::size_t byteCount = (bitEnd - byteStart * 8u + 7u) / 8u;
|
||
unsigned char buffer[9] = {};
|
||
std::memcpy(buffer, record + byteStart, byteCount);
|
||
|
||
if (spec.dataType == DataType::FLOAT_LE || spec.dataType == DataType::FLOAT_BE)
|
||
{
|
||
if (spec.bitOffset % 8u != 0u || (spec.bitCount != 32u && spec.bitCount != 64u))
|
||
{
|
||
return kMissingValue;
|
||
}
|
||
if (spec.dataType == DataType::FLOAT_LE)
|
||
{
|
||
if (spec.bitCount == 32u)
|
||
{
|
||
std::uint32_t bits = 0;
|
||
std::memcpy(&bits, buffer, 4);
|
||
float value = 0.0f;
|
||
std::memcpy(&value, &bits, 4);
|
||
return static_cast<double>(value);
|
||
}
|
||
double value = 0.0;
|
||
std::memcpy(&value, buffer, 8);
|
||
return value;
|
||
}
|
||
if (spec.bitCount == 32u)
|
||
{
|
||
std::uint32_t bits = (static_cast<std::uint32_t>(buffer[0]) << 24) |
|
||
(static_cast<std::uint32_t>(buffer[1]) << 16) |
|
||
(static_cast<std::uint32_t>(buffer[2]) << 8) |
|
||
static_cast<std::uint32_t>(buffer[3]);
|
||
float value = 0.0f;
|
||
std::memcpy(&value, &bits, 4);
|
||
return static_cast<double>(value);
|
||
}
|
||
std::uint64_t bits = 0;
|
||
for (int i = 0; i < 8; ++i)
|
||
{
|
||
bits = (bits << 8) | static_cast<std::uint64_t>(buffer[i]);
|
||
}
|
||
double value = 0.0;
|
||
std::memcpy(&value, &bits, 8);
|
||
return value;
|
||
}
|
||
|
||
// 整数:先按字节序拼成一个 64 位无符号数,再按位偏移右移并解释符号。
|
||
std::uint64_t raw = 0;
|
||
if (spec.dataType == DataType::UINT_LE || spec.dataType == DataType::INT_LE)
|
||
{
|
||
for (std::size_t i = byteCount; i-- > 0;)
|
||
{
|
||
raw = (raw << 8) | static_cast<std::uint64_t>(buffer[i]);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
for (std::size_t i = 0; i < byteCount; ++i)
|
||
{
|
||
raw = (raw << 8) | static_cast<std::uint64_t>(buffer[i]);
|
||
}
|
||
}
|
||
const std::size_t shift = spec.bitOffset % 8u;
|
||
raw >>= shift;
|
||
if (spec.bitCount < 64u)
|
||
{
|
||
raw &= (1ull << spec.bitCount) - 1ull;
|
||
}
|
||
|
||
const bool signedType = (spec.dataType == DataType::INT_LE || spec.dataType == DataType::INT_BE);
|
||
if (signedType && spec.bitCount < 64u && (raw & (1ull << (spec.bitCount - 1u))) != 0u)
|
||
{
|
||
// 符号扩展。
|
||
raw |= ~((1ull << spec.bitCount) - 1ull);
|
||
}
|
||
if (signedType)
|
||
{
|
||
return static_cast<double>(static_cast<std::int64_t>(raw));
|
||
}
|
||
return static_cast<double>(raw);
|
||
}
|
||
|
||
/**
|
||
* @brief 按 `##CC` 换算把原始值转换成物理值。
|
||
*
|
||
* 支持 `cc_type`:
|
||
* * `0` 无换算 / `1` 线性;
|
||
* * `2` 有理式 `phys = (v0 + v1·raw) / (v2 + v3·raw)`;
|
||
* * `3` 代数式 `phys = (v0·raw² + v1·raw + v2) / (v3·raw² + v4·raw + v5)`;
|
||
* * `4` 值→值查表(**线性插值**,即标定的特性曲线);
|
||
* * `5` 值→值查表(**不插值**,取 `raw_i ≤ raw` 的最后一个值);
|
||
* * `6` 区间表(`(min,max,phys)` 三元组,命中区间才换算)。
|
||
*
|
||
* ⚠️ 参数个数与类型不匹配时**原样返回 raw**:宁可显示原始计数,
|
||
* 也绝不按错误的公式算出一个"看起来像物理值"的数字。
|
||
* ⚠️ 对于 `4/5/6`,本项目假定 `cc_val` 按「键在前、值在后」交错存放
|
||
* (与 ASAM MDF 4.1 规范一致);键单调递增。
|
||
*/
|
||
double ApplyConversion(const ChannelSpec& spec, double raw)
|
||
{
|
||
if (!std::isfinite(raw))
|
||
{
|
||
return raw;
|
||
}
|
||
const std::vector<double>& v = spec.conversionValues;
|
||
switch (spec.conversion)
|
||
{
|
||
case ConversionType::NONE:
|
||
case ConversionType::VALUE_TO_TEXT:
|
||
case ConversionType::VALUE_RANGE_TO_TEXT:
|
||
return raw;
|
||
case ConversionType::LINEAR:
|
||
if (v.size() >= 2)
|
||
{
|
||
return v[0] + v[1] * raw;
|
||
}
|
||
return raw;
|
||
case ConversionType::RATIONAL:
|
||
if (v.size() >= 4)
|
||
{
|
||
const double denominator = v[2] + v[3] * raw;
|
||
if (denominator == 0.0 || !std::isfinite(denominator))
|
||
{
|
||
return raw;
|
||
}
|
||
return (v[0] + v[1] * raw) / denominator;
|
||
}
|
||
return raw;
|
||
case ConversionType::ALGEBRAIC:
|
||
if (v.size() >= 6)
|
||
{
|
||
const double denominator = v[3] * raw * raw + v[4] * raw + v[5];
|
||
if (denominator == 0.0 || !std::isfinite(denominator))
|
||
{
|
||
return raw;
|
||
}
|
||
return (v[0] * raw * raw + v[1] * raw + v[2]) / denominator;
|
||
}
|
||
return raw;
|
||
case ConversionType::TAB_INTP:
|
||
{
|
||
// 交错点对:(key0, value0, key1, value1, ...),key 递增。
|
||
if (v.size() < 4 || (v.size() % 2) != 0)
|
||
{
|
||
return raw;
|
||
}
|
||
const std::size_t count = v.size() / 2;
|
||
if (raw <= v[0])
|
||
{
|
||
return v[1];
|
||
}
|
||
if (raw >= v[(count - 1) * 2])
|
||
{
|
||
return v[(count - 1) * 2 + 1];
|
||
}
|
||
for (std::size_t index = 0; index + 1 < count; ++index)
|
||
{
|
||
const double key0 = v[index * 2];
|
||
const double key1 = v[(index + 1) * 2];
|
||
if (raw >= key0 && raw <= key1)
|
||
{
|
||
if (key1 == key0)
|
||
{
|
||
return v[index * 2 + 1];
|
||
}
|
||
const double ratio = (raw - key0) / (key1 - key0);
|
||
return v[index * 2 + 1] + (v[(index + 1) * 2 + 1] - v[index * 2 + 1]) * ratio;
|
||
}
|
||
}
|
||
return raw;
|
||
}
|
||
case ConversionType::TAB:
|
||
{
|
||
if (v.size() < 4 || (v.size() % 2) != 0)
|
||
{
|
||
return raw;
|
||
}
|
||
const std::size_t count = v.size() / 2;
|
||
if (raw < v[0])
|
||
{
|
||
return v[1];
|
||
}
|
||
double result = v[1];
|
||
for (std::size_t index = 0; index < count; ++index)
|
||
{
|
||
if (raw >= v[index * 2])
|
||
{
|
||
result = v[index * 2 + 1];
|
||
}
|
||
else
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
case ConversionType::TAB_RANGE:
|
||
{
|
||
if (v.size() < 3 || (v.size() % 3) != 0)
|
||
{
|
||
return raw;
|
||
}
|
||
const std::size_t count = v.size() / 3;
|
||
for (std::size_t index = 0; index < count; ++index)
|
||
{
|
||
const double low = v[index * 3];
|
||
const double high = v[index * 3 + 1];
|
||
if (raw >= low && raw <= high)
|
||
{
|
||
return v[index * 3 + 2];
|
||
}
|
||
}
|
||
return raw;
|
||
}
|
||
default:
|
||
return raw;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 把 VLSD 通道的 `##SD`(信号数据)块内容读进内存。
|
||
*
|
||
* MDF4 的 VLSD 数据格式(实测 + 参考实现一致):
|
||
* * `##SD` 数据段是一串「`UINT32` 长度 + 该长度的字节」的样本;
|
||
* * 记录(`##DT`)在该通道的 `cn_byte_offset` 处放一个 **64 位偏移**,
|
||
* 指向样本长度前缀在 `##SD` 数据里的位置(偏移按各 `##SD` 数据段顺序累加)。
|
||
*
|
||
* 只有 VLSD 通道才需要这块数据,因此按通道按需加载,不做全局缓存。
|
||
*
|
||
* @param [IN] block 起始块(`##SD` / `##DZ` / `##DL` / `##HL`)。
|
||
* @param [IN,OUT] out 追加写入的逻辑字节流。
|
||
* @return 读到至少一段数据返回 true。
|
||
*/
|
||
bool AppendSignalData(const MdfFile& file, std::uint64_t block, std::vector<std::uint8_t>& out,
|
||
int depth)
|
||
{
|
||
if (block == 0 || depth > 8 || !file.IsBlock(block, ""))
|
||
{
|
||
return false;
|
||
}
|
||
// 防御:单个通道的 VLSD 数据超过 512MB 视为异常,避免损坏文件吃光内存。
|
||
constexpr std::size_t kMaxSignalDataBytes = 512u * 1024u * 1024u;
|
||
char id[5] = {};
|
||
file.BlockId(block, id);
|
||
const std::uint64_t data = file.DataOffset(block);
|
||
const std::uint64_t payload = file.BlockLength(block) - (data - block);
|
||
if (payload == 0)
|
||
{
|
||
return false;
|
||
}
|
||
if (std::memcmp(id, "##SD", 4) == 0)
|
||
{
|
||
if (out.size() + payload > kMaxSignalDataBytes)
|
||
{
|
||
return false;
|
||
}
|
||
const std::size_t base = out.size();
|
||
out.resize(base + static_cast<std::size_t>(payload));
|
||
if (!file.ReadAt(data, out.data() + base, static_cast<std::size_t>(payload)))
|
||
{
|
||
out.resize(base);
|
||
return false;
|
||
}
|
||
// `##SD` 可能还有同类型的后继块(链接 0)。
|
||
if (file.LinkCount(block) > 0)
|
||
{
|
||
AppendSignalData(file, file.Link(block, 0), out, depth + 1);
|
||
}
|
||
return true;
|
||
}
|
||
if (std::memcmp(id, "##DZ", 4) == 0)
|
||
{
|
||
// `##DZ` 数据段布局:original_type(4) + zip_type(1) + reserved(3) +
|
||
// param(4) + original_size(8) + zip_size(8) + 压缩数据。
|
||
const std::uint8_t zipType = file.U8(data + 4);
|
||
const std::uint64_t zipSize = file.U64(data + 20);
|
||
if (zipType != 0 || payload <= 28u) // 只支持纯 deflate(不支持转置/LZ4/ZSTD)
|
||
{
|
||
return false;
|
||
}
|
||
const std::uint64_t usable = std::min(payload - 28u, zipSize);
|
||
if (usable == 0 || out.size() + usable > kMaxSignalDataBytes)
|
||
{
|
||
return false;
|
||
}
|
||
std::vector<std::uint8_t> compressed(static_cast<std::size_t>(usable));
|
||
if (!file.ReadAt(data + 28u, compressed.data(), compressed.size()))
|
||
{
|
||
return false;
|
||
}
|
||
std::vector<std::uint8_t> inflated;
|
||
if (!InflateZlib(compressed.data(), compressed.size(), inflated))
|
||
{
|
||
return false;
|
||
}
|
||
out.insert(out.end(), inflated.begin(), inflated.end());
|
||
return true;
|
||
}
|
||
if (std::memcmp(id, "##DL", 4) == 0 || std::memcmp(id, "##HL", 4) == 0)
|
||
{
|
||
const std::uint64_t links = file.LinkCount(block);
|
||
bool any = false;
|
||
for (std::uint64_t index = 0; index < links && index < 1000000; ++index)
|
||
{
|
||
any = AppendSignalData(file, file.Link(block, index), out, depth + 1) || any;
|
||
}
|
||
return any;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* @brief 推断 VLSD 样本的元素位宽(`cn_bit_count` 为 0 时使用)。
|
||
*
|
||
* 相当一部分写盘器把 VLSD 通道的 `cn_bit_count` 写成 0(它描述的是"记录里那个
|
||
* 偏移字段",而偏移固定是 64 位)。此时按数据类别 + 样本长度推断:
|
||
* 浮点优先 8 字节(其次 4 字节),整数按 8 → 4 → 2 → 1 字节依次尝试。
|
||
*
|
||
* ⚠️ 长度同时是 8 与 4 的整数倍时(例如 4 个 float = 16 字节)无法区分,
|
||
* 这里取 8 字节(`double` 更常见)。只有 `cn_bit_count == 0` 才会走到这条路径。
|
||
*/
|
||
std::size_t InferVlsdElementBits(DataType type, std::size_t length)
|
||
{
|
||
const bool floating = (type == DataType::FLOAT_LE || type == DataType::FLOAT_BE);
|
||
if (length == 0)
|
||
{
|
||
return 0;
|
||
}
|
||
if (length % 8u == 0)
|
||
{
|
||
return 64;
|
||
}
|
||
if (length % 4u == 0)
|
||
{
|
||
return 32;
|
||
}
|
||
if (!floating && length % 2u == 0)
|
||
{
|
||
return 16;
|
||
}
|
||
return floating ? 0 : 8;
|
||
}
|
||
|
||
/**
|
||
* @brief 从记录中解出一个 VLSD 采样(取样本的第一个元素)。
|
||
*
|
||
* 记录里放的是 64 位偏移。两种写法都接受:
|
||
* * 直接偏移(部分写盘器);
|
||
* * `偏移 << 1 | 标志位`(标志位为 1 时表示长度,此时按 `>> 1` 回退)。
|
||
* 判据是「长度前缀自洽」——哪种解释能在 `##SD` 缓冲里定位到合法样本就用哪种。
|
||
*
|
||
* 元素位宽优先用 `cn_bit_count`;它为 0(常见)时按数据类型与样本长度推断
|
||
* (见 `InferVlsdElementBits`)。数值类型(无符号/有符号整数、32/64 位浮点)
|
||
* 全部支持;样本含多个元素时取**第一个元素**。
|
||
*
|
||
* @return 解出的数值;无法解释时返回 NaN。
|
||
*/
|
||
double DecodeVlsdValue(const unsigned char* record, std::size_t recordBytes, const ChannelSpec& spec,
|
||
const std::vector<std::uint8_t>& signalData)
|
||
{
|
||
if (signalData.empty())
|
||
{
|
||
return kMissingValue;
|
||
}
|
||
const std::size_t byteStart = spec.bitOffset / 8u;
|
||
if (byteStart + 8u > recordBytes)
|
||
{
|
||
return kMissingValue;
|
||
}
|
||
std::uint64_t raw = 0;
|
||
std::memcpy(&raw, record + byteStart, sizeof(raw));
|
||
for (int attempt = 0; attempt < 2; ++attempt)
|
||
{
|
||
const std::uint64_t offset = (attempt == 0) ? raw : (raw >> 1);
|
||
if (offset + 4u > signalData.size())
|
||
{
|
||
continue;
|
||
}
|
||
std::uint32_t length = 0;
|
||
std::memcpy(&length, signalData.data() + offset, sizeof(length));
|
||
if (static_cast<std::uint64_t>(length) > signalData.size() - offset - 4u)
|
||
{
|
||
continue;
|
||
}
|
||
if (length == 0)
|
||
{
|
||
return kMissingValue;
|
||
}
|
||
std::size_t elementBits = spec.bitCount;
|
||
if (elementBits == 0 || elementBits > 64u)
|
||
{
|
||
elementBits = InferVlsdElementBits(spec.dataType, static_cast<std::size_t>(length));
|
||
}
|
||
if (elementBits == 0 || elementBits > 64u)
|
||
{
|
||
return kMissingValue;
|
||
}
|
||
const std::size_t elementBytes = (elementBits + 7u) / 8u;
|
||
if (static_cast<std::size_t>(length) < elementBytes)
|
||
{
|
||
return kMissingValue;
|
||
}
|
||
ChannelSpec element = spec;
|
||
element.bitOffset = 0;
|
||
element.bitCount = elementBits;
|
||
element.vlsd = false;
|
||
// 只给"这一个元素"的字节数做边界检查(样本可能含多个元素)。
|
||
return DecodeRawValue(signalData.data() + offset + 4u, elementBytes, element);
|
||
}
|
||
return kMissingValue;
|
||
}
|
||
|
||
/**
|
||
* @brief 解析一个 CN 块为读取方案。
|
||
*
|
||
* ⚠️ **链接布局有两种写法**:MDF 4.0 写 8 个链接(没有 `cn_sr_source`),
|
||
* 4.1 写 9 个。二者共有的尾部 4 个链接是
|
||
* `cn_cc_conversion` / `cn_data` / `cn_md_unit` / `cn_md_comment`,
|
||
* 因此这里按**链接总数**反推换算/数据块下标,而不是写死 4/5。
|
||
*(实测 Simulink 导出的「4.10」文件仍使用 8 链接布局。)
|
||
*/
|
||
bool ParseChannel(const MdfFile& file, std::uint64_t cn, ChannelSpec& spec)
|
||
{
|
||
if (!file.IsBlock(cn, "##CN"))
|
||
{
|
||
return false;
|
||
}
|
||
const std::uint64_t data = file.DataOffset(cn);
|
||
const std::uint8_t type = file.U8(data + 0);
|
||
const std::uint8_t sync = file.U8(data + 1);
|
||
const std::uint8_t dataType = file.U8(data + 2);
|
||
const std::uint8_t bitOffset = file.U8(data + 3);
|
||
const std::uint32_t byteOffset = file.U32(data + 4);
|
||
const std::uint32_t bitCount = file.U32(data + 8);
|
||
const std::uint32_t flags = file.U32(data + 12);
|
||
const std::uint32_t invalidBit = file.U32(data + 16);
|
||
|
||
const std::uint64_t linkCount = file.LinkCount(cn);
|
||
// 8 链接布局:[4] = 换算、[5] = 数据块、[6] = 单位、[7] = 注释。
|
||
// 9 链接布局:[5] = 换算、[6] = 数据块、[7] = 单位、[8] = 注释。
|
||
const std::uint64_t conversionIndex = (linkCount >= 9) ? 5 : 4;
|
||
const std::uint64_t signalDataIndex = conversionIndex + 1;
|
||
|
||
spec.name = file.TextBlock(file.Link(cn, 2));
|
||
spec.dataType = static_cast<DataType>(dataType);
|
||
spec.bitCount = bitCount;
|
||
spec.bitOffset = static_cast<std::size_t>(byteOffset) * 8u + bitOffset;
|
||
spec.allInvalid = (flags & 0x1u) != 0u;
|
||
spec.hasInvalidBit = (flags & 0x2u) != 0u;
|
||
spec.invalidBitPos = invalidBit;
|
||
spec.type = static_cast<int>(type);
|
||
spec.syncType = static_cast<int>(sync);
|
||
spec.syncMaster = (sync >= 1 && sync <= 3);
|
||
spec.vlsd = (type == 1);
|
||
if (spec.vlsd && linkCount > signalDataIndex)
|
||
{
|
||
spec.signalData = file.Link(cn, signalDataIndex);
|
||
}
|
||
|
||
// 采样索引(`##SI`):只有当它确实是「多条记录下标」时才生效。
|
||
// ⚠️ 实测部分工具会给每个通道挂一个只含 1 个 8 字节值的 `##SI`(如 513),
|
||
// 那显然不是采样下标(远大于周期数)⇒ 必须忽略,否则会把整列清成 NaN。
|
||
{
|
||
const std::uint64_t si = (linkCount > 3) ? file.Link(cn, 3) : 0;
|
||
if (file.IsBlock(si, "##SI"))
|
||
{
|
||
const std::uint64_t siData = file.DataOffset(si);
|
||
const std::uint64_t usable = file.BlockLength(si) - (siData - si);
|
||
const std::size_t count = static_cast<std::size_t>(usable / 8u);
|
||
if (count >= 2 && count <= (1u << 24))
|
||
{
|
||
spec.sampleIndex.resize(count);
|
||
bool ok = file.ReadAt(siData, spec.sampleIndex.data(), count * 8u);
|
||
if (!ok)
|
||
{
|
||
spec.sampleIndex.clear();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 换算:数值换算(0–6)与「值 → 文本」(7)。
|
||
const std::uint64_t cc = (linkCount > conversionIndex) ? file.Link(cn, conversionIndex) : 0;
|
||
if (file.IsBlock(cc, "##CC"))
|
||
{
|
||
const std::uint64_t ccData = file.DataOffset(cc);
|
||
const std::uint8_t ccType = file.U8(ccData + 0);
|
||
const std::uint16_t valCount = file.U16(ccData + 6);
|
||
// `cc_val[]` 从数据段 +24 开始(每个 REAL 占 8 字节)。
|
||
const auto readValues = [&file, ccData, valCount](std::size_t wanted) -> std::vector<double>
|
||
{
|
||
std::vector<double> values;
|
||
for (std::uint16_t index = 0; index < valCount && values.size() < wanted; ++index)
|
||
{
|
||
values.push_back(file.F64(ccData + 24u + 8u * index));
|
||
}
|
||
return values;
|
||
};
|
||
const ConversionType kind = static_cast<ConversionType>(ccType);
|
||
switch (kind)
|
||
{
|
||
case ConversionType::LINEAR:
|
||
if (valCount >= 2)
|
||
{
|
||
spec.conversionValues = readValues(2);
|
||
// 换算表未填写:按恒等处理,避免整列变成 0。
|
||
if (spec.conversionValues.size() >= 2 && spec.conversionValues[0] == 0.0 &&
|
||
spec.conversionValues[1] == 0.0)
|
||
{
|
||
spec.conversionValues[0] = 0.0;
|
||
spec.conversionValues[1] = 1.0;
|
||
}
|
||
spec.conversion = ConversionType::LINEAR;
|
||
}
|
||
break;
|
||
case ConversionType::RATIONAL:
|
||
if (valCount >= 4)
|
||
{
|
||
spec.conversionValues = readValues(4);
|
||
spec.conversion = kind;
|
||
}
|
||
break;
|
||
case ConversionType::ALGEBRAIC:
|
||
if (valCount >= 6)
|
||
{
|
||
spec.conversionValues = readValues(6);
|
||
spec.conversion = kind;
|
||
}
|
||
break;
|
||
case ConversionType::TAB_INTP:
|
||
case ConversionType::TAB:
|
||
if (valCount >= 4 && (valCount % 2) == 0)
|
||
{
|
||
spec.conversionValues = readValues(valCount);
|
||
spec.conversion = kind;
|
||
}
|
||
break;
|
||
case ConversionType::TAB_RANGE:
|
||
if (valCount >= 3 && (valCount % 3) == 0)
|
||
{
|
||
spec.conversionValues = readValues(valCount);
|
||
spec.conversion = kind;
|
||
}
|
||
break;
|
||
case ConversionType::VALUE_TO_TEXT:
|
||
if (valCount > 0)
|
||
{
|
||
// `cc_val[i]`(F64 数组,从数据段 +24 开始)与 `cc_ref[4 + i]` 一一对应;
|
||
// 最后一个引用(下标 4 + valCount)是「默认文本」,这里不使用。
|
||
const std::uint64_t ccLinkCount = file.LinkCount(cc);
|
||
spec.enumText.values.reserve(valCount);
|
||
spec.enumText.texts.reserve(valCount);
|
||
for (std::uint16_t index = 0; index < valCount; ++index)
|
||
{
|
||
const std::uint64_t textLink = 4u + index;
|
||
if (textLink >= ccLinkCount)
|
||
{
|
||
break;
|
||
}
|
||
std::wstring text = file.TextBlock(file.Link(cc, textLink));
|
||
if (text.empty())
|
||
{
|
||
continue; // 没有文本的取值不进入枚举表
|
||
}
|
||
spec.enumText.values.push_back(file.F64(ccData + 24u + 8u * index));
|
||
spec.enumText.texts.push_back(std::move(text));
|
||
}
|
||
if (spec.enumText.texts.size() != spec.enumText.values.size() ||
|
||
spec.enumText.texts.empty())
|
||
{
|
||
spec.enumText = SignalEnumText();
|
||
}
|
||
}
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
// `##CA`(通道数组):一个通道装一整条曲线/矩阵(标定的特性曲线、map)。
|
||
// 数据段布局(实测 CANape 写出的文件):type(1) + storage(1) + ndim(U16) + flags(U32)
|
||
// + [8..15 保留] + dim_size[](U64 × ndim)。
|
||
const std::uint64_t ca = (linkCount > 1) ? file.Link(cn, 1) : 0;
|
||
if (ca != 0 && file.IsBlock(ca, "##CA"))
|
||
{
|
||
const std::uint64_t caData = file.DataOffset(ca);
|
||
spec.arrayStorage = file.U8(caData + 1);
|
||
const std::uint16_t rank = file.U16(caData + 2);
|
||
std::size_t total = (rank > 0) ? 1 : 0;
|
||
spec.arrayDims.clear();
|
||
for (std::uint16_t axis = 0; axis < rank && axis < 8; ++axis)
|
||
{
|
||
const std::size_t dim =
|
||
static_cast<std::size_t>(file.U64(caData + 16u + 8u * axis));
|
||
if (dim == 0)
|
||
{
|
||
total = 0;
|
||
break;
|
||
}
|
||
spec.arrayDims.push_back(dim);
|
||
total *= dim;
|
||
}
|
||
spec.arrayElements = total;
|
||
}
|
||
|
||
// 通道类型 2 = 虚拟主通道(CANape 的时间轴),同样可以取值。
|
||
if (spec.vlsd)
|
||
{
|
||
// VLSD:`cn_bit_count` 描述的是记录里的偏移字段,元素位宽由样本自身决定
|
||
//(很多写盘器把 `cn_bit_count` 写成 0)⇒ 只按数据类型判定。
|
||
spec.numeric = IsNumericType(spec.dataType);
|
||
}
|
||
else
|
||
{
|
||
spec.numeric = IsNumericType(spec.dataType) && spec.bitCount > 0 && spec.bitCount <= 64;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @brief 把 `##CA` 数组通道展开成多个普通通道(每个元素一个)。
|
||
*
|
||
* CANape 的标定数组(特性曲线/断点表/map)在 MDF 里是「一个通道 + `##CA`」,
|
||
* `storage == 0` 时各元素**连续存放**在记录的 `cn_byte_offset` 处
|
||
*(实测:`WIN_Motor_StallCurrentVolBrk` 的 9 个 `uint16` 就是从记录 +8 起连续 18 字节)。
|
||
* 展开后每个元素都是普通标量信号,因此自动获得光标取值、枚举、图例等全部能力。
|
||
*
|
||
* ⚠️ 仅处理 `storage == 0`(记录内连续);`storage != 0`(元素分散在不同记录/
|
||
* 不同通道组/不同数据组)保持原样(只显示元素 0),避免解析出错值。
|
||
* ⚠️ 元素总数超过 `kMaxArrayElements` 时同样保持原样(防止巨型数组把信号列表撑爆)。
|
||
*/
|
||
void ExpandArrayChannel(ChannelSpec&& spec, std::vector<ChannelSpec>& out)
|
||
{
|
||
constexpr std::size_t kMaxArrayElements = 1024;
|
||
const std::size_t count = spec.arrayElements;
|
||
if (count <= 1 || spec.arrayStorage != 0 || count > kMaxArrayElements || spec.bitCount == 0)
|
||
{
|
||
spec.arrayElements = 0;
|
||
out.push_back(std::move(spec));
|
||
return;
|
||
}
|
||
const std::size_t strideBits = spec.bitCount;
|
||
const std::vector<std::size_t> dims = spec.arrayDims;
|
||
for (std::size_t index = 0; index < count; ++index)
|
||
{
|
||
ChannelSpec element = spec; // 复制:换算表 / 枚举表 / 数据类型全部沿用
|
||
element.arrayElements = 0;
|
||
element.bitOffset = spec.bitOffset + index * strideBits;
|
||
std::wstring suffix;
|
||
if (dims.size() == 2 && dims[1] > 0)
|
||
{
|
||
suffix = L"[" + std::to_wstring(index / dims[1]) + L"][" +
|
||
std::to_wstring(index % dims[1]) + L"]";
|
||
}
|
||
else
|
||
{
|
||
suffix = L"[" + std::to_wstring(index) + L"]";
|
||
}
|
||
element.name = spec.name + suffix;
|
||
out.push_back(std::move(element));
|
||
}
|
||
}
|
||
|
||
/** @brief 在一条 `next` 链上迭代块偏移(带访问次数上限,避免损坏文件死循环)。 */
|
||
template <typename Fn>
|
||
void WalkChain(const MdfFile& file, std::uint64_t first, const char* expectedId, std::size_t limit,
|
||
Fn&& visit)
|
||
{
|
||
std::uint64_t current = first;
|
||
std::size_t guard = 0;
|
||
while (current != 0 && guard++ < limit)
|
||
{
|
||
if (!file.IsBlock(current, expectedId))
|
||
{
|
||
break;
|
||
}
|
||
if (!visit(current))
|
||
{
|
||
break;
|
||
}
|
||
current = file.Link(current, 0);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 一段原始记录数据(MDF4 的 `##DT` / `##DZ` / `##DV` 块的数据区)。
|
||
*
|
||
* MDF4 允许把记录拆到多段里:
|
||
* * `##DT` 普通数据块(记录连续存放);
|
||
* * `##DZ` 压缩数据块(zlib 流,解压后是普通记录);
|
||
* * `##DV` 数据值块(`##DL` 列表指向的原始值);
|
||
* * `##DL` 数据列表(链接到多个 `##DT` / `##DZ`);
|
||
* * `##HL` 头部列表(链接到多个数据块,含压缩的)。
|
||
*/
|
||
struct DataSegment
|
||
{
|
||
std::uint64_t offset = 0; /**< 数据区起始绝对偏移。 */
|
||
std::uint64_t bytes = 0; /**< 数据区字节数(压缩段为压缩后长度)。 */
|
||
bool compressed = false; /**< 是否 need inflate。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 递归展开数据块,收集所有数据段。
|
||
*
|
||
* ⚠️ 用户 2026-10-19 报告:Simulink SDI 导出的 MF4 把记录写在 `##DL`(数据列表)
|
||
* 里,而旧实现只认 `##DT` ⇒ 整个数据组被跳过(8 个信号只能读出 4 个)。
|
||
*/
|
||
bool CollectDataSegments(const MdfFile& file, std::uint64_t block,
|
||
std::vector<DataSegment>& out, int depth)
|
||
{
|
||
if (block == 0 || depth > 4 || !file.IsBlock(block, ""))
|
||
{
|
||
return false;
|
||
}
|
||
char id[5] = {};
|
||
file.BlockId(block, id);
|
||
const std::uint64_t data = file.DataOffset(block);
|
||
const std::uint64_t payload = file.BlockLength(block) - (data - block);
|
||
if (payload == 0)
|
||
{
|
||
return false;
|
||
}
|
||
if (std::memcmp(id, "##DT", 4) == 0 || std::memcmp(id, "##DV", 4) == 0)
|
||
{
|
||
out.push_back(DataSegment{data, payload, false});
|
||
return true;
|
||
}
|
||
if (std::memcmp(id, "##DZ", 4) == 0)
|
||
{
|
||
// `##DZ` 数据段布局:original_type(4) + zip_type(1) + reserved(3) +
|
||
// param(4) + original_size(8) + zip_size(8) + 压缩数据。
|
||
// ⚠️ 载荷里含 28 字节头,不能把整段当 zlib 流(旧实现在这里错了)。
|
||
char originalType[5] = {};
|
||
file.BlockId(data, originalType); // 读 4 字节原始类型("DT"/"SD"/...)
|
||
const std::uint8_t zipType = file.U8(data + 4);
|
||
const std::uint64_t zipSize = file.U64(data + 20);
|
||
if (zipType != 0 || payload <= 28u)
|
||
{
|
||
return false; // 只支持纯 deflate
|
||
}
|
||
if (std::memcmp(originalType, "SD", 2) == 0)
|
||
{
|
||
return false; // 压缩的 VLSD 数据由 AppendSignalData 负责
|
||
}
|
||
const std::uint64_t usable = std::min(payload - 28u, zipSize);
|
||
out.push_back(DataSegment{data + 28u, usable, true});
|
||
return true;
|
||
}
|
||
if (std::memcmp(id, "##DL", 4) == 0 || std::memcmp(id, "##HL", 4) == 0)
|
||
{
|
||
const std::uint64_t links = file.LinkCount(block);
|
||
bool any = false;
|
||
for (std::uint64_t index = 0; index < links && index < 1000000; ++index)
|
||
{
|
||
any = CollectDataSegments(file, file.Link(block, index), out, depth + 1) || any;
|
||
}
|
||
return any;
|
||
}
|
||
// 到这里说明不是记录数据块:
|
||
// * `##SD`(Signal Data,VLSD 通道的样本)由 `AppendSignalData` 按通道读取;
|
||
// * `##SI` 是采样索引,不是数据块。
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* @brief 依次从若干数据段中读取连续记录,并对每条完整记录调用 `visit`。
|
||
*
|
||
* * 记录可能**跨段**(`##DT` 载荷不是记录长度的整数倍时)⇒ 用 `carry` 拼接;
|
||
* * 压缩段先用内置 inflate 展开(`##DZ`),再按普通字节流消费;
|
||
* * 每读一块调用 `cancelled`(可为空),返回 true 表示请求取消。
|
||
*
|
||
* @return 成功读取到至少 `cycles` 条记录返回 true。
|
||
*/
|
||
bool ReadRecordSegments(
|
||
const MdfFile& file, const std::vector<DataSegment>& segments, std::uint64_t cycles,
|
||
std::size_t recordBytes,
|
||
const std::function<void(const unsigned char* record, std::uint64_t row)>& visit,
|
||
const std::function<bool()>& cancelled = std::function<bool()>())
|
||
{
|
||
if (recordBytes == 0 || cycles == 0)
|
||
{
|
||
return false;
|
||
}
|
||
std::vector<unsigned char> carry(recordBytes);
|
||
std::size_t carryBytes = 0;
|
||
std::uint64_t row = 0;
|
||
std::vector<std::uint8_t> inflated;
|
||
|
||
for (const DataSegment& segment : segments)
|
||
{
|
||
if (row >= cycles)
|
||
{
|
||
break;
|
||
}
|
||
if (cancelled && cancelled())
|
||
{
|
||
return false;
|
||
}
|
||
const unsigned char* data = nullptr;
|
||
std::size_t bytes = 0;
|
||
if (segment.compressed)
|
||
{
|
||
std::vector<std::uint8_t> compressed(static_cast<std::size_t>(segment.bytes));
|
||
if (!file.ReadAt(segment.offset, compressed.data(), compressed.size()))
|
||
{
|
||
return false;
|
||
}
|
||
if (!InflateZlib(compressed.data(), compressed.size(), inflated))
|
||
{
|
||
return false;
|
||
}
|
||
data = inflated.data();
|
||
bytes = inflated.size();
|
||
}
|
||
else
|
||
{
|
||
if (segment.offset + segment.bytes > file.Size())
|
||
{
|
||
return false;
|
||
}
|
||
// 非压缩段同样分块读,避免一次分配整段(巨型数据组可能是 GB 级)。
|
||
std::vector<unsigned char> buffer;
|
||
std::uint64_t consumed = 0;
|
||
while (consumed < segment.bytes && row < cycles)
|
||
{
|
||
const std::size_t want = static_cast<std::size_t>(
|
||
std::min<std::uint64_t>(kReadChunkBytes, segment.bytes - consumed));
|
||
buffer.resize(std::max<std::size_t>(want, recordBytes));
|
||
if (!file.ReadAt(segment.offset + consumed, buffer.data(), want))
|
||
{
|
||
return false;
|
||
}
|
||
data = buffer.data();
|
||
bytes = want;
|
||
|
||
std::size_t position = 0;
|
||
while (position < bytes && row < cycles)
|
||
{
|
||
const std::size_t need = recordBytes - carryBytes;
|
||
const std::size_t take = std::min(need, bytes - position);
|
||
std::memcpy(carry.data() + carryBytes, data + position, take);
|
||
carryBytes += take;
|
||
position += take;
|
||
if (carryBytes == recordBytes)
|
||
{
|
||
visit(carry.data(), row++);
|
||
carryBytes = 0;
|
||
}
|
||
}
|
||
consumed += want;
|
||
if (cancelled && cancelled())
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 压缩段:整段已在内存里,直接消费。
|
||
std::size_t position = 0;
|
||
while (position < bytes && row < cycles)
|
||
{
|
||
const std::size_t need = recordBytes - carryBytes;
|
||
const std::size_t take = std::min(need, bytes - position);
|
||
std::memcpy(carry.data() + carryBytes, data + position, take);
|
||
carryBytes += take;
|
||
position += take;
|
||
if (carryBytes == recordBytes)
|
||
{
|
||
visit(carry.data(), row++);
|
||
carryBytes = 0;
|
||
}
|
||
}
|
||
}
|
||
return row > 0;
|
||
}
|
||
|
||
/** @brief 一个通道组(CG)解析出的时间轴 + 信号列。 */
|
||
struct ChannelGroupData
|
||
{
|
||
std::wstring timeName; /**< 时间列名(主通道名或 `time`)。 */
|
||
std::vector<ChannelSpec> channels; /**< 非时间通道。 */
|
||
std::vector<double> time; /**< 时间轴数值。 */
|
||
std::vector<std::vector<double>> values; /**< 每个非时间通道的采样值。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 把 `##SI` 采样索引转换成「逐记录有效位图」。
|
||
*
|
||
* 只有当索引表**至少两条**且**全部落在记录范围内**时才生效;
|
||
* 否则(例如某些工具给每个通道挂一个只含 `513` 的 `##SI`)返回空向量表示「全有效」,
|
||
* 与不支持 `##SI` 时的行为完全一致。
|
||
*/
|
||
std::vector<std::uint8_t> BuildSampleMask(const ChannelSpec& spec, std::uint64_t cycles)
|
||
{
|
||
std::vector<std::uint8_t> mask;
|
||
if (spec.sampleIndex.size() < 2 || cycles == 0)
|
||
{
|
||
return mask;
|
||
}
|
||
for (const std::uint64_t index : spec.sampleIndex)
|
||
{
|
||
if (index >= cycles)
|
||
{
|
||
return mask; // 不像采样索引:忽略
|
||
}
|
||
}
|
||
mask.assign(static_cast<std::size_t>(cycles), 0);
|
||
for (const std::uint64_t index : spec.sampleIndex)
|
||
{
|
||
mask[static_cast<std::size_t>(index)] = 1;
|
||
}
|
||
return mask;
|
||
}
|
||
|
||
/**
|
||
* @brief 读取一个通道组的全部记录(流式,按 4MB 分块)。
|
||
*
|
||
* @param [IN] file 文件访问器。
|
||
* @param [IN] segments 数据段列表(`##DT` / `##DL` / `##HL` / `##DZ`)。
|
||
* @param [IN] cycles 记录条数。
|
||
* @param [IN] recordBytes 单条记录字节数。
|
||
* @param [IN] masterIndex 时间主通道下标。
|
||
* @param [IN,OUT] group 输出(`timeName`/`channels` 已填好,函数填充 `time`/`values`)。
|
||
* @param [IN] cancelled 取消检查(可为空)。
|
||
*/
|
||
bool ReadChannelGroupRecords(const MdfFile& file, const std::vector<DataSegment>& segments,
|
||
std::uint64_t cycles, std::size_t recordBytes,
|
||
std::size_t masterIndex, ChannelGroupData& group,
|
||
const std::function<bool()>& cancelled = std::function<bool()>())
|
||
{
|
||
if (cycles == 0 || recordBytes == 0 || segments.empty())
|
||
{
|
||
return false;
|
||
}
|
||
group.time.assign(static_cast<std::size_t>(cycles), kMissingValue);
|
||
group.values.assign(group.channels.size(),
|
||
std::vector<double>(static_cast<std::size_t>(cycles), kMissingValue));
|
||
|
||
// `##SI`:每个通道各自的「有效记录」位图(空 = 每个记录都有效)。
|
||
std::vector<std::vector<std::uint8_t>> masks(group.channels.size());
|
||
for (std::size_t index = 0; index < group.channels.size(); ++index)
|
||
{
|
||
masks[index] = BuildSampleMask(group.channels[index], cycles);
|
||
}
|
||
|
||
// VLSD 通道的 `##SD` 数据:按通道加载(只有 VLSD 通道才需要)。
|
||
std::vector<std::vector<std::uint8_t>> signalData(group.channels.size());
|
||
for (std::size_t index = 0; index < group.channels.size(); ++index)
|
||
{
|
||
const ChannelSpec& spec = group.channels[index];
|
||
if (spec.vlsd && spec.signalData != 0 && spec.numeric)
|
||
{
|
||
AppendSignalData(file, spec.signalData, signalData[index], 0);
|
||
}
|
||
}
|
||
|
||
std::vector<const std::vector<std::uint8_t>*> signalDataPtr(group.channels.size(), nullptr);
|
||
for (std::size_t index = 0; index < group.channels.size(); ++index)
|
||
{
|
||
signalDataPtr[index] = signalData[index].empty() ? nullptr : &signalData[index];
|
||
}
|
||
|
||
// ⚠️ 记录可能分散在多个 `##DT` / `##DZ`(经 `##DL` / `##HL` 列表)中,
|
||
// 由 `ReadRecordSegments` 负责跨段拼接与解压(见该函数的说明)。
|
||
return ReadRecordSegments(
|
||
file, segments, cycles, recordBytes,
|
||
[&group, masterIndex, recordBytes, &masks, &signalDataPtr](const unsigned char* record,
|
||
std::uint64_t row)
|
||
{
|
||
// 时间主通道。
|
||
{
|
||
const ChannelSpec& master = group.channels[masterIndex];
|
||
double value = DecodeRawValue(record, recordBytes, master);
|
||
if (master.allInvalid)
|
||
{
|
||
value = kMissingValue;
|
||
}
|
||
else if (master.hasInvalidBit)
|
||
{
|
||
const std::size_t byteIndex = master.invalidBitPos / 8u;
|
||
if (byteIndex < recordBytes &&
|
||
((record[byteIndex] >> (master.invalidBitPos % 8u)) & 1u) != 0u)
|
||
{
|
||
value = kMissingValue;
|
||
}
|
||
}
|
||
if (std::isfinite(value))
|
||
{
|
||
value = ApplyConversion(master, value);
|
||
}
|
||
group.time[static_cast<std::size_t>(row)] = value;
|
||
}
|
||
|
||
// 各信号通道。
|
||
for (std::size_t c = 0; c < group.channels.size(); ++c)
|
||
{
|
||
if (c == masterIndex)
|
||
{
|
||
continue;
|
||
}
|
||
const ChannelSpec& spec = group.channels[c];
|
||
// `##SI` 采样索引:只有表中列出的记录才有采样,其余保持 NaN。
|
||
if (!masks[c].empty() && masks[c][static_cast<std::size_t>(row)] == 0)
|
||
{
|
||
continue;
|
||
}
|
||
double value = kMissingValue;
|
||
if (spec.vlsd)
|
||
{
|
||
// VLSD:记录里是 64 位偏移,真正的取值在 `##SD` 里。
|
||
if (signalDataPtr[c] != nullptr)
|
||
{
|
||
value = DecodeVlsdValue(record, recordBytes, spec, *signalDataPtr[c]);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
value = DecodeRawValue(record, recordBytes, spec);
|
||
}
|
||
if (spec.allInvalid)
|
||
{
|
||
value = kMissingValue;
|
||
}
|
||
else if (spec.hasInvalidBit)
|
||
{
|
||
const std::size_t byteIndex = spec.invalidBitPos / 8u;
|
||
if (byteIndex < recordBytes &&
|
||
((record[byteIndex] >> (spec.invalidBitPos % 8u)) & 1u) != 0u)
|
||
{
|
||
value = kMissingValue;
|
||
}
|
||
}
|
||
if (std::isfinite(value))
|
||
{
|
||
value = ApplyConversion(spec, value);
|
||
}
|
||
group.values[c][static_cast<std::size_t>(row)] = value;
|
||
}
|
||
},
|
||
cancelled);
|
||
}
|
||
|
||
/** @brief 主通道名字判定用的 ASCII 小写化。 */
|
||
std::wstring LowerAscii(const std::wstring& text)
|
||
{
|
||
std::wstring lower;
|
||
lower.reserve(text.size());
|
||
for (const wchar_t c : text)
|
||
{
|
||
lower.push_back((c >= L'A' && c <= L'Z') ? static_cast<wchar_t>(c - L'A' + L'a') : c);
|
||
}
|
||
return lower;
|
||
}
|
||
|
||
/**
|
||
* @brief 从通道组里选出时间主通道。
|
||
*
|
||
* 判定顺序(越靠前越可信):
|
||
* 1. `cn_sync_type` = 1(时间)/ 2(角度)/ 3(距离)且为数值通道;
|
||
* 2. 名字为 `t` / `time` / `timestamp` / `zeit` / `时间`(含 `t[s]` 这类带单位后缀);
|
||
* 3. 第一个数值通道(兜底:至少让数据能画出来,而不是整组丢弃)。
|
||
*
|
||
* ⚠️ `cg_flags` 的第 2 位表示「远程主通道」(主通道不在本 CG 里)。此时跳过第 1 步,
|
||
* 直接按名字 / 首个数值通道兜底,避免把某个普通信号误当成时间轴。
|
||
*
|
||
* @return 主通道下标;一个数值通道都没有时返回 `channels.size()`。
|
||
*/
|
||
std::size_t SelectMasterChannel(const std::vector<ChannelSpec>& channels, std::uint16_t cgFlags)
|
||
{
|
||
const bool remoteMaster = (cgFlags & 0x4u) != 0u;
|
||
if (!remoteMaster)
|
||
{
|
||
// 优先同步主通道,但 VLSD 通道不能当时间轴(记录里只有偏移)。
|
||
for (std::size_t i = 0; i < channels.size(); ++i)
|
||
{
|
||
if (channels[i].syncMaster && channels[i].numeric && !channels[i].vlsd)
|
||
{
|
||
return i;
|
||
}
|
||
}
|
||
}
|
||
for (std::size_t i = 0; i < channels.size(); ++i)
|
||
{
|
||
if (!channels[i].numeric)
|
||
{
|
||
continue;
|
||
}
|
||
const std::wstring lower = LowerAscii(channels[i].name);
|
||
if (lower == L"time" || lower == L"t" || lower == L"timestamp" || lower == L"zeit" ||
|
||
lower == L"时间" || lower.rfind(L"t[", 0) == 0 || lower.rfind(L"time[", 0) == 0)
|
||
{
|
||
return i;
|
||
}
|
||
}
|
||
for (std::size_t i = 0; i < channels.size(); ++i)
|
||
{
|
||
if (channels[i].numeric && !channels[i].vlsd)
|
||
{
|
||
return i;
|
||
}
|
||
}
|
||
return channels.size();
|
||
}
|
||
} // namespace
|
||
|
||
bool LoadMf4Source(const std::wstring& filePath, LoadedSource& source, std::wstring& error,
|
||
const LoadProgress& progress)
|
||
{
|
||
source = LoadedSource();
|
||
error.clear();
|
||
|
||
MdfFile file;
|
||
if (!file.Open(filePath, error))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const std::size_t slashPos = filePath.find_last_of(L"\\/");
|
||
source.sourceName = (slashPos == std::wstring::npos) ? filePath : filePath.substr(slashPos + 1);
|
||
|
||
// 头块 → 第一个数据组。
|
||
std::uint64_t hd = 0;
|
||
{
|
||
char id[5] = {};
|
||
// 头块通常紧跟在 64 字节 ID 块之后;用一个小的扫描窗口定位,兼容前置填充。
|
||
for (std::uint64_t offset = 24; offset + 64 <= file.Size() && offset < 4096; offset += 8)
|
||
{
|
||
if (file.BlockId(offset, id) && std::memcmp(id, "##HD", 4) == 0)
|
||
{
|
||
hd = offset;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (hd == 0)
|
||
{
|
||
error = L"未找到 MDF 头块(##HD)。";
|
||
return false;
|
||
}
|
||
|
||
const std::uint64_t firstDg = file.Link(hd, 0);
|
||
|
||
// 先走一遍 DG 链只收集偏移(只读块头,代价极小):
|
||
// 进度分母必须是**真实数据组数量**(旧实现写死 64,组数少的文件会「停在 44% 再跳 100%」)。
|
||
std::vector<std::uint64_t> dataGroups;
|
||
WalkChain(file, firstDg, "##DG", kMaxGroups,
|
||
[&](std::uint64_t dg)
|
||
{
|
||
dataGroups.push_back(dg);
|
||
return true;
|
||
});
|
||
|
||
std::size_t processedGroups = 0;
|
||
double lastRatio = -1.0;
|
||
const auto emitProgress = [&]() -> bool
|
||
{
|
||
if (!progress)
|
||
{
|
||
return true;
|
||
}
|
||
LoadProgressInfo info;
|
||
const double byGroup =
|
||
dataGroups.empty() ? 1.0
|
||
: static_cast<double>(processedGroups) /
|
||
static_cast<double>(dataGroups.size());
|
||
double ratio = 0.98 * byGroup;
|
||
ratio = std::max(ratio, lastRatio); // 保证单调
|
||
lastRatio = ratio;
|
||
info.ratio = ratio;
|
||
// 同时上报真实字节数:`MdfFile::ReadAt` 逐次累计(随机访问可能重复读元数据,需夹住)。
|
||
info.hasBytes = true;
|
||
info.bytesRead = std::min<std::uint64_t>(file.BytesRead(), file.Size());
|
||
info.totalBytes = file.Size();
|
||
return progress(info);
|
||
};
|
||
|
||
std::size_t skippedNoLayout = 0;
|
||
std::size_t skippedEmpty = 0;
|
||
std::size_t skippedNoAxis = 0;
|
||
bool cancelRequested = false;
|
||
|
||
for (const std::uint64_t dg : dataGroups)
|
||
{
|
||
if (cancelRequested)
|
||
{
|
||
break;
|
||
}
|
||
++processedGroups;
|
||
const std::uint64_t cg = file.Link(dg, 1);
|
||
const std::uint64_t dataBlock = file.Link(dg, 2);
|
||
if (!file.IsBlock(cg, "##CG") || dataBlock == 0)
|
||
{
|
||
++skippedNoLayout; // 不完整的数据组(没有 CG 或没有数据块)
|
||
if (!emitProgress())
|
||
{
|
||
cancelRequested = true;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 记录可能写在 `##DT` / `##DL`(数据列表)/ `##HL`(头部列表)/ `##DZ`(压缩)里:
|
||
// 旧实现只认 `##DT`,于是用 `##DL` 存放记录的文件会**整组被跳过**
|
||
//(用户 2026-10-19 报告:Simulink SDI 导出的 8 个信号只读出 4 个)。
|
||
std::vector<DataSegment> segments;
|
||
if (!CollectDataSegments(file, dataBlock, segments, 0) || segments.empty())
|
||
{
|
||
++skippedNoLayout;
|
||
if (!emitProgress())
|
||
{
|
||
cancelRequested = true;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
const std::uint64_t cgData = file.DataOffset(cg);
|
||
const std::uint16_t cgFlags = file.U16(cgData + 0);
|
||
const std::uint64_t cycles = file.U64(cgData + 8);
|
||
if (cycles == 0)
|
||
{
|
||
++skippedEmpty; // 空数据组(例如只有注释通道的组)
|
||
if (!emitProgress())
|
||
{
|
||
cancelRequested = true;
|
||
}
|
||
continue;
|
||
}
|
||
// `cg_data_bytes`(+24)是单条记录字节数,4.1 起由文件直接给出,最可靠。
|
||
const std::uint32_t cgDataBytes = file.U32(cgData + 24);
|
||
|
||
// 解析通道链。
|
||
std::vector<ChannelSpec> channels;
|
||
WalkChain(file, file.Link(cg, 1), "##CN", kMaxChannelsPerGroup,
|
||
[&](std::uint64_t cn)
|
||
{
|
||
ChannelSpec spec;
|
||
if (ParseChannel(file, cn, spec))
|
||
{
|
||
// `##CA` 数组通道在这里就地展开成多个标量通道
|
||
//(一个通道装整条曲线时,旧实现只能看到元素 0)。
|
||
ExpandArrayChannel(std::move(spec), channels);
|
||
}
|
||
return true;
|
||
});
|
||
if (channels.empty())
|
||
{
|
||
++skippedNoLayout;
|
||
if (!emitProgress())
|
||
{
|
||
cancelRequested = true;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 记录长度:优先用 `cg_data_bytes`;否则用「数据块大小 / 记录数」反推;
|
||
// ⚠️ 压缩段(`##DZ`)的载荷是压缩后长度,不能用来反推 ⇒ 退回按通道位宽推算。
|
||
std::uint64_t payload = 0;
|
||
bool anyCompressed = false;
|
||
for (const DataSegment& segment : segments)
|
||
{
|
||
payload += segment.bytes;
|
||
anyCompressed = anyCompressed || segment.compressed;
|
||
}
|
||
std::size_t recordBytes = 0;
|
||
if (cgDataBytes > 0 && cgDataBytes <= (1u << 20))
|
||
{
|
||
recordBytes = static_cast<std::size_t>(cgDataBytes);
|
||
}
|
||
else if (!anyCompressed && payload % cycles == 0 && payload / cycles > 0 &&
|
||
payload / cycles <= (1u << 20))
|
||
{
|
||
recordBytes = static_cast<std::size_t>(payload / cycles);
|
||
}
|
||
else
|
||
{
|
||
std::size_t bits = 0;
|
||
for (const ChannelSpec& spec : channels)
|
||
{
|
||
bits = std::max(bits, spec.bitOffset + spec.bitCount);
|
||
}
|
||
recordBytes = (bits + 31u) / 32u * 4u;
|
||
}
|
||
if (recordBytes == 0)
|
||
{
|
||
++skippedNoLayout;
|
||
if (!emitProgress())
|
||
{
|
||
cancelRequested = true;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
const std::size_t masterIndex = SelectMasterChannel(channels, cgFlags);
|
||
if (masterIndex >= channels.size())
|
||
{
|
||
++skippedNoAxis; // 没有任何数值通道,无法建立时间轴
|
||
if (!emitProgress())
|
||
{
|
||
cancelRequested = true;
|
||
}
|
||
continue;
|
||
}
|
||
// 主通道自身若是 VLSD/字符串,取值无法作为时间轴 ⇒ 直接跳过该组。
|
||
if (channels[masterIndex].vlsd)
|
||
{
|
||
++skippedNoAxis;
|
||
if (!emitProgress())
|
||
{
|
||
cancelRequested = true;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
ChannelGroupData group;
|
||
group.timeName = channels[masterIndex].name;
|
||
if (group.timeName.empty())
|
||
{
|
||
group.timeName = L"time";
|
||
}
|
||
group.channels = std::move(channels);
|
||
bool readCancelled = false;
|
||
const auto cancelled = [&]() -> bool
|
||
{
|
||
if (emitProgress())
|
||
{
|
||
return false;
|
||
}
|
||
readCancelled = true;
|
||
return true;
|
||
};
|
||
const bool readOk =
|
||
ReadChannelGroupRecords(file, segments, cycles, recordBytes, masterIndex, group, cancelled);
|
||
if (readCancelled)
|
||
{
|
||
cancelRequested = true;
|
||
break;
|
||
}
|
||
if (!readOk)
|
||
{
|
||
++skippedNoLayout;
|
||
if (!emitProgress())
|
||
{
|
||
cancelRequested = true;
|
||
break;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 组织成一个时间轴块:时间列 + 其余数值通道。
|
||
TableBlock block;
|
||
block.timeHeader = group.timeName;
|
||
block.table.headers.push_back(group.timeName);
|
||
block.table.columns.push_back(std::move(group.time));
|
||
block.table.columnEnumText.emplace_back(); // 时间列没有枚举信息
|
||
for (std::size_t c = 0; c < group.channels.size(); ++c)
|
||
{
|
||
if (c == masterIndex || !group.channels[c].numeric)
|
||
{
|
||
continue;
|
||
}
|
||
std::wstring name = group.channels[c].name;
|
||
if (name.empty())
|
||
{
|
||
name = L"Channel" + std::to_wstring(c + 1);
|
||
}
|
||
block.table.headers.push_back(name);
|
||
block.table.columns.push_back(std::move(group.values[c]));
|
||
block.table.columnEnumText.push_back(std::move(group.channels[c].enumText));
|
||
}
|
||
if (block.table.columns.size() < 2)
|
||
{
|
||
++skippedEmpty; // 只有时间列:没有可绘制信号
|
||
continue;
|
||
}
|
||
block.table.rowCount = static_cast<std::size_t>(cycles);
|
||
source.blocks.push_back(std::move(block));
|
||
}
|
||
|
||
if (cancelRequested)
|
||
{
|
||
error.clear(); // 用户取消:不显示错误
|
||
return false;
|
||
}
|
||
|
||
if (source.blocks.empty())
|
||
{
|
||
// 说明「为什么一个信号都没有」,而不是笼统地说找不到数值通道。
|
||
std::wstring detail;
|
||
if (skippedNoLayout > 0)
|
||
{
|
||
detail += L"数据块布局无法解析 " + std::to_wstring(skippedNoLayout) + L" 组;";
|
||
}
|
||
if (skippedNoAxis > 0)
|
||
{
|
||
detail += L"缺少可用时间通道 " + std::to_wstring(skippedNoAxis) + L" 组;";
|
||
}
|
||
if (skippedEmpty > 0)
|
||
{
|
||
detail += L"空数据组 " + std::to_wstring(skippedEmpty) + L" 组;";
|
||
}
|
||
error = L"未在文件中找到可绘制的数值通道。";
|
||
if (!detail.empty())
|
||
{
|
||
error += L"(" + detail + L")";
|
||
}
|
||
return false;
|
||
}
|
||
|
||
if (progress)
|
||
{
|
||
LoadProgressInfo info;
|
||
info.ratio = 1.0;
|
||
info.hasBytes = true;
|
||
info.bytesRead = std::min<std::uint64_t>(file.BytesRead(), file.Size());
|
||
info.totalBytes = file.Size();
|
||
progress(info);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
} // namespace jdi
|