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
+151 -1
View File
@@ -25,6 +25,13 @@
* v1.0 2026-10-25 王建锋 创建(道E task-14:契约 §4 快照层落地;相对契约
* 两通道新增第三条 info,划分理由见本头"通道划分"注
* 与契约 §4.4)
* v1.1 2026-10-26 DTU 项目组 道F task-1(docs/任务规划/UI补齐.md §2 第 1 行):
* 新增定值写路径(param_request/param_state/
* param_drain,契约 §6.2 深度 4 环形队列)+ 参数帧尾
* 追加 cal_block(UI_DATA_VER 1→2,整定页"未标定"徽标
* 的数据源);写路径仍不引入任何 App 依赖(执行事实
* 经 ui_data_param_ops_t 由 App 侧注入,同契约 §7.2
* 记录页注入式 reader 的路子)
*/
#ifndef UI_DATA_H
@@ -41,7 +48,9 @@ extern "C" {
* (禁止删除或重排字段——页面按名字取,重排是静默错位;
* 口径 docs/UI设计/UI_数据供给设计说明.md §4.3)
*/
#define UI_DATA_VER 1u
/* v2(2026-10-26,道F task-1):ui_data_par_t 尾追加 cal_block——版本号是页面与
* 写侧唯一的对齐凭据,追加字段不升版 = 老页面按新帧长 memcpy 时静默错位 */
#define UI_DATA_VER 2u
/* ── 通道数量 ─────────────────────────────────────────────────────────────── */
@@ -157,6 +166,16 @@ typedef struct {
uint8_t rsv; /* 预留 */
uint64_t bad_mask; /* 40 位:置 1 = 该定值项越界(页面反显该行) */
int32_t q10[UI_DATA_SET_N]; /* Q10 原值,下标 = prot_set_id_t 枚举序 */
/* v2 尾追加:未标定闭锁(整定页"未标定"徽标的数据源)。
* 1 = UA/UN/IA 三路里至少一路的标定 VALID 未置位 → 保护判据当前停走,页面必须
* 让用户看见"改了定值也不会动作";0 = 三路全 VALID(判据在跑)。
* ⚠ 口径**不是** rt.eng:eng 是整帧量纲档(要求电气 8 路全 VALID),V0.1 双
* 转换器下 UB/UC/IB/IC/IN 永不置位 → eng 恒为引脚档,照 eng 画徽标就成了
* "标定做完屏上仍永不解锁"。三路口径的权威实现 = App/task/protect_task.c
* 的 s_cal_locked()(写侧 App/app/ui_data_feed.c 按同一条判据现算,本层只存
* 真值、不判断)。与上面 locked(定值校验和自检闭锁 = 禁编辑)是两件事,
* 屏上不得互认(三词三源见 docs/任务规划/数据可信.md §4.3) */
uint8_t cal_block;
} ui_data_par_t;
/* ── 通道三:装置信息(静态身份 + 通讯参数 + 运行时长)───────────────────── */
@@ -276,6 +295,137 @@ uint32_t ui_data_par_seq(void);
*/
uint32_t ui_data_info_seq(void);
/* ── 写路径:定值整定请求(页面只投递,执行在 App 侧)───────────────────── */
/*
* 请求队列深度(契约 docs/UI设计/UI_数据供给设计说明.md §6.2 定死 4)
* 为什么是 4 不是 1:整定页"保存"是用户按键,一条请求要等 App 侧 drain 才落定;
* 深度 1 会让连按两次保存把第二条判成"忙",而 4 格 = 本层 .bss 多
* 4 × sizeof(ui_data_param_req_t)(168 B/格,主机单测 TC-UIDATA-016 钉住尺寸,
* 内存台账 docs/内存管理/RAM使用分析与优化方案.md)。加深 = 纯 RAM 代价,
* 本层不做任何按内存自适应
*/
#define UI_DATA_PARAM_QD 4u
/*
* 一条定值写入请求 = "本次保存改了哪些项 + 它们的 Q10 新值"
*
* 为什么是「位图 + 定长 40 槽」而不是「项数 + 变长 id 数组」:表现层编辑叠层本就
* 是一张改动位图(uint64_t)+ 一张工作值表(int32_t[UI_DATA_SET_N]),本形状与它
* 逐位同构 → 提交侧一次 memcpy、执行侧逐项按位取,两侧都不必再维护第三种表示;
* 定长还让队列退化成 O(1) 静态环形数组(零堆、零变长拷贝、零 malloc 门禁风险)。
*
* ⚠ mask 只有 64 位 → 定值项数硬上限 64;越界位由 ui_data.c 的编译期守卫拦住,
* 将来项数破 64 必须换表示(不会静默丢项)。
* ⚠ **mask 未置位的槽本次不提交**:本层与执行侧都不读它——调用方可以只填 mask 内
* 的槽(整帧 memcpy 入队会把 mask 外的字节一并存下来,但那字节永不被读,不影响
* 任何判定;先 memset 再填也完全可以,两种写法都安全)。
* 绝不允许"从显示值回提全表"——Q10 存不下 0.95 这类返回系数(真存 972/1024),
* 按显示域往返会把用户没动的项顺手改掉
* (docs/任务规划/UI补齐.md §2 第 2 行判据"值不落盘"的前提)。
*/
typedef struct {
uint64_t mask; /* bit i = 1 → 第 i 项随本请求提交 */
int32_t q10[UI_DATA_SET_N]; /* Q10 新值;仅 mask 置位的槽有意义 */
} ui_data_param_req_t;
/*
* 写路径状态(页面保存行显示;语义表 = 契约 §6.3)
* IDLE - 从未投递 → 页面显 ENTER
* WAIT - 已投递未落定(队列里还有请求在路上)→ 页面显 WAIT
* OK - 最近一条已逐项写入并置脏 → 页面显 OK
* ERR_RANGE - 最近一条整批拒绝:任一项越界 → 一字未写 → 页面显 ERR 并高亮
* 越界行(越界项经 ui_data_param_err_mask() 取)
* ERR_LOCKED - 最近一条被定值闭锁拒(写侧自检失败态)→ 页面显 LOCK 并禁编辑
* ERR_BUSY - 最近一次投递被拒:队列满 → 页面显 ERR、页脚"忙,请重试"
* 状态变量的三条写入路径(本层唯二写者 = request 与 drain,同在 uiTask):
* 投递成功 → WAIT;投递被拒(满)→ ERR_BUSY;drain 落定 → 终态,但队列仍有
* 残留时改写回 WAIT。故"满被拒"看得见(ERR_BUSY),随后逐步 drain 又回到 WAIT
*/
#define UI_DATA_PARAM_ST_IDLE 0
#define UI_DATA_PARAM_ST_WAIT 1
#define UI_DATA_PARAM_ST_OK 2
#define UI_DATA_PARAM_ST_ERR_RANGE 3
#define UI_DATA_PARAM_ST_ERR_LOCKED 4
#define UI_DATA_PARAM_ST_ERR_BUSY 5
/*
* 执行侧函数表(由 App 在 ui_data_feed_tick() 每次调用时传入;本层不保存副本)
*
* 为什么用注入而不是直接调 prot_set:分层方向硬约束——Drivers/BSP 不得 include
* App 头(scripts/check_layer_deps.py 拦提交),而"什么算越界、写了要不要置脏"
* 全在 App/algo/prot_set 那侧。同契约 §7.2 记录页的注入式 reader:BSP 声明抽象
* 函数表,App 给实现,主机单测给桩 → 队列与状态机的**生产代码**能在 PC 上跑
* 全表(越界不改值 / 闭锁拒写 / 正常往返三条判据都覆盖到真代码,不是副本)。
* ⚠ 本表不含任何"范围数值"——上下限真源仍是 App/algo/prot_set.c 的
* g_prot_set_meta,本层只按 check() 的返回值决定整批写不写(不新增第二真源)
*/
typedef struct {
uint8_t (*locked)(void); /* 1 = 定值闭锁中 */
int (*check)(uint8_t a_id, int32_t a_q10); /* 0 合法 / -1 越界 */
int (*apply)(uint8_t a_id, int32_t a_q10); /* 0 成功 / -1 失败 */
} ui_data_param_ops_t;
/*
* 函数功能:投递一条定值写入请求(页面"保存"行的唯一出口——只投递,不执行)
* 入口参数:a_req - 请求(mask 与 q10 整条拷入队列,调用方可复用其栈空间)
* const ui_data_param_req_t *
* 返回值:0 - 已入队(状态转 WAIT);mask 为 0 的空请求也返回 0,但按"无改动可
* 保存"直接落终态 OK 且**不占槽位**(保存行 ENTER 时一个都没改
* 是正常操作,报 ERR 会让现场以为设备有问题);
* -1 - 队列满(状态转 ERR_BUSY,在队请求一条不动);
* -2 - 参数无效:NULL、或 mask 含 UI_DATA_SET_N 以上的位
* (状态与队列都不动:调用方的编程错误不该落成屏上的"越界") int
* 限定条件:uiTask 上下文(页面按键钩子);与 drain 同上下文 → 无锁
* 函数说明:本函数**不**校验值域(越界与否是执行侧事实,在 drain 里整批预检),
* 也不碰 prot_set、不碰文件系统(落盘归 defaultTask,AGENTS 模块约定)
*/
int ui_data_param_request(const ui_data_param_req_t *a_req);
/*
* 函数功能:取写路径状态(页面保存行显示)
* 入口参数:无
* 返回值:UI_DATA_PARAM_ST_* 之一 int
* 限定条件:无(只读单字,任意上下文安全)
* 函数说明:ERR_TIMEOUT(契约 §6.3 的"3 s 未取走")本层**不产出**——uiTask 每
* 10 ms 就 drain 一次,真出现"永不落定"说明 App 侧没接线,属装配缺陷;
* 页面若要兜底可自己按投递时刻计时(docs/UI设计/UI_数据供给设计说明.md
* §6.3 已把它列为页面本地可选项)
*/
int ui_data_param_state(void);
/*
* 函数功能:取最近一条请求的越界项位图(页面高亮越界行)
* 入口参数:无
* 返回值:bit i = 1 → 第 i 项越界;最近终态不是 ERR_RANGE 时恒 0 uint64_t
* 限定条件:无(只读单字)
* 函数说明:与 ui_data_par_t.bad_mask 是两条不同来源——bad_mask 是 prot_set 整表
* 装载/解析留下的越界位图(App 侧事实),本函数是**本次提交**里被预检
* 挡下的项;页面在 ERR_RANGE 那一帧用本函数,别把两者混成一份
*/
uint64_t ui_data_param_err_mask(void);
/*
* 函数功能:处理一条定值写入请求(App 装配层节拍入口;出队 → 判定 → 落定)
* 入口参数:a_ops - 执行侧函数表(三个函数指针都不得为 NULL)
* const ui_data_param_ops_t *
* 返回值:1 - 处理并提交了一条(状态已落终态或转 WAIT);0 - 队列空(状态不动);
* -1 - 参数无效(ops 或其函数指针为 NULL,队列与状态都不动) int
* 限定条件:uiTask 上下文,紧跟页面之后调(App/app/ui_data_feed.c 的 tick);
* 每轮最多处理一条——整定是低频人工事件,一轮一条已够,且把最坏耗时
* 钉在 40 次 check + 40 次 apply,不会随队列堆积而抖动
* 函数说明:固定四步(顺序即判据,契约 §6.2)——
* ① 取队首一条;② ops->locked() 非 0 → ERR_LOCKED,**本条丢弃**(闭锁
* 是业务态,用户解闭锁后重新提交;不做"排队等解锁",那会让屏上的 WAIT
* 无限期挂着);③ 按 mask 逐项 ops->check() 整批预检,任一项越界 →
* ERR_RANGE 且**一字不写**(all-or-nothing,与 prot_set_load_raw 同纪律),
* 越界项记入 err_mask;④ 预检全过再逐项 ops->apply(),全成 → OK。
* ④ 中途失败(预检已过仍失败 = 执行侧与预检不一致,理论不可达)不回滚:
* prot_set 无回滚接口,谎称"整批未写"比如实报 ERR_RANGE 更坏,故按实际
* 失败项置 err_mask 后报 ERR_RANGE,并在注释里写明可能部分已生效
*/
int ui_data_param_drain(const ui_data_param_ops_t *a_ops);
#ifdef __cplusplus
}
#endif