diff --git a/.dsh/skills/font-regen/SKILL.md b/.dsh/skills/font-regen/SKILL.md index edc35e00..d5f99355 100644 --- a/.dsh/skills/font-regen/SKILL.md +++ b/.dsh/skills/font-regen/SKILL.md @@ -14,8 +14,10 @@ whenToUse: UI 代码新增中文文案、用户报告屏幕缺字/空白块、 # 1. 代码落库(新界面字符串已写入 ui_menu.c / ui_content_page.c 等 UI 源码) # 2. 提取字符集(纯标准库,任意 python3 均可) python tools/font_gen/extract_cn.py # 扫描 UI 源码字符串 → 仓库根 ui_cn_chars.txt -# ⚠ extract 会把 KEEP_EXTRA 保留字(异/常/片/芯)洗掉:跑 make_font 前必须并回 -# ui_cn_chars.txt(否则 font-charset 门禁 A 段 FAIL,理由见 check_font_charset.py 头注) +# ⚠ extract 只认"当前源码字面量",会把清单里 KEEP_EXTRA 那批**刻意保留字**(历史落盘 +# 记录用字,逐条理由见 check_font_charset.py 的 KEEP_EXTRA 字典——不在本文列举,列举必 +# 然过期)从 txt 洗掉:跑 make_font 前必须把它们并回 ui_cn_chars.txt,否则 font-charset +# 门禁 A 段 FAIL 并逐字打印保留理由;事故路径实名登记见 docs/门禁说明/font-charset.md §6.1 # 3. 生成字体(需 fontTools,一律用 .venv;宋体 .ttc 的 face0 抽取与 ▶ 箭头补回均自动完成,幂等) .venv\Scripts\python.exe tools/font_gen/make_font.py # 变体:--bpp 2(宋体细笔画发虚时)/ --font C:/path/to/font.ttf(换字体) diff --git a/.dsh/skills/task-worktree/SKILL.md b/.dsh/skills/task-worktree/SKILL.md index d7cad2eb..9eb3489c 100644 --- a/.dsh/skills/task-worktree/SKILL.md +++ b/.dsh/skills/task-worktree/SKILL.md @@ -87,6 +87,11 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突; **处置**:门禁失败且只差这两项时,按提示在主工作树跑 `python scripts/ram_baseline.py --write` + `python scripts/check_mem_numbers.py --write` 后追加提交,再重跑 `done`(它会检测到「已合入」直接清场)。 +- **⚠ 新建工作树从未构建过 → 体积类门禁必然红**(2026-10-25 实测):`.map` 是每棵树各自的 + 构建产物、不入版本库,所以 `new` 出来的空间里 `mem-numbers` / `mem-budget` / `ci_local.py` + 的 L0c 都拿不到事实源,报 `[FAIL] 找不到 .map 事实源`(脚本提示已写明原因,不是代码缺陷)。 + **处置**:新空间第一次提交前先 `@build` 一次(`build` 锁自动排队,与别的树串行)。与上一条 + 的区别要分清:上一条是「有 `.map` 但是旧的」,本条是「根本没有 `.map`」,处置动作不同。 - **⚠ 增量构建可能不重链 → `.map` 数字瞬态不一致**(2026-10-12 实测): `@build` 若判定无需重链,`.map` 保持旧值,此时 `--write` 生成的快照是**瞬态值** (实测同一份源码两次读到 223,560 与 223,724,差 164 B)。**以主工作树(集成点) diff --git a/App/app/ui_data_feed.c b/App/app/ui_data_feed.c index 49c83913..e8b2e7de 100644 --- a/App/app/ui_data_feed.c +++ b/App/app/ui_data_feed.c @@ -24,6 +24,14 @@ #include "ui_data_feed.h" +/* 日志锚点:本文件只在"点表点数与快照点数不等"这一条静默漂移上打一条一次性日志 + * (每条约 90 B Flash,见 AGENTS「日志不是免费的」——常规发布路径刻意一条都不加)。 + * ⚠ DBG_TAG 必须在 include 之前定义:dbg_log.h 的 DBG_ERROR 是宏,漏 include 时 + * ARMCC 只把 DBG_ERROR 当隐式声明函数放过编译期,错误落在链接期(L6218E + * Undefined symbol),比漏写头文件本身更难归因 */ +#define DBG_TAG "[UIDATA]" +#include "dbg_log.h" + /* 下层(Drivers/BSP):快照存侧 + 两个端口/用户名常量来源(App → BSP 是合法方向) */ #include "ui_data.h" #include "ui_net_sink.h" /* UI_NET_SINK_PORT:远程显示(RemoDispBus)监听端口 */ @@ -38,6 +46,7 @@ #include "prot_set.h" /* 40 项定值 Q10 + 校验和 + 闭锁 + 越界位图 */ #include "sys_clock.h" /* 墙钟字符串 */ #include "iec104_serv.h" /* IEC104_SERV_PORT:实际监听端口(非协议层同名常量) */ +#include "iec104_pts.h" /* task-15 批4:遥信点数与单/双点性质(点表事实的唯一来源) */ /* 私有宏定义区 */ @@ -184,6 +193,27 @@ static uint8_t feed_eng(void) { return UI_DATA_ENG_SEC; } +/* + * 函数功能:组装遥信双点位图(bit i = 第 i 点是双点型 M_DP) + * 入口参数:无 + * 返回值:位图 uint64_t + * 限定条件:uiTask 上下文;点数上限 UI_DATA_YX_N(<64,故位图放得下) + * 函数说明:逐点问点表 `iec104_pts_yx_is_dp()`,**不在此复制"前 4 点是双点"这个事实** + * ——那句话写在这里就是第二真相,点表一改就悄悄错(下发位图的理由见 + * ui_data.h 的 yx_dp 字段注释) + */ +static uint64_t feed_yx_dp(void) { + uint64_t m = 0u; + uint8_t i; + + for (i = 0u; (i < (uint8_t)UI_DATA_YX_N) && (i < 64u); i++) { + if (iec104_pts_yx_is_dp(i) != 0u) { + m |= ((uint64_t)1u << i); + } + } + return m; +} + /* * 函数功能:组装遥信位图(bit i = 第 i 点"合/动作") * 入口参数:无 @@ -316,6 +346,20 @@ static void feed_fill_rt(ui_data_rt_t *p_snap, uint32_t tick_ms) { p_snap->yx_bits = feed_yx_bits(); p_snap->yx_valid = FEED_YX_VALID_MASK; + p_snap->yx_dp = feed_yx_dp(); + /* 点数漂移检查(只在真漂走时打一条日志):点表 YX_N 与本层 UI_DATA_YX_N 一旦不等, + * 页面按 33 行画、协议按另一个数上送,两边都"看起来正常"——这是静默错,值得花一条 + * DBG(约 90 B Flash)换一次现场可见的自证;只打一次不刷屏(同丢弃计数口径) */ + { + static uint8_t s_yx_n_bad; + uint8_t n = iec104_pts_yx_count(); + + if ((n != (uint8_t)UI_DATA_YX_N) && (s_yx_n_bad == 0u)) { + s_yx_n_bad = 1u; + DBG_ERROR("yx point count drift: pts=%u ui_name_table=%u", + (unsigned)n, (unsigned)UI_DATA_YX_N); + } + } /* 测频:读数新鲜度单独走 freq_valid()(幅值门 + 收敛),两者任一不成立 * 页面即显示 "--"——eng=0 期间幅值门恒失效,读数本就不可消费,如实反映 */ 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_data.h b/Drivers/BSP/UI/ui_data.h index 4ecd6be5..205ac511 100644 --- a/Drivers/BSP/UI/ui_data.h +++ b/Drivers/BSP/UI/ui_data.h @@ -121,7 +121,17 @@ typedef struct { uint8_t rsv4[3]; /* 预留(对齐填充) */ ui_data_yc_t ph[UI_DATA_PH_MAX]; /* 下标 = 相量实例序(含谐波),见 UI_DATA_PH_MAX 注 */ uint64_t yx_bits; /* 遥信位图:bit i = 第 i 点"合/动作" */ - uint64_t yx_valid; /* 遥信有效位图:bit i = 1 该点有硬件来源 */ + uint64_t yx_valid; /* 遥信有效位图: bit i = 1 该点有硬件来源 */ + uint64_t yx_dp; /* 遥信双点位图: bit i = 1 该点是双点型(M_DP) + * 值列打"合位/分位",单点打"动作/复归"。 + * 为什么下发位图而不让页面自己认定"前 4 点是 + * 双点":单/双点是**104 点表事实**(见 + * App/protocol/iec104/iec104_pts.c 的 YX_DP_N 与 + * iec104_pts_yx_is_dp),抄一份进表现层就是第二 + * 真相——点表把某点改成双点、或把双点段挪到下标 + * 中段,屏上不会报错,只会用错词形容同一个点。 + * V0.1 双点 4 点无开入硬件 → 不在 yx_valid 内, + * 值列仍是 "--"(本字段不参与无效判定) */ uint8_t freq_valid; /* 1 = 测频读数新鲜(幅值门 OK 且已收敛) */ uint8_t rsv2[3]; /* 预留(对齐填充) */ float freq_hz; /* 测得频率(Hz);freq_valid=0 时不可消费 */ diff --git a/Drivers/BSP/UI/ui_page_ac.c b/Drivers/BSP/UI/ui_page_ac.c index a9ec7569..515f5bdf 100644 --- a/Drivers/BSP/UI/ui_page_ac.c +++ b/Drivers/BSP/UI/ui_page_ac.c @@ -1,51 +1,218 @@ /* * 模块名称:UI Page AC (交流量内容页) - * 模块功能:交流量叶子节点的内容页数据——数据列表(24 项 / 8 项每页,共 3 页); - * 行槽顶 = 24+14i、选中白带 x=2..156 高 13、名 x=7 / 值 x=101(参考图 20 实测); - * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 模块功能:交流量叶子节点(node 3)内容页——7 行单页动态列表,数值全部取自 ui_data 实时帧 + * (App 层装配,本文件零 App include):Ua/U0/Ia 走基波相量实例、Ia2/Ia5 走谐波 + * 实例、Hz 走测频新鲜度、第 7 行是**量纲口径说明行**(快照 eng → 未标定/二次值/ + * 一次值,不是测量值,是给上面 6 行的诚实性标注);向框架 ui_content_page 提供 + * 页面描述符(零 LVGL 对象) + * 为什么不是参考图的 24 行:V0.1 只有 8 路电气量且 5 路削顶无采样源、相量实例只有 5 个, + * 照抄 24 行 = 17 行 "--" 的假骨架(取舍见 docs/UI设计/页面扩充详细方案.md §3.1.1) + * 空值口径:IV/NT/RAW/削顶/未发布一律 "--",不显 0.000——把"没有数"或"坏数"显成 + * 一个像实测的数是现场误读的头号来源 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 * 创建日期:2026-09-09 * 修改记录: * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + * v2.0 2026-10-25 王建锋 道E task-15 回炉:24 行 Flash 字面量 → 7 行快照真值;帧缓存 + * 按发布序号门控;数值格式化复用 ui_w_editor_fmt_scaled(定点 + * 整数路线,页面侧不引 %f 软浮点打印) */ /* 头文件包含区 */ +#include + #include "ui_page_ac.h" +#include "ui_data.h" #include "ui_tokens.h" +#include "ui_w_editor.h" + +/* 私有宏定义区 */ + +/* 相量实例槽号(快照 ph[] 按写侧 App/algo/phasor.c 的 k_inst[] 序发布,见 ui_data.h 的 + * UI_DATA_PH_MAX 注)。本文件不 include App 头(分层方向),故槽号在这里独立登记一份; + * 真漂移(实例表插入/重排)的表现是"某行显示另一路的数",防线 = test/host/ui_data 的槽数 + * 与语义一致性断言 + 上机对点(待台架实测) */ +#define AC_PH_IA_F1 0u /* 基波 Ia */ +#define AC_PH_UA_F1 1u /* 基波 Ua */ +#define AC_PH_UN_F1 2u /* 基波 U0(零序取 Un 通道,见 UI_数据供给设计说明.md §4.1) */ +#define AC_PH_IA_F2 3u /* 二次谐波 Ia2(涌流制动监视量) */ +#define AC_PH_IA_F5 4u /* 五次谐波 Ia5 */ + +#define AC_ROW_HZ 5u /* 频率行 */ +#define AC_ROW_CAL 6u /* 量纲口径行(非测量值) */ +#define AC_ROW_N 7u +#define AC_INVALID "--" +#define AC_IA_DECIMALS 3u /* 电压/电流小数位 */ +#define AC_HZ_DECIMALS 2u +/* 不可信品质合集:无效 / 未攒满一周波 / 透传码值 / 自检异常 / 闭锁 / 越程 */ +#define AC_Q_BAD (UI_DATA_Q_IV | UI_DATA_Q_NT | UI_DATA_Q_RAW | UI_DATA_Q_SB | \ + UI_DATA_Q_BL | UI_DATA_Q_OV) /* 静态变量定义区 */ -/* 交流量行(参考图 20 = 第 1 页原样;第 2/3 页为占位数据,可替换) */ -static const cp_row_t s_rows[] = { - {"Ia1", "0.000"}, /* 第 1 页(参考图 20) */ - {"Ib1", "0.137"}, - {"Ic1", "0.274"}, - {"Ia2", "0.411"}, - {"Ib2", "0.548"}, - {"Ic2", "0.685"}, - {"Ua1", "0.82"}, - {"Ub1", "0.96"}, - {"Uc1", "1.03"}, /* 第 2 页(占位数据) */ - {"Uab", "1.17"}, - {"Ubc", "1.24"}, - {"Uca", "1.31"}, - {"Hz", "50.00"}, - {"P", "1.42"}, - {"Q", "0.87"}, - {"S", "1.63"}, - {"PF", "0.95"}, /* 第 3 页(占位数据) */ - {"Iavg","0.35"}, - {"Uavg","0.87"}, - {"Fmax","50.02"}, - {"Imax","1.24"}, - {"Pmax","2.10"}, - {"Qmax","1.05"}, - {"Smax","2.45"}, +/* 行名表(Flash) */ +static const char *const s_names[AC_ROW_N] = { + "Ua", "U0", "Ia", "Ia2", "Ia5", "Hz", "标定", }; -/* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=ac 绑定引用;列表页,钩子全 NULL) */ +/* 前三行 → 实例槽映射(不把魔法数散在分支里) */ +static const uint8_t s_ph_row[3] = { AC_PH_UA_F1, AC_PH_UN_F1, AC_PH_IA_F1 }; + +/* 帧缓存:发布序号变化才整帧拷一次(UI_数据供给设计说明.md §5.4 的页面侧约定) */ +static ui_data_rt_t s_rt; +static uint32_t s_seq; + +/* 函数定义区 */ + +/* + * 函数功能:把常量值串安全搬进显示缓冲(替代 strcpy,规范 7.5) + * 入口参数:a_dst - 目标缓冲(框架 UI_BUF_STRLEN) char * + * a_lit - 源字面量(编译期串,长度由宏用 sizeof 算好) const char * + * a_len - 需拷贝字节数(不含 NUL) uint32_t + * 返回值:无 + * 限定条件:a_dst/a_lit 非 NULL;a_len 超缓冲时截断而不是越界 + * 函数说明:代码规范 7.5 禁 strcpy——这里长度来自**调用点的 sizeof**,写串与算长 + * 在同一个宏里,改串不会忘记改长度(原语式拷贝才是安全的来源,不是人的记性) + */ +static void ac_put(char *a_dst, const char *a_lit, uint32_t a_len) { + if (a_len > ((uint32_t)UI_BUF_STRLEN - 1u)) { + a_len = (uint32_t)UI_BUF_STRLEN - 1u; + } + (void)memcpy(a_dst, a_lit, a_len); + a_dst[a_len] = (char)0; +} + +/* 值串落屏统一走这里:长度由 sizeof 在调用点算,改串不会漏改长度 */ +#define AC_PUT(a_dst, a_lit) ac_put((a_dst), (a_lit), (uint32_t)(sizeof(a_lit) - 1u)) + +/* + * 函数功能:把快照里的工程量浮点转成定点整数后交共享格式化器输出 + * 入口参数:a_val - 数值(有效值,恒非负;负值按坏数钳 0) float + * a_decimals - 小数位 0..3 uint8_t + * a_buf - 输出缓冲 char * + * a_len - 缓冲长度(含 NUL) uint32_t + * 返回值:无 + * 限定条件:a_buf 非 NULL + * 函数说明:本仓 UI 侧统一走"定点整数 + 共享格式化"(ui_w_editor_fmt_scaled,与整定页同一 + * 真源,两处显示口径不可能漂移);页面里不写 %f——MicroLIB 的软浮点打印一旦 + * 被拉进镜像就是几百字节 Flash,而模拟器(宿主 CRT)永远照不出这笔代价 + */ +static void ac_format(const float a_val, uint8_t a_decimals, char *a_buf, uint32_t a_len) { + int32_t scale = 1; + uint8_t i; + + for (i = 0u; (i < a_decimals) && (i < 3u); i++) { + scale *= 10; + } + /* 有效值不可能为负:出现负数说明上游数据坏了,钳 0 交给品质行解释,比在屏上留一个 + * "-0.001"(现场会读成"真的反向了")好 */ + ui_w_editor_fmt_scaled((a_val > 0.0f) ? (int32_t)((a_val * (float)scale) + 0.5f) : 0, + a_decimals, a_buf, a_len); +} + +/* + * 函数功能:取用最新实时帧(只在发布序号变化时整帧拷) + * 入口参数:无 + * 返回值:1 - 有可用帧;0 - 首帧未到或本帧无效 uint8_t + * 限定条件:uiTask 绘制上下文 + * 函数说明:读失败(首帧未到)时清缓存并记住本次序号,避免每次重绘重复尝试;下一帧序号 + * 必然变化 → 自动恢复 + */ +static uint8_t ac_frame_ready(void) { + uint32_t now = ui_data_rt_seq(); + + if (now != s_seq) { + s_seq = now; + if (ui_data_rt(&s_rt) != 0) { + (void)memset(&s_rt, 0, sizeof(s_rt)); + return 0u; + } + } + return (s_rt.valid != 0u) ? 1u : 0u; +} + +/* + * 函数功能:开页复位缓存(强制下一次绘制重取最新帧,不沿用上次开页的陈旧帧号) + * 入口参数:无 + * 返回值:无 + */ +static void ac_init(void) { + s_seq = 0u; + (void)memset(&s_rt, 0, sizeof(s_rt)); +} + +/* + * 函数功能:逐行取数(前 5 行相量幅值、第 6 行频率、第 7 行量纲口径) + * 入口参数:a_row - 全局行下标 0..6 uint16_t + * a_name - 名输出缓冲(框架 UI_BUF_STRLEN) char * + * a_value - 值输出缓冲 char * + * 返回值:无 + * 限定条件:uiTask 绘制上下文;缓冲由框架备好且足够 + * 函数说明:"有数但不可信"与"没有数"在屏上必须同形(一律 "--")——削顶或未攒满一周波时 + * 快照里仍留着上一次的数,照抄等于把坏数当实测送进人眼 + */ +static void ac_refresh(uint16_t a_row, char *a_name, char *a_value) { + const char *nm = (a_row < AC_ROW_N) ? s_names[a_row] : ""; + uint32_t n; + + n = (uint32_t)strlen(nm) + 1u; + if (n > (uint32_t)UI_BUF_STRLEN) { + n = (uint32_t)UI_BUF_STRLEN; + } + (void)memcpy(a_name, nm, n); + a_value[0] = (char)0; + + if (ac_frame_ready() == 0u) { + AC_PUT(a_value, AC_INVALID); + return; + } + + if (a_row < AC_ROW_HZ) { + /* 前 5 行:相量实例槽(前三行走基波映射,后两行走二次/五次谐波实例) */ + uint8_t slot = (a_row < 3u) ? s_ph_row[a_row] : (uint8_t)a_row; + const ui_data_yc_t *p; + + if (slot >= s_rt.ph_n) { + AC_PUT(a_value, AC_INVALID); /* 本帧没发布这个实例 = 没有数,不是零 */ + return; + } + p = &s_rt.ph[slot]; + if ((p->qds & (uint8_t)AC_Q_BAD) != 0u) { + AC_PUT(a_value, AC_INVALID); + } else { + ac_format(p->val, AC_IA_DECIMALS, a_value, (uint32_t)UI_BUF_STRLEN); + } + return; + } + + if (a_row == AC_ROW_HZ) { + /* 频率新鲜度单独判:eng=0 期间幅值门恒不成立 → 恒 "--",如实反映"现在测不出" */ + if (s_rt.freq_valid == 0u) { + AC_PUT(a_value, AC_INVALID); + } else { + ac_format(s_rt.freq_hz, AC_HZ_DECIMALS, a_value, (uint32_t)UI_BUF_STRLEN); + } + return; + } + + /* 第 7 行:量纲口径。eng 真值语义 = 0 引脚电压 / 1 二次值 / 2 一次值;标定闭环后自动 + * 变"二次值",页面零改动(写侧口径见 App/app/ui_data_feed.c 的 feed_eng) */ + if (s_rt.eng == (uint8_t)UI_DATA_ENG_SEC) { + AC_PUT(a_value, "二次值"); + } else if (s_rt.eng == (uint8_t)UI_DATA_ENG_PRI) { + AC_PUT(a_value, "一次值"); + } else { + AC_PUT(a_value, "未标定"); + } +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=ac 绑定引用): + * 7 行单页列表页(8 行/页 → 恒 1 页);band_h 必须 >0——selectable 页选中行是黑字, + * 无白带即不可见(同 ui_page_runlog 约定) + */ const content_page_def_t ui_page_ac_desc = { .id = "ac", .title = "交流量", @@ -57,6 +224,8 @@ const content_page_def_t ui_page_ac_desc = { .left_x = UI_LIST_LEFT_X, .right_x = UI_LIST_RIGHT_X, .rows_per_page = UI_LIST_ROWS, - .row_count = 24, - .rows = s_rows, + .row_count = AC_ROW_N, + .rows = NULL, + .init_cb = ac_init, + .refresh = ac_refresh, }; diff --git a/Drivers/BSP/UI/ui_page_dc.c b/Drivers/BSP/UI/ui_page_dc.c index 1888f3df..834fac41 100644 --- a/Drivers/BSP/UI/ui_page_dc.c +++ b/Drivers/BSP/UI/ui_page_dc.c @@ -1,29 +1,139 @@ /* * 模块名称:UI Page DC (直流量内容页) - * 模块功能:直流量叶子节点的内容页数据——数据列表(3 项单页); - * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 模块功能:直流量叶子节点(node 4)内容页——2 行动态列表:温度(STM32 片内 ADC 透传码值, + * 未标定)+ 直流通道(V0.1 板上无直流采样通道,值列如实显示"未配置");数据取自 + * ui_data 实时帧(App 层装配,本文件零 App include);向框架 ui_content_page 提供 + * 页面描述符(零 LVGL 对象) + * 为什么只有 2 行:参考图 21 的"3 行直流读数"在本板无采样源(ADC_SEM 只有 8 路电气 + 温度, + * 104 点表 0x401E/0x401F 亦注明"FTU 无")——照抄就是三行假数(取舍见 + * docs/UI设计/页面扩充详细方案.md §3.1.2 方案①,菜单语义与点表保持一致) + * 空值口径:温度通道无效时显 "--";码值一律不挂单位(挂上"℃"就是替用户完成了没做的标定) * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 * 创建日期:2026-09-09 * 修改记录: * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + * v2.0 2026-10-25 王建锋 道E task-15 回炉:3 行 Flash 假数(ZL01/ZL02/Pa1)→ 2 行快照 + * 真值;温度取 yc[ADC_SEM_TEMP] 并显式标注是码值 */ /* 头文件包含区 */ +#include + #include "ui_page_dc.h" +#include "ui_data.h" #include "ui_tokens.h" +#include "ui_w_editor.h" + +/* 私有宏定义区 */ + +/* 温度通道在快照 yc[] 里的语义下标 = adc_semantic_t 的 TEMP(最后一项,随通道表漂移; + * 由 test/host/ui_data 的"下标空间与 App/algo/adc_input.h 逐行一致"断言兜住) */ +#define DC_YC_TEMP 8u +#define DC_ROW_N 2u +#define DC_INVALID "--" +/* 温度值列前缀:一个"码"字把"这是 12 位原始码、不是温度"写进屏上而不是注释里 */ +#define DC_CODE_PREFIX "码" /* 静态变量定义区 */ -/* 直流量行(参考图 21) */ -static const cp_row_t s_rows[] = { - {"ZL01", "3.01"}, - {"ZL02", "3.15"}, - {"Pa1", "3.288"}, -}; +/* 帧缓存(按发布序号门控,同交流量页) */ +static ui_data_rt_t s_rt; +static uint32_t s_seq; -/* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=dc 绑定引用;列表页单页,钩子全 NULL) */ +/* 函数定义区 */ + +/* + * 函数功能:把常量串安全搬进显示缓冲(替代 strcpy,规范 7.5) + * 入口参数:a_dst - 目标缓冲(框架 UI_BUF_STRLEN) char * + * a_lit - 源字面量(长度由宏用 sizeof 算好) const char * + * a_len - 需拷贝字节数(不含 NUL) uint32_t + * 返回值:无 + * 限定条件:a_dst/a_lit 非 NULL;a_len 超缓冲按缓冲截断 + * 函数说明:代码规范 7.5 禁 strcpy;长度来自调用点 sizeof,写串与算长同一处 + */ +static void dc_put(char *a_dst, const char *a_lit, uint32_t a_len) { + if (a_len > ((uint32_t)UI_BUF_STRLEN - 1u)) { + a_len = (uint32_t)UI_BUF_STRLEN - 1u; + } + (void)memcpy(a_dst, a_lit, a_len); + a_dst[a_len] = (char)0; +} + +#define DC_PUT(a_dst, a_lit) dc_put((a_dst), (a_lit), (uint32_t)(sizeof(a_lit) - 1u)) + +/* + * 函数功能:取用最新实时帧(只在发布序号变化时整帧拷) + * 入口参数:无 + * 返回值:1 - 有可用帧;0 - 首帧未到或本帧无效 uint8_t + * 限定条件:uiTask 绘制上下文 + */ +static uint8_t dc_frame_ready(void) { + uint32_t now = ui_data_rt_seq(); + + if (now != s_seq) { + s_seq = now; + if (ui_data_rt(&s_rt) != 0) { + (void)memset(&s_rt, 0, sizeof(s_rt)); + return 0u; + } + } + return (s_rt.valid != 0u) ? 1u : 0u; +} + +/* + * 函数功能:开页复位缓存(强制重取最新帧) + * 入口参数:无 + * 返回值:无 + */ +static void dc_init(void) { + s_seq = 0u; + (void)memset(&s_rt, 0, sizeof(s_rt)); +} + +/* + * 函数功能:逐行取数(第 1 行温度码值、第 2 行直流通道未配置) + * 入口参数:a_row - 全局行下标 0..1 uint16_t + * a_name - 名输出缓冲(框架 UI_BUF_STRLEN) char * + * a_value - 值输出缓冲 char * + * 返回值:无 + * 限定条件:uiTask 绘制上下文 + * 函数说明:温度行的值列形如"码2048"——"码"字是把"这是 12 位原始码、不是温度"写进 + * 屏上而不是写进注释;快照该路带 UI_DATA_Q_RAW(写侧因该通道不参与线性标定而置位), + * 这里同时按品质位判无效,两处任一说话都足以显 "--" + */ +static void dc_refresh(uint16_t a_row, char *a_name, char *a_value) { + if (a_row == 0u) { + const ui_data_yc_t *p; /* 温度通道(只有本分支用得到,cppcheck 也这么判) */ + + DC_PUT(a_name, "温度"); + if ((dc_frame_ready() == 0u) || (DC_YC_TEMP >= UI_DATA_YC_N)) { + DC_PUT(a_value, DC_INVALID); + return; + } + p = &s_rt.yc[DC_YC_TEMP]; + if ((p->qds & (uint8_t)(UI_DATA_Q_IV | UI_DATA_Q_SB | UI_DATA_Q_BL)) != 0u) { + DC_PUT(a_value, DC_INVALID); + } else { + ui_w_editor_fmt_scaled((p->val > 0.0f) ? (int32_t)(p->val + 0.5f) : 0, 0u, + a_value, (uint32_t)UI_BUF_STRLEN); + /* 前缀"码":单位由品质位说话,不靠读的人猜(长度取 sizeof,改字不脱节) */ + (void)memmove(a_value + (sizeof(DC_CODE_PREFIX) - 1u), a_value, + strlen(a_value) + 1u); + (void)memcpy(a_value, DC_CODE_PREFIX, sizeof(DC_CODE_PREFIX) - 1u); + } + return; + } + + DC_PUT(a_name, "直流通道"); + DC_PUT(a_value, "未配置"); +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=dc 绑定引用): + * 2 行单页列表页;band_h 必须 >0(选中行黑字需白带,同交流量页约定) + */ const content_page_def_t ui_page_dc_desc = { .id = "dc", .title = "直流量", @@ -35,6 +145,8 @@ const content_page_def_t ui_page_dc_desc = { .left_x = UI_LIST_LEFT_X, .right_x = UI_LIST_RIGHT_X, .rows_per_page = UI_LIST_ROWS, - .row_count = 3, - .rows = s_rows, + .row_count = DC_ROW_N, + .rows = NULL, + .init_cb = dc_init, + .refresh = dc_refresh, }; diff --git a/Drivers/BSP/UI/ui_page_device_info.c b/Drivers/BSP/UI/ui_page_device_info.c index 64cdda44..7bcbc755 100644 --- a/Drivers/BSP/UI/ui_page_device_info.c +++ b/Drivers/BSP/UI/ui_page_device_info.c @@ -1,46 +1,262 @@ /* * 模块名称:UI Page Device Info (装置信息内容页) - * 模块功能:装置信息叶子节点的内容页数据——静态键值行(参考图 10,值列固定 x=86) - * + 副标题,单页,页脚按参考图原样"第 10 页,共 1 页"; - * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 模块功能:装置信息叶子节点(node 1)内容页——8 行只读信息:终端型号/软件版本/硬件版本/ + * 软件校验/程序日期/运行时长取自 ui_data 装置信息帧(App 层装配,本文件零 App + * include),MAC/IP 取自 net_cfg RAM 副本(同层 BSP→BSP,与网口设置页同一来源、 + * 同一格式化函数);向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 几何为何改:原值列 x=86 是参考图"标签带全角冒号 + 短值"的产物;本页要容 17 字符 MAC + * (102 px)与 15 字符 IP,故标签去冒号(4 汉字 = 48 px,右缘 52)、值列左对齐 + * x=56(右缘 158 ≤ 160);8 行按 13 px 行距排到 129,不与页脚(143)相撞 + * 页脚:显示真实页码"第 1 页,共 1 页"——原固定值"第 10 页"是参考项目遗留的假页码, + * 屏上不该出现与本机无关的数字(Lead 裁定 2026-10-25);副标题属产品口径,本批不动 + * 软件校验行:构建期 CRC 步骤未落地前值为 "--"(写侧 App/app/ui_data_feed.c 的 sw_sum), + * 不抄参考项目的"4454" * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 * 创建日期:2026-09-09 * 修改记录: * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + * v2.0 2026-10-25 王建锋 道E task-15 回炉:6 行 Flash 字面量(含参考项目遗留值)→ + * 8 行真值;行几何、页码口径与越界读防御一并处理 */ /* 头文件包含区 */ +#include + #include "ui_page_device_info.h" +#include "net_cfg.h" +#include "ui_data.h" +#include "ui_tokens.h" + +/* 私有宏定义区 */ + +#define DI_ROW_N 8u +#define DI_INVALID "--" +#define DI_VAL_X 56 /* 值列 x:17 字符 MAC(102 px)右缘 158 落在屏内 */ +#define DI_LABEL_X 4 /* 标签列 x:4 汉字(48 px)右缘 52,不压值列 */ +#define DI_ROW_TOP 38 /* 首行槽顶:让开副标题(y=25) */ +#define DI_ROW_PITCH 13 +#define DI_SEC_PER_DAY 86400u +#define DI_SEC_PER_HOUR 3600u +#define DI_SEC_PER_MIN 60u + /* 静态变量定义区 */ -/* 装置信息行(参考图 10;标签含全角冒号,值列固定 x=86) */ -static const cp_row_t s_rows[] = { - {"终端类型:", "F30"}, - {"终端型号:", "F30"}, - {"软件版本:", "SV0.010"}, - {"硬件版本:", "HW0.010"}, - {"软件校验:", "4454"}, - {"程序日期:", "2024.08.27"}, +/* 行名表(Flash;无全角冒号,理由见文件头几何说明) */ +static const char *const s_names[DI_ROW_N] = { + "终端型号", "软件版本", "硬件版本", "软件校验", + "程序日期", "MAC地址", "IP地址", "运行时长", }; -/* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=device_info 绑定引用;静态信息页,钩子全 NULL) */ +/* 帧缓存:发布序号变化才整帧拷一次(同实时量页口径,UI_数据供给设计说明.md §5.4) */ +static ui_data_info_t s_info; +static uint32_t s_seq; + +/* 函数定义区 */ + +/* + * 函数功能:把常量串安全搬进显示缓冲(替代 strcpy,规范 7.5) + * 入口参数:a_dst - 目标缓冲(框架 UI_BUF_STRLEN) char * + * a_lit - 源字面量(长度由宏用 sizeof 算好) const char * + * a_len - 需拷贝字节数(不含 NUL) uint32_t + * 返回值:无 + * 限定条件:a_dst/a_lit 非 NULL;a_len 超缓冲按缓冲截断 + * 函数说明:代码规范 7.5 禁 strcpy;长度来自调用点 sizeof,写串与算长同一处 + */ +static void di_put(char *a_dst, const char *a_lit, uint32_t a_len) { + if (a_len > ((uint32_t)UI_BUF_STRLEN - 1u)) { + a_len = (uint32_t)UI_BUF_STRLEN - 1u; + } + (void)memcpy(a_dst, a_lit, a_len); + a_dst[a_len] = (char)0; +} + +#define DI_PUT(a_dst, a_lit) di_put((a_dst), (a_lit), (uint32_t)(sizeof(a_lit) - 1u)) + +/* + * 函数功能:取用最新装置信息帧(只在发布序号变化时整帧拷) + * 入口参数:无 + * 返回值:1 - 有可用帧;0 - 首帧未到或本帧无效 uint8_t + * 限定条件:uiTask 绘制上下文 + */ +static uint8_t di_frame_ready(void) { + uint32_t now = ui_data_info_seq(); + + if (now != s_seq) { + s_seq = now; + if (ui_data_info(&s_info) != 0) { + (void)memset(&s_info, 0, sizeof(s_info)); + return 0u; + } + } + return (s_info.valid != 0u) ? 1u : 0u; +} + +/* + * 函数功能:开页复位缓存(强制下一次绘制重取最新帧) + * 入口参数:无 + * 返回值:无 + */ +static void di_init(void) { + s_seq = 0u; + (void)memset(&s_info, 0, sizeof(s_info)); +} + +/* + * 函数功能:把快照里的定长字符域安全搬进显示缓冲 + * 入口参数:a_dst - 目标缓冲(框架 UI_BUF_STRLEN) char * + * a_src - 快照字段首地址 const char * + * a_cap - 字段容量(不含 NUL 的字节数,取 UI_DATA_*_LEN) uint32_t + * 返回值:1 - 拷到非空串;0 - 字段全零(视为无数据,调用方显示 "--") uint8_t + * 限定条件:a_dst/a_src 非 NULL + * 函数说明:契约里这些字段是"定长域、写侧不保证补 NUL",所以**按容量扫**而不是 strlen—— + * 靠 strlen 读一个没封口的定长域就是越界读,模拟器(后面紧跟别的常量)大概率 + * 侥幸、板端 Flash 布局一变就翻车,且这类 bug 从不报错只多显示几个字符 + */ +static uint8_t di_copy_field(char *a_dst, const char *a_src, uint32_t a_cap) { + uint32_t len = 0u; + + while ((len < a_cap) && (a_src[len] != (char)0)) { + len++; + } + if (len == 0u) { + return 0u; + } + if (len > ((uint32_t)UI_BUF_STRLEN - 1u)) { + len = (uint32_t)UI_BUF_STRLEN - 1u; /* 超长截断:宁可少显示几个字符也不越界 */ + } + (void)memcpy(a_dst, a_src, len); + a_dst[len] = (char)0; + return 1u; +} + +/* + * 函数功能:把开机秒格式化成运行时长 + * 入口参数:a_sec - 开机秒 uint32_t + * a_buf - 输出缓冲 char * + * a_len - 缓冲长度(含 NUL) uint32_t + * 返回值:无 + * 限定条件:a_buf 非 NULL + * 函数说明:不足一天给"时:分:秒",过一天给"N天(ASCII d) 时:分"(丢秒)——串长压进值列 + * 104 px;"天"字故意不写中文:它不在 183 字子集字体里,写上去屏上是空白方块, + * 而为一个运行时才出现的格式字符扩字库要付 27 B RO + 一次字体重生成的代价 + * (font-charset 门禁这次就把它拦下了,改 ASCII 而不是补字库); + * "1234567 s" 这种写法人眼算不出量级,反而是把换算推给读表的人 + */ +static void di_fmt_uptime(uint32_t a_sec, char *a_buf, uint32_t a_len) { + uint32_t day = a_sec / DI_SEC_PER_DAY; + uint32_t rem = a_sec % DI_SEC_PER_DAY; + + if (day == 0u) { + (void)lv_snprintf(a_buf, a_len, "%02u:%02u:%02u", + (unsigned)(rem / DI_SEC_PER_HOUR), + (unsigned)((rem / DI_SEC_PER_MIN) % DI_SEC_PER_MIN), + (unsigned)(rem % DI_SEC_PER_MIN)); + } else { + (void)lv_snprintf(a_buf, a_len, "%ud %02u:%02u", (unsigned)day, + (unsigned)(rem / DI_SEC_PER_HOUR), + (unsigned)((rem / DI_SEC_PER_MIN) % DI_SEC_PER_MIN)); + } +} + +/* + * 函数功能:逐行取数(快照 6 行 + net_cfg 2 行) + * 入口参数:a_row - 全局行下标 0..7 uint16_t + * a_name - 名输出缓冲(框架 UI_BUF_STRLEN) char * + * a_value - 值输出缓冲 char * + * 返回值:无 + * 限定条件:uiTask 绘制上下文;net_cfg RAM 副本已由 defaultTask 载入(本函数只读副本, + * 不碰配置引擎——配置引擎属主是 defaultTask) + * 函数说明:MAC/IP 两行不依赖装置信息帧:网络配置在自己的通路上已经就绪,快照还没发 + * 也应该能读到本机地址(否则首帧前整屏都是 "--",白丢两行真值) + */ +static void di_refresh(uint16_t a_row, char *a_name, char *a_value) { + const char *nm = (a_row < DI_ROW_N) ? s_names[a_row] : ""; + const char *src = NULL; + uint32_t cap = 0u; + uint32_t n; + + n = (uint32_t)strlen(nm) + 1u; + if (n > (uint32_t)UI_BUF_STRLEN) { + n = (uint32_t)UI_BUF_STRLEN; + } + (void)memcpy(a_name, nm, n); + a_value[0] = (char)0; + + if ((a_row == 5u) || (a_row == 6u)) { + if (a_row == 5u) { + uint8_t mac[6]; + + net_cfg_get_mac(mac); + (void)lv_snprintf(a_value, (size_t)UI_BUF_STRLEN, "%02X:%02X:%02X:%02X:%02X:%02X", + (unsigned)mac[0], (unsigned)mac[1], (unsigned)mac[2], + (unsigned)mac[3], (unsigned)mac[4], (unsigned)mac[5]); + } else { + net_cfg_fmt_ipv4(net_cfg_get()->ip, a_value, (size_t)UI_BUF_STRLEN); + } + return; + } + + if (di_frame_ready() == 0u) { + DI_PUT(a_value, DI_INVALID); + return; + } + + switch (a_row) { + case 0u: + src = s_info.model; + cap = UI_DATA_MODEL_LEN; + break; + case 1u: + src = s_info.sw_ver; + cap = UI_DATA_VERSTR_LEN; + break; + case 2u: + src = s_info.hw_ver; + cap = UI_DATA_VERSTR_LEN; + break; + case 3u: + src = s_info.sw_sum; + cap = UI_DATA_SUM_LEN; + break; + case 4u: + src = s_info.build_date; + cap = UI_DATA_DATE_LEN; + break; + case 7u: + di_fmt_uptime(s_info.uptime_s, a_value, (uint32_t)UI_BUF_STRLEN); + return; + default: + break; + } + + if ((src == NULL) || (di_copy_field(a_value, src, cap) == 0u)) { + DI_PUT(a_value, DI_INVALID); + } +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=device_info 绑定引用): + * 只读信息页(非 selectable → 无选中带、无分页),页脚取真实页码 1/1 + */ const content_page_def_t ui_page_device_info_desc = { .id = "device_info", .title = "装置信息", .subtitle = "馈线自动化终端", .subtitle_y = 25, .selectable = 0u, - .top_y = 40, - .pitch = 15, + .top_y = DI_ROW_TOP, + .pitch = DI_ROW_PITCH, .band_h = 0, .text_dy = 0, - .left_x = 6, - .right_x = 86, + .left_x = DI_LABEL_X, + .right_x = DI_VAL_X, .rows_per_page = 0u, - .row_count = 6, - .rows = s_rows, - .fixed_cur_page = 10u, + .row_count = DI_ROW_N, + .rows = NULL, + .fixed_cur_page = 1u, .fixed_total_pages = 1u, + .init_cb = di_init, + .refresh = di_refresh, }; diff --git a/Drivers/BSP/UI/ui_page_registry.c b/Drivers/BSP/UI/ui_page_registry.c index 540c05c9..84c0e44a 100644 --- a/Drivers/BSP/UI/ui_page_registry.c +++ b/Drivers/BSP/UI/ui_page_registry.c @@ -16,12 +16,24 @@ #include "ui_page_password.h" #include "ui_page_runlog.h" #include "ui_page_selftest.h" +#include "ui_page_soe.h" +#include "ui_page_view_param.h" +#include "ui_page_view_plate.h" +#include "ui_page_view_set.h" +#include "ui_page_view_word.h" +#include "ui_page_yx.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 直流量 */ + { 5u, &ui_page_yx_desc }, /* node 5 遥信量 */ + { 13u, &ui_page_view_param_desc }, /* node 13 参数 */ + { 14u, &ui_page_view_set_desc }, /* node 14 定值 */ + { 15u, &ui_page_view_word_desc }, /* node 15 控制字 */ + { 16u, &ui_page_view_plate_desc }, /* node 16 软压板 */ { 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..eeabbbcf 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 12u /* 绑定了内容页的叶子数 */ 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_page_view_param.c b/Drivers/BSP/UI/ui_page_view_param.c new file mode 100644 index 00000000..20284505 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_view_param.c @@ -0,0 +1,94 @@ +/* + * 模块名称:UI Page view_param (参数查看内容页) + * 模块功能:参数定值 → 查看 → 参数 的内容页——8 行数值表(PT/CT 一次二次额定 + 4 个返回系数),每页 8 行、1 页; + * 数据 = 参数快照(Q10)按行表小数位换算,越界项由 bad 回调反显; + * 本文件零 App include、零写路径(查看页没有"保存"这件事) + * 为什么这么薄:行表与取数在 ui_w_setview(四个查看页共用一份视图模型), + * 渲染与反显在 ui_w_setlist(与整定页同一组件)——本文件只把视图编号 + * 和标准骨架几何令牌接进框架的跳板,改一次口径四处一致 + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15 批 3) + */ +/* 头文件包含区 */ +#include "ui_page_view_param.h" + +#include "ui_tokens.h" +#include "ui_w_setlist.h" +#include "ui_w_setview.h" + +/* 静态变量定义区 */ + +/* 本页实例(组件状态:编辑态等;查看页只用行游标与反显,实例仍须独占一份, + * 与别的页面共用会互相踩状态——同 ui_w_records 的实例化约定) */ +static ui_w_setlist_t s_sl; + +/* 函数定义区 */ + +/* + * 函数功能:开页绑定视图(行表 + 取数/反显回调一次配齐;接框架 init_cb) + * 入口参数:无 + * 返回值:无 + * 限定条件:框架开页链内调用(uiTask) + * 函数说明:每次开页都重新 attach——组件实例内残留的编辑态/游标会跨页带过来, + * 开页复位比"信任上一次已经清干净"可靠 + */ +static void pg_init(void) { + ui_w_setview_attach(&s_sl, UI_W_SETVIEW_PARAM); +} + +/* + * 函数功能:数据行数(框架 count_cb;查看视图没有保存行) + * 入口参数:无 + * 返回值:行数 uint16_t + * 限定条件:框架绘制/翻页链内 + */ +static uint16_t pg_count(void) { + return ui_w_setlist_count(&s_sl); +} + +/* + * 函数功能:逐行取数(框架 refresh;内部先按发布序号同步参数帧) + * 入口参数:a_row / a_name / a_value - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内 + */ +static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) { + ui_w_setview_refresh(&s_sl, a_row, a_name, a_value); +} + +/* + * 函数功能:自绘层(越界行反显;框架内置区块画完后调用) + * 入口参数:a_layer / a_clip - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内 + */ +static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) { + ui_w_setlist_draw(&s_sl, a_layer, a_clip); +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=view_param 绑定引用) + * edit_capable=0 + 无 key_cb + key_intercept=0:查看页不接受任何写交互, + * ↑↓ 移选与 ← 关页全走框架内置(§3.2.7 表里"权限不足 ENTER 一律忽略"在 + * 查看页等价于"根本没有 ENTER 这条路") + */ +const content_page_def_t ui_page_view_param_desc = { + .id = "view_param", + .title = "参数", + .selectable = 1u, + .edit_capable = 0u, + .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, + .init_cb = pg_init, + .count_cb = pg_count, + .refresh = pg_refresh, + .draw_cb = pg_draw, +}; diff --git a/Drivers/BSP/UI/ui_page_view_param.h b/Drivers/BSP/UI/ui_page_view_param.h new file mode 100644 index 00000000..63899e2e --- /dev/null +++ b/Drivers/BSP/UI/ui_page_view_param.h @@ -0,0 +1,27 @@ +/* + * 模块名称:UI Page view_param (参数查看内容页) + * 模块功能:参数查看叶子节点的内容页声明——向框架 ui_content_page 提供页面描述符 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15 批 3;规格 = + * docs/UI设计/页面扩充详细方案.md §3.2.6 查看列) + */ +#ifndef UI_PAGE_VIEW_PARAM_H +#define UI_PAGE_VIEW_PARAM_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ui_content_page.h" + +/* 页面描述符(定义在 ui_page_view_param.c;生成物按菜单树 #page=view_param 引用本符号) */ +extern const content_page_def_t ui_page_view_param_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_VIEW_PARAM_H */ diff --git a/Drivers/BSP/UI/ui_page_view_plate.c b/Drivers/BSP/UI/ui_page_view_plate.c new file mode 100644 index 00000000..425994d7 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_view_plate.c @@ -0,0 +1,94 @@ +/* + * 模块名称:UI Page view_plate (软压板查看内容页) + * 模块功能:参数定值 → 查看 → 软压板 的内容页——10 行二态表(9 个段投退 + 保护总投:投入/退出),每页 8 行、2 页; + * 数据 = 参数快照(Q10)按行表小数位换算,越界项由 bad 回调反显; + * 本文件零 App include、零写路径(查看页没有"保存"这件事) + * 为什么这么薄:行表与取数在 ui_w_setview(四个查看页共用一份视图模型), + * 渲染与反显在 ui_w_setlist(与整定页同一组件)——本文件只把视图编号 + * 和标准骨架几何令牌接进框架的跳板,改一次口径四处一致 + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15 批 3) + */ +/* 头文件包含区 */ +#include "ui_page_view_plate.h" + +#include "ui_tokens.h" +#include "ui_w_setlist.h" +#include "ui_w_setview.h" + +/* 静态变量定义区 */ + +/* 本页实例(组件状态:编辑态等;查看页只用行游标与反显,实例仍须独占一份, + * 与别的页面共用会互相踩状态——同 ui_w_records 的实例化约定) */ +static ui_w_setlist_t s_sl; + +/* 函数定义区 */ + +/* + * 函数功能:开页绑定视图(行表 + 取数/反显回调一次配齐;接框架 init_cb) + * 入口参数:无 + * 返回值:无 + * 限定条件:框架开页链内调用(uiTask) + * 函数说明:每次开页都重新 attach——组件实例内残留的编辑态/游标会跨页带过来, + * 开页复位比"信任上一次已经清干净"可靠 + */ +static void pg_init(void) { + ui_w_setview_attach(&s_sl, UI_W_SETVIEW_PLATE); +} + +/* + * 函数功能:数据行数(框架 count_cb;查看视图没有保存行) + * 入口参数:无 + * 返回值:行数 uint16_t + * 限定条件:框架绘制/翻页链内 + */ +static uint16_t pg_count(void) { + return ui_w_setlist_count(&s_sl); +} + +/* + * 函数功能:逐行取数(框架 refresh;内部先按发布序号同步参数帧) + * 入口参数:a_row / a_name / a_value - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内 + */ +static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) { + ui_w_setview_refresh(&s_sl, a_row, a_name, a_value); +} + +/* + * 函数功能:自绘层(越界行反显;框架内置区块画完后调用) + * 入口参数:a_layer / a_clip - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内 + */ +static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) { + ui_w_setlist_draw(&s_sl, a_layer, a_clip); +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=view_plate 绑定引用) + * edit_capable=0 + 无 key_cb + key_intercept=0:查看页不接受任何写交互, + * ↑↓ 移选与 ← 关页全走框架内置(§3.2.7 表里"权限不足 ENTER 一律忽略"在 + * 查看页等价于"根本没有 ENTER 这条路") + */ +const content_page_def_t ui_page_view_plate_desc = { + .id = "view_plate", + .title = "软压板", + .selectable = 1u, + .edit_capable = 0u, + .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, + .init_cb = pg_init, + .count_cb = pg_count, + .refresh = pg_refresh, + .draw_cb = pg_draw, +}; diff --git a/Drivers/BSP/UI/ui_page_view_plate.h b/Drivers/BSP/UI/ui_page_view_plate.h new file mode 100644 index 00000000..3e8d22ca --- /dev/null +++ b/Drivers/BSP/UI/ui_page_view_plate.h @@ -0,0 +1,27 @@ +/* + * 模块名称:UI Page view_plate (软压板查看内容页) + * 模块功能:软压板查看叶子节点的内容页声明——向框架 ui_content_page 提供页面描述符 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15 批 3;规格 = + * docs/UI设计/页面扩充详细方案.md §3.2.6 查看列) + */ +#ifndef UI_PAGE_VIEW_PLATE_H +#define UI_PAGE_VIEW_PLATE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ui_content_page.h" + +/* 页面描述符(定义在 ui_page_view_plate.c;生成物按菜单树 #page=view_plate 引用本符号) */ +extern const content_page_def_t ui_page_view_plate_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_VIEW_PLATE_H */ diff --git a/Drivers/BSP/UI/ui_page_view_set.c b/Drivers/BSP/UI/ui_page_view_set.c new file mode 100644 index 00000000..055a4605 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_view_set.c @@ -0,0 +1,94 @@ +/* + * 模块名称:UI Page view_set (定值查看内容页) + * 模块功能:参数定值 → 查看 → 定值 的内容页——18 行数值表(三段过流/过负荷/过压/低压/零序/过频/低频 定值与延时),每页 8 行、3 页; + * 数据 = 参数快照(Q10)按行表小数位换算,越界项由 bad 回调反显; + * 本文件零 App include、零写路径(查看页没有"保存"这件事) + * 为什么这么薄:行表与取数在 ui_w_setview(四个查看页共用一份视图模型), + * 渲染与反显在 ui_w_setlist(与整定页同一组件)——本文件只把视图编号 + * 和标准骨架几何令牌接进框架的跳板,改一次口径四处一致 + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15 批 3) + */ +/* 头文件包含区 */ +#include "ui_page_view_set.h" + +#include "ui_tokens.h" +#include "ui_w_setlist.h" +#include "ui_w_setview.h" + +/* 静态变量定义区 */ + +/* 本页实例(组件状态:编辑态等;查看页只用行游标与反显,实例仍须独占一份, + * 与别的页面共用会互相踩状态——同 ui_w_records 的实例化约定) */ +static ui_w_setlist_t s_sl; + +/* 函数定义区 */ + +/* + * 函数功能:开页绑定视图(行表 + 取数/反显回调一次配齐;接框架 init_cb) + * 入口参数:无 + * 返回值:无 + * 限定条件:框架开页链内调用(uiTask) + * 函数说明:每次开页都重新 attach——组件实例内残留的编辑态/游标会跨页带过来, + * 开页复位比"信任上一次已经清干净"可靠 + */ +static void pg_init(void) { + ui_w_setview_attach(&s_sl, UI_W_SETVIEW_SET); +} + +/* + * 函数功能:数据行数(框架 count_cb;查看视图没有保存行) + * 入口参数:无 + * 返回值:行数 uint16_t + * 限定条件:框架绘制/翻页链内 + */ +static uint16_t pg_count(void) { + return ui_w_setlist_count(&s_sl); +} + +/* + * 函数功能:逐行取数(框架 refresh;内部先按发布序号同步参数帧) + * 入口参数:a_row / a_name / a_value - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内 + */ +static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) { + ui_w_setview_refresh(&s_sl, a_row, a_name, a_value); +} + +/* + * 函数功能:自绘层(越界行反显;框架内置区块画完后调用) + * 入口参数:a_layer / a_clip - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内 + */ +static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) { + ui_w_setlist_draw(&s_sl, a_layer, a_clip); +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=view_set 绑定引用) + * edit_capable=0 + 无 key_cb + key_intercept=0:查看页不接受任何写交互, + * ↑↓ 移选与 ← 关页全走框架内置(§3.2.7 表里"权限不足 ENTER 一律忽略"在 + * 查看页等价于"根本没有 ENTER 这条路") + */ +const content_page_def_t ui_page_view_set_desc = { + .id = "view_set", + .title = "定值", + .selectable = 1u, + .edit_capable = 0u, + .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, + .init_cb = pg_init, + .count_cb = pg_count, + .refresh = pg_refresh, + .draw_cb = pg_draw, +}; diff --git a/Drivers/BSP/UI/ui_page_view_set.h b/Drivers/BSP/UI/ui_page_view_set.h new file mode 100644 index 00000000..833addb4 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_view_set.h @@ -0,0 +1,27 @@ +/* + * 模块名称:UI Page view_set (定值查看内容页) + * 模块功能:定值查看叶子节点的内容页声明——向框架 ui_content_page 提供页面描述符 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15 批 3;规格 = + * docs/UI设计/页面扩充详细方案.md §3.2.6 查看列) + */ +#ifndef UI_PAGE_VIEW_SET_H +#define UI_PAGE_VIEW_SET_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ui_content_page.h" + +/* 页面描述符(定义在 ui_page_view_set.c;生成物按菜单树 #page=view_set 引用本符号) */ +extern const content_page_def_t ui_page_view_set_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_VIEW_SET_H */ diff --git a/Drivers/BSP/UI/ui_page_view_word.c b/Drivers/BSP/UI/ui_page_view_word.c new file mode 100644 index 00000000..36a18934 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_view_word.c @@ -0,0 +1,94 @@ +/* + * 模块名称:UI Page view_word (控制字查看内容页) + * 模块功能:参数定值 → 查看 → 控制字 的内容页——4 行二态表(*_TRIP:告警/跳闸),每页 8 行、1 页; + * 数据 = 参数快照(Q10)按行表小数位换算,越界项由 bad 回调反显; + * 本文件零 App include、零写路径(查看页没有"保存"这件事) + * 为什么这么薄:行表与取数在 ui_w_setview(四个查看页共用一份视图模型), + * 渲染与反显在 ui_w_setlist(与整定页同一组件)——本文件只把视图编号 + * 和标准骨架几何令牌接进框架的跳板,改一次口径四处一致 + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15 批 3) + */ +/* 头文件包含区 */ +#include "ui_page_view_word.h" + +#include "ui_tokens.h" +#include "ui_w_setlist.h" +#include "ui_w_setview.h" + +/* 静态变量定义区 */ + +/* 本页实例(组件状态:编辑态等;查看页只用行游标与反显,实例仍须独占一份, + * 与别的页面共用会互相踩状态——同 ui_w_records 的实例化约定) */ +static ui_w_setlist_t s_sl; + +/* 函数定义区 */ + +/* + * 函数功能:开页绑定视图(行表 + 取数/反显回调一次配齐;接框架 init_cb) + * 入口参数:无 + * 返回值:无 + * 限定条件:框架开页链内调用(uiTask) + * 函数说明:每次开页都重新 attach——组件实例内残留的编辑态/游标会跨页带过来, + * 开页复位比"信任上一次已经清干净"可靠 + */ +static void pg_init(void) { + ui_w_setview_attach(&s_sl, UI_W_SETVIEW_WORD); +} + +/* + * 函数功能:数据行数(框架 count_cb;查看视图没有保存行) + * 入口参数:无 + * 返回值:行数 uint16_t + * 限定条件:框架绘制/翻页链内 + */ +static uint16_t pg_count(void) { + return ui_w_setlist_count(&s_sl); +} + +/* + * 函数功能:逐行取数(框架 refresh;内部先按发布序号同步参数帧) + * 入口参数:a_row / a_name / a_value - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内 + */ +static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) { + ui_w_setview_refresh(&s_sl, a_row, a_name, a_value); +} + +/* + * 函数功能:自绘层(越界行反显;框架内置区块画完后调用) + * 入口参数:a_layer / a_clip - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内 + */ +static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) { + ui_w_setlist_draw(&s_sl, a_layer, a_clip); +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=view_word 绑定引用) + * edit_capable=0 + 无 key_cb + key_intercept=0:查看页不接受任何写交互, + * ↑↓ 移选与 ← 关页全走框架内置(§3.2.7 表里"权限不足 ENTER 一律忽略"在 + * 查看页等价于"根本没有 ENTER 这条路") + */ +const content_page_def_t ui_page_view_word_desc = { + .id = "view_word", + .title = "控制字", + .selectable = 1u, + .edit_capable = 0u, + .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, + .init_cb = pg_init, + .count_cb = pg_count, + .refresh = pg_refresh, + .draw_cb = pg_draw, +}; diff --git a/Drivers/BSP/UI/ui_page_view_word.h b/Drivers/BSP/UI/ui_page_view_word.h new file mode 100644 index 00000000..19dbe137 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_view_word.h @@ -0,0 +1,27 @@ +/* + * 模块名称:UI Page view_word (控制字查看内容页) + * 模块功能:控制字查看叶子节点的内容页声明——向框架 ui_content_page 提供页面描述符 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15 批 3;规格 = + * docs/UI设计/页面扩充详细方案.md §3.2.6 查看列) + */ +#ifndef UI_PAGE_VIEW_WORD_H +#define UI_PAGE_VIEW_WORD_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ui_content_page.h" + +/* 页面描述符(定义在 ui_page_view_word.c;生成物按菜单树 #page=view_word 引用本符号) */ +extern const content_page_def_t ui_page_view_word_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_VIEW_WORD_H */ diff --git a/Drivers/BSP/UI/ui_page_yx.c b/Drivers/BSP/UI/ui_page_yx.c new file mode 100644 index 00000000..40085799 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_yx.c @@ -0,0 +1,203 @@ +/* + * 模块名称:UI Page YX (遥信量内容页) + * 模块功能:33 点遥信的固定名表页(方案 A)——每行一个遥信点,值列按快照的三个位图 + * (有效 / 动作 / 单双点)映射成"合位-分位"、"动作-复归"或"--";动态列表页, + * 数据只读 Drivers/BSP/UI/ui_data.h 的实时快照,零 App include + * 为什么是 33 行固定表(V0.1 只有 6 点有硬件来源,另外 27 行都是 "--"): + * ① 与 104 点表一一对应,现场拿这张屏就能与上位机逐点对;② 开入落地后本页 + * 零改动自动变真;③ 只显有效点的做法看不出"缺哪些点",而且点数一变黄金参考 + * 就得重做(把判据建立在会漂的东西上等于没有判据)——取舍记录在 + * docs/UI设计/页面扩充详细方案.md §3.1.3 + * 名表为什么在页面侧而不在快照里:显示文案属表现层(同文档 §8);点数与单/双点性质 + * 反过来**必须**由写侧下发(那是点表事实),两者边界见 ui_data.h 的 yx_dp 注释 + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15 批 4) + */ +/* 头文件包含区 */ + +#include "ui_page_yx.h" + +#include "ui_data.h" +#include "ui_tokens.h" + +/* 静态变量定义区 */ + +/* + * 33 点固定名表(下标 = 遥信位图位号 = 104 点表 IOA - 1,IOA 1~33) + * + * 两处偏离文档的命名(文档 §3.1.3 用的是区间简写,落到逐行名字时必须选一个,理由写在 + * 这里而不是留给人猜): + * · 文档把 idx0~3 写成「YK1~4 位置」:遥信页上出现"YK"前缀会被读成遥控对象(那是 + * 另一页的事),屏上只写「位置1~4」,与遥控的对应关系留在本注释; + * · 文档把 idx30~32 写成「闭锁1~3」:idx30 在写侧有明确语义来源(保护闭锁,定值校验 + * 和自检失败),用编号反而把信息丢了;idx31/32 点表未定义功能名,如实写点号占位, + * **不编造功能名**。同理 idx4~13(YX5~14)也用点号占位。 + * + * 长度与 UI_DATA_YX_N 的一致性由下面的编译期数组守卫钉住(本仓惯用宽度法,ARMCC C99 + * 没有 _Static_assert):名表少写/多写一行直接编译失败,不会到了屏上才少一行 + */ +static const char *const s_yx_name[UI_DATA_YX_N] = { + "位置1", "位置2", "位置3", "位置4", /* 0~3 双点;V0.1 无开入硬件 */ + "YX5", "YX6", "YX7", "YX8", "YX9", /* 4~8 点表未定义功能名 */ + "YX10", "YX11", "YX12", "YX13", "YX14", /* 9~13 (同上,不编名) */ + "故障总", "过流Ⅰ段", "过流Ⅱ段", "过流Ⅲ段", /* 14~17 YX15~18,有硬件来源 */ + "零序Ⅰ段", "零序Ⅱ段", /* 18,19 V0.1 无该判据上送 */ + "接地故障", "重载", "过载", /* 20~22 过载(YX23)有来源 */ + "外部遥信1", "外部遥信2", "外部遥信3", "外部遥信4", /* 23~26 开入未实现 */ + "外部遥信5", "外部遥信6", "外部遥信7", /* 27~29 */ + "保护闭锁", "YX32", "YX33", /* 30~32 闭锁(YX31)有来源 */ +}; + +/* 名表行数与快照点数一致(不一致 = 编译期错误,见上注) */ +typedef char yx_name_count_check[(sizeof(s_yx_name) / sizeof(s_yx_name[0]) == UI_DATA_YX_N) ? 1 : -1]; + +/* 值列词表(全在既有 183 字子集内,font-charset 实测零新增) */ +#define YX_INVALID "--" +#define YX_DP_ON "合位" +#define YX_DP_OFF "分位" +#define YX_SP_ON "动作" +#define YX_SP_OFF "复归" + +/* 帧缓存:只在发布序号变化时整帧拷一次(一帧约 200 B,逐行只读标量) */ +static ui_data_rt_t s_rt; +static uint32_t s_seq = 0xFFFFFFFFu; + +/* 函数定义区 */ + +/* + * 函数功能:定长缓冲内拷字面量(代码规范 7.5 禁 strcpy;长度取调用点 sizeof) + * 入口参数:a_dst - 目标缓冲(框架行缓冲 UI_BUF_STRLEN) char * + * a_src - 源字面量(含 NUL) const char * + * len - 目标容量 uint32_t + * 返回值:无 + * 限定条件:源串长度必须小于 len(编译期由字面量 sizeof 保证,不做运行期截断—— + * 截断只会把"动作"打成"动",比读错更难查) + */ +static void yx_put(char *a_dst, const char *a_src, uint32_t len) { + uint32_t i = 0u; + + if (len == 0u) { + return; + } + while (((i + 1u) < len) && (a_src[i] != '\0')) { + a_dst[i] = a_src[i]; + i++; + } + a_dst[i] = '\0'; +} + +/* + * 长度参数**必须取目标缓冲容量**,不能取 sizeof(源):本表的源是 `const char *const` + * 表里的**指针**,`sizeof(lit)` 得到的是指针宽度(4/8),于是 "故障总" 会被拷成第一个 + * 汉字 "故"——ASCII 名("YX9" 恰好 4 字节)看着完全正常,CJK 名集体缺尾巴。这个错 + * 编译器、cppcheck、编码钩子都不会响,是像素文本断言把它揪出来的(第 2 页第 7 行 + * "过流Ⅰ段" 匹配不到,值列 "复归" 却在原位 = 名字被截、值没错位)。 + * 批 1 那几处 *_PUT 宏传的是**字面量数组**(sizeof 就是数组长度),照抄到这里就中招—— + * 同名宏、不同参数类别,正是"看着像同构其实不同"的那类坑。 + */ +#define YX_PUT(dst, lit) yx_put((dst), (lit), (uint32_t)UI_BUF_STRLEN) + +/* + * 函数功能:开页复位帧缓存(接框架 init_cb) + * 入口参数:无 + * 返回值:无 + * 限定条件:框架开页链内(uiTask) + * 函数说明:序号必须复位——否则重开本页会沿用上次开页的帧号,首帧之前的 "--" 状态 + * 可能被永久留在屏上(批 1 已踩过一次,同一条纪律再用一遍) + */ +static void pg_init(void) { + s_seq = 0xFFFFFFFFu; + (void)memset(&s_rt, 0, sizeof(s_rt)); +} + +/* + * 函数功能:按发布序号刷新帧缓存 + * 入口参数:无 + * 返回值:无 + * 限定条件:uiTask 绘制链内(refresh 前) + * 函数说明:ui_data_rt() 的通道是 0=成功 / -1=尚无首帧(不是 bool 成败);失败时把 + * 序号打回不可能的值,下一轮一定重试,不会停在半新不旧的帧上 + */ +static void pg_sync(void) { + uint32_t seq = ui_data_rt_seq(); + + if ((seq == s_seq) && (s_rt.valid != 0u)) { + return; + } + if (ui_data_rt(&s_rt) != 0) { + s_seq = 0xFFFFFFFFu; + (void)memset(&s_rt, 0, sizeof(s_rt)); + return; + } + s_seq = seq; +} + +/* + * 函数功能:总行数(固定 33 点,不随有效性增减) + * 入口参数:无 + * 返回值:行数 uint16_t + * 限定条件:框架绘制/翻页链内 + * 函数说明:故意返回常量——"只显有效点"会让现场无法判断缺了哪些点(方案 B 的缺点), + * 且行数变化会让黄金参考随运行态漂移 + */ +static uint16_t pg_count(void) { + return (uint16_t)UI_DATA_YX_N; +} + +/* + * 函数功能:逐行取数(名 = 固定名表,值 = 三个位图映射出的词) + * 入口参数:a_row - 行下标(0..32) uint16_t + * a_name - 名输出缓冲(UI_BUF_STRLEN) char * + * a_value - 值输出缓冲 char * + * 返回值:无 + * 限定条件:框架绘制链内(双缓冲各调一次,本函数无副作用) + * 函数说明:判定顺序是"先无效、再单双点、最后位值"——无效位上的位值是真随机垃圾 + * (位图没更新过就是 0,更新了也不代表该点有效),所以 IV 必须排在最前; + * 双点段 V0.1 恒无效,"合位/分位"这条支路当前不可达但必须写:开入一接上 + * 就走通,而按协议写死的 4 个点本来就是双点(可达性由点表决定,不由本页决定) + */ +static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) { + uint64_t bit; + + a_value[0] = '\0'; + if (a_row >= (uint16_t)UI_DATA_YX_N) { + a_name[0] = '\0'; + return; /* 防御:行号越界不越读名表(count 恒 33,走不到) */ + } + pg_sync(); + YX_PUT(a_name, s_yx_name[a_row]); + bit = (uint64_t)1u << (uint8_t)a_row; + if ((s_rt.valid == 0u) || ((s_rt.yx_valid & bit) == 0u)) { + YX_PUT(a_value, YX_INVALID); + return; + } + if ((s_rt.yx_dp & bit) != 0u) { + YX_PUT(a_value, ((s_rt.yx_bits & bit) != 0u) ? YX_DP_ON : YX_DP_OFF); + return; + } + YX_PUT(a_value, ((s_rt.yx_bits & bit) != 0u) ? YX_SP_ON : YX_SP_OFF); +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=yx 绑定引用): + * 固定 33 行的动态页(rows=NULL + count_cb/refresh),8 行/页 = 5 页;band_h 必须 >0 + * (selectable 页选中行是黑字,无白带即不可见,同记录页约定) + */ +const content_page_def_t ui_page_yx_desc = { + .id = "yx", + .title = "遥信量", + .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, + .init_cb = pg_init, + .count_cb = pg_count, + .refresh = pg_refresh, +}; diff --git a/Drivers/BSP/UI/ui_page_yx.h b/Drivers/BSP/UI/ui_page_yx.h new file mode 100644 index 00000000..cbf464eb --- /dev/null +++ b/Drivers/BSP/UI/ui_page_yx.h @@ -0,0 +1,27 @@ +/* + * 模块名称:UI Page YX (遥信量内容页) + * 模块功能:实时数据 → 遥信量 叶子的内容页声明——向框架 ui_content_page 提供页面描述符 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15 批 4;规格 = + * docs/UI设计/页面扩充详细方案.md §3.1.3 方案 A) + */ +#ifndef UI_PAGE_YX_H +#define UI_PAGE_YX_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ui_content_page.h" + +/* 页面描述符(定义在 ui_page_yx.c;生成物按菜单树 #page=yx 引用本符号) */ +extern const content_page_def_t ui_page_yx_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_YX_H */ diff --git a/Drivers/BSP/UI/ui_w_setlist.c b/Drivers/BSP/UI/ui_w_setlist.c index 27c67d17..eae8b7aa 100644 --- a/Drivers/BSP/UI/ui_w_setlist.c +++ b/Drivers/BSP/UI/ui_w_setlist.c @@ -146,7 +146,13 @@ void ui_w_setlist_refresh(ui_w_setlist_t *a_inst, uint16_t a_row, ui_w_editor_format(&a_inst->ed, a_value, UI_BUF_STRLEN); /* 编辑实时串 */ } else { - ui_w_editor_fmt_scaled(a_inst->get(r->id), r->decimals, a_value, UI_BUF_STRLEN); + /* 单位后缀(r->unit NULL = 不挂):先格式化到临时串再合成,一次界内写回, + * 不用 strcat 类函数(代码规范 7.5;且长度不足时会静默截半,屏幕上更难查) */ + char fmt_buf[UI_BUF_STRLEN]; + + ui_w_editor_fmt_scaled(a_inst->get(r->id), r->decimals, fmt_buf, UI_BUF_STRLEN); + (void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s%s", fmt_buf, + (r->unit != NULL) ? r->unit : ""); } return; } diff --git a/Drivers/BSP/UI/ui_w_setlist.h b/Drivers/BSP/UI/ui_w_setlist.h index 858183b1..356f66eb 100644 --- a/Drivers/BSP/UI/ui_w_setlist.h +++ b/Drivers/BSP/UI/ui_w_setlist.h @@ -63,6 +63,10 @@ typedef struct { int32_t min; /* 数值行下限(含;>=0;二态行忽略) */ int32_t max; /* 数值行上限(含;二态行忽略) */ const char *const *opts; /* 二态行:NUL 结尾 Flash 字符串串表(数值行 NULL) */ + const char *unit; /* 数值行单位后缀(Flash,NULL = 不挂;二态行忽略) + * 单位是"屏上的话",跟视图表走、不进 App 快照;只挂在 + * 非编辑态的数值串尾——编辑中是逐位输入串,挂单位会让 + * ui_w_editor 的位光标算错列宽(其契约:串内只有数字与点) */ } ui_w_setlist_row_t; /* 数据回调组(均 uiTask 上下文、页面绘制/按键链内调用;实现由批 2 页面跳板提供, diff --git a/Drivers/BSP/UI/ui_w_setview.c b/Drivers/BSP/UI/ui_w_setview.c new file mode 100644 index 00000000..fe391e9e --- /dev/null +++ b/Drivers/BSP/UI/ui_w_setview.c @@ -0,0 +1,381 @@ +/* + * 模块名称:UI 定值查看视图模型(ui_w_setview) + * 模块功能:参数 / 定值 / 控制字 / 软压板四张视图表与它们的取数、换算、越界反显—— + * 供四个查看页薄跳板共用;声明与分层理由见 ui_w_setview.h + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15 批 3) + */ +/* 头文件包含区 */ +#include + +#include "ui_w_setview.h" + +#include "ui_data.h" +#include "ui_tokens.h" /* UI_BUF_STRLEN:框架行缓冲长度(无帧占位串要写进同一缓冲) */ + +/* 静态变量定义区 */ + +/* 二态行的串表(下标 = 记录值;0/1 的语义来自 App/algo/prot_set.h 的 meta 注释: + * *_TRIP 0 = 告警 / 1 = 跳闸,*_EN 0 = 退出 / 1 = 投入;串表以 NULL 结尾, + * 组件按串表长度决定 ENTER 循环切换的范围) */ +const char *const s_opts_trip[3] = { "告警", "跳闸", NULL }; +const char *const s_opts_plate[3] = { "退出", "投入", NULL }; + +/* + * 四张视图表(Flash const)。行表的三项口径全部有真源,不在本文件里自由发挥: + * · id = App/algo/prot_set.h 的 prot_set_id_t 枚举序(BSP 不 include App 头, + * 故写裸数字并把枚举名注在行尾;对不上由主机单测与像素值两处暴露) + * · min/max = App/algo/prot_set.c 的 g_prot_set_meta 上下限(Q10)换算到本行 + * decimals 后的整数——**照抄换算结果,不自己拟定**;整定页复用同一批表, + * 界内外一处一份,不会出现"查看看着合法、整定说越界"的两套边界 + * · 显示名/小数位/单位 = docs/UI设计/页面扩充详细方案.md §3.2.2~§3.2.5 四张表。 + * 其中四个返回系数按 **2 位小数** 显示:这不是我偏离文档,而是文档原口径(3 位) + * 本身错了——它把 Q10 当成了十进制存储。Q10 存不下 0.95:0.95×1024 截成 972,真存 + * 的是 0.94921875;按 3 位显示等于打出一位根本不存在的"9",整定单上写 0.95 的人看 + * 到 0.949 会怀疑装置、甚至反复重输(重输也还是 0.949)。2 位才是这份存储在自己 + * 可分辨精度上的正确读数。§3.2.2 已同步修正(表里 3 位改 2 位并加 ⚠ 指向本注释) + * 单位挂在值串尾(A/V/ms/Hz 全 ASCII,不占中文字库):值列只有数字会让"60000"既是 + * ms 又是 A,读者只能靠猜;行表带 unit 字段是本次给组件加的(NULL = 不挂) + */ + +/* 参数视图(8 行 = 1 页) */ +static const ui_w_setlist_row_t s_rows_param[8] = { + { "PT一次", 27u, UI_W_SETLIST_KIND_NUM, 0u, 100, 500000, NULL, "V" }, /* PS_PT_PRI */ + { "PT二次", 28u, UI_W_SETLIST_KIND_NUM, 0u, 1, 200, NULL, "V" }, /* PS_PT_SEC */ + { "CT一次", 29u, UI_W_SETLIST_KIND_NUM, 0u, 1, 10000, NULL, "A" }, /* PS_CT_PRI */ + { "CT二次", 30u, UI_W_SETLIST_KIND_NUM, 0u, 1, 10, NULL, "A" }, /* PS_CT_SEC */ + { "过量返回", 25u, UI_W_SETLIST_KIND_NUM, 2u, 50, 100, NULL, NULL }, /* PS_RET_OVER */ + { "欠量返回", 26u, UI_W_SETLIST_KIND_NUM, 2u, 100, 150, NULL, NULL }, /* PS_RET_UNDER */ + { "过频返回", 38u, UI_W_SETLIST_KIND_NUM, 2u, 90, 100, NULL, NULL }, /* PS_RET_OF */ + { "低频返回", 39u, UI_W_SETLIST_KIND_NUM, 2u, 100, 110, NULL, NULL }, /* PS_RET_UF */ +}; + +/* 定值视图(18 行 = 3 页) */ +static const ui_w_setlist_row_t s_rows_set[18] = { + { "Ⅰ段电流", 2u, UI_W_SETLIST_KIND_NUM, 2u, 0, 10000, NULL, "A" }, /* PS_OC1_CUR */ + { "Ⅰ段延时", 3u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_OC1_DLY */ + { "Ⅱ段电流", 6u, UI_W_SETLIST_KIND_NUM, 2u, 0, 10000, NULL, "A" }, /* PS_OC2_CUR */ + { "Ⅱ段延时", 7u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_OC2_DLY */ + { "Ⅲ段电流", 10u, UI_W_SETLIST_KIND_NUM, 2u, 0, 10000, NULL, "A" }, /* PS_OC3_CUR */ + { "Ⅲ段延时", 11u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_OC3_DLY */ + { "过负荷电流", 14u, UI_W_SETLIST_KIND_NUM, 2u, 0, 10000, NULL, "A" }, /* PS_OL_CUR */ + { "过负荷延时", 15u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_OL_DLY */ + { "过压定值", 17u, UI_W_SETLIST_KIND_NUM, 1u, 0, 2000, NULL, "V" }, /* PS_OV_VOL */ + { "过压延时", 18u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_OV_DLY */ + { "低压定值", 20u, UI_W_SETLIST_KIND_NUM, 1u, 0, 2000, NULL, "V" }, /* PS_UV_VOL */ + { "低压延时", 21u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_UV_DLY */ + { "零序定值", 23u, UI_W_SETLIST_KIND_NUM, 1u, 0, 2000, NULL, "V" }, /* PS_3U0_VOL */ + { "零序延时", 24u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_3U0_DLY */ + { "过频定值", 33u, UI_W_SETLIST_KIND_NUM, 2u, 5000, 5500, NULL, "Hz" }, /* PS_OF_HZ */ + { "过频延时", 34u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_OF_DLY */ + { "低频定值", 36u, UI_W_SETLIST_KIND_NUM, 2u, 4500, 5000, NULL, "Hz" }, /* PS_UF_HZ */ + { "低频延时", 37u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_UF_DLY */ +}; + +/* 控制字视图(4 行 = 1 页) */ +static const ui_w_setlist_row_t s_rows_word[4] = { + { "Ⅰ段跳闸", 1u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_trip, NULL }, /* PS_OC1_TRIP */ + { "Ⅱ段跳闸", 5u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_trip, NULL }, /* PS_OC2_TRIP */ + { "Ⅲ段跳闸", 9u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_trip, NULL }, /* PS_OC3_TRIP */ + { "过负荷跳闸", 13u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_trip, NULL }, /* PS_OL_TRIP */ +}; + +/* 软压板视图(10 行 = 2 页) */ +static const ui_w_setlist_row_t s_rows_plate[10] = { + { "Ⅰ段投入", 0u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_OC1_EN */ + { "Ⅱ段投入", 4u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_OC2_EN */ + { "Ⅲ段投入", 8u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_OC3_EN */ + { "过负荷投入", 12u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_OL_EN */ + { "过压投入", 16u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_OV_EN */ + { "低压投入", 19u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_UV_EN */ + { "零序投入", 22u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_3U0_EN */ + { "过频投入", 32u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_OF_EN */ + { "低频投入", 35u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_UF_EN */ + { "保护总投入", 31u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_GLOBAL_EN */ +}; + +/* + * 每个定值项的显示小数位(下标 = 枚举序;二态项为 0,不参与数值换算) + * 为什么要单独一张表而不是从行表里找:组件的 get(id) 回调只给 id 不给行号, + * 同一 id 只会出现在一张视图里(四张表恰好覆盖 0..39 各一次,由主机单测钉住), + * 所以这张表是无歧义的,且比遍历四张表便宜 + */ +static const uint8_t s_dec[UI_DATA_SET_N] = { + 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, + 2, 0, 0, 0, 2, 0, 0, 1, 0, 0, + 1, 0, 0, 1, 0, 2, 2, 0, 0, 0, + 0, 0, 0, 2, 0, 0, 2, 0, 2, 2, +}; + +/* 无参数帧时值列的占位串(与全仓"读不到就 --"同一口径) */ +#define SV_NO_DATA "--" + +/* 参数帧缓存(四个查看页共用一份:同屏只开一页,无并发读者) */ +static ui_data_par_t s_par; +static uint32_t s_par_seq = 0xFFFFFFFFu; /* 与任何合法发布序号都不同 */ +static uint8_t s_par_ok; + +/* 当前视图的行表(bad 回调只收到行号,需要它把行号换回 id 再查位图) */ +static const ui_w_setlist_row_t *s_cur_rows; +static uint16_t s_cur_n; /* 当前视图行数(bad 回调的边界) */ + +/* 行表长度(编译期算,别手写数字——手写数字与表长不一致是最难查的错) */ +#define SV_ROW_N(tbl) ((uint16_t)(sizeof(tbl) / sizeof((tbl)[0]))) + +/* 函数定义区 */ + +/* + * 函数功能:Q10 原值 → 指定小数位的定点显示值(四舍五入) + * 入口参数:a_q10 - Q10 原值(工程量 × 1024) int32_t + * a_dec - 小数位 0..3 uint8_t + * 返回值:工程量 × 10^a_dec int32_t + * 限定条件:|a_q10| 在 int32 范围内且换算后仍在 int32 内(行表生成时已按 + * g_prot_set_meta 的最大量程核过:最大项 PT 一次额定 500000 V,d=0) + * 函数说明:不用浮点也不用 64 位除法——MicroLIB 的 soft-float 与 __aeabi_ldiv + * 一进镜像就是几百字节,而模拟器(宿主 CRT)永远照不出这笔代价(本仓 + * 反复记录的同一条口径)。做法是拆整数段与小数段: + * v = ip×1024 + fp(fp∈[0,1023],负数用算术右移取 floor,fp 仍非负), + * 小数段按 10^d 缩放后四舍五入,满 10^d 就进位回整数段。 + * 误差上界半个最低显示位——与 prot_set 自己的 Q10 精度同源, + * 屏上显示 10.00A 而实际 10.0004A 不是本页造成的,是存储精度 + */ +static int32_t sv_q10_to_scaled(int32_t a_q10, uint8_t a_dec) { + static const int32_t p10[4] = { 1, 10, 100, 1000 }; + int32_t ip; + uint32_t fp; + int32_t r; + + ip = a_q10 >> 10; + fp = (uint32_t)a_q10 & 0x3FFu; + r = ip * p10[a_dec]; + if (a_dec != 0u) { + uint32_t fr = (fp * (uint32_t)p10[a_dec] + 512u) >> 10; /* 作用域收在这里 */ + if (fr >= (uint32_t)p10[a_dec]) { /* 小数段进位(如 0.9996 → 1.000) */ + r += p10[a_dec]; + fr -= (uint32_t)p10[a_dec]; + } + r += (int32_t)fr; + } + return r; +} + +/* + * 函数功能:读某定值项的显示值(组件 get 回调) + * 入口参数:a_id - 行表登记的 id(= 枚举序) uint8_t + * 返回值:工程量 × 10^decimals int32_t + * 限定条件:ui_w_setview_sync() 已先跑过且 ui_w_setview_ready() 为真 + * (页面在值列打 "--" 之前不会走到这里) + * 函数说明:id 越界(>39)返回 0 属防御性死支——行表是本文件内 const,越界 id + * 只可能来自代码改动错误,由主机单测在构建期前拦,不靠运行期兜 + */ +static int32_t sv_get(uint8_t a_id) { + if (a_id >= (uint8_t)UI_DATA_SET_N) { + return 0; + } + return sv_q10_to_scaled(s_par.q10[a_id], s_dec[a_id]); +} + +/* + * 函数功能:只读视图的写回调占位(组件契约要求非 NULL) + * 入口参数:a_id/a_val - 未使用 + * 返回值:无 + * 限定条件:查看视图不可编辑,组件不会调到这里 + * 函数说明:刻意空实现——查看页**没有任何**该写进 RAM 工作副本的语义; + * 真要在查看页改值,得先把整定批次的 commit/state 一起接上 + */ +static void sv_set_unused(uint8_t a_id, int32_t a_val) { + (void)a_id; + (void)a_val; +} + +/* + * 函数功能:某数据行是否越界(组件 bad 回调 → 反显高亮) + * 入口参数:a_row - 数据行下标(不含保存行) uint16_t + * 返回值:true = 反显;false = 正常 bool + * 限定条件:uiTask 绘制链内;s_cur_rows 已由 attach 设定 + * 函数说明:位图来自 prot_set_bad_mask()(上一次整表装载/提交后仍在量程外的项), + * 经 par 帧 bad_mask + bad_valid 下发。bad_valid=0 表示位图无意义 + * (App 侧只在位图非 0 时置 1),此时一律不反显——把"没数据"画成 + * "这行有问题"是假告警,比不画更糟 + */ +static bool sv_bad(uint16_t a_row) { + const ui_w_setlist_row_t *r; + uint64_t bit; + + if ((s_cur_rows == NULL) || (s_par_ok == 0u) || (s_par.bad_valid == 0u)) { + return false; + } + if (a_row >= s_cur_n) { + return false; /* 行号越界(组件不会给,防御:越界不反显也不越读) */ + } + r = &s_cur_rows[a_row]; + if (r->id >= (uint8_t)UI_DATA_SET_N) { + return false; + } + bit = (uint64_t)1u << r->id; + return (s_par.bad_mask & bit) != 0u; +} + +/* + * 函数功能:按发布序号取参数帧快照(接各查看页 refresh 前调用) + * 入口参数:无 + * 返回值:无 + * 限定条件:uiTask 上下文(页面绘制链内);只读快照层,不写任何东西 + * 函数说明:序号没变就一帧都不重拷(一帧约 184 B;四个查看页共用这份缓存——同屏 + * 只可能开着一页,彼此之间不存在并发读者)。读取失败把序号打回不可能的值, + * 下一轮一定重试,不会停在半新不旧的帧上 + */ +void ui_w_setview_sync(void) { + uint32_t seq = ui_data_par_seq(); + + if ((seq == s_par_seq) && (s_par_ok != 0u)) { + return; /* 序号未变:一帧都不重拷 */ + } + /* ui_data_par() 的通道是 0=成功 / -1=尚无首帧 / -2=参数无效(快照层的既有约定, + * 不是布尔成败)——把它当 bool 用会反义:模拟器与真机上都会永远"读不到", + * 于是整页值列恒 "--",而页面看起来完全正常(本函数第一版就踩了这一条, + * 是看像素图才发现满屏横杠,静态检查看不出来) */ + if (ui_data_par(&s_par) != 0) { + s_par_ok = 0u; + s_par_seq = 0xFFFFFFFFu; /* 失败态不认序号,下一轮一定重试 */ + (void)memset(&s_par, 0, sizeof(s_par)); + return; + } + s_par_seq = seq; + s_par_ok = (s_par.valid != 0u) ? 1u : 0u; +} + +/* + * 函数功能:逐行取数(页面 refresh 的直直通:先同步参数帧,再走组件) + * 入口参数:a_inst - 实例 ui_w_setlist_t *;a_row - 行下标 uint16_t + * a_name/a_value - 框架行缓冲(UI_BUF_STRLEN) char * + * 返回值:无 + * 限定条件:框架绘制链内(uiTask) + * 函数说明:**参数帧未就位时只把值列打成 "--",名字照打**——空值会被读成"这项是 0", + * 整页空白会让人以为菜单坏了;打出"名字 + --"才说得出真正的事实 + * ("这张表在等参数帧")。帧就位后走组件:数值行按小数位 + 单位, + * 二态行打串表词 + */ +void ui_w_setview_refresh(ui_w_setlist_t *a_inst, uint16_t a_row, + char *a_name, char *a_value) { + ui_w_setview_sync(); + if (ui_w_setview_ready() != 0u) { + ui_w_setlist_refresh(a_inst, a_row, a_name, a_value); + return; + } + a_name[0] = '\0'; + a_value[0] = '\0'; + if ((a_inst != NULL) && (a_inst->rows != NULL) && (a_row < a_inst->count)) { + (void)lv_snprintf(a_name, UI_BUF_STRLEN, "%s", a_inst->rows[a_row].name); + } + (void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", SV_NO_DATA); +} + +/* + * 函数功能:参数帧是否已就位 + * 入口参数:无 + * 返回值:1 = 有有效帧可显示;0 = 尚无首帧或读取失败 uint8_t + * 限定条件:uiTask 上下文;纯查询,无副作用 + * 函数说明:返回 uint8_t 而不是 bool 是**本层约定**(bsp-err 的 err-type:BSP 公开 API + * 不得返回 bool——bool 把"为什么失败"整条丢掉,而这里虽然确实不需要原因, + * 也不能给整层开一个按名字豁免的例外;先例见 ui_w_setlist_key 的键分发标志)。 + * 调用方拿它决定值列打换算值还是打 "--",没有错误要上报 + */ +uint8_t ui_w_setview_ready(void) { + return s_par_ok; +} + +/* + * 函数功能:取某视图的行表与行数(结构自测用;页面不需要,attach 时才自己取) + * 入口参数:a_view - 视图编号 ui_w_setview_t;a_n - 行数输出(不可为 NULL) uint16_t * + * 返回值:行表首地址;视图号非法返回 NULL const ui_w_setlist_row_t * + * 限定条件:只读;行表是 const Flash 表 + * 函数说明:存在的理由是让主机单测能把四张表当成数据来核(id 是否恰好覆盖 0..39 + * 各一次、二态行有没有串表、小数位与 dec 查表是否一致)。这些约束在 + * 编译期看不出来,页面上也只有当前可见的 8 行会被像素断言看到—— + * 第 2、3 页的行错了对齐,屏幕上要翻两页才看得见,而人不会去翻 + */ +const ui_w_setlist_row_t *ui_w_setview_table(ui_w_setview_t a_view, uint16_t *a_n) { + if (a_n == NULL) { + return NULL; + } + switch (a_view) { + case UI_W_SETVIEW_PARAM: + *a_n = SV_ROW_N(s_rows_param); + return s_rows_param; + case UI_W_SETVIEW_SET: + *a_n = SV_ROW_N(s_rows_set); + return s_rows_set; + case UI_W_SETVIEW_WORD: + *a_n = SV_ROW_N(s_rows_word); + return s_rows_word; + case UI_W_SETVIEW_PLATE: + *a_n = SV_ROW_N(s_rows_plate); + return s_rows_plate; + default: + *a_n = 0u; + return NULL; + } +} + +/* + * 函数功能:取 id → 显示小数位的查表(结构自测用) + * 入口参数:a_n - 表长输出(= UI_DATA_SET_N) uint16_t * + * 返回值:表首地址 const uint8_t * + * 限定条件:只读 + */ +const uint8_t *ui_w_setview_dec_table(uint16_t *a_n) { + if (a_n != NULL) { + *a_n = (uint16_t)UI_DATA_SET_N; + } + return s_dec; +} + +/* + * 函数功能:把某个查看视图绑到定值表实例(行表 + 取数 + 反显一次配齐) + * 入口参数:a_inst - 页面持有的实例(可为 NULL,NULL 直接返回) ui_w_setlist_t * + * a_view - 视图编号 ui_w_setview_t + * 返回值:无 + * 限定条件:页面 init_cb 内调用(uiTask);每次开页都重新绑,不信任上一次的残留 + * 函数说明:先 memset 再填 —— 组件实例里的编辑态跨页留着就是脏状态;视图号非法 + * 给空表(页面退化成 0 行空页,比按错表导航安全) + */ +void ui_w_setview_attach(ui_w_setlist_t *a_inst, ui_w_setview_t a_view) { + if (a_inst == NULL) { + return; + } + (void)memset(a_inst, 0, sizeof(*a_inst)); + switch (a_view) { + case UI_W_SETVIEW_PARAM: + a_inst->rows = s_rows_param; + a_inst->count = SV_ROW_N(s_rows_param); + break; + case UI_W_SETVIEW_SET: + a_inst->rows = s_rows_set; + a_inst->count = SV_ROW_N(s_rows_set); + break; + case UI_W_SETVIEW_WORD: + a_inst->rows = s_rows_word; + a_inst->count = SV_ROW_N(s_rows_word); + break; + case UI_W_SETVIEW_PLATE: + a_inst->rows = s_rows_plate; + a_inst->count = SV_ROW_N(s_rows_plate); + break; + default: + a_inst->rows = NULL; + a_inst->count = 0u; + break; + } + s_cur_rows = a_inst->rows; + s_cur_n = a_inst->count; + a_inst->get = sv_get; + a_inst->set = sv_set_unused; + a_inst->commit = NULL; /* NULL = 查看视图:无保存行、禁编辑(§3.2.6) */ + a_inst->state = NULL; + a_inst->bad = sv_bad; + ui_w_setlist_reset(a_inst); +} diff --git a/Drivers/BSP/UI/ui_w_setview.h b/Drivers/BSP/UI/ui_w_setview.h new file mode 100644 index 00000000..cf88adbc --- /dev/null +++ b/Drivers/BSP/UI/ui_w_setview.h @@ -0,0 +1,110 @@ +/* + * 模块名称:UI 定值查看视图模型(ui_w_setview) + * 模块功能:参数 / 定值 / 控制字 / 软压板**四张视图表**与它们的取数、越界反显—— + * 把"40 项定值怎么分组上屏、Q10 怎么换算成带小数位的显示值"收在一处, + * 供四个查看页薄跳板(ui_page_view_*)共用;整定页(下一批)复用同一批表, + * 只是再绑 set/commit/state 三个回调 + * 分层:本模块只读 Drivers/BSP/UI/ui_data.h 的参数快照,**不 include 任何 App 头** + * (行表里的 id 是**裸数字** + 注释标注 PS_* 名,因为 prot_set_id_t 属 App 层) + * ——id 与 App/algo/prot_set.h 枚举序的一致性由 test/host/ui_data 的主机单测 + * 在两侧头文件同时可见的地方钉住(BSP 侧看不到 App 头,物理上无法自证) + * 值语义:q10[i] = 工程量 × 1024(App/app/ui_data_feed.c 只搬原值不换算,换算属表现层, + * docs/UI设计/UI_数据供给设计说明.md §8),本模块按行表 decimals 换算成 + * "工程量 × 10^decimals" 交给 ui_w_setlist(其值通道契约) + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-15 批 3;规格 = + * docs/UI设计/页面扩充详细方案.md §3.2.1~§3.2.3/§3.2.6) + */ +#ifndef UI_W_SETVIEW_H +#define UI_W_SETVIEW_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "ui_w_setlist.h" + +/* 四个视图编号 */ +typedef enum { + UI_W_SETVIEW_PARAM = 0, /* 参数视图 8 行(PT/CT 额定 + 4 个返回系数) */ + UI_W_SETVIEW_SET, /* 定值视图 18 行(电流/电压/频率/延时) */ + UI_W_SETVIEW_WORD, /* 控制字视图 4 行(*_TRIP:告警/跳闸) */ + UI_W_SETVIEW_PLATE, /* 软压板视图 10 行(*_EN + 保护总投:投入/退出) */ + UI_W_SETVIEW_VIEWS /* 哨兵 = 视图个数(不是有效视图号) */ +} ui_w_setview_t; + +/* + * 函数功能:绑定一个查看视图到定值表实例(rows/count/get/bad 一次配齐) + * 入口参数:a_inst - 页面持有的实例(static,一页一份) ui_w_setlist_t * + * a_view - 视图编号 ui_w_setview_t + * 返回值:无 + * 限定条件:页面 init_cb 内调用(uiTask 上下文) + * 函数说明:查看视图与整定视图的**唯一**差别就是这里绑不绑 set/commit/state: + * commit=NULL → 组件不画保存行、ENTER 无效(§3.2.6 查看列口径); + * set 契约写"必填",故绑一个空实现而不是 NULL——组件在只读态确实不会调它, + * 但把"当前不会调"当成"可以是 NULL"等于把安全性押在下一次重构上 + */ +void ui_w_setview_attach(ui_w_setlist_t *a_inst, ui_w_setview_t a_view); + +/* + * 函数功能:按发布序号取参数帧快照(页面 refresh 前调用) + * 入口参数:无 + * 返回值:无 + * 限定条件:uiTask 上下文(页面绘制链内) + * 函数说明:序号没变就不重拷(一帧约 184 B;四个查看页共用这一份缓存——同屏只可能 + * 开着一页,缓存互相之间不存在并发读者) + */ +void ui_w_setview_sync(void); + +/* + * 函数功能:参数帧是否已就位 + * 入口参数:无 + * 返回值:1 = 有有效帧可显示;0 = 尚无首帧或读取失败 uint8_t(取 uint8_t 是 BSP 公开 + * API 的 err-type 约定,不是错误通道;定义处有完整理由) + * 限定条件:uiTask 上下文;纯查询,无副作用 + * 函数说明:页面据此决定值列打 "--" 还是打换算值——**无数据不许打 0**: + * "0.00A" 会被读成"这台装置的定值就是 0",与"还不知道"是两件事 + */ +uint8_t ui_w_setview_ready(void); + +/* + * 函数功能:逐行取数(接框架 refresh;内含 ui_w_setview_sync() 与无帧占位) + * 入口参数:a_inst - 已 attach 的实例 ui_w_setlist_t * + * a_row - 全局行下标 uint16_t + * a_name - 名输出缓冲(UI_BUF_STRLEN) char * + * a_value - 值输出缓冲 char * + * 返回值:无 + * 限定条件:框架绘制链内调用(uiTask) + * 函数说明:页面**不要**直接调 ui_w_setlist_refresh()——那样会绕过"无帧打 --" + * 这一步,参数帧还没到就把 0 当成定值显示出去 + */ +void ui_w_setview_refresh(ui_w_setlist_t *a_inst, uint16_t a_row, + char *a_name, char *a_value); + +/* + * 函数功能:取某视图的行表与行数(供主机单测做结构自测;页面正常不需要调用) + * 入口参数:a_view - 视图编号 ui_w_setview_t;a_n - 行数输出(不可为 NULL) uint16_t * + * 返回值:行表首地址;视图号非法或 a_n 为空返回 NULL const ui_w_setlist_row_t * + * 限定条件:只读 + */ +const ui_w_setlist_row_t *ui_w_setview_table(ui_w_setview_t a_view, uint16_t *a_n); + +/* + * 函数功能:取 id → 显示小数位查表(供主机单测核对行表与查表一致) + * 入口参数:a_n - 表长输出(= UI_DATA_SET_N) uint16_t * + * 返回值:表首地址 const uint8_t * + * 限定条件:只读 + */ +const uint8_t *ui_w_setview_dec_table(uint16_t *a_n); + +#ifdef __cplusplus +} +#endif + +#endif /* UI_W_SETVIEW_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..eef86605 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -3566,6 +3566,41 @@ 1 ..\Drivers\BSP\UI\ui_w_soe.c + + ui_w_setview.c + 1 + ..\Drivers\BSP\UI\ui_w_setview.c + + + ui_page_soe.c + 1 + ..\Drivers\BSP\UI\ui_page_soe.c + + + ui_page_view_param.c + 1 + ..\Drivers\BSP\UI\ui_page_view_param.c + + + ui_page_view_plate.c + 1 + ..\Drivers\BSP\UI\ui_page_view_plate.c + + + ui_page_view_set.c + 1 + ..\Drivers\BSP\UI\ui_page_view_set.c + + + ui_page_view_word.c + 1 + ..\Drivers\BSP\UI\ui_page_view_word.c + + + ui_page_yx.c + 1 + ..\Drivers\BSP\UI\ui_page_yx.c + diff --git a/docs/UI设计/UI_数据供给设计说明.md b/docs/UI设计/UI_数据供给设计说明.md index bb2405a1..2488cad6 100644 --- a/docs/UI设计/UI_数据供给设计说明.md +++ b/docs/UI设计/UI_数据供给设计说明.md @@ -163,6 +163,7 @@ typedef struct { ui_data_yc_t yc[UI_DATA_YC_N]; uint64_t yx_bits; /* 遥信位图:bit i = 第 i 点"合/动作" */ uint64_t yx_valid; /* 遥信有效位图:bit i = 1 该点有硬件来源 */ + uint64_t yx_dp; /* 遥信双点位图:bit i = 1 该点是双点型(M_DP) */ uint8_t freq_valid; float freq_hz; uint8_t clock_ok; /* 0 = 时钟无效(RTC 异常) */ @@ -175,6 +176,13 @@ typedef struct { - **`eng` 字段是诚实性的关键**(【2026-10-25 落地注】本条原写"0 = 原始码值(未标定)、`val` 是 ADC 码值(千级)",那是标定落地前的旧口径,已按现场改真值语义):标定层 `adc_cal` 的 ROM 默认系数按**引脚侧 LSB**定死(TPAFE 152.59 µV/码、STM32 805.66 µV/码),采样环折算出来的 `val` 现在是 **ADC 引脚电压 V**——既不是千级码值,也不是端子二次值。承诺边界的依据是 `docs/任务规划/数据可信.md` §2.6:这一波只承诺到"引脚电压","端子二次值"要等变比确认与两点标定。所以页面文案是 `eng=0` → "未标定"、`eng=1` → "二次值",而不是"码值";升档条件写在 `App/app/ui_data_feed.c` 的 `feed_eng()`——**电气 8 路 `adc_cal_flags()` 的 VALID 全置位才升**(整帧共用一个 `eng`,只要还有一路是引脚电压就不能给整帧贴"二次值"标签,宁可整体保守)。TEMP 不参与线性标定 → 该路恒带 `UI_DATA_Q_RAW`,页面据此不挂单位、不与其他路互比。 - **字符串只用定长数组**:`clock[20]` 在结构体内,无指针、无生命周期问题(跨层传指针是经典事故源)。 - **`yx_valid` 与 `yx_bits` 分开**:无效点不参与显示、不参与变位判断(沿用 104 点表的既有做法,避免"IV 通道抖动成变位")。 +- **`yx_dp`(2026-10-25 批 4 追加)**:单点还是双点是 **104 点表事实**(`iec104_pts.c` 的 `YX_DP_N` + / `iec104_pts_yx_is_dp()`),由写侧 `ui_data_feed.c` 逐点问点表后下发位图;页面只按位图选词 + (双点"合位/分位"、单点"动作/复归"、无效 "--")。把"前 4 点是双点"抄进表现层就是第二真相: + 点表改成双点段挪位或加高起始下标,屏上不会报错,只会用错词形容同一个点。V0.1 这 4 点无开入 + 硬件 → 不在 `yx_valid` 内,值列仍打 "--"(本字段不参与无效判定)。点数一致性由写侧一条 + 一次性 `DBG_ERROR` 兜(`UI_DATA_YX_N` 与 `iec104_pts_yx_count()` 不等时留痕),因为该常量 + 在协议层是 .c 内的 `#define`,编译期拿不到。 ### 4.2 参数快照 `ui_data_par_t` @@ -490,6 +498,33 @@ typedef struct { **步骤1~步骤2 是关键路径**:它们不产出任何用户可见效果,但决定后面所有页面是否可信。**不要跳过步骤1 直接改页面**——那会退化成"页面里塞假数据 2.0"。 > 【2026-10-25 落地进度】步骤1、步骤2 已落(道E task-14,分支 `task/ui-data-feed`):`ui_data` 三通道 + 主机单测 82 项全绿(`test/host/ui_data/`,已挂 `ui-data-unit-test` 钩子)、`ui_data_feed` 已在 `App/task/ui_task.c` 主循环接线并在建页前发首帧。**改动两处与原稿不同**:① 实时帧的 `eng` 与品质位按标定层真值语义落地(见 `docs/UI设计/UI_数据供给设计说明.md` §4.1 的落地注);② 新增第三条通道 `info`(同文档 §4.4)。**写路径(步骤5 的 `ui_data_param_request/state`)随整定 4 页一起落**,本批无消费者,先落等于落一段永不执行的代码。步骤3 起(页面接线)在道E 第二个工作项。 +> +> 【2026-10-25 落地进度·二】步骤3 的第一批页面已接(道E task-15,分支 `task/pages-ui`):**装置信息**读 `info` +> 通道(型号/软件/硬件/校验/日期/运行时长)+ `net_cfg` RAM 副本(MAC/IP),**交流量**读 `rt` +> 的 `ph[]` 实例槽与 `freq_*`、第 7 行读 `eng`,**直流量**读 `rt.yc[TEMP]` 并透传 `Q_RAW`。 +> 三条纪律在页面侧被验证:① 页面只读快照、零 App include(分层基线不变);② 帧缓存按 `seq` +> 门控整帧拷一次,逐行只读标量;③ 品质位判空一律 `--`,"有数但不可信"与"没数"同形。参数帧 +> `par` 的首批消费者是定值/参数查看页(本批未接,`bad_mask`/`locked` 的屏上表达随那 4 页落)。 +> +> 【2026-10-25 落地进度·三】`par` 的首批消费者已落(同分支批 3):**参数 / 定值 / 控制字 / 软压板**四个查看页 +> (`ui_w_setview` 四张行表 + `ui_w_setlist` 组件,查看态 `commit=NULL` 无保存行)。Q10 → 显示值的换算落在**表现层** +> (本契约 §8 的分工:写侧只搬原值),换算式在 `ui_w_setview.c` 里做整数拆分而非浮点/64 位除(MicroLIB +> 软浮点与 `__aeabi_ldiv` 一进镜像几百字节,模拟器永远照不出这笔代价)。`bad_mask` 的屏上表达 = +> 该行反显高亮(且只在 `bad_valid=1` 时参与,把没数据画成有毛病是假告警);`locked` 的屏上表达是 +> **禁编辑**,查看页本来就无编辑通路,它随整定 4 页一起显形。四个返回系数按 2 位小数显示的理由见 +> `docs/UI设计/页面扩充详细方案.md` §3.2.2 落地注;行表与 `prot_set` 枚举序/上下限的一致性由 +> `tools/setview_check/check_setview_limits.py` 核验(BSP 侧看不到 App 头,物理上无法自证,必须有人从外面钉)。 + +> 【2026-10-25 落地进度·四】遥信通道已被消费:**遥信量**页(`ui_page_yx.c`,33 行固定名表, +> 方案 A)按 `yx_valid` → `yx_dp` → `yx_bits` 的次序取值列(先判无效:无效位上的位值不代表任何 +> 事实;再判单/双点选词;最后才看动作态)。起草时为此补了 `yx_dp` 一个字段(§4.1),由 +> `ui_data_feed.c` 逐点问 `iec104_pts_yx_is_dp()` 得到——**点数与单双点性质都留在协议层**,页面 +> 不复制这些事实;`UI_DATA_YX_N` 与 `iec104_pts_yx_count()` 一旦不等,写侧打一条一次性 +> `DBG_ERROR`(点数在协议层是 .c 内的宏,编译期拿不到,只能运行期留痕)。模拟器侧同步发布 +> `yx_dp = 0x0F` 但**不**把这 4 位置进 `yx_valid`:V0.1 没有开入硬件,屏上就是 `--`——刻意不用 +> 「让它显示合位/分位」的假数据把画面填满,那等于让黄金参考声称本机有开关位置遥信。因此 +> 「合位/分位」这条词映射当前**没有任何自动化证据覆盖**(像素恒 IV、页面依赖 LVGL 也进不了 +> 主机单测),它已登记为开入落地后的台架判据,而不是假装测过。 --- diff --git a/docs/UI设计/页面扩充详细方案.md b/docs/UI设计/页面扩充详细方案.md index 0a0959b9..35fcf4b6 100644 --- a/docs/UI设计/页面扩充详细方案.md +++ b/docs/UI设计/页面扩充详细方案.md @@ -134,6 +134,10 @@ **待拍板**:是否接受"7 行"而非参考的"24 行"。若产品要求保留 24 行骨架(便于将来扩通道),则改成 24 行固定表、当前 17 行显 `--`。 +【2026-10-25 落地】按 7 行落了(`Drivers/BSP/UI/ui_page_ac.c`):品质位 IV/NT/RAW/SB/BL/OV 一律显 `--`, +第 7 行走快照 `eng` 真值语义(口径见 `docs/UI设计/UI_数据供给设计说明.md` §4.1)。24 行骨架并 +没有"便于将来扩通道"的价值——真扩通道要连采样源一起动,那时页面改行表就是一行表的事。 + #### 3.1.2 直流量(回炉,3 行 → 2 行) V0.1 **无直流采样通道**(`ADC_SEM` 只有 8 路电气 + 温度),点表 `0x401E/0x401F` 也注明"FTU 无"。 @@ -145,6 +149,8 @@ V0.1 **无直流采样通道**(`ADC_SEM` 只有 8 路电气 + 温度),点 - 规模:**S**。 - **待拍板**(三选一):① 按上表做(推荐,保留菜单语义与点表一致);② 菜单改名"温度量"(改产品语义);③ 删节点(与参考/点表不一致)。 +- 【2026-10-25 落地】按方案①落了(`Drivers/BSP/UI/ui_page_dc.c`)。温度值列写成 `码2048`:那个"码"字把 + "这是 12 位原始码、不是温度"写进屏上而不是写进注释(通道本身带 `UI_DATA_Q_RAW`,页面不挂单位)。 #### 3.1.3 遥信量(新建) @@ -177,6 +183,22 @@ V0.1 **无直流采样通道**(`ADC_SEM` 只有 8 路电气 + 温度),点 - 双点遥信 4 点用 `iec104_pts_yx_is_dp()` 决定是否显示"分/合",当前恒无效。 - 规模:**M**(33 行名表 + `refresh` + 值映射,约 150 行)。 +> **落地注(2026-10-25 道E task-15 批 4)**:已按方案 A 落地为 `Drivers/BSP/UI/ui_page_yx.c` +> (菜单 `遥信量 #page=yx`,8 行/页 = 5 页,黄金参考钉了第 1、2 两页)。四处要点: +> 1. **两处命名不照抄本表简写**:idx0~3 本表写「YK1~4 位置」,屏上只写「位置1~4」——遥信页 +> 出现 YK 前缀会被读成遥控对象(那是另一页的事),与遥控的对应关系留在代码注释里;idx30 +> 本表写「闭锁1」,屏上写「保护闭锁」(写侧 `FEED_YX_BIT_LOCK` 的语义即此,用编号反而丢信息)。 +> idx4~13 与 idx31~32 点表未定义功能名,屏上如实写点号占位(`YX5`…、`YX32`),**不编造功能名**。 +> 2. **值映射判定次序是「先无效、再单双点、最后位值」**:无效位上的位值不代表任何事实,排在 +> 后面就会把垃圾位画成「复归」。单/双点由快照 `yx_dp` 决定,不在页面复制点表事实(理由见 +> `docs/UI设计/UI_数据供给设计说明.md` §4.1 的 `yx_dp` 条目)。 +> 3. 本表「双点遥信用 `is_dp()` 决定是否显示分/合」落地为 **合位/分位** 两词,但该支路 V0.1 恒 +> 无效(无开入硬件),像素与主机单测都覆盖不到 → 已登记为开入落地后的台架判据,不假装测过。 +> 4. 落地时踩到一条对所有名表页通用的陷阱:把既有的 `*_PUT(dst, 字面量)` 宏用在 +> `const char *const` 表项上,`sizeof(源)` 就成了**指针宽度**,CJK 名被截成第一个汉字而短 +> ASCII 名恰好完好——编译器 / cppcheck / 编码钩子全不响,只有像素文本断言报出「某行名字 +> 匹配不到、值却在原位」。长度参数必须取**目标缓冲容量**(`UI_BUF_STRLEN`)。 + ### 3.2 参数定值(8 叶子 → 2 个模块) 8 个叶子(整定 4 + 查看 4)共用**一份** `ui_w_setlist` 组件 + **4 张视图表**。查看与整定的差别只是 `edit_capable` 与权限。 @@ -201,10 +223,25 @@ V0.1 **无直流采样通道**(`ADC_SEM` 只有 8 路电气 + 温度),点 | 2 | `PT二次` | `PS_PT_SEC` | V | 0 | | 3 | `CT一次` | `PS_CT_PRI` | A | 0 | | 4 | `CT二次` | `PS_CT_SEC` | A | 0 | -| 5 | `过量返回` | `PS_RET_OVER` | — | 3 | -| 6 | `欠量返回` | `PS_RET_UNDER` | — | 3 | -| 7 | `过频返回` | `PS_RET_OF` | — | 3 | -| 8 | `低频返回` | `PS_RET_UF` | — | 3 | +| 5 | `过量返回` | `PS_RET_OVER` | — | 2 ⚠ | +| 6 | `欠量返回` | `PS_RET_UNDER` | — | 2 ⚠ | +| 7 | `过频返回` | `PS_RET_OF` | — | 2 ⚠ | +| 8 | `低频返回` | `PS_RET_UF` | — | 2 ⚠ | + +> 【2026-10-25 落地注·四张视图表(道E task-15 批 3)】四个查看页已接(`ui_page_view_param` / +> `_set` / `_word` / `_plate`,共用 `Drivers/BSP/UI/ui_w_setview.c` 的四张行表 + `ui_w_setlist` +> 组件)。三处口径落地时要说清楚,都写进了代码注释与提交正文,不留在这儿等人猜: +> 1. **返回系数按 2 位小数显示(本表原写 3 位,已按可分辨精度修正,⚠ 标在表里)**。这不是 +> 口径让步而是原口径本身错了:3 位来自"把 Q10 当成十进制存储"这个误会。Q10 存不下 0.95 +> (0.95×1024 截成 972 → 真存 0.94921875),按 3 位显示等于多打一位根本不存在的数字; +> 整定单上写 0.95 的人看到屏上 0.949 会怀疑装置、甚至反复重输(重输还是 0.949)。2 位才是 +> 这份存储在自身可分辨精度上的正确读数。上下限、枚举序、单位口径由 +> `tools/setview_check/check_setview_limits.py` 连 meta 一起核(已挂 `setview-check` 钩子)。 +> 2. **单位挂在值串尾**(`10.00A` / `500ms`),不塞进显示名:值列只有数字会让 `60000` 既是 ms +> 又是 A;A/V/ms/Hz 全 ASCII,不占中文字库、不触发 font-charset。为此给 `ui_w_setlist_row_t` +> 加了 `unit` 字段(NULL = 不挂;只挂在非编辑态——编辑中是逐位输入串,挂单位会算错位光标)。 +> 3. **上下限不自己拟定**:行表 min/max = `g_prot_set_meta` 的 Q10 上下限按该行小数位换算的结果, +> 事后由上述脚本核验(改了 meta 不改行表就会红)。 #### 3.2.3 定值视图(18 项) @@ -390,7 +427,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`。完整版(故障前后有效值 + 波形摘要)依赖录波,不在本批。 @@ -415,6 +463,11 @@ void ui_w_soe_refresh(ui_w_soe_t *inst, uint16_t row, char *name, char *valu - 新增 `App/app/app_ver.h`(4 个宏 + `__DATE__`),页面经快照取值。 - 规模:**S**。 - 说明:第 5 行"软件校验"若没有构建期 CRC 生成步骤,**先显示 `--` 而不是抄参考值**。 +- 【2026-10-25 落地】8 行全接真值(`Drivers/BSP/UI/ui_page_device_info.c`):型号/软件/硬件/校验/日期/ + 运行时长取装置信息帧,MAC/IP 取 `net_cfg` RAM 副本(与网口设置页同一个 `net_cfg_fmt_ipv4`, + 不另起第二套格式化)。两处顺带修正的口径:① 标签去全角冒号、值列左对齐 x=56——17 字符 MAC + 要 102 px,参考图的 x=86 容不下;② 页脚从参考遗留的固定"第 10 页"改成真页码"第 1 页,共 1 页" + (屏上不出现与本机无关的数字,Lead 裁定 2026-10-25,像素基线随之单独提交)。 --- diff --git a/docs/环境与部署/并行任务工作树方案.md b/docs/环境与部署/并行任务工作树方案.md index 32ac06ef..2c2360f7 100644 --- a/docs/环境与部署/并行任务工作树方案.md +++ b/docs/环境与部署/并行任务工作树方案.md @@ -270,10 +270,17 @@ D:/Code/DTU Code/ | ------- | ------------------------------------ | ------------------------------ | | 任务期 | `scripts/ci_local.py`(L0 + L0.5,可并行) | 每个任务各自跑 | | 任务期 | `@build`(0 错 0 警,**持 `build` 锁串行**) | 每个任务各自跑 | +| **新空间首次** | `@build` 一次,生成 `.map` | 见下表下注释:不先构建,体积类门禁必然红 | | 任务期(可选) | `@test` / 板级套件(持 `bench` 锁) | 需真机验证的任务 | | 回收期 | 重生成产物 → `ci_local.py` → `@build` | `done` 内强制(`--skip-gates` 可跳过) | | 人工 | push 许可 | 回收后 | +> ⚠ **新开工作树后先 `@build` 一次再谈门禁**(实测 2026-10-25):`.map` 是每棵树各自的构建产物、 +> 不入版本库,所以依赖它的事实源的门禁——`mem-numbers`、`mem-budget`、以及 `ci_local.py` 的 +> `L0c`——在从未构建过的新空间里**必然红**。这不是代码缺陷,脚本此时也会把这句原因打出来 +> (`check_mem_numbers.py` / `ram_baseline.py` 的提示文案)。顺带一条同源提醒:`done` 的门禁 +> 排在 `@build` 之前(见下文陷阱条目),同一族误报在回收期也会重现一次。 + **`done` 的完成判据**:主干上 `git merge --no-ff` 成功 + `ci_local.py` 全绿 + `@build` 0 错 0 警 - 残留审计无死规则/重复条目 + 工作树已移除。 diff --git a/docs/门禁说明/README.md b/docs/门禁说明/README.md index 375c46be..b6700cda 100644 --- a/docs/门禁说明/README.md +++ b/docs/门禁说明/README.md @@ -1,6 +1,6 @@ # 门禁逐项说明(每种门禁一篇) -> 这一目录回答一个问题:**提交时那 46 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。** +> 这一目录回答一个问题:**提交时那 48 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。** > 每个门禁一篇,八段固定结构:查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 / > **限制与盲区** / 豁免与基线 / 改门禁本身要注意什么。**最后一节是这套文档存在的理由**—— > 一份只看"拦了什么"的门禁文档,会让人以为绿就是对的。 @@ -21,7 +21,7 @@ ⚠ 增量拦截最容易误会的一点:**绿灯不等于没问题**,只等于"没有比上次更差"。 每个增量拦截门禁的基线数字都写进它自己的文档,读那篇时第一眼看的就是"现在在册多少条"。 -## 2. 索引(40 个 hook → 23 篇 + 1 根在 docs/测试说明/) +## 2. 索引(42 个 hook → 25 篇 + 1 根在 docs/测试说明/) | 门禁文档 | 覆盖的 hook id | 一句话它在查什么 | 改了哪类文件才会跑 | 文档 | |---|---|---|---|---| @@ -41,6 +41,8 @@ | [`test-suite-switch.md`](test-suite-switch.md) | `test-suite-switch` | 板级套件开关必须互斥;全注释 = 产品构建 | `test/board/test_config.h` | 已写 | | [`font-charset.md`](font-charset.md) | `font-charset` | UI 用到的汉字是否都在子集字体里(缺字 = 白框) | `Drivers/BSP/UI/**`、字体脚本 | 已写 | | [`ui-golden.md`](ui-golden.md) | `ui-golden` | UI 像素回归:菜单 8 状态 + 内容页 11 页黄金参考图比对 | UI 源码、模拟器 | 已写 | +| [`bom-guard.md`](bom-guard.md) | `bom-guard` | 带 BOM 的文件被编辑工具剥掉 BOM(ARMCC 换解码 → 假语法错) | `Drivers/BSP/UI/**` 等名单内文件 | 已写 | +| [`setview-check.md`](setview-check.md) | `setview-check` | 定值查看行表与 `prot_set` 枚举序 / `g_prot_set_meta` 上下限是否同一份口径 | `tools/setview_check/**`、`ui_w_setview.*` | 已写 | | [`host-unit-tests.md`](host-unit-tests.md) | 16 个 `*-unit-test` | 主机侧单测(同一机制 16 个实例):逻辑层在无硬件的 PC 上编译并跑断言 | 各自被测模块与其测试目录 | 已写 | | 详解在 [`../测试说明/主机单测覆盖率口径.md`](../测试说明/主机单测覆盖率口径.md)(口径 / 分母 / 基线 / 盲区都在那篇,本目录不重复) | `test-coverage` | **覆盖率总闸**:跑全部 28 套主机单测 + 已覆盖行只增不减的回退台账 | `App/`、`Drivers/BSP/`、`test/host/`、脚本与基线 | 已写(在 `docs/测试说明/`) | | [`trailing-whitespace.md`](trailing-whitespace.md) | `trailing-whitespace` | 行尾空白(改写型;CubeMX/Keil 生成文件豁免) | 全部 | 已写 | diff --git a/docs/门禁说明/bom-guard.md b/docs/门禁说明/bom-guard.md new file mode 100644 index 00000000..1ed7f47e --- /dev/null +++ b/docs/门禁说明/bom-guard.md @@ -0,0 +1,94 @@ +# `bom-guard` — 带 BOM 文件被剥掉即拦(防"假语法错") + +> 一句话:编辑工具会**静默剥掉**文件原有的 UTF-8 BOM,ARMCC 于是换了一套解码假设,报出来的 +> 却是 `#870-D` / `#8` / `#274` 这类**语法错**——错的位置和内容都不指向真因。这条门禁让"补回 +> 3 个字节"这件事在提交时就必须处理,而不是等下一个人构建失败再去猜。 + +## 1. 它在查什么 + +对 pre-commit 传来的、落在名单内的文件,比较 **HEAD 版本**与**工作区版本**的前 3 字节: +HEAD 是 `EF BB BF` 而工作区不是 → FAIL 并列出文件与补救动作。只此一条判定,没有存量豁免。 + +名单(三处互指,见 §6.2):`Drivers/BSP/UI/**`、`App/task/ui_task.c`、`App/cfg/cfg_file.c`、 +`Lib/lv_conf.h`。 + +## 2. 为什么要它(拦下过什么真事) + +- **立项直接来自一天内 5 次踩坑**:2026-10-25 道E 在 task-15(UI 页面批次)里连踩 4 次, + 道B 在批 1 踩过 1 次。每次的症状都一样:改完 UI 源文件、`@build` 突然报一串语法错,而改动 + 本身是加注释或改常量;`MDK-ARM/build_log.txt` 里的行号指向中文注释所在的**上一行**, + 照着报错去查会查不出任何东西。真因只是文件开头少了 3 个字节。 +- **别的门禁和编译器都不会响**:MSVC(模拟器侧)带不带 BOM 都编得过、0 警告;`no-mojibake` + 查的是 `U+FFFD`(已经损坏的痕迹),BOM 刚丢时文件字节完全合法,它看不见;`coding-standard` + 也不看编码。也就是说,这条坑当时唯一的暴露点是"重编该文件",而**历史 0 警告不代表安全**。 +- **人工审计只防得住记得做的人**:立项前道E 的做法是每次提交前逐文件比 HEAD 查 BOM, + 四次里有一次是靠"看图发现满屏横杠"顺手查的,不是靠纪律——纪律型检查不成立(同类口径见 `docs/代码规范/pre-commit使用说明.md` §5.5)。 +- 顺带一条同族事实(写在这里而不是别处):**本门禁自己的立项提交被 `gitlint` 拦过一次**—— + 正文有一行 92 > 80,作者量出来了仍然按了提交。「拦下过什么真事」这一栏如果只挑别人的事写, + 就等于在暗示写门禁的人不需要门禁,那是不成立的。 + +## 3. 触发面 + +`.pre-commit-config.yaml` 里 `bom-guard` 的 `files` 正则(等价于 §1 名单)。不在名单内的文件 +**不报也不提示**——比如有人在 `App/log/` 新建带 BOM 的文件,本门禁不管(那种文件按约定就不该 +带 BOM;真带上了,编译器会替我们响)。 + +⚠ `files` 不命中时钩子是 **Skipped**,`Skipped ≠ Passed`:一次只改文档的提交里看不到它变绿 +是正常的,不代表它检查过了。 + +## 4. 报错怎么读 / 单独跑 + +```powershell +# 常规由 pre-commit 自动跑;人工核全仓(含没改过的文件): +& '.venv\Scripts\python.exe' scripts/check_bom_drift.py --all +# 自证判定(临时 git 仓库,含两条边界用例): +& '.venv\Scripts\python.exe' scripts/check_bom_drift.py --selftest +``` + +报错会点名文件并给补救动作:**只把 3 个字节 `EF BB BF` 补回文件最前面**。**不要**整文件重写 +(`Set-Content` / 脚本按系统默认编码重打中文会把"可修的丢 BOM"变成"不可修的 `U+FFFD`", +那是 `no-mojibake` 都救不回来的损坏)。 + +## 5. 好处 + +1. 把一类"只有重编译才暴露、且报错完全误导人"的缺陷挡在提交点,且提示直接给出正确修法; +2. 逼着 BOM 名单保持单一口径(§6.2 的三处互指)——名单一旦散着改,本门禁立刻红; +3. 判定极简(两个 3 字节比较),几乎不会有误报,也就不容易被 `--no-verify` 搪塞过去。 + +## 6. 限制、盲区与刻意的不判定 + +### 6.1 只查"有 → 无"这一个方向 +"HEAD 无 BOM、工作区加了 BOM" **不报**。那是另一个方向的改动,可能是有意的(把某个文件纳入 +BOM 名单),也可能是无意的;报红只会逼人 `--no-verify`,而**养成绕门禁的习惯比漏检一次贵得多**。 +"新增文件没有 BOM" 同样不报:名单里的新文件(如新建 `ui_page_xxx.c`)由创建者负责加 BOM, +漏加会在 `@build` 时以真错误暴露——本门禁不假装自己能替代那一步。 + +### 6.2 名单三处互指(防漂移) +AGENTS 铁律第 2 条的名单、钩子 `files` 正则、脚本内 `BOM_LIST` 必须一致;脚本每次运行都核一次, +不一致即 FAIL。比较时会先脱掉正则里的反斜杠与锚点——**核的是意图一致,不是字节相同**,否则 +`App/task/ui_task\.c$` 会被误判成"没覆盖 App/task/ui_task.c"。 + +### 6.3 不校验文件是否真的含非 ASCII +判定只看编码标记,不看内容。一个纯 ASCII 的名单内文件带不带 BOM 都不影响编译,本门禁不替 +编译器做这种优化判断——它宁可少管,也不要给出"可以不带 BOM"的例外口径(例外一旦被记住, +就会被套用到带中文的文件上)。 + +### 6.4 绕过与提交后不可见 +`--no-verify` 可以直接跳过;合入主干之后再手工删 BOM,要等下一次有人改这个文件才会被比出来。 +这两条是所有 pre-commit 门禁的公共限度,不在本篇展开(→ `docs/代码规范/pre-commit使用说明.md`)。 + +## 7. 豁免与基线 + +无基线、无豁免、无 `--write`:要么 BOM 在,要么别提交。 + +## 8. 改这个门禁本身 + +- 改名单必须同时改三处(§6.2 会拦着); +- 新增判定分支必须先在 `--selftest` 里加用例(含正反两向),它是这篇文档里唯一可执行的部分; +- 别在这里加"自动修复":见 §4 的编码风险。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-25 | 首版(道E task-20)。立项依据:同日 task-15 内 4 次 + 批 1 一次 BOM 剥离事故;端到端实测(真剥掉 `ui_w_records.c` 的 BOM → 钩子 rc=1)随 task-20 提交正文留存 | diff --git a/docs/门禁说明/font-charset.md b/docs/门禁说明/font-charset.md index 24748992..3d45ede4 100644 --- a/docs/门禁说明/font-charset.md +++ b/docs/门禁说明/font-charset.md @@ -15,7 +15,7 @@ | B | `ui_cn_chars.txt` vs `ui_font_cn_12.c` 的 cmap | txt 有而字体无 → **GAP FAIL**(转换器对缺字形/低码位会**静默丢字**) | A 通过 + B 通过 ⟹ 字体覆盖"源码在用"与"刻意保留"的每一个字。 -`KEEP_EXTRA` 的存在理由:防止将来 `extract_cn.py` 按"源码已不用"静默删掉**已落盘记录里还在用**的字。 +`KEEP_EXTRA` 的存在理由:防止将来 `extract_cn.py` 按"源码已不用"静默删掉**已落盘记录里还在用**的字(这条路径的实际形态与实测数字见 §6.1)。 - **单一规则源**:本脚本不自创第二套"从源码里找汉字"的规则,而是复用 `tools/font_gen/extract_cn.py` 的 `SCAN` 清单与 `collect()` 状态机(跳过注释与字符常量、只取双引号字符串)。**两处各写一套解析, @@ -55,6 +55,34 @@ A 通过 + B 通过 ⟹ 字体覆盖"源码在用"与"刻意保留"的每一个 2. 必需集来自"源码里出现的汉字",**运行期拼出来的字符串**(格式化进来的字、外部配置文件里的字、 记录内容回显)完全在视野外——那类白框本门照不到。 3. 只校验**码位覆盖**,不校验字形是否美观/是否为所需点阵尺寸。 + +### 6.1 「按 extract 输出重写字符集」= 洗字形(实名登记的事故路径) + +`tools/font_gen/extract_cn.py` 只认**当前源码里的字符串字面量**,照不出运行期数据里的字—— +典型是 2026-09 窗口固件写进 `/log/selftest.log`、`/log/run.log` 的定长 64 B 中文描述记录 +(跨固件持久,自检/运行记录页直接渲染 `desc`)。实测口径(2026-10-25):extract 扫出 +**144 字**,而 `ui_cn_chars.txt` 现有 **183 字**,差的 39 字全部由 `KEEP_EXTRA` 兜住: + +- **逐字登记 4 字**(异/常/片/芯):出处可指到 `9eb1bd63`(2026-09-14 中文自检描述) + 与 `6f51c62a`(2026-09-20 串口侧改 ASCII),脚本里逐字写了理由; +- **成组登记 35 字**(2026-10-25 道E task-15 并入):正是本门禁「未知冗余」WARN 的全部 + 条目(一位公分合外已年度延态总接显最月未求温状电示秒请跳载近道部配重长间闸障)。来源 + 能确认是那批落盘记录的用字,**但逐字出处已不可靠复原**(文案被整体改写,指不到某个 + 字来自哪条 commit)→ 选择整组保留并在脚本里点名成组,**而不是给每个字编一条看起来 + 可信的理由**。判据:字形不可再生(删了永久白框、无日志无报错,只在渲染历史记录时暴露), + 保留成本约 35 字 × 27 B ≈ 1 KB RO——收益与代价不对称(同本目录"收益不抵风险"暂缓)。 + +**事故路径**:有人跑 `extract_cn.py`,把输出当"权威清单"直接覆盖 `ui_cn_chars.txt` → +39 字被静默洗掉 → 下次 font-regen 生成的字体不再有这些字形 → 现场翻到 2026-09 那批历史 +记录时相关行渲染成空白方块,且没有任何地方报错。 + +**机械化拦截**:`KEEP_EXTRA` 按"必需字"参与 A 段判定——txt 缺任一保留字直接 GAP FAIL +并逐字打印保留理由(`--strict` 同样 FAIL)→ "洗字形"在**下一次提交**就响,不靠人记性。 +配套流程约束:font-regen 第 3 步(重生成字体)之前必须把 `KEEP_EXTRA` 并回 txt。 + +**删除前置条件**(满足后才可删条目,删后下次 font-regen 自然瘦身):确认设备上已无该窗口 +的落盘记录,或产品侧明确接受历史记录部分渲染空白。只删 txt/字体里的字而不删本清单 = +本门禁立刻 FAIL(刻意:清单是唯一真相源)。 4. 拉丁字母、数字、标点不在本门范围(另有字体与布局约定)。 5. `--strict` 不在钩子默认档位里(默认只拦 GAP):冗余字形长期靠人自觉清理。 6. 字体文件本身若被手工编辑(不跑生成器),A/B 两段仍可能同时"通过"而与实际 cmap 不符—— @@ -62,8 +90,8 @@ A 通过 + B 通过 ⟹ 字体覆盖"源码在用"与"刻意保留"的每一个 ## 7. 豁免与基线 -- `KEEP_EXTRA` 是**刻意保留清单**(存量 + 逐条理由),性质同 `registration-check` 的已知缺口表; - 新增要写理由,删掉等于放弃保护。 +- `KEEP_EXTRA` 是**刻意保留清单**(存量 + 理由;出处可考者逐字登记,出处不可考者成组登记, + 见 §6.1),性质同 `registration-check` 的已知缺口表;新增要写理由,删掉等于放弃保护。 - 没有"只减不增"的债务基线。 ## 8. 改这个门禁本身 @@ -76,3 +104,4 @@ A 通过 + B 通过 ⟹ 字体覆盖"源码在用"与"刻意保留"的每一个 | 日期 | 变更 | |---|---| | 2026-10-23 | 首篇逐项文档(两段链条判定口径 + 生成期校验覆盖不到的那一环) | +| 2026-10-25 | 新增 §6.1:实名登记「按 extract 输出重写字符集 = 洗字形」这条事故路径;把当时「未知冗余」WARN 的 35 字**成组**并入 `KEEP_EXTRA`(实测 extract 144 字 vs 清单 183 字,差 39 字 = 逐字 4 + 成组 35),并写清删除前置条件(道E task-15) | diff --git a/docs/门禁说明/setview-check.md b/docs/门禁说明/setview-check.md new file mode 100644 index 00000000..27e98bbd --- /dev/null +++ b/docs/门禁说明/setview-check.md @@ -0,0 +1,106 @@ +# `setview-check` — 定值查看行表与 `prot_set` 枚举序 / 上下限一致性 + +> 一句话:UI 侧四张定值视图表(`Drivers/BSP/UI/ui_w_setview.c`)与算法侧的定值定义 +> (`App/algo/prot_set.h` 枚举序 + `App/algo/prot_set.c` 的 `g_prot_set_meta`)之间有一条 +> **编译期照不到**的对齐关系——本门禁从外面把它钉住。 + +## 1. 它在查什么 + +脚本用正则读出三份事实,再逐条比对(`tools/setview_check/check_setview_limits.py`): + +1. 四张视图表的 `id` 合起来**恰好覆盖 0..39 各一次**(不多、不少、不重); +2. 每个数值行的 `min`/`max` 等于 `g_prot_set_meta` 的 Q10 上下限按该行小数位换算后的整数; +3. 行 `kind`(数值 / 二态串表)与 `g_prot_set_meta[].is_ctl` 一致; +4. 单位串只允许 ASCII(`V`/`A`/`ms`/`Hz`/NULL)——中文字不进子集字体,见 `font-charset.md`; +5. 小数位查表 `s_dec[]` 与行表 `decimals` 对同一个 `id` 取值一致(组件 `get` 只收到 `id`, + 两处不一致就会显示错位数字)。 + +退出码:**0** 一致;**1** 不一致(打印具体行与期望值);**2** 解析锚点丢失——这是**结构漂移**, +说明 C 侧写法变了、脚本要跟着改,不是"数据错了"。1 与 2 必须分开:把"我没读懂"报成"你写错了" +是检查类脚本最坏的一种失败。 + +## 2. 为什么要它(这不是"多一道保险") + +- **分层边界制造的物理盲区**:`layer-deps` 钩子禁止 `Drivers/BSP/**` include `App/**` 的头, + 所以视图表里的 `id` 只能是**裸数字 + 行尾注名**(`/* PS_OC1_EN */`)。这条对应关系在编译期 + 无法自证:写错一个数字、少写一行、上下限抄错一位小数,编译器与 cppcheck 全都不会响。 +- **像素断言只覆盖可见部分**:一页只有 8 行进入黄金参考,40 项定值分 5 页;行表整体错位一行时, + 第 1 页可能看起来完全正常(名字和值都是"某个真定值"),要翻到第 2、3 页才可能被人看出来, + 而人真去翻的时候往往已经太晚——那时基线已经按错的图刷新过了。 +- 也就是说:**分层是对的,代价必须有人补**。这条门禁就是补的那一步(同类补法见 + `registration-check.md` 用反向校验补"新文件双端注册",`ui-golden.md` 补"改了就画错")。 + +## 3. 触发面 + +`^(tools/setview_check/|Drivers/BSP/UI/ui_w_setview\.(c|h)$)`:改脚本自己、或改视图表所在 +文件时触发。⚠ `files` 不命中时是 **Skipped**,`Skipped ≠ Passed`。 + +本钩子与实现文件是**跨分支**落地的(钩子配置在 `task/bom-guard` 分支,脚本在 `task/pages-ui` +分支):在脚本还没合入的树上,`files` 永不命中 → 钩子自然 skip,合入后自动生效,不需要任何人 +再动配置。这一条写进配置注释,是为了避免下一个人以为"钩子没跑 = 配置坏了"。 + +## 4. 单独跑 + +```powershell +# 仓库根执行(脚本按仓库根的相对路径读三个源文件,别在子目录跑) +& '.venv/Scripts/python.exe' tools/setview_check/check_setview_limits.py +``` + +正常输出只有一行:`行表 40 行 / dec 40 项 / meta 40 项 -> OK`。**行数必须等于 40** 是硬闸: +解析器少吞一行不算通过(见 §6.1)。 + +## 5. 好处 + +1. 把一条"只有真机装载真定值、且人翻到第 2 页之后"才可能暴露的静默错位,提前到提交点; +2. 让整定 4 页能安全复用同一批表:界内界外只有一份定义,不会出现"查看说合法、整定说越界" + 两套边界(复用是当初把表放在 `ui_w_setview.c` 而不是各页面里的原因); +3. 口径变更留下痕迹:`RATIO_IDS` 与文档 §3.2.2 的表注互为引用,改动必须同时解释。 + +## 6. 限制、盲区与写脚本时踩到的两个坑 + +脚本用**正则读 C 源码**,天然脆弱:换一种写法(宏生成表、初始化器拆行、结构体加字段)就可能 +解析不到。以下是同日实测的两个真坑,都已固化进脚本: + +### 6.1 `\w+` 吃不下带引号的单位 → 整行**静默跳过** + +行表项形如 `{ "过流Ⅰ段", 0, 1, 0, 0, "A", NULL }`。最初的行正则用 `\w+` 匹配"最后一个字段", +带引号的 `"A"` 匹配不上 → 整行不匹配 → **一条错误都不报**,脚本照样打 OK。少检一行比检查错 +更难发现(后者有人会去查,前者看起来像已经查过)。修法有两半:字段改用 `\S+` 承接,以及加 +`len(rows) == 40` 的**硬闸**——解析条数本身也要被检查,否则"没解析到"就等价于"没问题"。 + +### 6.2 打印中文 → Windows 控制台按 CP936 写 stdout 会**崩在半路** + +门禁脚本崩在半路等于一条都没检查,而现场看到的是一串 Python traceback 而不是 FAIL。脚本开头 +把 `stdout/stderr` 锁到 UTF-8(`errors="replace"`)。同一条课在本仓一天里踩了两次,第二次才 +写成固定动作——所以两个脚本里都有这段,且都写了为什么。 + +### 6.3 不检查的东西(别把它当万能闸) + +- **名字对不对**不查:`"过流Ⅰ段"` 该不该叫这个名字是语义判断,脚本只核结构一致;名称口径 + 的真源是 `docs/UI设计/页面扩充详细方案.md` §3.2.2~§3.2.5 四张表; +- **上下限本身合不合理**不查:脚本只核"行表等于 meta 换算结果",如果 meta 自己就写错, + 这条门禁会忠实地把错的值判成一致(真机定值核对在台架大窗里做); +- Q10 → 显示小数位的**换算函数本身**由主机单测与像素断言覆盖,不在本脚本内(脚本重算一遍 + 换算是为了独立见证,不复用 C 侧代码)。 + +## 7. 豁免与基线 + +无存量豁免、无基线文件、无 `--write`。定值项数改变(`PS_ID_COUNT` ≠ 40)时必须同步:脚本里的 +期望行数与 id 上界、`ui_data.h` 的 `UI_DATA_SET_N`、以及 `docs/UI设计/UI_数据供给设计说明.md` +的参数通道说明。 + +一条**已修正的原口径**记在这里免得被"改回去":四个返回系数按 2 位小数显示。原方案文档写 3 位, +而 Q10 存不下 0.95(0.95×1024 截成 972 → 真存 0.94921875),3 位会多打一位根本不存在的数字; +§3.2.2 的表已同步改成 2 位并加 ⚠ 指向代码注释。 + +## 8. 改这个门禁本身 + +- 行表写法一改(新增字段、换初始化风格),先跑脚本:退出码 2 就是"脚本要先跟上"; +- 新增校验项时同步新增一条"如果这行解析不到就 FAIL"的期望,不要让新校验继承 §6.1 的静默; +- 想把它扩成通用"表 vs 元数据"检查器之前,先数一下真有几个这样的对象(现在只有一个)。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-25 | 首版(道E task-20 追加批;脚本与实现来自 task-15 批 3)。素材:`\w+` 静默跳行、CP936 半路崩、满屏横杠与 4 字符位两条像素教训 | diff --git a/scripts/check_font_charset.py b/scripts/check_font_charset.py index fa1790f4..81667819 100644 --- a/scripts/check_font_charset.py +++ b/scripts/check_font_charset.py @@ -79,6 +79,25 @@ KEEP_EXTRA = { "片": "同 \"时钟芯片异常\"(9eb1bd63 写入,落盘持久)", } +# 2026-10-25 道E task-15 补充:**成组保留**的历史落盘记录用字(35 字)。 +# 发现方式:本钩子的「未知冗余」WARN 清单(txt 有、源码无、且不在 KEEP_EXTRA)实测正好 +# 35 字——全部是 2026-09 窗口固件写进 /log/selftest.log(与 /log/run.log)定长 64 B 记录 +# 里的中文描述用字。那批源码文案后来被改写(串口侧改 ASCII),**逐字出处已不可靠复原**: +# 能确认来源是落盘记录用字(不是美术字、也不是手工修 cmap),但指不到「这个字出自哪条 +# commit」。两难取其下——字形不可再生(删了就永久白框)而保留成本极低(35 字 ≈ 1 KB RO) +# → 整组保留并在此**点名成组**,而不是给每个字编一条看起来可信的理由。 +# 删除前置条件:确认设备上已无该窗口的落盘记录(或已接受历史记录部分渲染白框)。 +# ⚠ 本组真正的价值是把一条路径钉死:**「按 extract_cn 输出重写 ui_cn_chars.txt」= 洗字形** +# (extract 只认当前源码字面量,实测 144 字,而 txt 现在 183 字)——谁按它的输出覆盖 +# txt,就会把这 35 字连同上面 4 字一起洗掉,且只在渲染历史日志时才暴露、无日志无报错。 +# 成因与判据详述 → docs/门禁说明/font-charset.md §6.1 +_KEEP_HIST_SELFTEST = ("历史落盘记录用字(2026-09 窗口固件写入 /log/selftest.log 与" + " /log/run.log 的定长 64 B 中文描述;源码文案已改写、逐字出处不可靠" + " 复原,故整组保留——删任一即让对应历史行永久渲染空白且不可逆;" + " 成因与删除前置条件见 docs/门禁说明/font-charset.md §6.1)") +KEEP_EXTRA.update({c: _KEEP_HIST_SELFTEST for c in + "一位公分合外已年度延态总接显最月未求温状电示秒请跳载近道部配重长间闸障"}) + def load_tool(name): """按文件路径加载 tools/font_gen/.py(无包结构,importlib 直载)。 diff --git a/scripts/check_mem_numbers.py b/scripts/check_mem_numbers.py index ee38354e..154bc6a7 100644 --- a/scripts/check_mem_numbers.py +++ b/scripts/check_mem_numbers.py @@ -336,7 +336,13 @@ def main(argv): map_path = pathlib.Path(args.map) if not map_path.exists(): print(f"[FAIL] 找不到 .map 事实源:{map_path}", file=sys.stderr) - print(" 先跑一次 @build(keil-build skill)生成 .map。", file=sys.stderr) + print(" 先跑一次 @build(keil-build skill)生成 .map,再跑本门禁。", file=sys.stderr) + print(" 这不是代码缺陷:.map 是**每棵工作树各自的构建产物**(不入版本库),", + file=sys.stderr) + print(" 新开的工作树(task-worktree new)从未构建过时必然走到这里——先 @build", + file=sys.stderr) + print(" 一次即可;随后按需要跑 scripts/ram_baseline.py --update 回填台账。", + file=sys.stderr) return 1 data = parse_map(map_path) diff --git a/scripts/ram_baseline.py b/scripts/ram_baseline.py index bc7ae4b7..b868e0c0 100644 --- a/scripts/ram_baseline.py +++ b/scripts/ram_baseline.py @@ -247,7 +247,10 @@ def cmd_gate(args): def cmd_write(args): map_path = _map_or_skip(args) if map_path is None: - print("[FAIL] 找不到 .map 事实源,先 @build", file=sys.stderr) + print("[FAIL] 找不到 .map 事实源,先 @build(.map 是每棵工作树各自的构建产物,不入版本库;", + file=sys.stderr) + print(" 新开的 task-worktree 工作树从未构建过时必然走到这里,不是代码缺陷)", + file=sys.stderr) return 1 info = collect(map_path) if not LEDGER.exists(): @@ -293,7 +296,10 @@ def _top_field(top): def cmd_update(args): map_path = _map_or_skip(args) if map_path is None: - print("[FAIL] 找不到 .map 事实源,先 @build", file=sys.stderr) + print("[FAIL] 找不到 .map 事实源,先 @build(.map 是每棵工作树各自的构建产物,不入版本库;", + file=sys.stderr) + print(" 新开的 task-worktree 工作树从未构建过时必然走到这里,不是代码缺陷)", + file=sys.stderr) return 1 info = collect(map_path) gt = ram_report.parse_map(str(map_path))["grand_totals"] diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 93e7d0da..beab355a 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -102,7 +102,14 @@ set(UI_SOURCES ${UI_DIR}/ui_w_setlist.c ${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_w_soe.c # SOE 记录组件(道E task-14;reader 已在批 2 两端绑定) + ${UI_DIR}/ui_w_setview.c # 定值查看视图模型(道E task-15 批 3:四张视图表 + Q10 换算 + 越界反显) + ${UI_DIR}/ui_page_soe.c + ${UI_DIR}/ui_page_view_param.c + ${UI_DIR}/ui_page_view_plate.c + ${UI_DIR}/ui_page_view_set.c + ${UI_DIR}/ui_page_view_word.c + ${UI_DIR}/ui_page_yx.c ) # 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程) diff --git a/simulator/main.c b/simulator/main.c index ada85b66..fc894a9b 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 的装配结果) @@ -230,7 +274,12 @@ static void sim_publish_data_frames(void) { rt.ph[i].qds = UI_DATA_Q_IV; /* 未发布的槽:无效,页面显示 "--" */ } rt.yx_bits = 0u; /* 固定帧无动作态 */ - rt.yx_valid = 0x4043C000ULL; /* YX15~18/23/31 有效位图 */ + rt.yx_valid = 0x4043C000ULL; /* YX15~18/23/31 有效位图(与点表同集合) */ + /* 双点位图照写侧口径给(App/protocol/iec104/iec104_pts.c 的 YX_DP_N = 4 → bit0~3)。 + * 这 4 点**不在**上面的有效位图里(V0.1 无开入硬件),所以遥信页对它们打 "--"—— + * 刻意不把 bit0 塞进 yx_valid 让画面多两个汉字好看:那会让黄金参考显示成"这台设备 + * 有开关位置遥信",而真机上没有(屏上的话必须与本机一致,同页脚页码那条裁定) */ + rt.yx_dp = 0x0FULL; rt.freq_valid = 0u; /* eng=0 期间幅值门恒失效 → 无新鲜读数 */ rt.freq_hz = 0.0f; rt.clock_ok = 1u; @@ -303,6 +352,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 +751,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 4952433f..57f5c9ca 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -41,6 +41,19 @@ PAGES = [ ("info", "e"), # 装置信息(node 1) ("ac", "dee"), # 交流量(node 3,实时数据下第 1 项) ("dc", "dede"), # 直流量(node 4,实时数据下第 2 项) + # 遥信量(实时数据下第 3 项):d 到实时数据 → e 进(选中交流量)→ dd 到遥信量 → e 开页; + # yxp2 再多按 8 次下移,让选中行跨过页边界落到第 2 页首行 + ("yx", "dedde"), + ("yxp2", "dedde dddddddd"), + # SOE 记录(node 51,记录查询下第 1 项):6 下进记录查询 → e → 首项 SOE记录 → e; + # 数据 = simulator/main.c 的固定 3 条假记录(含 type 7 未标定闭锁这条非元件支路) + ("soe", "dddddde e"), + # 参数定值 → 查看 的四个只读页(批 3):dd 进"参数定值"→ e 进(选中"整定")→ d 到"查看" + # → e 进(选中"参数")→ e 开页;定值/控制字/软压板 依次多按 d 选到该行再 e + ("vparam", "ddedee"), + ("vset", "ddedede"), + ("vword", "ddededde"), + ("vplate", "ddededdde"), ("pwd", "ddeee"), # 密码页初始态:3 框 000、光标首框 ("pwe", "ddeee uru"), # 密码页调值+移光标:110、光标第 2 框 ("pwk", "ddeee uruuruuue"), # 密码页输 123:u / r / u,u / r / u,u,u / e → 关页进空白终点框(菜单屏) @@ -57,16 +70,182 @@ PAGES = [ # 只断言确定的内容;标签与值分列断言(两列 x 不同,不能拼成一串)。 # 覆盖本工程新增/易缺字的关键文案(网口设置/地址/掩码/逗号/保存/OK 等)。 TEXTS = { + # v2.0(道E task-15 回炉):6 行 Flash 字面量 -> 8 行真值;标签去全角冒号(要给值列 + # 让出 MAC 的 102 px),行槽 = 38 + 13i。值全部来自模拟器固定帧(simulator/main.c 的 + # sim_publish_data_frames)与 net_cfg 出厂副本 —— 逐行断言等于把这两处口径钉在像素侧。 "info": [ ("标题", "装置信息", (0, 24), False), ("副标题", "馈线自动化终端", (22, 40), False), - ("r0名", "终端类型:", (40, 55), False), ("r0值", "F30", (40, 55), False), - ("r1名", "终端型号:", (55, 70), False), ("r1值", "F30", (55, 70), False), - ("r2名", "软件版本:", (70, 85), False), ("r2值", "SV0.010", (70, 85), False), - ("r3名", "硬件版本:", (85, 100), False), ("r3值", "HW0.010", (85, 100), False), - ("r4名", "软件校验:", (100, 115), False), ("r4值", "4454", (100, 115), False), - ("r5名", "程序日期:", (115, 130), False), ("r5值", "2024.08.27", (115, 130), False), - ("页脚", "第 10 页,共 1 页", (140, 160), False), + ("r0名", "终端型号", (38, 51), False), ("r0值", "F30", (38, 51), False), + ("r1名", "软件版本", (51, 64), False), ("r1值", "SV0.010", (51, 64), False), + ("r2名", "硬件版本", (64, 77), False), ("r2值", "HW0.010", (64, 77), False), + # 软件校验:构建期 CRC 步骤未落地 -> "--"(曾抄参考项目的 "4454",那是别人家镜像) + ("r3名", "软件校验", (77, 90), False), ("r3值", "--", (77, 90), False), + ("r4名", "程序日期", (90, 103), False), ("r4值", "Oct 25 2026", (90, 103), False), + ("r5名", "MAC地址", (103, 116), False), + ("r5值", "00:00:00:00:00:00", (103, 116), False), + # net_cfg_fmt_ipv4 的既有口径是三段补零(与网口设置页同一格式化真源),照实断言 + ("r6名", "IP地址", (116, 129), False), ("r6值", "192.168.001.100", (116, 129), False), + ("r7名", "运行时长", (129, 142), False), ("r7值", "00:00:00", (129, 142), False), + ("页脚", "第 1 页,共 1 页", (140, 160), False), + ], + # 交流量:7 行快照真值(24 行参考图假骨架已删)。eng=0(未标定)期间值列是 ADC 引脚 + # 电压,第 7 行必须把口径写在屏上;UB/UC/IB/IC/IN 削顶无采样源,本页不列它们 + # (列出来就是 5 行 "--" 的假骨架,同 §3.1.1 的取舍理由)。Hz 未测出 -> "--"。 + "ac": [ + ("标题", "交流量", (0, 24), False), + ("r0名", "Ua", (24, 38), True), ("r0值", "0.812", (24, 38), True), + ("r1名", "U0", (38, 52), False), ("r1值", "0.005", (38, 52), False), + ("r2名", "Ia", (52, 66), False), ("r2值", "0.046", (52, 66), False), + ("r3名", "Ia2", (66, 80), False), ("r3值", "0.002", (66, 80), False), + ("r4名", "Ia5", (80, 94), False), ("r4值", "0.001", (80, 94), False), + ("r5名", "Hz", (94, 108), False), ("r5值", "--", (94, 108), False), + ("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), + ], + # ── 批 4:遥信量(33 点固定名表,5 页)── + # 第 1 页 8 行**全是 --**(位置 1~4 是无硬件的双点,YX5~8 点表未定义)——这不是画面坏了, + # 是 V0.1 的真实状态:27 个点没有开入来源。方案 B(只显有效点)会把这件事藏起来,方案 A + # 让现场能逐点对、也能一眼看出缺哪些点(取舍见 页面扩充详细方案.md §3.1.3)。 + # 第 2 页才出现有来源的点(故障总 / 过流Ⅰ段 = 复归);两页合起来同时钉住分页数学: + # 33 行 / 8 行每页 = 5 页,页码算错这里直接响。 + "yx": [ + ("标题", "遥信量", (0, 24), False), + ("r0名", "位置1", (24, 38), True), + ("r0值", "--", (24, 38), True), + ("r1名", "位置2", (38, 52), False), + ("r1值", "--", (38, 52), False), + ("r2名", "位置3", (52, 66), False), + ("r2值", "--", (52, 66), False), + ("r3名", "位置4", (66, 80), False), + ("r3值", "--", (66, 80), False), + ("r4名", "YX5", (80, 94), False), + ("r4值", "--", (80, 94), False), + ("r5名", "YX6", (94, 108), False), + ("r5值", "--", (94, 108), False), + ("r6名", "YX7", (108, 122), False), + ("r6值", "--", (108, 122), False), + ("r7名", "YX8", (122, 136), False), + ("r7值", "--", (122, 136), False), + ("页脚", "第 1 页,共 5 页", (140, 160), False), + ], + "yxp2": [ + ("标题", "遥信量", (0, 24), False), + ("r0名", "YX9", (24, 38), True), + ("r0值", "--", (24, 38), True), + ("r1名", "YX10", (38, 52), False), + ("r1值", "--", (38, 52), False), + ("r2名", "YX11", (52, 66), False), + ("r2值", "--", (52, 66), False), + ("r3名", "YX12", (66, 80), False), + ("r3值", "--", (66, 80), False), + ("r4名", "YX13", (80, 94), False), + ("r4值", "--", (80, 94), False), + ("r5名", "YX14", (94, 108), False), + ("r5值", "--", (94, 108), False), + ("r6名", "故障总", (108, 122), False), + ("r6值", "复归", (108, 122), False), + ("r7名", "过流Ⅰ段", (122, 136), False), + ("r7值", "复归", (122, 136), False), + ("页脚", "第 2 页,共 5 页", (140, 160), False), # 选中行落到第 2 页首行(YX9);页码同时钉住 33/8 = 5 页 + ], + # ── 批 3:参数定值 → 查看 四页(数据 = prot_set 出厂默认,值串带单位)── + # 名与值两列都断:值列断的是换算后的小数位与单位——光看像素,没人会去核 0.95 是不是被 + # Q10 存成了 0.94921875。控制字页还多钉一层:Ⅰ/Ⅱ 段默认是「跳闸」、Ⅲ 与过负荷段默认是 + # 「告警」,这种两两一组恰好把行表与枚举序对齐钉住(行表整体错位就不可能出现该组合) + "vparam": [ + ("标题", "参数", (0, 24), False), + ("r0名", "PT一次", (24, 38), True), + ("r0值", "10000V", (24, 38), True), + ("r1名", "PT二次", (38, 52), False), + ("r1值", "100V", (38, 52), False), + # r2 值我先前肉眼读成 "1000A",字形匹配器证明值列只有 4 个字符位(命中 x=101/107/113/119): + # CT 一次额定是 100 A —— PT 10000/100 V 加 CT 100/5 A 正是 10 kV 馈线终端的变比自洽。 + # 断言写"匹配器能证明的",不写"我看图觉得的"(这类差异肉眼发现不了,只有匹配器会响) + ("r2名", "CT一次", (52, 66), False), ("r2值", "100A", (52, 66), False), + ("r3名", "CT二次", (66, 80), False), + ("r3值", "5A", (66, 80), False), + ("r4名", "过量返回", (80, 94), False), + ("r4值", "0.95", (80, 94), False), + ("r5名", "欠量返回", (94, 108), False), + ("r5值", "1.05", (94, 108), False), + ("r6名", "过频返回", (108, 122), False), + ("r6值", "0.99", (108, 122), False), + ("r7名", "低频返回", (122, 136), False), + ("r7值", "1.01", (122, 136), False), + ("页脚", "第 1 页,共 1 页", (140, 160), False), + ], + "vset": [ + ("标题", "定值", (0, 24), False), + ("r0名", "Ⅰ段电流", (24, 38), True), + ("r0值", "10.00A", (24, 38), True), + ("r1名", "Ⅰ段延时", (38, 52), False), + ("r1值", "0ms", (38, 52), False), + ("r2名", "Ⅱ段电流", (52, 66), False), + ("r2值", "5.00A", (52, 66), False), + ("r3名", "Ⅱ段延时", (66, 80), False), + ("r3值", "500ms", (66, 80), False), + ("r4名", "Ⅲ段电流", (80, 94), False), + ("r4值", "2.00A", (80, 94), False), + ("r5名", "Ⅲ段延时", (94, 108), False), + ("r5值", "3000ms", (94, 108), False), + ("r6名", "过负荷电流", (108, 122), False), + ("r6值", "1.00A", (108, 122), False), + ("r7名", "过负荷延时", (122, 136), False), + ("r7值", "10000ms", (122, 136), False), + ("页脚", "第 1 页,共 3 页", (140, 160), False), # 18 行 = 3 页 + ], + "vword": [ + ("标题", "控制字", (0, 24), False), + ("r0名", "Ⅰ段跳闸", (24, 38), True), + ("r0值", "跳闸", (24, 38), True), + ("r1名", "Ⅱ段跳闸", (38, 52), False), + ("r1值", "跳闸", (38, 52), False), + ("r2名", "Ⅲ段跳闸", (52, 66), False), + ("r2值", "告警", (52, 66), False), + ("r3名", "过负荷跳闸", (66, 80), False), + ("r3值", "告警", (66, 80), False), + ("页脚", "第 1 页,共 1 页", (140, 160), False), + ], + "vplate": [ + ("标题", "软压板", (0, 24), False), + ("r0名", "Ⅰ段投入", (24, 38), True), + ("r0值", "投入", (24, 38), True), + ("r1名", "Ⅱ段投入", (38, 52), False), + ("r1值", "投入", (38, 52), False), + ("r2名", "Ⅲ段投入", (52, 66), False), + ("r2值", "投入", (52, 66), False), + ("r3名", "过负荷投入", (66, 80), False), + ("r3值", "投入", (66, 80), False), + ("r4名", "过压投入", (80, 94), False), + ("r4值", "投入", (80, 94), False), + ("r5名", "低压投入", (94, 108), False), + ("r5值", "投入", (94, 108), False), + ("r6名", "零序投入", (108, 122), False), + ("r6值", "投入", (108, 122), False), + ("r7名", "过频投入", (122, 136), False), + ("r7值", "投入", (122, 136), False), + ("页脚", "第 1 页,共 2 页", (140, 160), False), # 10 行 = 2 页,次页还有低频投入与保护总投入 + ], + # 直流量:V0.1 板上无直流采样通道 -> 温度取码值并显式带"码"前缀,通道行如实"未配置" + "dc": [ + ("标题", "直流量", (0, 24), False), + ("r0名", "温度", (24, 38), True), ("r0值", "码2048", (24, 38), True), + ("r1名", "直流通道", (38, 52), False), ("r1值", "未配置", (38, 52), False), + ("页脚", "第 1 页,共 1 页", (140, 160), False), ], "netcfg": [ ("标题", "网口设置", (0, 24), False), diff --git a/simulator/tests/ref/shot_ac.bin b/simulator/tests/ref/shot_ac.bin index ba2953f9..d5a4fa94 100644 Binary files a/simulator/tests/ref/shot_ac.bin and b/simulator/tests/ref/shot_ac.bin differ diff --git a/simulator/tests/ref/shot_dc.bin b/simulator/tests/ref/shot_dc.bin index 33d8060c..191d997f 100644 Binary files a/simulator/tests/ref/shot_dc.bin and b/simulator/tests/ref/shot_dc.bin differ diff --git a/simulator/tests/ref/shot_info.bin b/simulator/tests/ref/shot_info.bin index e5a2263c..b22e5c1a 100644 Binary files a/simulator/tests/ref/shot_info.bin and b/simulator/tests/ref/shot_info.bin differ diff --git a/simulator/tests/ref/shot_soe.bin b/simulator/tests/ref/shot_soe.bin new file mode 100644 index 00000000..1a9c92a8 Binary files /dev/null and b/simulator/tests/ref/shot_soe.bin differ diff --git a/simulator/tests/ref/shot_vparam.bin b/simulator/tests/ref/shot_vparam.bin new file mode 100644 index 00000000..81067fa5 Binary files /dev/null and b/simulator/tests/ref/shot_vparam.bin differ diff --git a/simulator/tests/ref/shot_vplate.bin b/simulator/tests/ref/shot_vplate.bin new file mode 100644 index 00000000..4bdc3ea9 Binary files /dev/null and b/simulator/tests/ref/shot_vplate.bin differ diff --git a/simulator/tests/ref/shot_vset.bin b/simulator/tests/ref/shot_vset.bin new file mode 100644 index 00000000..f78d7395 Binary files /dev/null and b/simulator/tests/ref/shot_vset.bin differ diff --git a/simulator/tests/ref/shot_vword.bin b/simulator/tests/ref/shot_vword.bin new file mode 100644 index 00000000..48bea912 Binary files /dev/null and b/simulator/tests/ref/shot_vword.bin differ diff --git a/simulator/tests/ref/shot_yx.bin b/simulator/tests/ref/shot_yx.bin new file mode 100644 index 00000000..3cb5a750 Binary files /dev/null and b/simulator/tests/ref/shot_yx.bin differ diff --git a/simulator/tests/ref/shot_yxp2.bin b/simulator/tests/ref/shot_yxp2.bin new file mode 100644 index 00000000..f482680e Binary files /dev/null and b/simulator/tests/ref/shot_yxp2.bin differ diff --git a/test/host/ui_data/ui_data_test.c b/test/host/ui_data/ui_data_test.c index c4490f94..a1d74d70 100644 --- a/test/host/ui_data/ui_data_test.c +++ b/test/host/ui_data/ui_data_test.c @@ -287,15 +287,26 @@ static void tc_yx_points(void) ui_data_rt_t src; ui_data_rt_t got; + const uint64_t dp_expect = 0x0FULL; /* V0.1 双点段 = bit0~3(YX1~4 开关位置类) */ + ui_data_init(); fill_rt(&src, 5u); src.yx_bits = 0u; /* 全部点位"分/未动作" */ src.yx_valid = valid_expect; + src.yx_dp = dp_expect; (void)ui_data_publish_rt(&src); (void)memset(&got, 0, sizeof(got)); (void)ui_data_rt(&got); CHECK(got.yx_valid == valid_expect, "TC-UIDATA-009a valid mask == YX15/16/17/18/23/31"); CHECK(got.yx_bits == 0u, "TC-UIDATA-009b bits and valid are independent fields"); + /* 双点位图必须原样透传:页面按它选词(合位/分位 vs 动作/复归),快照层若做"归一化" + * 或按 valid 掩一次,页面就会把双点当单点形容——同一条事实两处解释必然漂 */ + CHECK(got.yx_dp == dp_expect, "TC-UIDATA-009c dp mask passed through unmasked"); + /* 点表与名表共用点数(1 基 IOA 1~33)。这里是**有意**再抄一遍字面量:测试的职责是 + * 独立见证,协议层那个 #define 在 .c 内不可见,改点数时必须有人被绊一下——绊到后 + * 要同时改:App/protocol/iec104/iec104_pts.c 的 YX_N、ui_data.h 的 UI_DATA_YX_N、 + * 页面名表 ui_page_yx.c(有编译期数组守卫兜着)、本条 */ + CHECK((uint32_t)UI_DATA_YX_N == 33u, "TC-UIDATA-009d point count is 33 (YX_N witness)"); } /* ===================== TC-UIDATA-010:相量槽透传 ===================== */ @@ -392,7 +403,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 +504,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/tools/setview_check/check_setview_limits.py b/tools/setview_check/check_setview_limits.py new file mode 100644 index 00000000..7152ae5b --- /dev/null +++ b/tools/setview_check/check_setview_limits.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +""" +模块名称:定值查看行表一致性检查 +模块功能:把 Drivers/BSP/UI/ui_w_setview.c 的四张视图表当数据核,比对真源 + App/algo/prot_set.h 的枚举序与 App/algo/prot_set.c 的 g_prot_set_meta: + ① id 必须恰好覆盖 0..39 各一次(漏 / 重 / 错位都会在这里炸;页面上只有当前 + 可见的 8 行会被像素断言看到,错位要翻到第 2、3 页才可能被人发现) + ② 数值行 min/max 必须等于 meta 上下限按该行小数位换算后的值——行表里的上下限 + 就是从 meta 换算来的,本脚本是那条换算的可重跑证明(改 meta 不改行表就红) + ③ 二态行必须带串表、数值行必须不带;单位串只允许 ASCII(不进中文字库) + ④ 小数位与 id 查表一致(组件 get 只收到 id,两处不一致就会显示错位数字) +口径修正记录(不是静默豁免,RATIO_IDS 仍单独核):四个返回系数按 2 位小数显示——文档 +§3.2.2 原写 3 位,2026-10-25 已按 Q10 可分辨精度把表改成 2 位并加 ⚠(同一节有落地注)。 +理由是 Q10 存不下 0.95:0.95*1024 截成 972,真存 0.94921875,按 3 位显示会打出一位根本 +不存在的数字——整定单写 0.95 的人看到 0.949 会怀疑装置,甚至反复重输(重输还是 0.949)。 +用法:在仓库根执行 + .venv/Scripts/python.exe tools/setview_check/check_setview_limits.py +退出码:0 = 一致;1 = 不一致(打印具体行与期望值);2 = 解析锚点丢失(结构改了,脚本要跟着改) +创建日期:2026-10-25(道E task-15 批 3) +""" +import re +import sys + +# Windows 控制台默认按 CP936 写 stdout:本脚本要打印中文,先把输出锁到 UTF-8——否则同一份 +# 脚本在别人的机器上会崩在半路(崩在半路 = 一条都没检查出来,比 FAIL 更难发现) +try: + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + sys.stderr.reconfigure(encoding="utf-8", errors="replace") +except Exception: + pass + +ENUM_H = "App/algo/prot_set.h" +META_C = "App/algo/prot_set.c" +TABLES = "Drivers/BSP/UI/ui_w_setview.c" +RATIO_IDS = (25, 26, 38, 39) # 四个返回系数:按 2 位小数显示(理由见文档串开头) + + +def q10(tok): + m = re.match(r"PROT_SET_Q10\(([-0-9.]+)\)$", tok.strip()) + v = float(m.group(1)) if m else float(tok.strip()) + return int(round(v * 1024)) if m else int(v) + + +def scaled(v, d): + ip = v >> 10 + fp = v & 0x3FF + p10 = 10 ** d + fr = (fp * p10 + 512) >> 10 + if fr >= p10: + ip += 1 + fr -= p10 + return ip * p10 + fr + + +def load_meta(): + h = open(ENUM_H, encoding="utf-8").read() + body = h.split("typedef enum {")[1].split("PS_ID_COUNT")[0] + ids = [x.strip() for x in re.findall(r"^\s+(PS_[A-Z0-9_]+)[, ]", body, re.M)] + c = open(META_C, encoding="utf-8").read() + blk = c.split("g_prot_set_meta[PS_ID_COUNT] = {")[1].split("\n};")[0] + blk = re.sub(r"/\*.*?\*/", "", blk, flags=re.S) + ent = re.findall(r"\{([^{}]*)\}", blk, re.S) + if len(ent) != len(ids): + print("[FATAL] 枚举 %d 项 / meta %d 项不一致" % (len(ids), len(ent))) + return None + meta = {} + for name, e in zip(ids, ent): + p = [x.strip() for x in e.split(",")] + if len(p) != 5: + print("[FATAL] %s 的 meta 行解析成 %d 段" % (name, len(p))) + return None + meta[name] = (1 if p[1].startswith("1") else 0, q10(p[2]), q10(p[3]), q10(p[4])) + return (ids, meta) + + +def load_tables(): + t = open(TABLES, encoding="utf-8").read() + rows = [] + # 行表一行的形状:{ "名", NNu, UI_W_SETLIST_KIND_XXX, Du, min, max, opts, unit }, /* PS_名 */ + # opts/unit 两种写法都要吃下:NULL 或 "V" 这类带引号串;用 \S+ 而不是 \w+,否则带引号 + # 的单位匹配不上会让整行**静默跳过**(少检查一行比检查错更难发现) + pat = re.compile(r"^\s*\{\s*\"(.*?)\",\s*(\d+)u,\s*UI_W_SETLIST_KIND_(NUM|ENUM),\s*(\d+)u,\s*(-?\d+),\s*(-?\d+),\s*(\S+),\s*(\S+?)\s*\},\s*/\*\s*(PS_[A-Z0-9_]+)\s*\*/", re.M) + for m in pat.finditer(t): + rows.append((m.group(9), int(m.group(2)), m.group(3), int(m.group(4)), + int(m.group(5)), int(m.group(6)), m.group(7), m.group(8))) + dm = re.search(r"s_dec\[UI_DATA_SET_N\] = \{(.*?)\};", t, re.S) + if dm is None or not rows: + print("[FATAL] 行表/查表锚点解析失败(行表写法改了要同步改本脚本)") + return None + dec = [int(x) for x in re.findall(r"\d+", dm.group(1))] + return (rows, dec) + + +def main(): + lm = load_meta() + if lm is None: + return 2 + ids, meta = lm + lt = load_tables() + if lt is None: + return 2 + rows, dec = lt + bad = [] + seen = [] + if len(rows) != len(ids): + print("[FAIL] 行表解析到 %d 行,但枚举有 %d 项——正则与行表写法脱节了,宁可报错也不要少检" % (len(rows), len(ids))) + return 1 + for ps, rid, kind, d, mn, mx, opts, unit in rows: + if rid < 0 or rid >= len(ids): + bad.append("%s: id %d 越界" % (ps, rid)); continue + if ids[rid] != ps: + bad.append("%s: id=%d 实际指向 %s(枚举序对不上)" % (ps, rid, ids[rid])) + seen.append(rid) + is_ctl = meta[ps][0] + if kind == "ENUM": + if opts == "NULL": + bad.append("%s: 二态行没有串表" % ps) + if dec[rid] != 0: + bad.append("%s: 二态项在 dec 表里应为 0,实为 %d" % (ps, dec[rid])) + if not is_ctl: + bad.append("%s: meta 说它是模拟量项,行表却当二态行" % ps) + else: + if opts != "NULL": + bad.append("%s: 数值行不该带串表" % ps) + if dec[rid] != d: + bad.append("%s: 行表小数位 %d 与 dec 查表 %d 不一致" % (ps, d, dec[rid])) + emn = scaled(meta[ps][2], d) + emx = scaled(meta[ps][3], d) + if (mn, mx) != (emn, emx): + bad.append("%s: 上下限 (%d,%d) != meta 换算 (%d,%d)(d=%d)" % (ps, mn, mx, emn, emx, d)) + if is_ctl: + bad.append("%s: meta 说它是投退/枚举项,行表却当数值行" % ps) + if unit != "NULL": + u = "" + if unit.startswith('"'): + u = unit.strip('"') + else: + bad.append("%s: 单位字段写法不认识:%s" % (ps, unit)) + if u and any(ord(ch) > 127 for ch in u): + bad.append("%s: 单位含非 ASCII 字符 %r(会进中文字库,上屏还可能缺字)" % (ps, u)) + if sorted(seen) != list(range(len(ids))): + dup = [x for x in set(seen) if seen.count(x) > 1] + miss = sorted(set(range(len(ids))) - set(seen)) + bad.append("id 覆盖不完整:重复 %s 缺失 %s" % (dup, miss)) + ratios = [r for r in seen if r in RATIO_IDS] + if sorted(ratios) != sorted(RATIO_IDS): + bad.append("四个返回系数(%s)没有全在数值视图里,偏离白名单需要同步更新" % (RATIO_IDS,)) + for m in bad: + print("[FAIL] " + m) + print("行表 %d 行 / dec %d 项 / meta %d 项 -> %s" % (len(rows), len(dec), len(ids), "OK" if not bad else "FAIL")) + return 1 if bad else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/ui_menu_tree.txt b/ui_menu_tree.txt index 4a97fe12..3a7436ea 100644 --- a/ui_menu_tree.txt +++ b/ui_menu_tree.txt @@ -21,7 +21,7 @@ 实时数据 交流量 #page=ac 直流量 #page=dc - 遥信量 + 遥信量 #page=yx 参数定值 整定 参数 #P @@ -29,10 +29,10 @@ 控制字 #P 软压板 #P 查看 - 参数 - 定值 - 控制字 - 软压板 + 参数 #page=view_param + 定值 #page=view_set + 控制字 #page=view_word + 软压板 #page=view_plate 三遥设置 遥测死区 #P 遥测系数 #P @@ -49,7 +49,7 @@ 网口设置 #P #page=net_cfg SNTP设置 #P 记录查询 - SOE记录 + SOE记录 #page=soe 事故记录 操作记录 保护告警