MF4 ##CA 全字段/三维 + cc_type=8 区间枚举 + 稠密包络带线宽双向加粗
一、##CA 数组通道:从「按魔数猜」改成「按记录容量校验」
- 头字段全部按块长度做边界检查后读取:+0 ca_type / +1 ca_storage /
+2 ndim(U16) / +4 flags(U32) / +8 ca_byte_offset_base(U16) /
+10 ca_inval_bit_pos(U16) / +16 dim_size[](U64,步长 8),
其后才是**可选**的 cycle_count[](U32)。
⚠️ 实测 CANape 写出的文件没有 cycle_count(块长度恰好到 dim_size 就结束),
盲读会把下一个块头当数字(实测得到 1481909027)。
- 展开条件(任何一条不满足就原样保留,只显示元素 0,绝不猜):
ca_storage == 0;ca_type <= 2(0 数组 / 1 缩放轴 / 2 区间轴,三者物理布局相同);
cn_bit_count ∈ [1, 64];cycle_count 每维 <= 1(元素不被拆到多条记录);
bit_offset + 元素数 × 位宽 <= cg_data_bytes × 8(真正的完整性校验)。
规模护栏:元素数 <= 16384 且 元素数 × 记录数 <= 32M 单元格
(取代原来的 count > 1024 魔数)。
- 元素名按 dim_size 从末维向前取余,1/2/3 维分别得到 [i] / [i][j] / [i][j][k]。
- MF4-Record.MF4 仍是 283 个信号(25 个 ##CA 全部通过新的容量校验,最大 7x9=63)。
二、cc_type = 8(值 -> 文本,区间表)补齐
- cc_val[] 交错存放 (下限, 上限),cc_ref[4 + i] 是第 i 段文本(最后一个引用是默认文本,不使用);
cc_val_count 必须是偶数(奇数说明表残缺,整表丢弃);下限 > 上限时交换。
- SignalEnumText 新增 rangeMin / rangeMax / rangeTexts,并拆出 HasExact() / HasRanges();
Find() 先查精确表(cc_type = 7)再查区间表(cc_type = 8,闭区间)。
- 界面没有改动:两种枚举共用同一出口,只在**光标取值标注**追加 " = <文本>",
常规状态零变化。
三、修复:稠密包络带下改变线宽时竖直沿不变粗
- 根因:包络带每列只有 1 个 x 坐标,旧实现只在 top/bottom 各外扩半个线宽
=> 水平段会变粗,但方波竖直沿(上升/下降段)的横向宽度恒为 1 个像素列。
- 修法:EmitEnvelopeBand 新增 halfWidth 形参,先对上下包络做横向窗口膨胀
(top 取窗口最小、bottom 取窗口最大,半径 ceil(halfWidth / 列距)),再纵向各外扩;
膨胀只在同一段内生效,不会跨 NaN 断点连线。
验证(全离屏,不占用鼠标/键盘/屏幕)
- .preflight/convcheck 18/18:新增 3 维命名与取值、元素放不进记录时必须拒绝展开、
cc_type = 8 的命中/未命中。
- 新增 .preflight/round5check 5/5:合成 20 万采样数字信号(必然走包络带),
在网格中间行测量竖直沿横向像素宽度:线宽 1px -> 6px,10px -> 30px。
- A/B:临时把横向膨胀半径置 0(等价旧行为)=> 1px -> 2px、10px -> 2px 且如实 FAIL。
- 回归:check 137/137、editcheck 20/20、round3check 31/31、round4check 34/34;
真实 MF4 信号数 283/8/18/24 不变;make release / make debug 零警告;
TargetCheck.exe 808,448 字节(789.5 KiB)。
文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 165-170 条硬约束与文末本轮小节
(含「6 位有效数字」的口径说明)。
This commit is contained in:
1 parent
b315201598
commit
30f853a361
5 files changed
+460
-45
No files matched your search
+217
-21
@@ -12,6 +12,7 @@
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace jdi
|
||||
@@ -61,7 +62,21 @@ namespace jdi
|
||||
TAB = 5, /**< 值→值查表(**不插值**,取阶梯值)。 */
|
||||
TAB_RANGE = 6, /**< 值区间→值(`[min,max] → phys`,区间外不换算)。 */
|
||||
VALUE_TO_TEXT = 7, /**< 值 → 枚举文本(`cc_val[i]` ↔ `cc_ref[i]` 文本)。 */
|
||||
VALUE_RANGE_TO_TEXT = 8 /**< 值区间 → 文本(未支持,按恒等处理)。 */
|
||||
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 /**< 区间轴(每维断点成对给出)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -381,13 +396,29 @@ namespace jdi
|
||||
* 查表类(4/5)为交错的 `(raw, phys)` 点对,区间表(6)为 `(min, max, phys)` 三元组。
|
||||
*/
|
||||
std::vector<double> conversionValues;
|
||||
SignalEnumText enumText; /**< 值 → 文本枚举表(`cc_type == 7`,可能为空)。 */
|
||||
/** 值 → 文本枚举表(`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;
|
||||
};
|
||||
@@ -989,35 +1020,125 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
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 写出的文件):type(1) + storage(1) + ndim(U16) + flags(U32)
|
||||
// + [8..15 保留] + dim_size[](U64 × ndim)。
|
||||
// 数据段布局(实测 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);
|
||||
spec.arrayStorage = file.U8(caData + 1);
|
||||
const std::uint16_t rank = file.U16(caData + 2);
|
||||
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)
|
||||
{
|
||||
total = 0;
|
||||
dimsOk = false;
|
||||
break;
|
||||
}
|
||||
spec.arrayDims.push_back(dim);
|
||||
total *= dim;
|
||||
// 元素总数太大时不再累乘(只用它判断「是否可展开」)。
|
||||
if (total <= (1u << 24))
|
||||
{
|
||||
total *= dim;
|
||||
if (total > (1u << 24))
|
||||
{
|
||||
total = (1u << 24) + 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
spec.arrayElements = total;
|
||||
|
||||
// `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 的时间轴),同样可以取值。
|
||||
@@ -1042,20 +1163,81 @@ namespace jdi
|
||||
*(实测:`WIN_Motor_StallCurrentVolBrk` 的 9 个 `uint16` 就是从记录 +8 起连续 18 字节)。
|
||||
* 展开后每个元素都是普通标量信号,因此自动获得光标取值、枚举、图例等全部能力。
|
||||
*
|
||||
* ⚠️ 仅处理 `storage == 0`(记录内连续);`storage != 0`(元素分散在不同记录/
|
||||
* 不同通道组/不同数据组)保持原样(只显示元素 0),避免解析出错值。
|
||||
* ⚠️ 元素总数超过 `kMaxArrayElements` 时同样保持原样(防止巨型数组把信号列表撑爆)。
|
||||
* 展开的**必要条件**(任何一条不满足就原样保留,只显示元素 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::vector<ChannelSpec>& out)
|
||||
void ExpandArrayChannel(ChannelSpec&& spec, std::uint64_t cycles, std::size_t recordBytes,
|
||||
std::vector<ChannelSpec>& out)
|
||||
{
|
||||
constexpr std::size_t kMaxArrayElements = 1024;
|
||||
/** 单个通道最多展开成多少条信号。 */
|
||||
constexpr std::size_t kMaxArrayElements = 16384;
|
||||
/** 元素总数 × 记录数的上限(≈ 生成的数据点数 × 8 字节的内存/绘制代价)。 */
|
||||
constexpr std::size_t kMaxArrayCells = 32u << 20;
|
||||
|
||||
const std::size_t count = spec.arrayElements;
|
||||
if (count <= 1 || spec.arrayStorage != 0 || count > kMaxArrayElements || spec.bitCount == 0)
|
||||
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)
|
||||
@@ -1063,11 +1245,24 @@ namespace jdi
|
||||
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 && dims[1] > 0)
|
||||
if (dims.size() >= 2)
|
||||
{
|
||||
suffix = L"[" + std::to_wstring(index / dims[1]) + L"][" +
|
||||
std::to_wstring(index % dims[1]) + L"]";
|
||||
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
|
||||
{
|
||||
@@ -1646,6 +1841,7 @@ namespace jdi
|
||||
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)
|
||||
@@ -1653,9 +1849,9 @@ namespace jdi
|
||||
ChannelSpec spec;
|
||||
if (ParseChannel(file, cn, spec))
|
||||
{
|
||||
// `##CA` 数组通道在这里就地展开成多个标量通道
|
||||
//(一个通道装整条曲线时,旧实现只能看到元素 0)。
|
||||
ExpandArrayChannel(std::move(spec), channels);
|
||||
// 一个通道装整条曲线时,旧实现只能看到元素 0。
|
||||
ExpandArrayChannel(std::move(spec), cycles, static_cast<std::size_t>(cgDataBytes),
|
||||
channels);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user