feat(soe): B9 动作记录 + 时标 SOE(L4-4/L4-5)+ 104 M_SP_TB_1 自发

- 新增 App/log/soe_rec.{c,h}:64B 定长 SOE 记录纯编解码(CP56Time2a
  时标 + 类型 + 元件号 + 单调序号 + 值 + sum8 撕裂校验),零 I/O 零 OS
  依赖,主机 L0.5 覆盖(32 断言,含与 iec104 CP56 解码器交叉互证)
- 新增 App/log/soe_store.{c,h}:protectTask 边沿投递(取时标 + 组记录 +
  SPSC 待发环/消息队列,零文件系统);defaultTask 经 syslog_drain 尾部
  排空落 /log/soe.log(卷锁 try / FA_WRITE|FA_OPEN_APPEND / 校验字节数 /
  容量满 unlink 重建,全套照 run.log 实证纪律;无卷损坏归类路径)
- protect_task.c:元件动作/返回、软压板投退、采样闭锁/恢复六类边沿
  → soe_store_post;post 内禁 DBG_*(protect 静态最坏链 872 B 不变,
  改前/改后 stack_report 九任务逐字节一致),丢弃留痕挪 drain 侧
- iec104:单点 YX 自发改 M_SP_TB_1(30) 带 CP56 时标(总召/周期保持
  M_SP_NA_1,单测两种都断言);link cfg 新增 epoch_ms 时标源回调
  (NULL/无效 → IV 位如实置位);serv 绑 sys_clock(秒域时基,
  ms 域 = 秒×1000,不造假亚秒)
- 主机单测:test/host/soe/(32 断言)+ iec104 94→97(TB 时标/IV);
  iec104_master.py 解析 type 30(B6/B7 流不回归);decode_soe.py
  台架解码断言工具(seq 单调/校验和/UTC 交叉)
- 注册:uvprojx + check_registration SIM_EXEMPT 点名(MCU-only 胶水)
- 文档(我方属主区):`App/log/README.md` 增 soe 节(64B 记录/单写者/
  post 禁 DBG/UTC 交叉坑);`docs/子系统设计/IEC104上行设计说明.md`
  §1 范围行 + §3.2 时标注 + §7 台架行 + §8 第 2 条销项 + 变更记录行

台架腿(bench 锁 B9,release --bench-pass):run_boot_test PASS;
--b7-pre 21/21、--b7-post 13/13(B6/B7 回归 + 确定性动作边沿);
自发 YX 上送 M_SP_TB_1 带 ts(11:00:51 动作 / 11:00:53 复归);
FTP 下载 /log/soe.log 30 条记录 decode PASS(seq 0..28 单调、
ELM_ASSERT 14 条、UTC 交叉一致);定值/压板已恢复默认。

台账 +1088 B(SPSC 环 512 + 静态 FIL 528 + 计数器)随合并由 Lead
回填(ram_baseline --write + check_mem_numbers --write)。

gate: only mem-budget/mem-numbers pending lead ledger backfill
(pre-commit --all-files: 29 Passed / 2 Failed / 0 Skipped;
hygiene self-check OK: no BOM/CRLF/mojibake/trailws/non-ASCII literal)

sync 追平 v2-on-ui(B8 freq + N33 抬栈)后复跑:@build 0/0
(Code=209492 RW=1104 ZI=154184);stack_report 九任务全 PASS、
protectTask 872 B/57% 不变;ci_local L0.5 15 runner 951/951
(freq 46 / soe 32 / iec104 97 / prot_set 90 / prot_elm 47)。
顺带修 protect_task.c 陈旧注释:定值巡检 31 项 → 40 项(B8 后)。
This commit is contained in:
2026-10-12 19:16:12 +08:00
parent 0d39cf63b1
commit 1d96fc9f87
19 changed files with 1161 additions and 29 deletions
+213
View File
@@ -0,0 +1,213 @@
/*
* 模块名称:SOE 记录编解码主机侧单元测试(B9,L0.5)
* 模块功能:离线验证 App/log/soe_rec.c——64B 定长记录构建/解析往返、
* 校验和硬门槛(撕裂/半写拒绝)、版本门槛、CP56Time2a 时标
* 编码正确性(与 iec104_asdu.c 的 CP56 解码交叉验证:同一算法族
* 两个实现互证)、IV 语义、钳位、保留区清零。
*
* 被测对象:真实源文件(#include 原样纳入):
* soe_rec.c(B9 记录编解码)
* iec104_asdu.c(交叉验证用 CP56 解码器,纯逻辑)
*
* 覆盖点:
* TC-SOE-001 构建/解析字段往返(type/elm/flags/seq/epoch/value)
* TC-SOE-002 校验和硬门槛:单字节翻转 → -3;版本篡改 → -2
* TC-SOE-003 CP56 编码正确性:已知 epoch → 期望字节序列;
* 与 iec104_cp56_to_epoch_ms 往返互证(含闰日 2024-02-29)
* TC-SOE-004 IV 语义:flags IV → CP56 byte2 bit7 置位 + epoch 字段 0
* TC-SOE-005 钳位:epoch < 2000 → 2000-01-01;epoch > 2127 上界 → 2127-12-31
* TC-SOE-006 结构不变量:size=64、保留区 [23..62] 恒 0、sum=sum8([0..62])
* TC-SOE-007 序号域:0 / 0xFFFFFFFF 边界往返(uint32 回绕语义)
*
* 通过:全部 TC PASS(main 返回 0)。
* 创建日期:2026-10-12(B9)
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static int s_total = 0, s_passed = 0, s_failed = 0;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
#include "soe_rec.c"
#include "iec104_asdu.c"
/* ---------------- TC-SOE-001 字段往返 ---------------- */
static void tc_roundtrip(void)
{
uint8_t rec[SOE_REC_SIZE];
soe_rec_t out;
int rc;
soe_rec_build(rec, (uint8_t)SOE_T_ELM_ASSERT, 2u, 0u, 12345u,
1760234567u, 0xDEADBEEFu);
rc = soe_rec_parse(rec, &out);
CHECK(rc == 0, "TC-001 parse ok");
CHECK(out.ver == SOE_REC_VER, "TC-001 ver");
CHECK(out.type == (uint8_t)SOE_T_ELM_ASSERT, "TC-001 type");
CHECK(out.elm_id == 2u, "TC-001 elm_id");
CHECK(out.flags == 0u, "TC-001 flags");
CHECK(out.seq == 12345u, "TC-001 seq");
CHECK(out.epoch_s == 1760234567u, "TC-001 epoch");
CHECK(out.value == 0xDEADBEEFu, "TC-001 value");
CHECK(out.cp56[6] == (uint8_t)((2025u - 2000u) & 0x7Fu), "TC-001 cp56 year=2025");
}
/* ---------------- TC-SOE-002 校验和/版本硬门槛 ---------------- */
static void tc_gates(void)
{
uint8_t rec[SOE_REC_SIZE];
soe_rec_t out;
uint32_t i;
int bad = 0;
soe_rec_build(rec, (uint8_t)SOE_T_ELM_RETURN, 0u, 0u, 7u, 1760234567u, 3u);
/* 每个数据字节单独翻转:解析必须全部拒绝(sum 覆盖 [0..62]) */
for (i = 0u; i < SOE_OFF_SUM; i++) {
uint8_t save = rec[i];
rec[i] ^= 0x01u;
if (soe_rec_parse(rec, &out) == 0) {
bad = 1;
}
rec[i] = save;
}
CHECK(bad == 0, "TC-002 any byte flip rejected");
CHECK(soe_rec_parse(rec, &out) == 0, "TC-002 intact parses ok");
rec[SOE_OFF_VER] = (uint8_t)(SOE_REC_VER + 1u); /* 版本篡改:sum 仍按新
* 字节算会自洽,但版本
* 门槛先拒 */
CHECK(soe_rec_parse(rec, &out) == -2, "TC-002 bad version -> -2");
CHECK(soe_rec_parse(NULL, &out) == -1, "TC-002 null args -> -1");
}
/* ---------------- TC-SOE-003 CP56 正确性 + 交叉往返 ---------------- */
static void tc_cp56(void)
{
uint8_t rec[SOE_REC_SIZE];
soe_rec_t out;
uint64_t back;
int rc;
/* 2026-10-12 12:34:56 UTC = 1791808496(手算:2026-10-12 = epoch 1791806400
*(当日 00:00 UTC)+ 12*3600+34*60+56 = 45296 → 1791851696? 以解码器为准,
* 这里用"往返互证 + 显式字段"双保险,不依赖手算) */
soe_rec_build(rec, (uint8_t)SOE_T_ELM_ASSERT, 0u, 0u, 1u, 1791808496u, 0u);
(void)soe_rec_parse(rec, &out);
/* 显式字段:12:34:56 → ms 域 = 56*1000(秒域时基如实编码,亚秒 0) */
CHECK(((uint32_t)out.cp56[0] | ((uint32_t)out.cp56[1] << 8)) == 56000u,
"TC-003 ms field = sec*1000 (56000)");
rc = iec104_cp56_to_epoch_ms(out.cp56, &back);
CHECK(rc == 0, "TC-003 cross decode ok");
CHECK(back == 1791808496000ULL, "TC-003 cross roundtrip exact");
/* 闰日 2024-02-29 00:00:00 UTC = 1709164800 */
soe_rec_build(rec, (uint8_t)SOE_T_SAMPLE_BLOCK, 0u, 0u, 2u, 1709164800u, 0u);
(void)soe_rec_parse(rec, &out);
CHECK(out.cp56[4] == 29u && out.cp56[5] == 2u && out.cp56[6] == 24u,
"TC-003 leap day fields");
rc = iec104_cp56_to_epoch_ms(out.cp56, &back);
CHECK((rc == 0) && (back == 1709164800000ULL), "TC-003 leap roundtrip");
/* 2000-01-01 00:00:00 边界 */
soe_rec_build(rec, (uint8_t)SOE_T_PLATE_IN, 0u, 0u, 3u, 946684800u, 0u);
(void)soe_rec_parse(rec, &out);
rc = iec104_cp56_to_epoch_ms(out.cp56, &back);
CHECK((rc == 0) && (back == 946684800000ULL), "TC-003 year-2000 boundary");
}
/* ---------------- TC-SOE-004 IV 语义 ---------------- */
static void tc_iv(void)
{
uint8_t rec[SOE_REC_SIZE];
soe_rec_t out;
uint64_t back;
soe_rec_build(rec, (uint8_t)SOE_T_ELM_ASSERT, 1u, SOE_FLG_IV, 9u, 0u, 1u);
(void)soe_rec_parse(rec, &out);
CHECK((out.cp56[2] & 0x80u) != 0u, "TC-004 cp56 IV bit set");
CHECK(out.epoch_s == 0u, "TC-004 epoch field zeroed");
CHECK(iec104_cp56_to_epoch_ms(out.cp56, &back) == -2,
"TC-004 cross decoder rejects IV");
}
/* ---------------- TC-SOE-005 钳位 ---------------- */
static void tc_clamp(void)
{
uint8_t rec[SOE_REC_SIZE];
soe_rec_t out;
uint64_t back;
soe_rec_build(rec, (uint8_t)SOE_T_ELM_ASSERT, 0u, 0u, 4u, 100u, 0u);
(void)soe_rec_parse(rec, &out);
CHECK((out.cp56[4] == 1u) && (out.cp56[5] == 1u) && (out.cp56[6] == 0u),
"TC-005 clamp low -> 2000-01-01");
(void)iec104_cp56_to_epoch_ms(out.cp56, &back);
CHECK(back == 946684800000ULL, "TC-005 clamp low exact");
/* epoch_s 为 uint32 → 最大 2106-02-07(0xFFFFFFFF),恒在 CP56 域内:
* 上界钳位是防御性死分支(epoch 域扩展时生效),此处断言精确往返 */
soe_rec_build(rec, (uint8_t)SOE_T_ELM_ASSERT, 0u, 0u, 5u, 0xFFFFFFFFu, 0u);
(void)soe_rec_parse(rec, &out);
CHECK(out.cp56[6] == 106u, "TC-005 uint32 max -> year 2106 (no clamp needed)");
(void)iec104_cp56_to_epoch_ms(out.cp56, &back);
CHECK(back == 4294967295000ULL, "TC-005 uint32 max exact roundtrip");
}
/* ---------------- TC-SOE-006 结构不变量 ---------------- */
static void tc_layout(void)
{
uint8_t rec[SOE_REC_SIZE];
uint32_t i;
uint8_t s = 0u;
int zero_ok = 1;
CHECK(SOE_REC_SIZE == 64u, "TC-006 record size 64");
soe_rec_build(rec, (uint8_t)SOE_T_SAMPLE_RESUME, SOE_ELM_NONE, 0u,
0u, 946684800u, 0u);
for (i = 23u; i <= 62u; i++) {
if (rec[i] != 0u) {
zero_ok = 0;
}
}
CHECK(zero_ok, "TC-006 reserved [23..62] zero");
for (i = 0u; i < SOE_OFF_SUM; i++) {
s = (uint8_t)(s + rec[i]);
}
CHECK(rec[SOE_OFF_SUM] == s, "TC-006 sum8 covers [0..62]");
CHECK(rec[SOE_OFF_ELM] == SOE_ELM_NONE, "TC-006 elm none");
}
/* ---------------- TC-SOE-007 序号边界 ---------------- */
static void tc_seq_edges(void)
{
uint8_t rec[SOE_REC_SIZE];
soe_rec_t out;
soe_rec_build(rec, (uint8_t)SOE_T_ELM_ASSERT, 0u, 0u, 0u, 946684800u, 0u);
CHECK((soe_rec_parse(rec, &out) == 0) && (out.seq == 0u), "TC-007 seq 0");
soe_rec_build(rec, (uint8_t)SOE_T_ELM_ASSERT, 0u, 0u, 0xFFFFFFFFu,
946684800u, 0u);
CHECK((soe_rec_parse(rec, &out) == 0) && (out.seq == 0xFFFFFFFFu),
"TC-007 seq 0xFFFFFFFF");
}
int main(void)
{
printf("=== SOE Record Codec Host Tests ===\n");
tc_roundtrip();
tc_gates();
tc_cp56();
tc_iv();
tc_clamp();
tc_layout();
tc_seq_edges();
printf("=== SOE Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}