From 67c469fa401154df5374030e7c9e7397c5d43209 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sun, 25 Oct 2026 13:17:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui-pages):=20SOE=20=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E9=A1=B5=E6=8E=A5=20soe=5Fstore=20=E5=8F=AA=E8=AF=BB=E9=81=8D?= =?UTF-8?q?=E5=8E=86=EF=BC=88=E7=B1=BB=E5=9E=8B=207/8=20=E9=A6=96=E6=AC=A1?= =?UTF-8?q?=E4=B8=8A=E5=B1=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ui_w_soe 组件(task-14 落)此前两端都没绑 reader,只能显示"无记录"空态。本批补齐三件事: App 侧只读遍历、两端 reader 绑定、SOE 记录页与菜单注册。 App/log/soe_store 只读 API(Lead 已放行,本批独占这两个文件) - soe_store_count() / soe_store_read(idx, soe_rec_t*):与 syslog_count / syslog_get_record 同构——8 条一窗(512 B)整块读、命中窗口零 I/O、卷锁 try(0 超时) 非阻塞;定长记录逐条 open 等于同一扇区开 4 次,SPI NAND 上这个差距是肉眼可见的卡顿 - 读者不得看到半成品记录,三道保障缺一不可(规则写进 soe_store.h 头注,不只是提交信息): ① 条数只数整条 64 B 槽(写者留下的半条尾槽天然不计入);② 窗口代数:写者每次成功落盘 / 容量重建在**卷锁区间内**自增代数,读者比对填窗时记下的代数,不符即重填(对齐 32 位读写 在 ARM 上原子,跨任务无需临界区,最坏多读一轮);③ 单条硬门槛:soe_rec_parse() 校版本 + 校验和,撕裂/半写一律拒收 - 容忍的竞态只有一个:count 与 read 之间新落一条 → 本轮少看一条,下轮重绘(200 ms)自然 追上;所以边界一律用锁内重取的条数,不信调用方传进来的旧值 - API **不带类型掩码**(Lead 裁定 2026-10-25):5 个记录页怎么分组是界面语义,不进日志存储 层;环容量小、页渲染全扫无成本,真变大了再加带 mask 的优化 API(YAGNI) ⚠ 顺带修掉一颗定时炸弹:菜单生成器与生成物漂移 - 提交里的 Drivers/BSP/UI/ui_menu_tab.h 带着 5 个表查询 API 的规范注释块,而 tools/menu_gen/gen_menu.py 只会输出"声明 + 行尾短注释"——说明历史上有人**手改生成物** 补注释、没回灌生成器。我这次加 #page=soe 后重新生成,等于把那些注释抹掉,bsp-err 立刻 报 5 条 return-doc - 修法不是把生成物改回去(下次重新生成还会再抹一次),而是把注释块**回灌进生成器**; 回灌后重跑 gen_menu.py 产出的 ui_menu_tab.h 与 HEAD **逐字节一致**(git diff 为空), 漂移关闭。生成物里的手工优化 = 一定会被覆盖的优化,要么进生成器要么别改生成物 时标口径(跨模块陷阱,注释即防线) - 页面时标取记录内的 CP56(事件自己的时标,不是"现在")。它与运行/自检记录页同口径,靠两条 事实叠出来:sys_clock 的 epoch 域本身就是本地时间(App/util/sys_clock.h 的 sys_clock_set_unix 注明"Unix epoch time (秒,本地时间)"),而 soe_rec.c 的 epoch→CP56 分解不加任何时区偏移。**谁"顺手补 8 小时",SOE 页就会和文本记录页差 8 小时**;要引显式 时区必须两侧一起改 显示名回正:类型 7 用源词"未标定闭锁" - 组件名字表里原写作"标定闭锁"——缩掉一个"未"字后意思可以读成"把标定闭锁了",与"因未标定 而闭锁"几乎相反;源词见 App/log/soe_rec.h 与"数据可信"卡 5,UI 表现层无权改写事实词 - 模拟器的 3 条假记录刻意含一条 type 7 + ELM_NONE(非元件支路):这是 7/8 两个新类型 **真的接上屏**的第一个像素级证据——只有主机断言时,名字表写错也照样一片绿 - 主机测试的宽度护栏随之按"画到哪一列"分列:动作/返回进值列(4 汉字 = 12 字节),其余类型 名进名列(7 汉字 = 21 字节);原来一刀切 12 字节会把正常名字判成违规 页面与注册 - 新建 Drivers/BSP/UI/ui_page_soe.{c,h}:薄跳板,一条记录两行共用组件内单条读盘缓存, 实例独立(两页共用一份缓存会互相命中对方记录号,task-6 的教训) - 菜单树 SOE记录 加 #page=soe;重生成 4 个生成物,uvprojx / CMakeLists 两处注册由生成器 自动写入(本批 uvprojx 只含这一条新增,无 UV4 抖动) - reader 绑定在两端适配层:真机 App/task/ui_task.c(CP56 → UI 视图 + 容量钳位), 模拟器 simulator/main.c(固定 3 条、时标固定串,保证黄金参考可复现) 两处被门禁当场抓住的记录(都是有效拦截,不是噪音) - id-check 拦下注释里裸引"P2-10":编号引用必须可判定归属,代码注释也一样 → 改为指向 App/log/sys_log_mcu.c 与事实本身,不留裸编号 - 又一次 BOM 铁律现场复现:一次编辑剥掉 ui_w_soe.c 的 BOM,ARMCC 立刻报一串 #8/#7/#20 假语法错(missing closing quote 之类),而 MSVC 侧 0 警告完全照不出来;补回 EF BB BF 即 0/0。本批提交前逐文件比 HEAD 做了 BOM 漂移审计(临时脚本,不入库)——建议把这条 审计固化进 ui-verify skill 或门禁(不在本道写域,已回报 Lead) 验证 - @build:0 错误 0 警告;模拟器:0 警告 - 内容页回归 15 项 PASS:新增 soe 页 9 条文本断言(两条时标、过流Ⅰ段+动作、+返回、 未标定闭锁、页码),其余 11 页逐字节不变;菜单 8 状态 PASS、池 5368 B - soe 页刷新黄金参考前出图肉眼确认:6 行两两成对、19 字符时标在 94 px 名列内不越屏、 非元件行的类型名落在名列且值列为空 - test/host/ui_data 83/83、test/host/soe 32/32;13 个代码/文档/注册类钩子(含 coding-standard、bsp-err、id-check、cppcheck、registration-check、layer-deps、 font-charset、doc-links)全 Passed - 全量钩子红项只有同族三条:mem-numbers / mem-budget / size-gate(L0c),root cause 一条: Flash/RAM 台账与同码凭证回填归单写者 → 本批 --no-verify 体积(相对 b746a1f 台账 Code 217204 / RO 21504 / RW 1256 / ZI 132680) - 实测 Code 219712、RO 21388、RW 1288、ZI 134272 → Code +2508、RO −116、RW +32、ZI +1592 → Flash 合计 +2424 B、RAM +1624 B(含本批之前的三页回炉) - 本批新增里约 1064 B ZI 是 SOE 读窗口(512 B)+ static FIL(528 B),一次性投入、页面打开 才用得到;RAM 再紧张可把窗口降到 4 条(256 B)拿回一半,代价是跨窗记录多一次 I/O 待台架实测:真机 /log/soe.log 有记录时的翻页与尾部行为、卷忙时该行退化显示、SOE 页与文本 记录页时标一致性(跨 8 小时口径的现场核对)、记录增长时窗口代数失效后的重读节拍。 gate: only mem-budget/mem-numbers/size-gate pending lead ledger backfill (同族三条,root cause 一条:台账与同码凭证回填归单写者) 像素基线单独提交(下一笔新增 simulator/tests/ref/shot_soe.bin):本笔单独 checkout 时 ui-golden 会报"缺参考",属预期拆分;上述钩子结论取自"本笔 + 下一笔"的合并树。 自查:BOM 漂移审计 17 个改动文件全对齐 HEAD;CR = 0;U+FFFD = 0;字面量纯 ASCII。 --- App/log/soe_store.c | 132 +++++++++++++++++++++++++ App/log/soe_store.h | 37 +++++++ App/task/ui_task.c | 60 +++++++++++ Drivers/BSP/UI/ui_page_registry.c | 2 + Drivers/BSP/UI/ui_page_registry.h | 2 +- Drivers/BSP/UI/ui_page_soe.c | 77 +++++++++++++++ Drivers/BSP/UI/ui_page_soe.h | 28 ++++++ Drivers/BSP/UI/ui_w_soe.c | 4 +- MDK-ARM/STM32F407-Demo.uvprojx | 5 + docs/UI设计/页面扩充详细方案.md | 11 +++ simulator/CMakeLists.txt | 1 + simulator/main.c | 46 +++++++++ simulator/tests/check_content_pages.py | 16 +++ test/host/ui_data/ui_data_test.c | 15 ++- tools/menu_gen/gen_menu.py | 53 +++++++++- ui_menu_tree.txt | 2 +- 16 files changed, 480 insertions(+), 11 deletions(-) create mode 100644 Drivers/BSP/UI/ui_page_soe.c create mode 100644 Drivers/BSP/UI/ui_page_soe.h diff --git a/App/log/soe_store.c b/App/log/soe_store.c index 77b65864..141497c5 100644 --- a/App/log/soe_store.c +++ b/App/log/soe_store.c @@ -48,6 +48,16 @@ static osMessageQueueId_t s_queue; /* 运行期队列(drain 首次调 static const osMessageQueueAttr_t s_queue_attr = { .name = "soe" }; static volatile uint32_t s_seq; /* 单调序号(RAM 生命周期内;仅复位重置) */ + +/* 只读遍历(UI 记录页)用的读盘窗口,与 App/log/sys_log_mcu.c 读侧同构: + * 定长记录按 8 条一窗整块取回(8 × 64 B = 512 B),命中窗口零 I/O; + * 代数由写者在卷锁区间内自增,读者也在卷锁区间内采样代数 → 窗口不会跨写失效 */ +#define SOE_RD_WINDOW 8u +static uint8_t s_rd_win[SOE_RD_WINDOW][SOE_REC_SIZE]; +static uint32_t s_rd_win_base; +static uint32_t s_rd_win_cnt; +static volatile uint32_t s_rd_gen; /* 写者落盘/重建次数(对齐 32 位读写在 ARM 上原子) */ +static uint32_t s_rd_win_gen; static volatile uint32_t s_drop_cnt; /* 丢弃计数(队列满/环满) */ static uint32_t s_drop_logged; /* 丢弃留痕水位(drain 侧差值上报) */ static uint8_t s_fault_reported; /* 写故障留痕去重(成功写复位) */ @@ -164,6 +174,9 @@ static int s_soe_append(const uint8_t *rec) { ? ((uint32_t)finfo.fsize / SOE_REC_SIZE) : 0u; if (cnt >= SOE_STORE_MAX) { (void)f_unlink(SOE_PATH); + /* 代数自增(读者窗口失效):**unlink 当场就加**,不等写成功——文件已没了, + * 旧窗口里那几条记录在任何索引上都不再存在,晚一步都会有读者按陈旧窗口读数 */ + s_rd_gen++; DBG_INFO("cap full, recreate " SOE_PATH); } @@ -193,6 +206,7 @@ static int s_soe_append(const uint8_t *rec) { } if (rc == 0) { s_fault_reported = 0u; + s_rd_gen++; /* 落盘成功 → 读者窗口作废(在卷锁区间内自增,与读者同锁同调) */ } else if (s_fault_reported == 0u) { s_fault_reported = 1u; DBG_ERROR("write " SOE_PATH " fail (%d)", (int)fr); @@ -254,3 +268,121 @@ void soe_store_drain(void) { uint32_t soe_store_dropped(void) { return s_drop_cnt; } +/* ========================= 只读遍历(UI 记录页) ============================ */ +/* + * 函数功能:已落盘的 SOE 记录条数(= filesize / SOE_REC_SIZE,O(1)) + * 入口参数:无 + * 返回值:条数;文件缺失、未挂载或 f_stat 失败返回 0 uint32_t + * 限定条件:任意任务上下文可调(uiTask 的 SOE 记录页用);只做 f_stat,不取卷锁 + * ——与 App/log/sys_log_mcu.c 的 syslog_count() 同一口径,不另起第二套 + * 函数说明:条数按**整条 64 B 槽**相除,写者留下的半条尾槽天然不计入,这是 + * "读者看不到半成品记录"的第一道保障(另两道见 soe_store_read 头注) + */ +uint32_t soe_store_count(void) { + FILINFO finfo; + + if (f_stat(SOE_PATH, &finfo) != FR_OK) { + return 0u; + } + return (uint32_t)finfo.fsize / SOE_REC_SIZE; +} + +/* + * 函数功能:把包含 idx 的那一窗(最多 8 条)整块读进 RAM 并取出目标记录 + * 入口参数:idx - 记录序号 uint32_t(调用方已保证 < count) + * p_rec - 目标记录输出(SOE_REC_SIZE 字节) uint8_t * + * 返回值:0 - 成功;-1 - 卷忙 / 越界 / 打开定位读失败 / 短读 int + * 限定条件:内部函数;卷锁 try 语义(0 超时),拿不到锁立即返回不阻塞 + * 函数说明:定长记录一次取 8 条(8 × 64 B = 512 B)而不是逐条 open/read:一页 8 行 + * = 4 条记录,逐条读等于同一扇区 open 四次。记录页只在开页与翻页时重算,命中 + * 窗口即零 I/O——整套"窗口 + 代数 + try 锁"结论与 App/log/sys_log_mcu.c 读侧 + * 同源,刻意不另起第二套读盘路径(两套读盘规则必然漂移,是本仓反复在防的) + */ +static int s_rd_fill(uint32_t idx, uint8_t *p_rec) { + static FIL f; /* static:FF_FS_TINY=0 → FIL=528 B 栈; + * open~close 全程在卷锁区间内,跨任务由卷锁串行化 */ + FRESULT fr; + UINT br = 0u; + uint32_t total; + uint32_t base; + uint32_t want; + + if (vol_lock_wait(0u) == 0u) { + return -1; /* 卷忙不是故障:uiTask 下一轮重绘自然重试 */ + } + /* 等锁期间写者可能已推进(甚至已 unlink 重建)→ 锁内重新取条数再校验边界 */ + total = soe_store_count(); + if ((total == 0u) || (idx >= total)) { + vol_lock_release(); + return -1; + } + base = (idx / SOE_RD_WINDOW) * SOE_RD_WINDOW; + want = total - base; + if (want > SOE_RD_WINDOW) { + want = SOE_RD_WINDOW; + } + fr = f_open(&f, SOE_PATH, FA_READ); + if (fr == FR_OK) { + fr = f_lseek(&f, (FSIZE_t)base * SOE_REC_SIZE); + if (fr == FR_OK) { + fr = f_read(&f, s_rd_win, want * SOE_REC_SIZE, &br); + } + (void)f_close(&f); + } + if ((fr != FR_OK) || (br != (UINT)(want * SOE_REC_SIZE))) { + s_rd_win_cnt = 0u; /* 作废窗口:坏窗口留着会让后续行继续读到垃圾 */ + s_rd_win_gen = 0u; + vol_lock_release(); + return -1; + } + s_rd_win_base = base; + s_rd_win_cnt = want; + /* 代数在**卷锁区间内**采样:写者也只在持锁时自增,二者同侧 → 窗口与代数一致 */ + s_rd_win_gen = s_rd_gen; + vol_lock_release(); + + (void)memcpy(p_rec, (const uint8_t *)s_rd_win + ((size_t)(idx - base) * SOE_REC_SIZE), + SOE_REC_SIZE); + return 0; +} + +/* + * 函数功能:读取第 idx 条 SOE 记录(解析后的结构体;idx 0 = 最早一条) + * 入口参数:idx - 记录序号 uint32_t + * p_out - 输出(不可为 NULL) soe_rec_t * + * 返回值:0 - 成功;-1 - p_out 为空 / 越界 / 卷忙 / I/O 失败 / 记录校验不过 int + * 限定条件:uiTask 上下文(记录页逐行取数);**不阻塞**——卷忙立即 -1,页面这行 + * 退化显示"读失败",下一轮重绘再试;SOE 页坏不得拖住主界面节拍 + * 函数说明:读者不得看到半成品记录,三道保障缺一不可: + * ① count 只数整条 64 B 槽(半条尾槽不计入); + * ② 窗口代数:写者每次成功落盘/重建在卷锁区间内自增 s_rd_gen,读者 + * 比对填充时记下的代数,不匹配即重填——对齐 32 位读写在 ARM 上原子, + * 跨任务无需临界区,最坏多读一轮(同 syslog 读侧结论); + * ③ 单条硬门槛:soe_rec_parse() 校版本 + 校验和,撕裂/半写一律 -3 拒收。 + * count 与 read 之间新落一条的竞态**容忍**为"本次遍历少看到一条" + * (200 ms 重绘自然追上),绝不容忍读到半条——所以边界一律以锁内 + * 重取到的 total 为准,不用调用方传进来的旧 total。 + * soe_rec_parse 的 -1/-2/-3 在本 API 折成一个 -1:调用方(UI)对三者 + * 的处理完全相同(这行显示读不到),把内部码外溢只会诱导页面写死分支 + */ +int soe_store_read(uint32_t idx, soe_rec_t *p_out) { + uint8_t rec[SOE_REC_SIZE]; + + if (p_out == NULL) { + return -1; + } + /* 快路径:同代数且落在窗口内 → 零 I/O(代数比较用无锁读,最坏多刷一轮) */ + if ((s_rd_win_cnt != 0u) && (s_rd_win_gen == s_rd_gen) && + (idx >= s_rd_win_base) && (idx < s_rd_win_base + s_rd_win_cnt)) { + (void)memcpy(rec, (const uint8_t *)s_rd_win + + ((size_t)(idx - s_rd_win_base) * SOE_REC_SIZE), SOE_REC_SIZE); + } else if (s_rd_fill(idx, rec) != 0) { + return -1; + } else { + /* 窗口未命中的分支已在 s_rd_fill 里完成取数与校验,这里不重复读盘 */ + } + if (soe_rec_parse(rec, p_out) != 0) { + return -1; + } + return 0; +} diff --git a/App/log/soe_store.h b/App/log/soe_store.h index 48f10fdd..6057c935 100644 --- a/App/log/soe_store.h +++ b/App/log/soe_store.h @@ -59,6 +59,43 @@ void soe_store_drain(void); */ uint32_t soe_store_dropped(void); +/* ---------------- 只读遍历(UI 的 SOE 记录页用;写侧零影响) ---------------- + * 读者不得看到半成品记录,三道保障缺一不可(实现见 soe_store.c 同号函数): + * 1) count 只数**整条 64 B 槽**(filesize / SOE_REC_SIZE 整除),写者留下的 + * 半条尾槽天然不计入; + * 2) 窗口代数:写者每次成功落盘 / 容量重建,在**卷锁区间内**自增内部代数, + * 读者比对填窗时记下的代数,不符即重填。对齐的 32 位读写在 ARM 上原子, + * 跨任务不需要临界区;最坏多读一轮,不会读出错数据; + * 3) 单条硬门槛:soe_rec_parse() 校版本 + 校验和,撕裂/半写一律拒收(-3)。 + * 容忍的竞态只有一个:count 与 read 之间新落一条 → 本次遍历少看到一条,下一轮 + * 重绘(uiTask 200 ms 节拍)自然追上。**不容忍**的是读到半条,所以边界一律以 + * 卷锁内重新取到的条数为准,不用调用方传进来的旧值。 + * 与 App/log/sys_log_mcu.c 的 syslog_count/syslog_get_record 同构(同一套窗口 + + * 代数 + try 锁纪律),刻意不另起第二套读盘路径。 + */ + +/* + * 函数功能:已落盘的 SOE 记录条数(= filesize / SOE_REC_SIZE,O(1)) + * 入口参数:无 + * 返回值:条数;文件缺失、未挂载或 f_stat 失败返回 0 uint32_t + * 限定条件:任意任务上下文可调(uiTask 记录页用);只 f_stat,不取卷锁 + * 函数说明:见上方"只读遍历"总说明第 1 道保障 + */ +uint32_t soe_store_count(void); + +/* + * 函数功能:读取第 idx 条 SOE 记录(解析后的结构体,idx 0 = 最早一条) + * 入口参数:idx - 记录序号 uint32_t + * p_out - 输出(不可为 NULL) soe_rec_t * + * 返回值:0 - 成功;-1 - p_out 为空 / 越界 / 卷忙 / I/O 失败 / 版本或校验和不过 int + * 限定条件:uiTask 等读上下文;**不阻塞**——卷被占用立即返回 -1(调用方这行退化 + * 显示"读失败",下轮重绘再试),排障页不得拖住主界面节拍 + * 函数说明:soe_rec_parse 的 -1/-2/-3 在此折成一个 -1:调用方对三者的处置完全相同 + * (这行读不到),把内部码外溢只会诱导页面为区分不出来的差异写分支。 + * 时间字段取记录内的 CP56(事件自己的时标),不用"现在的时间"换算 + */ +int soe_store_read(uint32_t idx, soe_rec_t *p_out); + #ifdef __cplusplus } #endif diff --git a/App/task/ui_task.c b/App/task/ui_task.c index 53429c13..a9d46375 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -31,9 +31,12 @@ #include "ui_net_sink.h" #include "ui_standby.h" #include "ui_w_records.h" /* task-6 记录 reader 注入点(UI 页面不再 include sys_log.h) */ +#include "ui_w_soe.h" /* task-15 SOE 记录 reader 注入点(同上:UI 不 include soe_store.h) */ +#include /* task-15 snprintf:CP56 → UI 视图里的 char[20] 时标 */ #include "ui_data.h" /* task-14 快照存侧(BSP):本任务只负责先复位再喂帧 */ #include "ui_data_feed.h" /* task-14 数据装配节拍入口(App/app,UI 侧唯一发布者) */ #include "sys_log.h" +#include "soe_store.h" /* task-15 SOE 只读遍历(count/read);写侧仍归 protectTask/defaultTask */ #include "ui_tokens.h" /* UI 设计令牌:循环周期 UI_TASK_TICK_MS / 待机阈值 UI_STANDBY_IDLE_MS */ #include "sys_clock.h" #include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ @@ -88,6 +91,62 @@ static int ui_task_rec_get(uint8_t log_id, uint32_t idx, char *ts, uint32_t ts_l static const ui_rec_reader_t s_rec_reader = { ui_task_rec_count, ui_task_rec_get }; +/* + * 函数功能:SOE 记录条数适配(道E task-15 reader 注入)——读落盘文件的完整槽数 + * 入口参数:无 + * 返回值:条数;超过 uint16 上限时钳到 65535(组件契约是 uint16_t) uint16_t + * 限定条件:uiTask 上下文同步调用(SOE 记录页绘制路径) + */ +static uint16_t ui_task_soe_count(void) { + uint32_t n = soe_store_count(); + + return (n > 65535u) ? 65535u : (uint16_t)n; +} + +/* + * 函数功能:读第 idx 条 SOE 记录并翻成 UI 视图(二进制 → 不透明显示字段) + * 入口参数:idx - 记录序号 uint32_t + * p_out - UI 侧记录视图(契约见 Drivers/BSP/UI/ui_w_soe.h) ui_w_soe_rec_t * + * 返回值:0 - 成功;-1 - 读失败 / 卷忙 / 校验和不过(页面这行退化显示,不猜) int + * 限定条件:uiTask 绘制路径;soe_store_read() 是 try 锁非阻塞,绝不拖住 UI 节拍 + * 函数说明:时标**取记录内的 CP56**(事件自己的时间),不用"现在"换算。两条口径 + * 叠起来正好与文本记录页一致,别"顺手补 8 小时": + * ① sys_clock 的 epoch 域本身就是本地时间(App/util/sys_clock.h 的 + * sys_clock_set_unix 注明"Unix epoch time (秒,本地时间)"); + * ② App/log/soe_rec.c 的 epoch→CP56 分解不加任何时区偏移。 + * 所以这里打出来的墙上时钟 == 运行/自检记录页里 sys_clock_get_str() 的口径; + * 哪天真要引入显式时区,两侧必须一起改(跨模块陷阱,注释即防线)。 + * 时钟无效(IV)不写在这里:它是显示语义,由 UI 侧按 SOE_FLG_IV 决定 + * (组件把那行名字打"--"、值打"时钟无效"),App 只交代原始记录。 + */ +static int ui_task_soe_get(uint32_t idx, ui_w_soe_rec_t *p_out) { + soe_rec_t rec; + uint32_t sec; + + if ((p_out == NULL) || (soe_store_read(idx, &rec) != 0)) { + return -1; + } + /* CP56 字节表见 App/log/soe_rec.c 的 s_soe_epoch_to_cp56: + * [0..1]=分内毫秒(秒域时基 → 恒为秒×1000,亚秒不造假)[2]=分 [3]=时 + * [4]=日 [5]=月 [6]=年-2000 */ + sec = (((uint32_t)rec.cp56[1] << 8) | (uint32_t)rec.cp56[0]) / 1000u; + (void)snprintf(p_out->ts, sizeof(p_out->ts), "%04u-%02u-%02u %02u:%02u:%02u", + (unsigned)(2000u + ((uint32_t)rec.cp56[6] & 0x7Fu)), + (unsigned)((uint32_t)rec.cp56[5] & 0x0Fu), + (unsigned)((uint32_t)rec.cp56[4] & 0x1Fu), + (unsigned)((uint32_t)rec.cp56[3] & 0x1Fu), + (unsigned)((uint32_t)rec.cp56[2] & 0x3Fu), + (unsigned)sec); + p_out->type = rec.type; + p_out->elm = rec.elm_id; + p_out->flags = rec.flags; + p_out->seq = rec.seq; + p_out->value = rec.value; + return 0; +} + +static const ui_soe_reader_t s_soe_reader = { ui_task_soe_count, ui_task_soe_get }; + #define DBG_TAG "[UI_TASK]" #include "dbg_log.h" @@ -145,6 +204,7 @@ void ui_task_func(void *argument) { /* 记录 reader 注入:必须在 ui_menu_init 之前(记录页在此前不可能被打开, * 但契约把"启动期先绑定"写死;未绑定窗口组件按空态防御——见 ui_w_records.h) */ ui_w_records_bind(&s_rec_reader); + ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页数据入口(未绑定按"无记录"空态) */ /* 快照层复位:必须先于 ui_menu_init(首帧之前任何页面被打开都按"无数据" * 渲染 "--",不会读到上电残留)——UI_数据供给设计说明.md §5.3 */ ui_data_init(); diff --git a/Drivers/BSP/UI/ui_page_registry.c b/Drivers/BSP/UI/ui_page_registry.c index 540c05c9..367bdfd4 100644 --- a/Drivers/BSP/UI/ui_page_registry.c +++ b/Drivers/BSP/UI/ui_page_registry.c @@ -16,12 +16,14 @@ #include "ui_page_password.h" #include "ui_page_runlog.h" #include "ui_page_selftest.h" +#include "ui_page_soe.h" const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = { { 1u, &ui_page_device_info_desc }, /* node 1 装置信息 */ { 3u, &ui_page_ac_desc }, /* node 3 交流量 */ { 4u, &ui_page_dc_desc }, /* node 4 直流量 */ { 30u, &ui_page_net_cfg_desc }, /* node 30 网口设置 */ + { 33u, &ui_page_soe_desc }, /* node 33 SOE记录 */ { 39u, &ui_page_selftest_desc }, /* node 39 自检记录 */ { 40u, &ui_page_runlog_desc }, /* node 40 运行记录 */ }; diff --git a/Drivers/BSP/UI/ui_page_registry.h b/Drivers/BSP/UI/ui_page_registry.h index 02bb5422..c135240a 100644 --- a/Drivers/BSP/UI/ui_page_registry.h +++ b/Drivers/BSP/UI/ui_page_registry.h @@ -25,7 +25,7 @@ typedef struct { const content_page_def_t *desc; /* 页面描述符(Flash const) */ } ui_page_entry_t; -#define UI_PAGE_MAP_COUNT 6u /* 绑定了内容页的叶子数 */ +#define UI_PAGE_MAP_COUNT 7u /* 绑定了内容页的叶子数 */ extern const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT]; diff --git a/Drivers/BSP/UI/ui_page_soe.c b/Drivers/BSP/UI/ui_page_soe.c new file mode 100644 index 00000000..d56c5ee5 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_soe.c @@ -0,0 +1,77 @@ +/* + * 模块名称:UI Page SOE (SOE 记录内容页) + * 模块功能:SOE 记录叶子节点(node 51)的内容页数据——二进制记录的动态列表,每条记录 2 行 + * (偶数行 = 时标、奇数行 = 元件名 + 动作/返回;非元件事件把类型名打在名列), + * 数据经 ui_w_soe 注入的 reader 取(本文件零 App include、零文件系统 include); + * 未绑定 reader 时按"无记录"空态渲染;向框架 ui_content_page 提供页面描述符 + * 为什么不在这个文件读盘:SOE 是 /log/soe.log 上的 64 B 定长二进制记录,读盘、卷锁、窗口 + * 代数与校验和判定全在 App/log/soe_store 的只读 API 里(App/task/ui_task.c 绑定), + * UI 侧只按不透明编号显示——与运行/自检记录页同一分层(分层方向基线 3 条不变) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15:SOE 记录页;规格 = + * docs/UI设计/页面扩充详细方案.md §3.6「SOE记录」行) + */ + +/* 头文件包含区 */ +#include "ui_page_soe.h" + +#include "ui_tokens.h" +#include "ui_w_soe.h" + +/* 静态变量定义区 */ + +/* + * 本实例自带一条读盘缓存(组件内 cache_valid/last_rec):一条记录画两行, + * 奇数行复用同一次读取——与运行/自检记录页同构,但**实例独立**:两页共用一份 + * 缓存会互相命中对方的记录号,切页时显示另一页的陈旧行(task-6 的教训) + */ +static ui_w_soe_t s_soe; + +/* 函数定义区 */ + +/* + * 函数功能:总行数(每条记录 2 行;未绑定 reader 或无记录 = 单条伪行"无记录") + * 入口参数:无 + * 返回值:总行数 uint16_t + * 限定条件:uiTask 绘制路径(组件内部按记录数换算,不做 I/O 之外的状态依赖) + */ +static uint16_t pg_count(void) { + return ui_w_soe_count(&s_soe); +} + +/* + * 函数功能:逐行取数(偶数行 = 时标,奇数行 = "元件 + 事件") + * 入口参数:a_row - 全局行下标 uint16_t + * a_name - 名输出缓冲(框架 UI_BUF_STRLEN) char * + * a_value - 值输出缓冲 char * + * 返回值:无 + * 限定条件:uiTask 绘制路径;reader 已在两端适配层绑定(未绑定按空态渲染) + * 函数说明:类型/元件的**显示名映射在组件里**(UI 侧表现层),二进制编号由 App + * 注入的 reader 原样交上来——与"屏幕上的话归 UI、数据归 App"一致 + */ +static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) { + ui_w_soe_refresh(&s_soe, a_row, a_name, a_value); +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=soe 绑定引用): + * 动态列表页,每记录 2 行、UI_LIST_ROWS(8) 行/页 = 4 条/页;band_h 必须 >0 + * (selectable 页选中行是黑字,无白带即不可见,同记录页约定) + */ +const content_page_def_t ui_page_soe_desc = { + .id = "soe", + .title = "SOE记录", + .selectable = 1u, + .top_y = UI_LIST_TOP, + .pitch = UI_ROW_PITCH, + .band_h = UI_ROW_BAND_H, + .text_dy = 0, + .left_x = UI_LIST_LEFT_X, + .right_x = UI_LIST_RIGHT_X, + .rows_per_page = UI_LIST_ROWS, + .count_cb = pg_count, + .refresh = pg_refresh, +}; diff --git a/Drivers/BSP/UI/ui_page_soe.h b/Drivers/BSP/UI/ui_page_soe.h new file mode 100644 index 00000000..85306a50 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_soe.h @@ -0,0 +1,28 @@ +/* + * 模块名称:UI Page SOE (SOE 记录内容页) + * 模块功能:SOE 记录叶子节点的内容页声明——向框架 ui_content_page 提供页面描述符 + * (数据经 ui_w_soe 组件的注入 reader 取,本模块零 App include、零文件系统) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15:SOE 二进制记录页,规格 = + * docs/UI设计/页面扩充详细方案.md §3.6「SOE记录」行) + */ +#ifndef UI_PAGE_SOE_H +#define UI_PAGE_SOE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ui_content_page.h" + +/* 页面描述符(定义在 ui_page_soe.c;生成物按菜单树 #page=soe 引用本符号) */ +extern const content_page_def_t ui_page_soe_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_SOE_H */ diff --git a/Drivers/BSP/UI/ui_w_soe.c b/Drivers/BSP/UI/ui_w_soe.c index e8c564c8..3803a10a 100644 --- a/Drivers/BSP/UI/ui_w_soe.c +++ b/Drivers/BSP/UI/ui_w_soe.c @@ -52,7 +52,9 @@ static const char *const s_type_name[UI_W_SOE_T_COUNT] = { "压板投入", /* 4 软压板投入 */ "采样闭锁", /* 5 采样故障闭锁 */ "采样恢复", /* 6 采样故障恢复 */ - "标定闭锁", /* 7 未标定闭锁 */ + "未标定闭锁", /* 7 未标定闭锁(照 App/log/soe_rec.h 与 数据可信 卡5 的原词, + * 不缩成"标定闭锁"——那个写法可以读成"把标定闭锁了", + * 而这一行要说的是"因未标定而闭锁",一字之差含义相反) */ "标定复归", /* 8 标定复归 */ }; diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index 836bbf2a..241055c9 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -3566,6 +3566,11 @@ 1 ..\Drivers\BSP\UI\ui_w_soe.c + + ui_page_soe.c + 1 + ..\Drivers\BSP\UI\ui_page_soe.c + diff --git a/docs/UI设计/页面扩充详细方案.md b/docs/UI设计/页面扩充详细方案.md index cefe4e5f..e3e66aad 100644 --- a/docs/UI设计/页面扩充详细方案.md +++ b/docs/UI设计/页面扩充详细方案.md @@ -396,7 +396,18 @@ void ui_w_soe_refresh(ui_w_soe_t *inst, uint16_t row, char *name, char *valu ``` - 数据经注入的 reader 取(不 include `soe_store.h`),与 `ui_rec_reader_t` 同一模式但返回 `soe_rec_t`。 + 【2026-10-25 落地】页已接:`Drivers/BSP/UI/ui_page_soe.c`(菜单树 `SOE记录 #page=soe`, + `tools/menu_gen/gen_menu.py` 重生成 + 双端注册自动化),reader 在两端适配层绑定 + (真机 `App/task/ui_task.c` / 模拟器 `simulator/main.c` 的固定 3 条假记录)。 + 只读 API **不带类型掩码**(Lead 裁定 2026-10-25):5 个记录页怎么分组是界面语义,不进 + 日志存储层;环容量小,页渲染全扫无成本问题,真变大了再加带 mask 的优化 API(YAGNI)。 + 显示名(含类型 7 未标定闭锁 / 8 标定复归)留在 UI 侧——与"屏上的话归 UI、数据归 App"一致。 - 需要 `soe_store` 新增只读 API:`soe_count()` / `soe_read(idx, &rec)`(FatFs `f_open` + `f_lseek`,与 `syslog_get_record` 同构)。 + 【2026-10-25 落地】实到为 `App/log/soe_store.c` 的 `soe_store_count()` / `soe_store_read()`,照 + `syslog_get_record` 那套**窗口 + 代数 + try 锁**纪律实现(8 条 / 512 B 一窗,命中即零 I/O)。 + 可见性规则钉在 `App/log/soe_store.h` 头注:条数只数整条 64 B 槽、写者在卷锁区间内自增代数、 + 单条版本/校验和不过一律 `-1`——**读者不得看到半成品记录**;`count` 与 `read` 之间新落一条只 + 容忍"本轮少看一条"(200 ms 重绘自然追上),不容忍读到半条。 - 每条记录 2 行(时间戳 / "元件+类型"),与文本记录页一致 → 用户学习成本为零。 **运行报告(最小可用版)**:3 行——`最近动作`(元件名)、`动作时间`、`当前状态`。数据取最后一条 `SOE_T_ELM_ASSERT`。完整版(故障前后有效值 + 波形摘要)依赖录波,不在本批。 diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 93e7d0da..23e88c3c 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -103,6 +103,7 @@ set(UI_SOURCES ${UI_DIR}/ui_page_action.c ${UI_DIR}/ui_data.c # 数据快照存侧(道E task-14;写侧 App/app/ui_data_feed.c 不进模拟器,见 scripts/check_registration.py) ${UI_DIR}/ui_w_soe.c # SOE 记录组件(道E task-14;reader 注入点待绑定,未绑定按空态渲染) + ${UI_DIR}/ui_page_soe.c ) # 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程) diff --git a/simulator/main.c b/simulator/main.c index ada85b66..b86f454c 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -69,6 +69,7 @@ #include "cfg_file_store.h" #include "sys_log.h" #include "ui_w_records.h" /* task-6 记录 reader 注入点(镜像 App/task/ui_task.c 的绑定) */ +#include "ui_w_soe.h" /* task-15 SOE 记录 reader 注入点(同上:镜像真机绑定) */ #include "ui_data.h" /* task-14 快照存侧:模拟器在此直接发布固定帧(装配器不进模拟器) */ #include "prot_set.h" /* task-14 定值出厂默认(App/algo 纯算法,双端可编,见 simulator/CMakeLists) */ @@ -172,6 +173,49 @@ static int sim_rec_get(uint8_t log_id, uint32_t idx, char *ts, uint32_t ts_len, } static const ui_rec_reader_t s_rec_reader = { sim_rec_count, sim_rec_get }; +/* + * 函数功能:SOE 记录假源——固定 3 条(镜像 App/task/ui_task.c 真机绑定的形态) + * 入口参数:无 + * 返回值:条数 3 uint16_t + * 限定条件:模拟器侧(PC 上没有 /log/soe.log,读盘侧不参与模拟器构建) + * 函数说明:三条刻意覆盖组件的三条显示支路——① 元件事件(元件名 + "动作"); + * ② 同元件复归("返回");③ **非元件**的未标定闭锁(type 7 + ELM_NONE, + * 类型名进名列)。第 ③ 条是"新增的两个事件类型 7/8 到底有没有接上屏"的 + * 唯一像素级证据:少了它,名字表写错也照样一片绿 + */ +static uint16_t sim_soe_count(void) { + return 3u; +} + +/* + * 函数功能:读第 idx 条假 SOE 记录 + * 入口参数:idx - 记录序号 uint32_t + * p_out - UI 视图输出 ui_w_soe_rec_t * + * 返回值:0 - 成功;-1 - 空指针或越界 int + * 限定条件:时标用**固定串**——黄金参考要求逐像素可复现,跑一次一个样就等于没有基线 + */ +static int sim_soe_get(uint32_t idx, ui_w_soe_rec_t *p_out) { + static const uint8_t k_type[3] = { UI_W_SOE_T_ELM_ASSERT, UI_W_SOE_T_ELM_RETURN, + UI_W_SOE_T_CAL_BLOCK }; + /* 元件号:0 = 过流 Ⅰ 段;第 3 条是非元件事件 → 不透明"无元件"值(与 App 同值) */ + static const uint8_t k_elm[3] = { 0u, 0u, UI_W_SOE_ELM_NONE }; + static const char k_ts[3][UI_W_SOE_TS_LEN] = { "2026-10-25 08:00:01", + "2026-10-25 08:00:03", + "2026-10-25 08:00:05" }; + + if ((p_out == NULL) || (idx >= 3u)) { + return -1; + } + (void)snprintf(p_out->ts, sizeof(p_out->ts), "%s", k_ts[idx]); + p_out->type = k_type[idx]; + p_out->elm = k_elm[idx]; + p_out->flags = 0u; + p_out->seq = idx + 1u; + p_out->value = idx + 1u; + return 0; +} + +static const ui_soe_reader_t s_soe_reader = { sim_soe_count, sim_soe_get }; /* * 函数功能:发布固定数据帧(镜像 App/app/ui_data_feed.c 的装配结果) @@ -303,6 +347,7 @@ static int run_headless(const char * keys, const char * path, int standby) { /* task-6 记录 reader 注入(镜像 ui_task.c:先于 ui_menu_init,两端同形) */ ui_w_records_bind(&s_rec_reader); + ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页假源(形态镜像真机绑定) */ /* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页, * 页面任何时刻打开都拿得到帧,不必等装配节拍) */ sim_publish_data_frames(); @@ -701,6 +746,7 @@ int main(int argc, char **argv) { /* 构建菜单 UI(与 MCU 端共用 ui_menu.c) */ /* task-6 记录 reader 注入(镜像 ui_task.c:先于 ui_menu_init,两端同形) */ ui_w_records_bind(&s_rec_reader); + ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页假源(形态镜像真机绑定) */ /* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页, * 页面任何时刻打开都拿得到帧,不必等装配节拍) */ sim_publish_data_frames(); diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py index 7c947ebb..30c63741 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -41,6 +41,9 @@ PAGES = [ ("info", "e"), # 装置信息(node 1) ("ac", "dee"), # 交流量(node 3,实时数据下第 1 项) ("dc", "dede"), # 直流量(node 4,实时数据下第 2 项) + # SOE 记录(node 51,记录查询下第 1 项):6 下进记录查询 → e → 首项 SOE记录 → e; + # 数据 = simulator/main.c 的固定 3 条假记录(含 type 7 未标定闭锁这条非元件支路) + ("soe", "dddddde e"), ("pwd", "ddeee"), # 密码页初始态:3 框 000、光标首框 ("pwe", "ddeee uru"), # 密码页调值+移光标:110、光标第 2 框 ("pwk", "ddeee uruuruuue"), # 密码页输 123:u / r / u,u / r / u,u,u / e → 关页进空白终点框(菜单屏) @@ -90,6 +93,19 @@ TEXTS = { ("r6名", "标定", (108, 122), False), ("r6值", "未标定", (108, 122), False), ("页脚", "第 1 页,共 1 页", (140, 160), False), ], + # SOE 记录:每条记录 2 行(时标 / 元件名+动作-返回)。三条假记录刻意覆盖三条显示 + # 支路:元件事件、元件复归、**非元件**的未标定闭锁(type 7,类型名进名列)。 + # 第 5 行是"新增事件类型有没有接上屏"的像素级证据——只测 1/2 的话,7/8 写错也绿。 + "soe": [ + ("标题", "SOE记录", (0, 24), False), + ("r0时标", "2026-10-25 08:00:01", (24, 38), True), + ("r1名", "过流Ⅰ段", (38, 52), False), ("r1值", "动作", (38, 52), False), + ("r2时标", "2026-10-25 08:00:03", (52, 66), False), + ("r3名", "过流Ⅰ段", (66, 80), False), ("r3值", "返回", (66, 80), False), + ("r4时标", "2026-10-25 08:00:05", (80, 94), False), + ("r5名", "未标定闭锁", (94, 108), False), + ("页脚", "第 1 页,共 1 页", (140, 160), False), + ], # 直流量:V0.1 板上无直流采样通道 -> 温度取码值并显式带"码"前缀,通道行如实"未配置" "dc": [ ("标题", "直流量", (0, 24), False), diff --git a/test/host/ui_data/ui_data_test.c b/test/host/ui_data/ui_data_test.c index c4490f94..b9fe964b 100644 --- a/test/host/ui_data/ui_data_test.c +++ b/test/host/ui_data/ui_data_test.c @@ -392,7 +392,13 @@ static void tc_soe_codes(void) } } CHECK(ok == 1, "TC-UIDATA-012l every type has a display name"); - CHECK(longest <= 12u, "TC-UIDATA-012m type names fit value column (<=4 CJK)"); + /* 宽度按**实际画到哪一列**判:1/2 两类(动作/返回)画在值列(4 汉字 = 12 字节), + * 3..8 这些非元件事件的类型名画在名列(7 汉字 = 21 字节)。一刀切按 12 判会把 + * "未标定闭锁"这类正常名字判成违规,一刀切按 21 判又放过了值列超宽——分列才咬手 */ + CHECK((strlen(ui_w_soe_type_name(UI_W_SOE_T_ELM_ASSERT)) <= 12u) + && (strlen(ui_w_soe_type_name(UI_W_SOE_T_ELM_RETURN)) <= 12u), + "TC-UIDATA-012m element event names fit value column (<=4 CJK)"); + CHECK(longest <= 21u, "TC-UIDATA-012m2 type names fit name column (<=7 CJK)"); CHECK(strcmp(ui_w_soe_type_name(0u), "--") == 0, "TC-UIDATA-012n code 0 -> placeholder"); CHECK(strcmp(ui_w_soe_type_name(UI_W_SOE_T_COUNT), "--") == 0, "TC-UIDATA-012o out-of-range -> placeholder"); @@ -487,13 +493,16 @@ static void tc_soe_render(void) CHECK((strcmp(name, "--") == 0) && (strlen(value) == 12u), "TC-UIDATA-014e IV timestamp suppressed, marked in value column"); - /* 非元件事件:类型名进名列,值列留空 */ + /* 非元件事件:类型名进名列,值列留空。15 字节 = "未标定闭锁" 5 汉字 × 3 B; + * 主机测试的字面量必须纯 ASCII(无 BOM 文件里中文串会吞引号),所以这里只按 + * 字节数 + "不是占位串"判,串内容由 simulator 侧的 TEXTS 断言在像素上核 */ s_fake_rec.flags = 0u; s_fake_rec.type = UI_W_SOE_T_CAL_BLOCK; s_fake_rec.elm = UI_W_SOE_ELM_NONE; (void)memset(&inst, 0, sizeof(inst)); ui_w_soe_refresh(&inst, 3u, name, value); - CHECK((strlen(name) == 12u) && (value[0] == 0), "TC-UIDATA-014f non-element row: type in name column"); + CHECK((strlen(name) == 15u) && (name[0] != '-') && (value[0] == 0), + "TC-UIDATA-014f non-element row: 5-CJK type name in name column"); /* 越界记录号:读失败退化为占位,不留脏缓存 */ ui_w_soe_refresh(&inst, 998u, name, value); diff --git a/tools/menu_gen/gen_menu.py b/tools/menu_gen/gen_menu.py index 1f910adb..e6e6085e 100644 --- a/tools/menu_gen/gen_menu.py +++ b/tools/menu_gen/gen_menu.py @@ -182,11 +182,54 @@ def emit(nodes): ' * 表查询 API(导航逻辑层 ui_menu.c 与显示层 ui_menu_view.c 共用):', ' * 实现随表生成,改菜单(ui_menu_tree.txt)调用方零代码改动。', ' */', - 'uint8_t ui_menu_tab_children(uint8_t node, uint8_t out[], uint8_t max); /* 取 node 直接子项(按表序),最多 max 个;返回实际个数 */', - 'bool ui_menu_tab_has_children(uint8_t node); /* 有子项=菜单(带▶);无=叶子 */', - 'const char *ui_menu_tab_name(uint8_t node); /* 显示名 */', - 'bool ui_menu_tab_has_pwd(uint8_t node); /* 需密码(密码页按所输密码授权);false = 直接进(只读) */', - 'bool ui_menu_tab_valid(void); /* 启动自检:非根 parent 下标均有效、pwd 为 0/1 */', + "", + "/*", + " * 函数功能:取某节点的直接子项(按表序)", + " * 入口参数:node - 节点下标 uint8_t 0 - 表长-1", + " * out - 子项下标输出 uint8_t[] 长度 >= max,可为 NULL(只数个数时)", + " * max - 最多写出个数 uint8_t >= 1", + " * 返回值:实际写出的子项个数 uint8_t 0 - max(0 = 叶子节点)", + " * 限定条件:node 越界时返回 0,不写 out(与\"叶子\"同形,调用方需要区分就先用 has_children)", + " * 函数说明:不分配内存、不递归,只查一层。", + " */", + "uint8_t ui_menu_tab_children(uint8_t node, uint8_t out[], uint8_t max);", + "", + "/*", + " * 函数功能:判断节点是否为菜单节点(有子项)", + " * 入口参数:node - 节点下标 uint8_t 0 - 表长-1", + " * 返回值:true - 有子项(显示为菜单,带 ▶);false - 无子项(叶子,可进内容页)", + " * 限定条件:纯查询,无副作用;node 越界返回 false", + " * 函数说明:这是**谓词**(bool 合法),不是成败码——S-5 的 err-type 检查里白名单放行的形态。", + " */", + "bool ui_menu_tab_has_children(uint8_t node);", + "", + "/*", + " * 函数功能:取节点的显示名", + " * 入口参数:node - 节点下标 uint8_t 0 - 表长-1", + " * 返回值:显示名字符串指针 const char* 指向表内静态存储,**不为 NULL**(越界返回空串)", + " * 限定条件:返回的是静态存储地址,调用方不得写、不得 free", + " * 函数说明:编码随表来源(ui_menu_tree.txt),显示层直接喂 LVGL。", + " */", + "const char *ui_menu_tab_name(uint8_t node);", + "", + "/*", + " * 函数功能:判断该叶子是否需要密码", + " * 入口参数:node - 节点下标 uint8_t 0 - 表长-1", + " * 返回值:true - 需密码(先进密码页,按所输密码授权);false - 直接进(只读)", + " * 限定条件:纯查询;node 越界返回 false(保守取\"只读\",不放开写权限)", + " * 函数说明:授权强度由密码页所输密码决定,本函数只回答\"要不要过这一关\"。", + " */", + "bool ui_menu_tab_has_pwd(uint8_t node);", + "", + "/*", + " * 函数功能:菜单表启动自检(表与生成器是否自洽)", + " * 入口参数:无", + " * 返回值:true - 表自洽(非根节点的 parent 下标均有效、pwd 只取 0/1);", + " * false - 表被手工改坏,调用方应拒绝进入菜单而不是继续用", + " * 限定条件:ui_menu_init() 阶段调用一次", + " * 函数说明:宁可拒绝显示,也不要按一张错表导航——错表可能把只读叶子指到写页面上。", + " */", + "bool ui_menu_tab_valid(void);", '', '#endif', '', diff --git a/ui_menu_tree.txt b/ui_menu_tree.txt index 4a97fe12..444960d8 100644 --- a/ui_menu_tree.txt +++ b/ui_menu_tree.txt @@ -49,7 +49,7 @@ 网口设置 #P #page=net_cfg SNTP设置 #P 记录查询 - SOE记录 + SOE记录 #page=soe 事故记录 操作记录 保护告警