feat(ui+记录): 整定写路径与四类页落地,五类记录写入方补齐

UI 面:ui_data 增加深度 4 参数写队列(请求 168 B、队列 .bss 672 B)与状态机
OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY,ui_data_feed 出队后按注入 ops 走「闭锁 → 整批
预检 → 逐项 prot_set_set_sealed」,只提交改动位图;par 帧尾追加 cal_block
(口径 = UA/UN/IA 三路 ADC_CAL_F_VALID,与 protect_task 判据同构,不用 rt.eng
反推,V0.1 下 eng 恒 0)、UI_DATA_VER 升到 2。参数/定值/控制字/软压板四叶绑定
内容页(UI_PAGE_MAP_COUNT 12→16,复用四个查看模块,零新增 .c),编辑态按
is_editable 分派,LOCK(写不进去)与「未标定」(仍可改值)两源两通道两词。

记录面:syslog_request_clear/state/apply 三段式删除 API(UI 只发请求,落地在
defaultTask 的 drain 尾部且必须后置,卷锁一律 try,幂等只认 FR_NO_FILE);
事故/告警/启动/操作/遥控五类各有一个真实写入点,记录类型 = 日志 ID 本身
(一类一个 /log 严格 8.3 文件;64B 定长体没有类型字段位,塞字段会砸掉既有读者)。
为守 protectTask 872 B 静态最坏链另拆 syslog_post_quiet()(无 DBG 的投递核,
丢弃留痕挪到 drain 侧差值补发),syslog_post 对外语义逐字不变。

为什么两道合成一笔而不是各一笔(实测,不是省事):simulator/main.c 同时消费
两道新增 API(整定 pump 与 logtest 清除用例),按道切文件会让中间提交编不过;
RAM 受管快照与体积同码凭证描述的是合并后的 .map,拆开必落在「台账与 .map 不同
码」这条红线上。pre-commit 跑钩子前会 stash 未 staged 部分,这类耦合没法绕。

实测:@build 0 错误 0 警告;Code 228,260 / RO 24,960 / RW 1,440 / ZI 135,976 B;
RW_IRAM1 79,992 B = 61.0%、RW_CCM 87.9%(阈值 92%);九个任务静态最坏链全 PASS,
protectTask 872 B / 57% 改前后逐字节不变(记录投递没垫栈)。主机单测 ui_data 82→
164、sys_log_mcu 106→137、prot_elm 47→60、iec104 141→154、iec104_serv 28→34,
既有断言零删改;48 个钩子 --all-files 全绿;编号门禁可判定 86.34%(基线 86.0%)、
不可判定 1,153 处低于上限 1,166;doc-links 无法判定 0;UI 黄金 18 页扩到 27 页
连跑两次逐字节一致;字库 diff 空。

顺手修掉三处既有缺陷(都是本轮第一个消费者才暴露):sim_syslog.c 的 s_paths 行宽
16 而 "sim_selftest.log" 正好 16 字符没有 NUL 位(stat/remove 读越过数组尾,此前
靠后面 .rodata 碰巧为 0);ui_w_setlist 二态行 ENTER 不回绕(控制字与软压板出厂值
全在最后一项,按了像没反应而叠层里已是越界值);生成物 ui_menu_tab.h 与 gen_menu.py
模板漂移(上一笔改了模板没重跑生成器),现随生成器输出对齐。

未收口(不算完成):保存后重启值保持与主站 C_RD 读回两条只能上板,台架窗口执行;
LOCK 屏显与越界反显的页面侧在模拟器造不出来(模拟器不跑定值自检、无 NAND),
已由生成侧单测与代码注释点名差距;同类页数与钩子数陈述残留另立待办。
This commit is contained in:
2026-10-26 15:51:12 +08:00
parent 0f7b5a87fb
commit 77779d49d6
55 changed files with 4236 additions and 289 deletions
+147 -4
View File
@@ -1,12 +1,30 @@
/*
* 模块名称:System Log (模拟器后端) — 运行/自检记录落盘
* 模块功能:宿主文件 sim_run.log / sim_selftest.log(工作目录),与 MCU 端同格式
* (定长 64B 记录);单线程直写,无队列
* 模块功能:宿主文件七份(sim_run / sim_selftest / sim_accid / sim_alarm / sim_start /
* sim_oper / sim_yk,均 .log,工作目录),与 MCU 端同格式、同 id 口径
* (定长 64B 记录);单线程直写,无队列;含「清除记录」后端(与 MCU 端
* 同一套 API 与同一组判据:删后重建可继续写 / 计数归零 / 文件不在时幂等)
* 适用平台:PC 模拟器
* 作者:王建锋
* 创建日期:2026-09-14
* 修改记录:
* v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4)
* v1.1 2026-10-26 王建锋 新增「记录删除」后端(道F task-4,与 sys_log_mcu.c
* 同一份 sys_log.h 契约):syslog_request_clear() /
* syslog_clear_state()。两端**时序有别、判据相同**:
* 模拟器单线程(本文件里 syslog_post 也是直写不排队,
* 同一条先例),请求在调用内同步落地,因此
* SYSLOG_CLEAR_WAIT 在本端永不出现;判据(幂等 0 /
* 删后可重建继续写 / 失败不动原文件)逐条与 MCU 端一致
* v1.2 2026-10-26 王建锋 task-10 扩五类记录:s_paths/s_caps 同步到七项。
* **顺手修一处既有隐患**(Lead 批准,须在提交信息留痕):
* 原 `s_paths[SYSLOG_COUNT][16]` 里 "sim_selftest.log"
* 正好 16 字符 → 初值**装不满、没有 NUL 位**,而它又是
* 数组末行,后面没有任何保证为 0 的字节:stat()/remove()
* 拿它当 C 串会读越过数组尾,目前纯粹是"后面那块 .rodata
* 碰巧是 0"才没出事(未定义行为,换编译器布局即可能变成
* 打开一个错名字的文件)。行宽加到 24 并逐项写出,
* 越界这条随扩容一起消失。
*/
/* 头文件包含区 */
@@ -19,8 +37,24 @@
/* 静态变量定义区 */
static const char s_paths[SYSLOG_COUNT][16] = { "sim_run.log", "sim_selftest.log" };
static const uint32_t s_caps[SYSLOG_COUNT] = { SYSLOG_RUN_MAX, SYSLOG_SELFTEST_MAX };
/* 行宽 24 = 最长宿主名 "sim_selftest.log"(16) + NUL,留余量给后续类别。
* ⚠ 原来这里是 [16]:正好等于最长名的字符数,**没给 NUL 留位**(v1.2 修的隐患,
* 详见文件头修改记录)——加宽数组是这条修复的一部分,不要"看着没用"再改回去。
* ⚠ 下标 = syslog_id_t 枚举序,与 sys_log_mcu.c 的 s_paths[] 逐项同序(两端一致是
* "宿主与真机行为等价"这条口径的前提)。 */
static const char s_paths[SYSLOG_COUNT][24] = {
"sim_run.log", "sim_selftest.log", "sim_accid.log", "sim_alarm.log",
"sim_start.log", "sim_oper.log", "sim_yk.log"
};
static const uint32_t s_caps[SYSLOG_COUNT] = {
SYSLOG_RUN_MAX, SYSLOG_SELFTEST_MAX, SYSLOG_ACCIDENT_MAX, SYSLOG_ALARM_MAX,
SYSLOG_START_MAX, SYSLOG_OPER_MAX, SYSLOG_YK_MAX
};
/* 「清除记录」的结果字节(SYSLOG_CLEAR_NONE/OK/FAIL)。本端同步落地,没有
* "排队中"这一态,所以只需要结果一枚,MCU 端那枚 s_clear_req[] 在这里是多余的。
* 宿主进程态,不参与 MCU 的 RAM 预算台账。 */
static int8_t s_clear_st[SYSLOG_COUNT];
/* 函数定义区 */
@@ -126,6 +160,115 @@ void syslog_post(syslog_id_t id, const char *desc) {
(void)s_append(id, desc);
}
/*
* 函数功能:投递一条日志(零日志缓冲版本;宿主实现)
* 入口参数:id - 日志 ID syslog_id_t
* desc - 描述串(UTF-8,超长截断) const char *
* 返回值:无
* 函数说明:MCU 端设这个入口是为了绕开 DBG 的内联行缓冲(protectTask /
* iec104Task 的静态最坏链,理由写在 sys_log.h 同号声明处);宿主进程
* 没有那条硬线,也**没有队列与丢弃**,所以这里与 syslog_post 完全等价。
* 刻意不做成"宿主端另起一套语义"——五类记录的写入点在两端跑同一份
* 调用,语义差异越少,模拟器作为回归面的价值越高。
*/
void syslog_post_quiet(syslog_id_t id, const char *desc) {
(void)s_append(id, desc);
}
/*
* 函数功能:取 quiet 通道累计丢弃条数(宿主实现)
* 入口参数:无
* 返回值:恒 0——本端直写宿主文件、无队列无数组,不存在"满态丢弃"这条路径 uint32_t
* 函数说明:不要为了"看起来对称"在这里造一个假计数;MCU 端那条路径的丢条数由
* test/host/sys_log_mcu 的 TC 直接注入队列满来验,宿主验不了也不该装。
*/
uint32_t syslog_post_dropped(void) {
return 0u;
}
/*
* 函数功能:删掉本端的记录文件(「清除记录」的落地半边,宿主实现)
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:0 - 已删除,或文件本就不存在(幂等成功,对齐 MCU 端的 FR_NO_FILE);
* -1 - 删除失败,**原文件未被改动** int
* 限定条件:调用方即本端写者(模拟器单线程,syslog_post 在这里也是直写不排队);
* 本端没有卷锁,也就没有 MCU 端"卷占用 = 顺延"那一态
* 函数说明:先 stat 再 remove,是为了把"文件不在"与"删除失败"分开——
* remove() 对不存在的文件同样返回 -1,直接拿它的返回值判失败会把
* **幂等成功误报成故障**(与 MCU 端把 FR_NO_FILE 算成功不对称)。
* 与 MCU 端同一条纪律:只动自己那两个记录文件,不碰同目录里的其它文件。
*/
static int s_clear_apply(syslog_id_t id) {
struct stat st;
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
return -1;
}
if (stat(s_paths[id], &st) != 0) {
return 0;
}
return (remove(s_paths[id]) == 0) ? 0 : -1;
}
/*
* 函数功能:登记「清除某日志全部记录」的请求(宿主后端:调用内同步落地)
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:0 - 已执行(结果读 syslog_clear_state);-1 - id 越界(存量文件未动) int
* 限定条件:任意调用点(模拟器单线程,这里没有"非写者上下文"可言)
* 函数说明:与 MCU 端的**唯一**差异是时序:MCU 端只置请求位、等 defaultTask 的
* syslog_drain() 尾部才碰盘,本端在本调用内就删完了,因此
* SYSLOG_CLEAR_WAIT 在模拟器上永不出现(同 syslog_post:MCU 有队列与
* 丢弃、本端直写无队列)。判据一条没少:删后再投日志会重建文件并正常
* 写入、syslog_count() 归零、文件不在时幂等、失败不动原文件。
*/
int syslog_request_clear(syslog_id_t id) {
/* 与 MCU 端 s_clear_rec[] 同一组文案(一类一枚 ROM 常量、零拼接;改动必须
* 两处同改,两端"同格式同口径"是模拟器作为回归面的前提)。SYSLOG_OPER
* 那一格同样永不被使用——清 oper.log 时不投递,否则"我清除了记录"这条会把
* 刚删空的 oper.log 又写成 1 条,砸掉「删除成功后 count 归零」这条判据。 */
static const char *const s_clear_rec[SYSLOG_COUNT] = {
"clear run.log", "clear selftest.log", "clear accid.log", "clear alarm.log",
"clear start.log", "clear oper.log", "clear yk.log"
};
int rc;
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
return -1;
}
rc = s_clear_apply(id);
s_clear_st[id] = (rc == 0) ? (int8_t)SYSLOG_CLEAR_OK : (int8_t)SYSLOG_CLEAR_FAIL;
/* 成功才留痕(失败那次什么都没改),且跳过 oper.log 自己——理由见上面表头。
* 本端 syslog_post_quiet 等价于直写,所以这条操作记录在函数返回前就已落盘,
* 与 MCU 端"下一轮 drain 才落"时序不同(两端差异已登记在
* docs/测试说明/模拟器差距清单.md,task-4 那次也写在那里)。 */
if ((rc == 0) && ((uint32_t)id != (uint32_t)SYSLOG_OPER)) {
syslog_post_quiet(SYSLOG_OPER, s_clear_rec[id]);
}
return 0;
}
/*
* 函数功能:查「清除记录」的状态(宿主后端)
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:SYSLOG_CLEAR_NONE(0) / OK(1) / FAIL(-2);本端不产出 WAIT(-1),
* id 越界按 NONE 返回 int
* 限定条件:任意调用点(单线程只读一字节)
* 函数说明:与 MCU 端共用同一组常量与同一段消费代码语义,UI 页面两端一份逻辑
* 就能同时处理"排队中"(真机)与"立即完成"(模拟器)。
*/
int syslog_clear_state(syslog_id_t id) {
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
return (int)SYSLOG_CLEAR_NONE;
}
if (s_clear_st[id] == (int8_t)SYSLOG_CLEAR_OK) {
return (int)SYSLOG_CLEAR_OK;
}
if (s_clear_st[id] == (int8_t)SYSLOG_CLEAR_FAIL) {
return (int)SYSLOG_CLEAR_FAIL;
}
return (int)SYSLOG_CLEAR_NONE;
}
/*
* 函数功能:获取记录条数(= filesize / SYSLOG_REC_SIZE,O(1))
* 入口参数:id - 日志 ID syslog_id_t