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:
@@ -125,9 +125,16 @@ static int f_clock(void *ctx, uint64_t ms)
|
||||
s_clock_ms = ms;
|
||||
return s_clock_rc;
|
||||
}
|
||||
/* B9 时标源假回调:s_epoch_val = UINT64_MAX → 时钟无效(CP56 IV) */
|
||||
static uint64_t s_epoch_val;
|
||||
static uint64_t f_epoch(void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return s_epoch_val;
|
||||
}
|
||||
|
||||
static iec104_link_t g_lk;
|
||||
static const iec104_link_cfg_t g_cfg = { 1u, cap_send, f_clock };
|
||||
static const iec104_link_cfg_t g_cfg = { 1u, cap_send, f_clock, f_epoch };
|
||||
|
||||
/* ---------------- 主站侧模拟(N(R) 单调,照真实主站) ---------------- */
|
||||
static uint16_t m_ns;
|
||||
@@ -204,6 +211,7 @@ static void reset_all(uint32_t now)
|
||||
| (1uLL << 22) | (1uLL << 30);
|
||||
s_clock_rc = 0;
|
||||
s_clock_ms = 0u;
|
||||
s_epoch_val = 1760234567000ULL; /* B9:固定时标(2025-10-12 UTC) */
|
||||
iec104_link_init(&g_lk, &g_cfg, NULL);
|
||||
iec104_link_open(&g_lk, now);
|
||||
m_reset();
|
||||
@@ -460,6 +468,7 @@ static void tc_cov_yx(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
cap_t c;
|
||||
uint64_t back;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
@@ -467,16 +476,30 @@ static void tc_cov_yx(void)
|
||||
CHECK(cap_i_count() == 0, "TC-007 首 poll 无自发");
|
||||
s_f_bits |= (1uLL << 15); /* YX16(idx15)动作 */
|
||||
iec104_link_poll(&g_lk, now + 100u);
|
||||
CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 1u)
|
||||
/* B9:单点 YX 自发 = M_SP_TB_1(30)带 CP56Time2a 时标(SOE 上送路径) */
|
||||
CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 30u)
|
||||
&& (c.cot == IEC104_COT_SPONT) && (c.ioa == 16u)
|
||||
&& (c.obj_len == 1u) && (c.obj[0] == 1u), "TC-007 YX16 变位 COT3");
|
||||
&& (c.obj_len == 8u) && (c.obj[0] == 1u), "TC-007 YX16 变位 COT3 TB");
|
||||
if ((cap_i(0, &c) == 0) && (c.obj_len == 8u)) {
|
||||
CHECK(iec104_cp56_to_epoch_ms(&c.obj[1], &back) == 0,
|
||||
"TC-007 CP56 可解");
|
||||
CHECK(back == s_epoch_val, "TC-007 时标 = epoch_ms 源");
|
||||
}
|
||||
s_f_bits &= ~(1uLL << 15); /* 复归 */
|
||||
iec104_link_poll(&g_lk, now + 200u);
|
||||
CHECK((cap_i_count() == 2) && (cap_i(1, &c) == 0) && (c.ioa == 16u)
|
||||
&& (c.obj[0] == 0u), "TC-007 YX16 复归再变");
|
||||
s_f_bits |= (1uLL << 0); /* idx0 无效位 → 不触发 */
|
||||
CHECK((cap_i_count() == 2) && (cap_i(1, &c) == 0) && (c.type == 30u)
|
||||
&& (c.ioa == 16u) && (c.obj[0] == 0u), "TC-007 YX16 复归再变");
|
||||
/* B9 IV 路径:时标源无效 → CP56 IV 位如实置位 */
|
||||
s_epoch_val = UINT64_MAX;
|
||||
s_f_bits |= (1uLL << 16); /* YX17(idx16)动作 */
|
||||
iec104_link_poll(&g_lk, now + 300u);
|
||||
CHECK(cap_i_count() == 2, "TC-007 无效位变位不触发");
|
||||
CHECK((cap_i_count() == 3) && (cap_i(2, &c) == 0) && (c.type == 30u)
|
||||
&& (c.ioa == 17u) && ((c.obj[3] & 0x80u) != 0u),
|
||||
"TC-007 时钟无效 → CP56 IV 位");
|
||||
s_epoch_val = 1760234567000ULL;
|
||||
s_f_bits |= (1uLL << 0); /* idx0 无效位 → 不触发 */
|
||||
iec104_link_poll(&g_lk, now + 400u);
|
||||
CHECK(cap_i_count() == 3, "TC-007 无效位变位不触发");
|
||||
}
|
||||
|
||||
static void tc_cov_yc(void)
|
||||
|
||||
@@ -55,6 +55,7 @@ U_STARTDT_ACT, U_STARTDT_CON = 0x07, 0x0B
|
||||
U_TESTFR_ACT, U_TESTFR_CON = 0x43, 0x83
|
||||
|
||||
T_M_SP_NA_1, T_M_DP_NA_1, T_M_ME_NC_1 = 1, 3, 13
|
||||
T_M_SP_TB_1 = 30 # single point + CP56Time2a (B9 SOE spontaneous)
|
||||
T_C_SE_NC_1, T_C_DC_NA_1 = 50, 52
|
||||
T_C_IC_NA_1, T_C_RD_NA_1 = 100, 102
|
||||
|
||||
@@ -109,6 +110,28 @@ def asdu(type_id, vsq, cot, cot_hi, ioa=None, payload=b""):
|
||||
return h + payload
|
||||
|
||||
|
||||
def cp56_str(b):
|
||||
"""Decode CP56Time2a (7B) -> 'YYYY-MM-DD HH:MM:SS.mmm' (UTC) or 'IV-...'.
|
||||
B9: M_SP_TB_1 spontaneous time tag (sec-resolution, ms field = sec*1000)."""
|
||||
iv = (b[2] & 0x80) != 0
|
||||
ms_min = b[0] | (b[1] << 8)
|
||||
minute = b[2] & 0x3F
|
||||
hour = b[3] & 0x1F
|
||||
day = b[4] & 0x1F
|
||||
mon = b[5] & 0x0F
|
||||
year = 2000 + (b[6] & 0x7F)
|
||||
sec = ms_min // 1000
|
||||
ms = ms_min % 1000
|
||||
try:
|
||||
import datetime
|
||||
d = datetime.datetime(year, mon, day, hour, minute, sec, ms * 1000)
|
||||
s = d.strftime("%Y-%m-%d %H:%M:%S") + (".%03d" % ms)
|
||||
except ValueError:
|
||||
s = "%04d-%02d-%02d %02d:%02d:%02d.%03d" % (
|
||||
year, mon, day, hour, minute, sec, ms)
|
||||
return ("IV " if iv else "") + s
|
||||
|
||||
|
||||
class Master(object):
|
||||
"""Minimal 104 master. k/w/t are covered by "S after every recv batch"
|
||||
(far faster than t1=15s / t2=10s), so no timers are needed here."""
|
||||
@@ -208,26 +231,32 @@ class Master(object):
|
||||
n = vsq & 0x7F
|
||||
obj = a[6:]
|
||||
|
||||
if type_id in (T_M_SP_NA_1, T_M_DP_NA_1):
|
||||
pts = []
|
||||
if type_id in (T_M_SP_NA_1, T_M_DP_NA_1, T_M_SP_TB_1):
|
||||
# B9: M_SP_TB_1 (30) = SIQ + CP56Time2a (8B/point); NA forms = 1B.
|
||||
esz = 8 if type_id == T_M_SP_TB_1 else 1
|
||||
pts = [] # (ioa, raw, elem_off_in_obj)
|
||||
if sq:
|
||||
base = get_ioa(obj)
|
||||
for i in range(n):
|
||||
pts.append((base + i, obj[3 + i]))
|
||||
pts.append((base + i, obj[3 + i * esz], 3 + i * esz))
|
||||
else:
|
||||
off = 0
|
||||
for _ in range(n):
|
||||
pts.append((get_ioa(obj, off), obj[off + 3]))
|
||||
off += 4
|
||||
for ioa, raw in pts:
|
||||
pts.append((get_ioa(obj, off), obj[off + 3], off + 3))
|
||||
off += 3 + esz
|
||||
for ioa, raw, eoff in pts:
|
||||
val = (raw & 0x03) if type_id == T_M_DP_NA_1 else (raw & 0x01)
|
||||
iv = (raw & 0x80) != 0
|
||||
self.yx[ioa] = (val, iv)
|
||||
ts = ""
|
||||
if type_id == T_M_SP_TB_1:
|
||||
ts = " ts=%s" % cp56_str(obj[eoff + 1:eoff + 8])
|
||||
self.events.append(("yx", (ioa, val, iv, cot)))
|
||||
if ioa in YX_NAMES or cot == COT_SPONT:
|
||||
log("RX I %s ioa=%d val=%d iv=%d cot=%d [N(R)=%d]"
|
||||
% ("M_DP" if type_id == T_M_DP_NA_1 else "M_SP",
|
||||
ioa, val, 1 if iv else 0, cot, nr))
|
||||
log("RX I %s ioa=%d val=%d iv=%d cot=%d%s [N(R)=%d]"
|
||||
% ({T_M_DP_NA_1: "M_DP", T_M_SP_NA_1: "M_SP",
|
||||
T_M_SP_TB_1: "M_SP_TB"}[type_id],
|
||||
ioa, val, 1 if iv else 0, cot, ts, nr))
|
||||
elif type_id == T_M_ME_NC_1:
|
||||
pts = []
|
||||
if sq:
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""decode_soe.py - /log/soe.log 主机解码器 + 断言(B9 台架腿证据工具,纯标准库)。
|
||||
|
||||
做什么:把 FTP 下载的设备端 /log/soe.log(定长 64B 二进制记录,格式见
|
||||
App/log/soe_rec.h)逐条解码成可读行,并断言:
|
||||
1) 文件大小 = 64 的整数倍(定长不变量);
|
||||
2) 每条记录版本 = 1 且 sum8 校验和通过(撕裂/半写检测);
|
||||
3) seq 单调:同一 boot 段内严格 +1(seq 回落 = 设备复位/容量重建段边界,
|
||||
如实分段计数,不算失败);
|
||||
4) 时标:IV 记录如实报告;非 IV 记录时标必须落在合理窗口
|
||||
(2020..2099,对时链已就绪的台架前提);
|
||||
5) 类型合法(1..6);元件事件 elm_id < --max-elm(默认 16 留余量;
|
||||
PE_COUNT 现 7,B8 过频/低频并入后 9——写死 7 会在 B8 合并后误报)。
|
||||
任一断言失败 -> 退出码 1(台架腿 FAIL 判据之一)。
|
||||
|
||||
用法(仓库根目录):
|
||||
& '.venv\\Scripts\\python.exe' test/host/soe/decode_soe.py <soe.log 路径>
|
||||
可选:--expect-assert N 断言至少 N 条"元件动作"记录(台架腿用 1)
|
||||
|
||||
退出码:0=全部断言 PASS / 1=断言失败 / 2=参数或文件错误。
|
||||
|
||||
格式镜像:App/log/soe_rec.h(改格式必须同步本工具)。
|
||||
创建日期:2026-10-12(B9)
|
||||
"""
|
||||
import argparse
|
||||
import calendar
|
||||
import datetime
|
||||
import struct
|
||||
import sys
|
||||
|
||||
REC_SIZE = 64
|
||||
VER = 1
|
||||
OFF_VER, OFF_TYPE, OFF_ELM, OFF_FLAGS = 0, 1, 2, 3
|
||||
OFF_SEQ, OFF_CP56, OFF_EPOCH, OFF_VALUE, OFF_SUM = 4, 8, 15, 19, 63
|
||||
FLG_IV = 0x80
|
||||
|
||||
TYPE_NAMES = {
|
||||
1: "ELM_ASSERT", 2: "ELM_RETURN", 3: "PLATE_OUT", 4: "PLATE_IN",
|
||||
5: "SAMPLE_BLOCK", 6: "SAMPLE_RESUME",
|
||||
}
|
||||
|
||||
|
||||
def cp56_decode(b):
|
||||
"""CP56Time2a 7B -> (datetime|None, iv). ms 域 = sec*1000(B9 秒域时基)。"""
|
||||
iv = (b[2] & 0x80) != 0
|
||||
ms_min = b[0] | (b[1] << 8)
|
||||
minute = b[2] & 0x3F
|
||||
hour = b[3] & 0x1F
|
||||
day = b[4] & 0x1F
|
||||
mon = b[5] & 0x0F
|
||||
year = 2000 + (b[6] & 0x7F)
|
||||
sec, ms = divmod(ms_min, 1000)
|
||||
try:
|
||||
dt = datetime.datetime(year, mon, day, hour, minute, sec, ms * 1000)
|
||||
except ValueError:
|
||||
dt = None
|
||||
return dt, iv
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="decode /log/soe.log")
|
||||
ap.add_argument("path", help="downloaded soe.log file")
|
||||
ap.add_argument("--expect-assert", type=int, default=0,
|
||||
help="min ELM_ASSERT records required (bench leg: 1)")
|
||||
ap.add_argument("--max-elm", type=int, default=16,
|
||||
help="exclusive upper bound for element elm_id "
|
||||
"(PE_COUNT: 7 pre-B8, 9 post-B8; 16 = headroom)")
|
||||
args = ap.parse_args()
|
||||
|
||||
try:
|
||||
with open(args.path, "rb") as f:
|
||||
data = f.read()
|
||||
except OSError as e:
|
||||
print("ERROR: cannot read %s: %s" % (args.path, e))
|
||||
return 2
|
||||
|
||||
print("soe.log: %d bytes, %d records" % (len(data), len(data) // REC_SIZE))
|
||||
fails = []
|
||||
if len(data) == 0:
|
||||
fails.append("empty file (no SOE records)")
|
||||
if len(data) % REC_SIZE != 0:
|
||||
fails.append("size %d not multiple of %d (torn tail)"
|
||||
% (len(data), REC_SIZE))
|
||||
|
||||
n_seg = 0
|
||||
prev_seq = None
|
||||
n_assert = 0
|
||||
for i in range(len(data) // REC_SIZE):
|
||||
rec = data[i * REC_SIZE:(i + 1) * REC_SIZE]
|
||||
if rec[OFF_VER] != VER:
|
||||
fails.append("rec %d: bad version %d" % (i, rec[OFF_VER]))
|
||||
continue
|
||||
s = sum(rec[:OFF_SUM]) & 0xFF
|
||||
if rec[OFF_SUM] != s:
|
||||
fails.append("rec %d: checksum mismatch (stored 0x%02X calc 0x%02X)"
|
||||
% (i, rec[OFF_SUM], s))
|
||||
continue
|
||||
rtype = rec[OFF_TYPE]
|
||||
elm = rec[OFF_ELM]
|
||||
flags = rec[OFF_FLAGS]
|
||||
seq = struct.unpack_from("<I", rec, OFF_SEQ)[0]
|
||||
epoch = struct.unpack_from("<I", rec, OFF_EPOCH)[0]
|
||||
value = struct.unpack_from("<I", rec, OFF_VALUE)[0]
|
||||
dt, iv = cp56_decode(rec[OFF_CP56:OFF_CP56 + 7])
|
||||
|
||||
if rtype not in TYPE_NAMES:
|
||||
fails.append("rec %d: bad type %d" % (i, rtype))
|
||||
if rtype in (1, 2) and elm >= args.max_elm:
|
||||
fails.append("rec %d: elm_id %d out of range (max %d)"
|
||||
% (i, elm, args.max_elm - 1))
|
||||
|
||||
# seq monotonic within a boot segment; drop = new segment (reset/rebuild)
|
||||
if prev_seq is None or seq <= prev_seq:
|
||||
n_seg += 1
|
||||
elif seq != prev_seq + 1:
|
||||
fails.append("rec %d: seq gap %d -> %d (not strictly +1)"
|
||||
% (i, prev_seq, seq))
|
||||
prev_seq = seq
|
||||
|
||||
if (flags & FLG_IV) != 0:
|
||||
if not iv:
|
||||
fails.append("rec %d: flags IV but CP56 IV clear" % i)
|
||||
ts_s = "IV"
|
||||
else:
|
||||
if iv:
|
||||
fails.append("rec %d: CP56 IV set but flags IV clear" % i)
|
||||
if dt is None or not (2020 <= dt.year <= 2099):
|
||||
fails.append("rec %d: timestamp out of window: %r" % (i, dt))
|
||||
ts_s = dt.strftime("%Y-%m-%d %H:%M:%S") if dt else "BAD"
|
||||
# CP56 = UTC 分解;epoch 字段 = UTC 秒 → 用 calendar.timegm(UTC)。
|
||||
# ⚠ 本机时区 +08 时 dt.timestamp() 会差 28800 s(首跑实证坑)
|
||||
if dt and epoch and abs(calendar.timegm(dt.utctimetuple()) - epoch) > 2:
|
||||
fails.append("rec %d: cp56/epoch cross-check off (%s vs %d)"
|
||||
% (i, ts_s, epoch))
|
||||
|
||||
if rtype == 1:
|
||||
n_assert += 1
|
||||
print(" #%03d seq=%-10d %-13s elm=%-3s val=0x%08X ts=%s"
|
||||
% (i, seq, TYPE_NAMES.get(rtype, "?%d" % rtype),
|
||||
"-" if elm == 0xFF else str(elm), value, ts_s))
|
||||
|
||||
print("segments (boot/rebuild boundaries): %d, ELM_ASSERT records: %d"
|
||||
% (n_seg, n_assert))
|
||||
if n_assert < args.expect_assert:
|
||||
fails.append("ELM_ASSERT count %d < expected %d"
|
||||
% (n_assert, args.expect_assert))
|
||||
|
||||
if fails:
|
||||
for x in fails:
|
||||
print("FAIL: %s" % x)
|
||||
print("SUMMARY: FAIL (%d)" % len(fails))
|
||||
return 1
|
||||
print("SUMMARY: PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""SOE 记录编解码(B9)主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:用本机 MSVC 离线编译 test/host/soe/soe_test.c(#include 纳入
|
||||
生产源文件 soe_rec.c + iec104_asdu.c),验证 64B 定长记录构建/解析
|
||||
往返、sum8 校验和硬门槛、CP56Time2a 时标编码(与 iec104 解码器交叉
|
||||
互证)、IV 语义、钳位、结构不变量。
|
||||
|
||||
用法(仓库根目录):
|
||||
& '.venv\\Scripts\\python.exe' test/host/soe/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误。
|
||||
|
||||
设计依据:docs/算法/继电保护功能复现规划.md §3.4(L4-4/L4-5,B9)。
|
||||
创建日期:2026-10-12
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
|
||||
|
||||
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
|
||||
|
||||
BUILD = TestBuild(
|
||||
name="SOE Record Tests",
|
||||
src=os.path.join(HERE, "soe_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(ROOT, "App", "log"),
|
||||
os.path.join(ROOT, "App", "protocol", "iec104"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== SOE Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user