feat(UI): 装置维护 3 页落地(时钟 / 强制复归 / 清除记录),内容页黄金 27→40
三个薄页面模块 ui_page_clock / ui_page_reset / ui_page_clear,页面注册由生成器 自动写双端(裁定 A-1:「三叶各绑一页」与「零新增 .c」不可兼得,选薄页面加生成器, 不写手写 node switch——那种分发在增删叶子时会静默换功能,且没有任何门禁照得到)。 数据侧是注入式函数表:UI 不 include App 层的头。两条硬理由都实测过——分层方向 (layer-deps)之外,模拟器根本不链 sys_clock,ALLOW 豁免挡得住 layer-deps, 挡不住模拟器链接期。面板对时的唯一真判据是回读 RTC:sys_clock_set() 恒返 0 (RTC 写失败只打日志),所以真机侧写 1 后回读 1,最坏约 200 ms 落在一次「保存」 按键上,IWDG 800 ms 窗有余;上界与依据都写进注释。 「无时钟」徽标位置有两个像素级实测坑:放标题带右侧会被 4 字标题连成「时钟设置 无时钟」一句话;放页脚带左侧 x=7 会压住页脚——页脚从 x=6 起排,不是居中。 最终落在清单末行与页脚之间的空带 y=124。clear_done 那张顺带给出像素证据: 清 run.log 之后「操作记录」那一行从 2 变 3,页面上没缓存、没撒谎。 ui_w_records 的 UI_W_REC_LOG_* 顺序必须等于 syslog_id_t 顺序(7 类,只准尾追加), 已加编译期顺序守卫;执行侧 7 条映射表同样带编译期守卫。 测试与真值:内容页黄金 27 → 40 页(+13),文本断言 +68、反向断言 ABSENT +19; 27 张旧参考 git diff 为空 = 逐字节未变。往返池 5448 B 与菜单常驻堆 5368 B 均未变; 字库未扩(源码在用 171 并 KEEP_EXTRA 39 = 清单 183),font-charset 与 cn-chars、 cn-font 字节一致。@build 0 错误 0 警告;uiTask 静态最坏链 1,336 B / 16,384 (余 91%)与基线相同,页面钩子没进最坏链。 一条测试学结论单独留痕:反向断言不能当「位置不重叠」的证据——本脚本的字形匹配 把 y 带判得比字面宽(徽标在 125..136、页脚在 144..155,仍被 (140,160) 判命中)。 该防线改由正向断言「页脚未被徽标压住」承担(压字那一版实测过不了它)。 两条自我更正一并记档(作者自报,按真值而非原口径): ① 先前报「文案零新增汉字」是错的——观测行拟用「动作元件」,「元」不在子集, cn-chars 与 font-charset 如实红;最终改「保护动作 N」才让「零新增」成为事实。 顺序是:先按机器红,再按真值写结论。 ② 先前报「6 个文件 BOM 逐个核过」被 bom-guard 证伪:丢 BOM 发生在上一次编译之后, 正是 AGENTS 那句「历史 0 警告不代表安全」的活例;此后一律以退出码为准。 未覆盖项不假装:台架「N>0 → N=0」在产品构建的未标定闭锁期拿不到(闭锁期 N 恒 0), 判据显式退化为「页面受理 + 落一条操作记录」,写在 rs_watch 注释里。 生成物入库例外照用:uvprojx 只加 3 个 ui_page_ 的 FilePath(真增删源文件), CMakeLists 同批;两处都由 gen_menu.py 自己写,没手写。 本笔先走了一次真钩子:20 条钩子实跑通过(ui-golden 菜单 8 状态加内容页 40 页、 generated-fresh、registration-check、font-charset、ui-data 单测、cppcheck、 coding-standard、bom、mojibake、id-check、doc-links 等),只有 mem-numbers 与 mem-budget 两条红——磁盘上的 .map 出自 20:39 那次构建、含三条在飞道的代码, 与 HEAD 台账必然不同码。按仓库既有约定(这两条等台账回填时允许 --no-verify 并 正文留证)落这一笔;回填由我统一在收尾一笔做。 三条在飞道(task-26 崩溃证据路径、task-16 与 task-17 谓词收敛和卷锁观测、 task-36 台架工具)的未暂存改动被 autostash 暂时收走,钩子看到的是 HEAD 加本笔; 过程中曾把 comm 道未跟踪的 iec104_comm.c 临时移出(它不在 stash 覆盖面内), 那次移出反而让 ui-golden 撞见 CMake 缓存里的悬空引用而红——已改用「先落 comm 那笔、再落本笔」的顺序消掉这个中间态。ui_data_maint_ready() 待删(只有测试在读 的自检口),排在本笔之后单独一笔。
This commit is contained in:
+222
-16
@@ -26,6 +26,12 @@
|
||||
* 按位连击判据 = docs/子系统设计/看门狗设计说明.md §5.3)。
|
||||
* 单个往返自身仍无上界——该上界与彻底解耦(事件驱动
|
||||
* accept)归登记簿「net 层跨上下文收口」(批 3),本票只做心跳续命。
|
||||
* v1.5 2026-10-26 DTU 项目组 道F task-3 装置维护三页的执行侧:新增
|
||||
* ui_data_maint_ops_t 六件适配(时钟=写+回读 RTC、
|
||||
* 复归=prot_elm_request_reset、清除=syslog_request_clear
|
||||
* /clear_state 直通)并在 ui_menu_init 之前注入;记录
|
||||
* reader 的 2 条 if 链改成 7 条映射表 s_log_map,并加
|
||||
* 编译期守卫把 UI_W_REC_LOG_* == syslog_id_t 枚举序钉死
|
||||
*/
|
||||
#include "ui_task.h"
|
||||
|
||||
@@ -47,6 +53,7 @@
|
||||
#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 "prot_elm.h" /* 道F task-3 装置维护:强制复归请求 + 动作态元件位图 */
|
||||
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
|
||||
#include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */
|
||||
#include "task_watchdog.h"
|
||||
@@ -65,38 +72,231 @@ static void ui_task_time_cb(char *buf, size_t len) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:记录 reader 适配(task-6 注入)——UI 不透明编号 0/1 → syslog_id_t 映射
|
||||
* UI 记录源编号 → syslog_id_t 映射表(道F task-6 起 7 类,task-3 装置维护「清除记录」
|
||||
* 要在 7 行上分行显示;表长 = UI_W_REC_LOG_N,两端映射表同源同长)
|
||||
*
|
||||
* ⚠ 顺序契约(Lead 裁定 2026-10-26):UI_W_REC_LOG_* 逐字等于 syslog_id_t 枚举序,
|
||||
* 只准尾部追加——枚举序就是 App/log 两端 s_paths[]/s_caps[] 的下标,中间插值会
|
||||
* 安静地把事故读成告警。下面的编译期守卫就是这条契约在本端的机械防线(记录侧
|
||||
* 已有 TC-SYSLOG-130/131/131b,UI 侧对应的一条在 test/host/ui_data TC-UIDATA-024,
|
||||
* 两端各钉一次,谁中间插号谁编译不过 / 单测红)。
|
||||
* 即便如此,适配层**始终显式逐项映射**、从不把 UI 编号强转成 syslog_id_t:
|
||||
* 等值是今天的事实,不是页面可以依赖的契约(UI_W_REC_LOG_* 对页面仍是不透明号)。
|
||||
*/
|
||||
static const syslog_id_t s_log_map[UI_W_REC_LOG_N] = {
|
||||
SYSLOG_RUN, SYSLOG_SELFTEST, SYSLOG_ACCIDENT, SYSLOG_ALARM,
|
||||
SYSLOG_START, SYSLOG_OPER, SYSLOG_YK
|
||||
};
|
||||
|
||||
#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)
|
||||
typedef char ui_task_log_order_check_t[
|
||||
((UI_W_REC_LOG_RUN == (uint8_t)SYSLOG_RUN) &&
|
||||
(UI_W_REC_LOG_SELFTEST == (uint8_t)SYSLOG_SELFTEST) &&
|
||||
(UI_W_REC_LOG_ACCIDENT == (uint8_t)SYSLOG_ACCIDENT) &&
|
||||
(UI_W_REC_LOG_ALARM == (uint8_t)SYSLOG_ALARM) &&
|
||||
(UI_W_REC_LOG_START == (uint8_t)SYSLOG_START) &&
|
||||
(UI_W_REC_LOG_OPER == (uint8_t)SYSLOG_OPER) &&
|
||||
(UI_W_REC_LOG_YK == (uint8_t)SYSLOG_YK) &&
|
||||
(UI_W_REC_LOG_N == (uint8_t)SYSLOG_COUNT)) ? 1 : -1];
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 函数功能:记录 reader 适配——UI 不透明编号 → syslog_id_t 后取条数
|
||||
* 入口参数:log_id - UI 记录源编号(UI_W_REC_LOG_*) uint8_t
|
||||
* 返回值:记录条数;未知编号返回 0(页面退化为空态) uint16_t
|
||||
* 限定条件:uiTask 上下文同步调用(记录页绘制路径,与合并前直调同形)
|
||||
* 返回值:记录条数;未知编号返回 0(页面退化为空态,与合并前直调同形) uint16_t
|
||||
* 限定条件:uiTask 上下文同步调用(记录页与清除记录页的绘制路径,无 I/O 之外开销:
|
||||
* syslog_count 读的是文件长度/内存计数,不逐条读盘)
|
||||
*/
|
||||
static uint16_t ui_task_rec_count(uint8_t log_id) {
|
||||
if (log_id == UI_W_REC_LOG_RUN) {
|
||||
return (uint16_t)syslog_count(SYSLOG_RUN);
|
||||
if (log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||||
return 0u;
|
||||
}
|
||||
if (log_id == UI_W_REC_LOG_SELFTEST) {
|
||||
return (uint16_t)syslog_count(SYSLOG_SELFTEST);
|
||||
}
|
||||
return 0u;
|
||||
return (uint16_t)syslog_count(s_log_map[log_id]);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:记录 reader 适配(task-6 注入)——读记录字段直通 sys_log(错误码不改写)
|
||||
* 函数功能:记录 reader 适配——读记录字段直通 sys_log(错误码不改写)
|
||||
* 入口参数:log_id/idx/ts/ts_len/desc/desc_len - 契约见 ui_w_records.h ui_rec_reader_t
|
||||
* 返回值:0 成功;-1 编号/序号越界(未知编号同 -1);-2 读失败 int
|
||||
* 限定条件:同上
|
||||
*/
|
||||
static int ui_task_rec_get(uint8_t log_id, uint32_t idx, char *ts, uint32_t ts_len,
|
||||
char *desc, uint32_t desc_len) {
|
||||
if (log_id == UI_W_REC_LOG_RUN) {
|
||||
return syslog_get_record(SYSLOG_RUN, idx, ts, (size_t)ts_len, desc, (size_t)desc_len);
|
||||
if (log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||||
return -1;
|
||||
}
|
||||
if (log_id == UI_W_REC_LOG_SELFTEST) {
|
||||
return syslog_get_record(SYSLOG_SELFTEST, idx, ts, (size_t)ts_len, desc, (size_t)desc_len);
|
||||
}
|
||||
return -1;
|
||||
return syslog_get_record(s_log_map[log_id], idx, ts, (size_t)ts_len, desc,
|
||||
(size_t)desc_len);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* ============================ 道F task-3:装置维护写通道(UI 注入执行侧)
|
||||
*
|
||||
* 为什么在这一层实现而不是在页面里:页面属 Drivers/BSP/UI,反调 App 侧的
|
||||
* sys_clock / prot_elm / sys_log 是分层方向的反例(layer-deps 拦),更关键的是
|
||||
* **模拟器根本不链 sys_clock**(simulator/dbg_log.h:4「模拟器无 sys_clock/RTC」)——
|
||||
* 页面里用一次就是"MCU 编得过、模拟器链接期爆"。写动作一律走 ui_data_maint_ops_t,
|
||||
* 两端各给实现(口径 docs/UI设计/UI_数据供给设计说明.md §6.2/§7.2)。
|
||||
*
|
||||
* uiTask 阻塞上界(IWDG 800ms 窗、本任务在 required_mask 内):本通道里只有
|
||||
* 时钟设置会碰硬件——sys_clock_set_unix 内含 1 次 SD2506 写,再叠加本适配层刻意
|
||||
* 加的回读复核 1 次读,各 SD2506_I2C_TIMEOUT_MS=100ms ⇒ 最坏 ≈200ms 落在一次
|
||||
* "保存"按键上(心跳在循环尾,仍远小于 800ms;对时任务同样在本机上做过这件事)。
|
||||
* 复归/清除两条只写内存标志或投递给别的任务,无 I/O。
|
||||
*/
|
||||
|
||||
/*
|
||||
* 函数功能:设置墙钟并**回读复核**(真机 = 写 RAM 基准 + 写 SD2506 RTC)
|
||||
* 入口参数:a_epoch - Unix 秒(本地时间域,与 sys_clock 同一口径,无时区偏移) uint32_t
|
||||
* 返回值:0 - 已生效且 RTC 回读一致;-1 - 越界 / 未生效 / RTC 回读不符 int
|
||||
* 限定条件:uiTask 上下文(页面"保存"按键直达,同步返回终态)
|
||||
* 函数说明:⚠ **sys_clock_set() 恒返 0**(App/util/sys_clock.c:462 起:RTC 写失败
|
||||
* 只打 DBG_ERROR,不改返回值),所以本函数不能拿它的返回值当结论——
|
||||
* 回读是唯一真判据;而只回读 sys_clock_get() 证明的仅是"RAM 基准换了",
|
||||
* 面板对时的用户意图是**掉电后还要是对的**,故必须回读 RTC 本身。
|
||||
* RTC 坏了但墙钟已改时本函数返 -1(屏上"保存行=ERR + 值列跟着新时间",
|
||||
* 两个事实各说各的,比笼统报 OK 诚实);秒位允许 ±3 与"分/时/日回绕"两种
|
||||
* 形态,因为两次读之间墙钟与 RTC 都在走。
|
||||
* 上限 UI_DATA_CLOCK_EPOCH_MAX(2038) 在 UI 层已挡,这里再挡一次:
|
||||
* 执行侧不信任调用侧(0 还兼作"未建钟"哨兵,面板永远不该发 0)
|
||||
*/
|
||||
static int ui_task_maint_clock_set(uint32_t a_epoch) {
|
||||
sd2506_time_t want;
|
||||
sd2506_time_t got;
|
||||
uint32_t now;
|
||||
int ds;
|
||||
|
||||
if ((a_epoch == 0u) || (a_epoch > (uint32_t)UI_DATA_CLOCK_EPOCH_MAX)) {
|
||||
return -1;
|
||||
}
|
||||
if (sys_clock_set_unix(a_epoch) != 0) {
|
||||
return -1; /* 今天到不了这条腿(sys_clock_set 恒 0),留着是因为它是契约 */
|
||||
}
|
||||
now = sys_clock_get();
|
||||
if ((now == UINT32_MAX) || ((now + 2u) < a_epoch) || (now > (a_epoch + 2u))) {
|
||||
return -1; /* RAM 基准没跟上:连"现在就对"都没成立 */
|
||||
}
|
||||
if (sys_clock_get_time(&want) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (sd2506_get_time(&got) != BSP_OK) {
|
||||
return -1; /* RTC 读不动:掉电保持这件事无法证明 */
|
||||
}
|
||||
if ((got.year != want.year) || (got.month != want.month) || (got.day != want.day) ||
|
||||
(got.hour != want.hour) || (got.minute != want.minute)) {
|
||||
return -1;
|
||||
}
|
||||
ds = (int)got.second - (int)want.second;
|
||||
if (ds < 0) {
|
||||
ds = -ds;
|
||||
}
|
||||
if ((ds > 3) &&
|
||||
!(((want.second >= 58u) && (got.second <= 3u)) ||
|
||||
((got.second >= 58u) && (want.second <= 3u)))) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:请求强制复归(跨上下文:protectTask 下一拍消费)
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 已投递(prot_elm_request_reset 无拒绝分支) int
|
||||
* 限定条件:uiTask 上下文;不等待执行结果
|
||||
* 函数说明:只投递不阻塞(契约:UI 页面二次确认后调,页面立刻给"已请求复归")。
|
||||
* **不在这里投操作记录**:远端 104 YK11 那条已经在
|
||||
* App/protocol/iec104/iec104_pts.c 投过,在这里再投一次就会给每一次
|
||||
* 主站复归多记一条"面板复归",审计面直接失真。本地这条由页面投
|
||||
* (只有页面知道"这是人按的"),措辞也不同串:见 Drivers/BSP/UI/ui_page_reset.c
|
||||
*/
|
||||
static int ui_task_maint_reset_req(void) {
|
||||
prot_elm_request_reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取当前处于动作态的保护元件个数(复归页观测行 "保护动作 N" 的 N)
|
||||
* 入口参数:无
|
||||
* 返回值:位图中置 1 的位数(纯 RAM 读,无锁) uint32_t
|
||||
* 限定条件:uiTask 绘制路径每帧可直调
|
||||
* 函数说明:给个数而不是位图,是为了让 160x160 上一行放得下、也让"复归前 N>0 →
|
||||
* 复归后 N=0"这个台架判据肉眼可读;判据前提(产品构建处于未标定闭锁,
|
||||
* App/task/protect_task.c:98 停走判据 ⇒ 闭锁期 N 恒 0)已写进页面注释
|
||||
*/
|
||||
static uint32_t ui_task_maint_active_count(void) {
|
||||
uint32_t mask = prot_elm_active_mask();
|
||||
uint32_t n = 0u;
|
||||
|
||||
while (mask != 0u) {
|
||||
n += (mask & 0x1u);
|
||||
mask >>= 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:投一条本地操作记录(面板动作留痕)
|
||||
* 入口参数:a_desc - 描述串(ASCII,长度须 <= SYSLOG_DESC_MAX=28) const char *
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 上下文;只在动作真正被受理之后由页面调用
|
||||
* 函数说明:用 quiet 口(与 iec104_serv.c 对时留痕同一选择):uiTask 不属于深链
|
||||
* 任务,但 quiet 口的收/丢判据与 syslog_post 一致且不在调用链上摊开
|
||||
* 行缓冲,留痕这件事没理由选更贵的那个口。
|
||||
* 丢条不回报页面:syslog 侧有累计丢弃计数(syslog_post_quiet_dropped())
|
||||
* 可查;页面上"动作做了没"与"这条审计写没写"是两件事,不该混在一个
|
||||
* 态标里——把后者做成 ERR 会让人以为复归没做而再按一次
|
||||
*/
|
||||
static void ui_task_maint_oper_note(const char *a_desc) {
|
||||
if (a_desc == NULL) {
|
||||
return;
|
||||
}
|
||||
syslog_post_quiet(SYSLOG_OPER, a_desc);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:请求清除某一类记录(只投递,落地在 defaultTask 的 syslog_drain 尾部)
|
||||
* 入口参数:a_log_id - UI 记录源编号(UI_W_REC_LOG_*) uint8_t
|
||||
* 返回值:0 - 已受理(MCU 端随后通常是 SYSLOG_CLEAR_WAIT);-1 - 越界编号 int
|
||||
* 限定条件:uiTask 上下文
|
||||
* 函数说明:本层是 UI 编号 → syslog_id_t 的**唯一**映射点之一(与 s_log_map 同表),
|
||||
* 绝不在 UI 侧做 f_unlink / 也不调 MCU 专属的 syslog_clear_apply():
|
||||
* 记录卷的属主是 defaultTask,uiTask 去碰 FAT 既违反唯一写者,又给
|
||||
* IWDG required_mask 内的任务添一条无上界的存储路径(口径 App/log/README.md)
|
||||
*/
|
||||
static int ui_task_maint_clear_req(uint8_t a_log_id) {
|
||||
if (a_log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||||
return -1;
|
||||
}
|
||||
return (syslog_request_clear(s_log_map[a_log_id]) == 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取上条清除请求的状态(值直通,不改写、不翻译)
|
||||
* 入口参数:a_log_id - UI 记录源编号 uint8_t
|
||||
* 返回值:UI_DATA_CLEAR_ST_NONE/_OK/_WAIT/_FAIL;越界编号按 NONE int
|
||||
* 限定条件:uiTask 绘制路径每帧可直调(纯内存状态读)
|
||||
* 函数说明:SYSLOG_CLEAR_* 与 UI_DATA_CLEAR_ST_* 逐值相等(0/1/-1/-2),由
|
||||
* test/host/ui_data TC-UIDATA-024 钉住;这里保持直通,是为了让页面
|
||||
* 显示的就是日志侧的事实,而不是"适配层以为的事实"
|
||||
*/
|
||||
static int ui_task_maint_clear_state(uint8_t a_log_id) {
|
||||
if (a_log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||||
return UI_DATA_CLEAR_ST_NONE;
|
||||
}
|
||||
return syslog_clear_state(s_log_map[a_log_id]);
|
||||
}
|
||||
|
||||
/* 维护通道函数表(六件齐全;ui_data_maint_bind 拒绝残表,缺件就是装配错) */
|
||||
static const ui_data_maint_ops_t s_maint_ops = {
|
||||
ui_task_maint_clock_set,
|
||||
ui_task_maint_reset_req,
|
||||
ui_task_maint_active_count,
|
||||
ui_task_maint_oper_note,
|
||||
ui_task_maint_clear_req,
|
||||
ui_task_maint_clear_state
|
||||
};
|
||||
|
||||
static const ui_rec_reader_t s_rec_reader = { ui_task_rec_count, ui_task_rec_get };
|
||||
|
||||
/*
|
||||
@@ -225,6 +425,12 @@ void ui_task_func(void *argument) {
|
||||
* 但契约把"启动期先绑定"写死;未绑定窗口组件按空态防御——见 ui_w_records.h) */
|
||||
ui_w_records_bind(&s_rec_reader);
|
||||
ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页数据入口(未绑定按"无记录"空态) */
|
||||
/* 装置维护写通道注入(道F task-3:时钟设置 / 强制复归 / 清除记录):同样必须先于
|
||||
* ui_menu_init。未注入的窗口里六个入口一律安全返回(页面显 ERR 或空态),
|
||||
* 既不崩也不假成功;但残表是真故障,必须能在启动日志里看见,不能静默 */
|
||||
if (ui_data_maint_bind(&s_maint_ops) != 0) {
|
||||
DBG_ERROR("UI maint bind: FAIL");
|
||||
}
|
||||
/* 快照层复位:必须先于 ui_menu_init(首帧之前任何页面被打开都按"无数据"
|
||||
* 渲染 "--",不会读到上电残留)——UI_数据供给设计说明.md §5.3 */
|
||||
ui_data_init();
|
||||
|
||||
+475
-4
@@ -14,10 +14,16 @@
|
||||
* App 侧节拍里的 ui_data_param_drain(),两者同在 uiTask → 环形队列照旧
|
||||
* 无锁;若将来出现第二个投递上下文,唯一要做的是给 request/drain 两侧
|
||||
* 各加一把互斥(队列本身是单字 head/cnt 推进,页面侧不会看到半成品)
|
||||
* 模块功能补充:除三通道快照外,本模块还持有定值写路径的请求队列与状态字
|
||||
* (ui_data_param_request/state/drain)——本层只做"排队 + 状态机",
|
||||
* 不判断值域、不碰 prot_set、不碰文件系统(值域与写入事实经
|
||||
* ui_data_param_ops_t 由 App 侧注入,理由见 ui_data.h 写路径节)
|
||||
* 模块功能补充:除三通道快照外,本模块还持有三条与页面相关的东西:
|
||||
* ① 定值写路径的请求队列与状态字(ui_data_param_request/state/drain)
|
||||
* ——只做"排队 + 状态机",不判断值域、不碰 prot_set、不碰文件系统
|
||||
* (值域与写入事实经 ui_data_param_ops_t 由 App 侧注入);
|
||||
* ② 装置维护写通道(ui_data_maint_*:墙钟设置 / 强制复归 / 清除记录)
|
||||
* ——常驻注入的 ui_data_maint_ops_t 转发,本层不解释业务含义;
|
||||
* 为什么这里常驻注入而 ① 每次传表,见 ui_data.h 通道四的函数说明;
|
||||
* ③ 时钟字段换算(ui_data_clock_*:本地域、无时区偏移、纯整数零依赖)
|
||||
* ——「无效值拒收」的唯一判据处,也是唯一能在主机单测里跑全表的
|
||||
* 日历判据(页面 .c 依赖 lvgl.h,编不进主机测试单元)
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-25
|
||||
@@ -27,6 +33,15 @@
|
||||
* 与状态机(IDLE/WAIT/OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY),
|
||||
* 执行经注入函数表回调 App 侧;参数帧新增 cal_block
|
||||
* 字段随 publish 原样入存(本层不解释含义)
|
||||
* v1.2 2026-10-26 DTU 项目组 道F task-3(装置维护 3 页的前置通道):新增维护
|
||||
* 写通道 ui_data_maint_*(常驻注入 ui_data_maint_ops_t:
|
||||
* clock_set / reset_req / active_count / oper_note /
|
||||
* clear_req / clear_state 六口,未注入一律安全返回)与
|
||||
* 时钟字段换算 ui_data_clock_*(本地域无时区偏移;
|
||||
* to_epoch 同时是"无效值拒收"的唯一判据处)。
|
||||
* 三通道快照结构一个字节都没动 ⇒ UI_DATA_VER 保持 2
|
||||
* (版本号是页面与写侧唯一的对齐凭据,无字段变更却升版
|
||||
* = 让老页面以为新帧换了布局)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -420,3 +435,459 @@ int ui_data_param_drain(const ui_data_param_ops_t *a_ops) {
|
||||
s_pq_state = (s_pq_cnt != 0u) ? UI_DATA_PARAM_ST_WAIT : settled;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ── 通道四:装置维护写请求(时钟 / 强制复归 / 清除记录)──────────────────── */
|
||||
|
||||
/*
|
||||
* 注入的维护执行侧函数表(NULL = 未注入)。
|
||||
* 只存指针不拷内容:注入方(App/task/ui_task.c 与 simulator/main.c)的两端实现
|
||||
* 都是文件级 static const 表,生命周期 = 整个运行期,与 ui_w_records_bind() 存的
|
||||
* reader 指针同一条约定。撤销只可能出现在宿主测试里。
|
||||
*/
|
||||
static const ui_data_maint_ops_t *s_maint;
|
||||
|
||||
/*
|
||||
* 函数功能:注入装置维护执行侧函数表(启动期一次)
|
||||
* 入口参数:ops - 函数表;六个成员指针都不得为 NULL;NULL = 撤销注入
|
||||
* const ui_data_maint_ops_t *
|
||||
* 返回值:0 - 已注入(或已撤销);-1 - ops 非空但某成员为 NULL(存量不动) int
|
||||
* 限定条件:uiTask 启动期,先于 ui_menu_init()(与 ui_w_records_bind 同一处);
|
||||
* uiTask 之外的上下文不得调用(本层无锁,与快照层同一条并发前提)
|
||||
* 函数说明:成员逐条校验而不只看表指针——半途接错一条(例如忘了注 clear_state)
|
||||
* 会让清除记录页在"按下才崩"的位置崩,生成期/启动期就拒掉整表才是省事
|
||||
*/
|
||||
int ui_data_maint_bind(const ui_data_maint_ops_t *a_ops) {
|
||||
if (a_ops == NULL) {
|
||||
s_maint = NULL; /* 撤销注入:页面随即按"能力不可用"渲染 */
|
||||
return 0;
|
||||
}
|
||||
if ((a_ops->clock_set == NULL) || (a_ops->reset_req == NULL) ||
|
||||
(a_ops->active_count == NULL) || (a_ops->oper_note == NULL) ||
|
||||
(a_ops->clear_req == NULL) || (a_ops->clear_state == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
s_maint = a_ops;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查询维护通道是否已注入
|
||||
* 入口参数:无
|
||||
* 返回值:1 - 已注入;0 - 未注入 uint8_t
|
||||
* 限定条件:无(只读单字,任意上下文安全)
|
||||
* 函数说明:页面用它把"没接线"和"操作被拒"分开显示——前者是装配缺陷,
|
||||
* 现场再按一百次也不会好,必须看得见
|
||||
*/
|
||||
uint8_t ui_data_maint_ready(void) {
|
||||
return (s_maint != NULL) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:请求设置墙钟(时钟设置页"保存"行的唯一出口)
|
||||
* 入口参数:unix_epoch - 已整组校验过的秒(见 ui_data_clock_to_epoch) uint32_t
|
||||
* 返回值:0 - 执行侧受理;-1 - 未注入或执行侧失败;-2 - 参数无效(0 = 本层保留的
|
||||
* "未初始化"哨兵,与 sys_clock 的 UINT32_MAX 哨兵不同族,
|
||||
* 这里只挡 0 这一个明显不该写进 RTC 的值) int
|
||||
* 限定条件:uiTask 上下文(页面按键钩子)
|
||||
* 函数说明:本层不判日期合法性——判据只有一份,在 ui_data_clock_to_epoch();
|
||||
* 也不做回读,回读属执行侧(它才知道 sys_clock_get() 的真值)
|
||||
*/
|
||||
int ui_data_maint_clock_set(uint32_t a_unix_epoch) {
|
||||
if (s_maint == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (a_unix_epoch == 0u) {
|
||||
return -2;
|
||||
}
|
||||
return s_maint->clock_set(a_unix_epoch);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:请求复归全部保护元件(强制复归页的执行出口)
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 已受理;-1 - 未注入或执行侧拒收 int
|
||||
* 限定条件:uiTask 上下文
|
||||
* 函数说明:本函数**不投**操作记录,页面在返 0 之后自己投("本地面板发起"这个
|
||||
* 事实只有页面知道;远端 104 那条另有投递点,两边各一条、不得互替);
|
||||
* 返 -1 时页面必须一条都不投——给一次没发生的操作留审计记录,比少一条
|
||||
* 记录坏得多
|
||||
*/
|
||||
int ui_data_maint_reset_req(void) {
|
||||
if (s_maint == NULL) {
|
||||
return -1;
|
||||
}
|
||||
return s_maint->reset_req();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取当前动作元件数(复归页"保护动作 N"行的数据源)
|
||||
* 入口参数:无
|
||||
* 返回值:元件数;未注入返回 0 uint32_t
|
||||
* 限定条件:页面绘制路径可直调(一次函数指针转发,零 I/O、零锁)
|
||||
* 函数说明:返回类型取 uint32_t 与执行侧位图同宽,不在本层压成 uint8_t——
|
||||
* 元件数将来超 8 时,窄化会在页面里静默截成 0("明明动作了屏上说没有")
|
||||
*/
|
||||
uint32_t ui_data_maint_active_count(void) {
|
||||
if (s_maint == NULL) {
|
||||
return 0u;
|
||||
}
|
||||
return s_maint->active_count();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:投一条操作记录留痕
|
||||
* 入口参数:desc - 描述串(ASCII 常量;NULL = 什么都不做) const char *
|
||||
* 返回值:0 - 已投递;-1 - 未注入或 desc 为 NULL int
|
||||
* 限定条件:uiTask 上下文
|
||||
* 函数说明:投递可能被队列丢弃(执行侧口径)——本层不因此把屏上的"操作成功"
|
||||
* 翻成失败:记录通道故障不等于操作没发生,混起来只会让现场误判装置
|
||||
*/
|
||||
int ui_data_maint_oper_note(const char *a_desc) {
|
||||
if ((s_maint == NULL) || (a_desc == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
s_maint->oper_note(a_desc);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:请求清除某类记录(清除记录页的执行出口)
|
||||
* 入口参数:log_id - UI 侧记录源编号(UI_W_REC_LOG_*) uint8_t
|
||||
* 返回值:0 - 已受理;-1 - 未注入或执行侧拒绝 int
|
||||
* 限定条件:uiTask 上下文;"受理"≠"已清除",终态读 ui_data_maint_clear_state()
|
||||
* 函数说明:不在本层判 log_id 上界——上界的真源是记录侧有几个文件(映射表在
|
||||
* 适配层),这里再抄一份长度就成了第二真源,记录加一类时必然漏改
|
||||
*/
|
||||
int ui_data_maint_clear_req(uint8_t a_log_id) {
|
||||
if (s_maint == NULL) {
|
||||
return -1;
|
||||
}
|
||||
return s_maint->clear_req(a_log_id);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查某类记录的清除状态
|
||||
* 入口参数:log_id - 记录源编号 uint8_t
|
||||
* 返回值:UI_DATA_CLEAR_ST_NONE / _OK / _WAIT / _FAIL;未注入返回 NONE int
|
||||
* 限定条件:页面绘制路径可直调
|
||||
* 函数说明:返回值直通执行侧、不做二次加工(与 ui_data_param_state() 同理:
|
||||
* 两处解释同一件事迟早漂移);WAIT 是正常态,页面不得画成失败
|
||||
*/
|
||||
int ui_data_maint_clear_state(uint8_t a_log_id) {
|
||||
if (s_maint == NULL) {
|
||||
return UI_DATA_CLEAR_ST_NONE;
|
||||
}
|
||||
return s_maint->clear_state(a_log_id);
|
||||
}
|
||||
|
||||
/* ── 时钟字段换算(本地域,无时区偏移)──────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* 私有宏定义区(本小节自带,不与其它小节共用):
|
||||
* 算法 = Howard Hinnant 的 days_from_civil / civil_from_days(以 0000-03-01
|
||||
* 为纪元、400 年一周期),与 App/protocol/iec104/iec104_asdu.c 的正反两套实现
|
||||
* 同族;本工程年域 2000..2038,全程非负,用 uint32 就够,不引 int64 也不引 time.h
|
||||
*/
|
||||
#define CL_SECONDS_PER_DAY 86400u
|
||||
#define CL_SECONDS_OF_DAY_MAX 11647u /* 0x7FFFFFFF % 86400 = 2038-01-19 的 03:14:07 */
|
||||
#define CL_DAYS_EPOCH_MAX 24855u /* 0x7FFFFFFF / 86400 = 2038-01-19(距 1970-01-01 天数) */
|
||||
#define CL_DAYS_TO_0000 719468u /* 1970-01-01 距 0000-03-01 纪元的天数(Hinnant 偏移量) */
|
||||
#define CL_DAYS_PER_ERA 146097u /* 400 年一周期的天数 */
|
||||
|
||||
/*
|
||||
* 函数功能:公历日期 → 距 1970-01-01 的天数(调用方保证字段已判过合法性)
|
||||
* 入口参数:year - 年(>= 1) uint16_t month - 月 1..12 uint8_t day - 日 1..31 uint8_t
|
||||
* 返回值:天数(本工程年域内恒非负) uint32_t
|
||||
* 限定条件:月/日未做合法性判定——**合法性判定在 to_epoch(),不在这里**,
|
||||
* 换算函数保持"给什么算什么"才能与反向函数严格互逆
|
||||
* 函数说明:先做 1/2 月归到上一年的 11/12 月(y 相应减一),使 3 月成为岁首、
|
||||
* 闰日落在周期末端,之后每月天数变成 31 天/30.6 天的规整式子
|
||||
*/
|
||||
static uint32_t ui_data_clock_days_from_civil(uint16_t a_year, uint8_t a_month, uint8_t a_day) {
|
||||
uint32_t y = (a_month <= 2u) ? ((uint32_t)a_year - 1u) : (uint32_t)a_year;
|
||||
uint32_t era = y / 400u;
|
||||
uint32_t yoe = y - (era * 400u); /* [0, 399] */
|
||||
uint32_t mp = (a_month > 2u) ? ((uint32_t)a_month - 3u) : ((uint32_t)a_month + 9u);
|
||||
uint32_t doy = ((153u * mp + 2u) / 5u) + (uint32_t)a_day - 1u; /* [0, 365] */
|
||||
uint32_t doe = (yoe * 365u) + (yoe / 4u) - (yoe / 100u) + doy; /* [0, 146096] */
|
||||
|
||||
return (era * CL_DAYS_PER_ERA) + doe - CL_DAYS_TO_0000;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:判断是否闰年(完整格里历规则)
|
||||
* 入口参数:year - 年 uint16_t
|
||||
* 返回值:1 - 闰年;0 - 平年 uint8_t
|
||||
* 限定条件:与 App/util/sys_clock.c 的 unix_to_sd2506() 内联判据逐字同一条规则
|
||||
* (4 年 && 非 100 年 || 400 年);2100 不在允许年域内,无需讨论该例外
|
||||
* 函数说明:取 uint8_t 而不是 bool——本工程布尔判定允许 bool,但本函数参与
|
||||
* 天数计算,返回宽度明确的整型省掉一次隐式转换(同 ui_w_editor_key()
|
||||
* 把"分发标志"写成 uint8_t 的口径)
|
||||
*/
|
||||
static uint8_t ui_data_clock_is_leap(uint16_t a_year) {
|
||||
if ((((uint32_t)a_year % 4u) == 0u) && (((uint32_t)a_year % 100u) != 0u)) {
|
||||
return 1u;
|
||||
}
|
||||
if (((uint32_t)a_year % 400u) == 0u) {
|
||||
return 1u;
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取某年某月的天数(含闰年 2 月)
|
||||
* 入口参数:year - 年 uint16_t month - 月 1..12 uint8_t
|
||||
* 返回值:28 / 29 / 30 / 31;月份为 0 或 > 12 时返回 0(调用方以 0 判非法,
|
||||
* 不开第二条错误码) uint8_t
|
||||
* 限定条件:无(纯查表 + 闰年判定,零依赖)
|
||||
* 函数说明:表序 = 1 月起(下标 0 是 1 月),与 sys_clock.c:145 那份 mdays[]
|
||||
* 的"0 月起"排法**故意不同**——本表按屏上月的直觉排(下标 = 月 - 1),
|
||||
* 少一次心算 -1 就少一次错位;两侧同一张表的等价性由主机单测逐月核对
|
||||
*/
|
||||
uint8_t ui_data_clock_days_in_month(uint16_t a_year, uint8_t a_month) {
|
||||
static const uint8_t k_mdays[12] = {
|
||||
31u, 28u, 31u, 30u, 31u, 30u, 31u, 31u, 30u, 31u, 30u, 31u
|
||||
};
|
||||
|
||||
if ((a_month < 1u) || (a_month > 12u)) {
|
||||
return 0u;
|
||||
}
|
||||
if ((a_month == 2u) && (ui_data_clock_is_leap(a_year) != 0u)) {
|
||||
return 29u;
|
||||
}
|
||||
return k_mdays[a_month - 1u];
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:日历字段 → 本地域 epoch 秒,同时充当这组字段的合法性判据
|
||||
* 入口参数:p_in - 六个字段 const ui_data_clock_t *
|
||||
* p_out - 秒 uint32_t *
|
||||
* 返回值:0 - 合法并已输出;-1 - 参数为 NULL;-2 - 字段组合非法(任一段越界,
|
||||
* 含 2 月 30 日、非闰年 2 月 29 日、月 13、时 24、分/秒 60);
|
||||
* -3 - 字段各自合法但换算结果越界(> UI_DATA_CLOCK_EPOCH_MAX) int
|
||||
* 限定条件:年段先判(2000..2038),因此天数计算全程在非负 uint32 域内
|
||||
* 函数说明:三段边界分开返码,是为了让页面能给出现场听得懂的话:"2 月 30 日"
|
||||
* 是输入错(-2,光标回到出问题那一行),而"2038 年之后"是能力边界
|
||||
* (-3,说这一版装置的墙钟就到这儿)——两种情况混成一个 ERR,
|
||||
* 用户会反复试一个永远试不出的日期
|
||||
* ⚠ 全程不含时区偏移(口径见 ui_data.h 本节注释与
|
||||
* App/task/ui_task.c 的 SOE 时标注记):加 28800 的后果是"屏上输
|
||||
* 08:00、存进去 16:00",且模拟器里往返照样自洽、照不出来
|
||||
*/
|
||||
int ui_data_clock_to_epoch(const ui_data_clock_t *p_in, uint32_t *p_out) {
|
||||
uint32_t days;
|
||||
uint32_t sod;
|
||||
uint32_t dmax;
|
||||
|
||||
if ((p_in == NULL) || (p_out == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
if ((p_in->year < (uint16_t)UI_DATA_CLOCK_YEAR_MIN) ||
|
||||
(p_in->year > (uint16_t)UI_DATA_CLOCK_YEAR_MAX)) {
|
||||
return -2;
|
||||
}
|
||||
if ((p_in->month < 1u) || (p_in->month > 12u)) {
|
||||
return -2;
|
||||
}
|
||||
dmax = (uint32_t)ui_data_clock_days_in_month(p_in->year, p_in->month);
|
||||
if ((p_in->day < 1u) || ((uint32_t)p_in->day > dmax)) {
|
||||
return -2;
|
||||
}
|
||||
if (p_in->hour > 23u) {
|
||||
return -2;
|
||||
}
|
||||
if ((p_in->minute > 59u) || (p_in->second > 59u)) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
days = ui_data_clock_days_from_civil(p_in->year, p_in->month, p_in->day);
|
||||
sod = ((uint32_t)p_in->hour * 3600u) + ((uint32_t)p_in->minute * 60u) +
|
||||
(uint32_t)p_in->second;
|
||||
if ((days > CL_DAYS_EPOCH_MAX) || ((days == CL_DAYS_EPOCH_MAX) && (sod > CL_SECONDS_OF_DAY_MAX))) {
|
||||
return -3; /* 2038-01-19 03:14:07 之后:uint32 秒域到头,不是用户输错 */
|
||||
}
|
||||
|
||||
*p_out = (days * CL_SECONDS_PER_DAY) + sod;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:本地域 epoch 秒 → 日历字段(ui_data_clock_to_epoch 的反函数)
|
||||
* 入口参数:unix_epoch - 秒 uint32_t p_out - 输出 ui_data_clock_t *
|
||||
* 返回值:0 - 已输出;-1 - p_out 为 NULL;-2 - 超出允许域(0 哨兵、年 < 2000、
|
||||
* 或 > UI_DATA_CLOCK_EPOCH_MAX) int
|
||||
* 限定条件:与 to_epoch 严格互逆(主机单测按往返相等钉,见 test/host/ui_data);
|
||||
* 刻意把 0 也判越界:0 = 1970-01-01,落在 RTC 年域(2000..2099)之外,
|
||||
* 而它又恰好是"墙钟无效"在记录侧的占位起点,绝不允许被当真实时间显示
|
||||
*/
|
||||
int ui_data_clock_from_epoch(uint32_t a_unix_epoch, ui_data_clock_t *p_out) {
|
||||
uint32_t days;
|
||||
uint32_t sod;
|
||||
uint32_t z;
|
||||
uint32_t era;
|
||||
uint32_t doe;
|
||||
uint32_t yoe;
|
||||
uint32_t doy;
|
||||
uint32_t mp;
|
||||
uint32_t y;
|
||||
uint32_t m;
|
||||
|
||||
if (p_out == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if ((a_unix_epoch == 0u) || (a_unix_epoch > UI_DATA_CLOCK_EPOCH_MAX)) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
days = a_unix_epoch / CL_SECONDS_PER_DAY;
|
||||
sod = a_unix_epoch % CL_SECONDS_PER_DAY;
|
||||
z = days + CL_DAYS_TO_0000; /* 移到 0000-03-01 纪元 */
|
||||
era = z / CL_DAYS_PER_ERA;
|
||||
doe = z - (era * CL_DAYS_PER_ERA); /* [0, 146096] */
|
||||
yoe = (doe - (doe / 1460u) + (doe / 36524u) - (doe / 146096u)) / 365u; /* [0, 399] */
|
||||
doy = doe - ((365u * yoe) + (yoe / 4u) - (yoe / 100u));
|
||||
mp = (5u * doy + 2u) / 153u;
|
||||
|
||||
/* 3 月纪元月序转公历月序:mp < 10 落在 3..12 月,否则落在 1/2 月并归上一年 */
|
||||
y = yoe + (era * 400u);
|
||||
if (mp < 10u) {
|
||||
m = mp + 3u;
|
||||
} else {
|
||||
m = mp - 9u;
|
||||
y += 1u; /* 1/2 月属于上一年 */
|
||||
}
|
||||
|
||||
p_out->year = (uint16_t)y;
|
||||
p_out->month = (uint8_t)m;
|
||||
p_out->day = (uint8_t)(doy - ((153u * mp + 2u) / 5u) + 1u);
|
||||
p_out->hour = (uint8_t)(sod / 3600u);
|
||||
p_out->minute = (uint8_t)((sod / 60u) % 60u);
|
||||
p_out->second = (uint8_t)(sod % 60u);
|
||||
|
||||
if ((p_out->year < (uint16_t)UI_DATA_CLOCK_YEAR_MIN) ||
|
||||
(p_out->year > (uint16_t)UI_DATA_CLOCK_YEAR_MAX)) {
|
||||
return -2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:解析定长时标串为日历字段
|
||||
* 入口参数:ts - "YYYY-MM-DD HH:MM:SS"(快照 rt.clock 的形状) const char *
|
||||
* p_out - 输出 ui_data_clock_t *
|
||||
* 返回值:0 - 成功;-1 - 参数为 NULL;-2 - 串型不对(不是 19 字符、分隔位不是
|
||||
* '-'/' '/':'、出现非数字)**或**型对但值非法
|
||||
* (组合再过 ui_data_clock_to_epoch() 把关) int
|
||||
* 限定条件:空串(墙钟无效时快照里的取值,ui_data.h:149)走 -2;
|
||||
* 本函数**不猜**:"1970-01-01 08:00:00"这类记录侧占位串在这里同样返 -2
|
||||
* (年 1970 出域),页面据此显示"无时钟"而不是把占位串画成真实时间
|
||||
* 函数说明:手写十进制累加而不用 sscanf——本模块是零 LVGL、零 App 的纯内存叶子,
|
||||
* 拉进 stdio 就等于给主机单测加依赖、给 MCU 端加一份 printf 家族
|
||||
*/
|
||||
int ui_data_clock_parse(const char *a_ts, ui_data_clock_t *p_out) {
|
||||
ui_data_clock_t t;
|
||||
uint32_t i;
|
||||
uint32_t acc;
|
||||
const char *p;
|
||||
|
||||
if ((a_ts == NULL) || (p_out == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)memset(&t, 0, sizeof(t));
|
||||
|
||||
/* 五段数字:年 4 位、月/日/时/分/秒各 2 位;段间分隔位逐位核对 */
|
||||
p = a_ts;
|
||||
acc = 0u;
|
||||
for (i = 0u; i < 4u; i++) {
|
||||
if ((p[i] < '0') || (p[i] > '9')) {
|
||||
return -2;
|
||||
}
|
||||
acc = (acc * 10u) + (uint32_t)(p[i] - '0');
|
||||
}
|
||||
t.year = (uint16_t)acc;
|
||||
if (p[4] != '-') {
|
||||
return -2;
|
||||
}
|
||||
p += 5u;
|
||||
|
||||
{
|
||||
uint8_t *const fields[5] = { &t.month, &t.day, &t.hour, &t.minute, &t.second };
|
||||
const char seps[4] = { '-', ' ', ':', ':' };
|
||||
|
||||
for (i = 0u; i < 5u; i++) {
|
||||
if (((p[0] < '0') || (p[0] > '9')) || ((p[1] < '0') || (p[1] > '9'))) {
|
||||
return -2;
|
||||
}
|
||||
*fields[i] = (uint8_t)(((uint32_t)(p[0] - '0') * 10u) + (uint32_t)(p[1] - '0'));
|
||||
p += 2u;
|
||||
if (i < 4u) {
|
||||
if (*p != seps[i]) {
|
||||
return -2;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (*p != '\0') {
|
||||
return -2; /* 型对但尾部还有字符:不是本工程认的时标串 */
|
||||
}
|
||||
{
|
||||
uint32_t probe;
|
||||
if (ui_data_clock_to_epoch(&t, &probe) != 0) {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
|
||||
*p_out = t;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:日历字段 → 定长时标串(与 ui_data_clock_parse 同串型)
|
||||
* 入口参数:p_in - 字段 const ui_data_clock_t *
|
||||
* buf - 输出缓冲 char *
|
||||
* len - 缓冲容量(含 NUL,至少 UI_DATA_CLOCK_TS_LEN + 1 = 20) uint32_t
|
||||
* 返回值:0 - 已写出;-1 - 参数无效或容量不足(**不写半个串**,避免调用方
|
||||
* 把截断串当完整时标显示) int
|
||||
* 限定条件:不判字段合法性(调用方刚从 parse/to_epoch 那条链出来,再判一遍
|
||||
* 就是第二条判据)
|
||||
*/
|
||||
int ui_data_clock_format(const ui_data_clock_t *p_in, char *a_buf, uint32_t a_len) {
|
||||
uint8_t i;
|
||||
char *p;
|
||||
uint32_t v;
|
||||
|
||||
if ((p_in == NULL) || (a_buf == NULL) ||
|
||||
(a_len < (UI_DATA_CLOCK_TS_LEN + 1u))) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
p = a_buf;
|
||||
v = (uint32_t)p_in->year;
|
||||
for (i = 0u; i < 4u; i++) { /* 年份 4 位补零 */
|
||||
p[3u - i] = (char)('0' + (uint32_t)(v % 10u));
|
||||
v /= 10u;
|
||||
}
|
||||
p[4] = '-';
|
||||
p += 5;
|
||||
|
||||
{
|
||||
const uint8_t fields[5] = { p_in->month, p_in->day, p_in->hour,
|
||||
p_in->minute, p_in->second };
|
||||
const char seps[4] = { '-', ' ', ':', ':' };
|
||||
|
||||
for (i = 0u; i < 5u; i++) {
|
||||
p[0] = (char)('0' + ((uint32_t)fields[i] / 10u));
|
||||
p[1] = (char)('0' + ((uint32_t)fields[i] % 10u));
|
||||
p += 2;
|
||||
if (i < 4u) {
|
||||
*p = seps[i];
|
||||
p++;
|
||||
}
|
||||
}
|
||||
}
|
||||
*p = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,16 @@
|
||||
* 的数据源);写路径仍不引入任何 App 依赖(执行事实
|
||||
* 经 ui_data_param_ops_t 由 App 侧注入,同契约 §7.2
|
||||
* 记录页注入式 reader 的路子)
|
||||
* v1.2 2026-10-26 DTU 项目组 道F task-3(docs/任务规划/UI补齐.md §2 第 3 行
|
||||
* 「装置维护 3 页」的前置):新增通道四——维护写请求
|
||||
* (ui_data_maint_*:墙钟设置 / 强制复归 / 清除记录,
|
||||
* 常驻注入 ui_data_maint_ops_t)与时钟字段换算
|
||||
* (ui_data_clock_*,本地域无时区偏移,兼作"无效值
|
||||
* 拒收"的唯一判据处)。三通道快照结构未变 ⇒
|
||||
* UI_DATA_VER 仍为 2。仍然零 App include:维护动作的
|
||||
* 真源全在 App 侧,本层只转达请求与结果(同 §7.2);
|
||||
* ⚠ 页面也不得直调 sys_clock_*——模拟器没链 sys_clock,
|
||||
* 那种写法只在模拟器链接期才炸,理由见通道四表注
|
||||
*/
|
||||
|
||||
#ifndef UI_DATA_H
|
||||
@@ -426,6 +436,259 @@ uint64_t ui_data_param_err_mask(void);
|
||||
*/
|
||||
int ui_data_param_drain(const ui_data_param_ops_t *a_ops);
|
||||
|
||||
/* ── 通道四:装置维护写请求(时钟 / 强制复归 / 清除记录)───────────────────── */
|
||||
|
||||
/*
|
||||
* 执行侧函数表(道F task-3 新增;注入式,与 ui_data_param_ops_t、
|
||||
* ui_w_records.h 的 ui_rec_reader_t 同一条路子)
|
||||
*
|
||||
* 为什么是注入而不是直接调:分层方向硬约束——BSP 层目录 Drivers/BSP 不得反向
|
||||
* include App 层的头(scripts/check_layer_deps.py 拦提交),而 sys_clock_set_unix /
|
||||
* prot_elm_request_reset / syslog_request_clear 三件事实的属主全在 App 侧。
|
||||
* ⚠ **另有一条同样硬的理由**:模拟器根本没有 sys_clock
|
||||
* (simulator/dbg_log.h:4「模拟器无 sys_clock/RTC」,CMakeLists 也没这份源),
|
||||
* 所以页面里哪怕只用一次 sys_clock_*,也是"MCU 编得过、模拟器链接期爆",
|
||||
* 而这种错只在另一端构建时才暴露。写动作一律走本表,两端各给实现。
|
||||
* ⚠ 本表刻意不含任何"范围数值/枚举值":什么是有效时间、哪条记录对应哪个文件,
|
||||
* 真源仍分别在各属主模块(本层不做第二真源,只转达请求与结果)。
|
||||
*/
|
||||
typedef struct {
|
||||
/*
|
||||
* 函数功能:设置系统墙钟(本地域 epoch 秒)
|
||||
* 入口参数:unix_epoch - 要设置的秒 uint32_t
|
||||
* 返回值:0 - 已生效;-1 - 设置失败 int
|
||||
* 限定条件:真机实现**必须**在 sys_clock_set_unix() 之后回读 sys_clock_get()
|
||||
* 复核(差值容差见 App 侧实现),不得"调了就算成"
|
||||
*/
|
||||
int (*clock_set)(uint32_t a_unix_epoch);
|
||||
/*
|
||||
* 函数功能:请求复归全部保护元件状态
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 已受理;-1 - 拒收 int
|
||||
* 限定条件:真机实现 = prot_elm_request_reset()(只置单字节标志,由
|
||||
* protectTask 下一拍消费),本层不等执行结果
|
||||
*/
|
||||
int (*reset_req)(void);
|
||||
/*
|
||||
* 函数功能:取当前处于动作态的保护元件数("动作位复归"的观测量)
|
||||
* 入口参数:无
|
||||
* 返回值:元件数(0 = 无元件动作) uint32_t
|
||||
* 限定条件:真机实现 = popcount(prot_elm_active_mask())
|
||||
*/
|
||||
uint32_t (*active_count)(void);
|
||||
/*
|
||||
* 函数功能:往"操作记录"投一条留痕(人工在面板上做过一件事)
|
||||
* 入口参数:desc - 描述串(NUL 结尾,调用方持有的常量串;投递侧即拷贝)
|
||||
* const char *
|
||||
* 返回值:无
|
||||
* 限定条件:真机实现 = syslog_post_quiet(SYSLOG_OPER, desc)。**谁触发谁投**:
|
||||
* 远端 104 遥控那条已由 App/protocol/iec104/iec104_pts.c 投,
|
||||
* 本地面板这条由页面在受理成功后投,两边各一条、不得互替
|
||||
*/
|
||||
void (*oper_note)(const char *a_desc);
|
||||
/*
|
||||
* 函数功能:请求清除某类记录(只登记请求,不在本上下文删任何东西)
|
||||
* 入口参数:log_id - UI 侧记录源编号(UI_W_REC_LOG_*,与 ui_rec_reader_t 同一编号空间) uint8_t
|
||||
* 返回值:0 - 请求已登记;-1 - 编号越界或登记失败 int
|
||||
* 限定条件:真机实现 = syslog_request_clear(映射后 id)。⚠ 共享代码**严禁**
|
||||
* 调 MCU 后端的 syslog_clear_apply()(模拟器无实现 = 链接期报错),
|
||||
* 也严禁 UI 直接 f_unlink / 碰卷锁(defaultTask 是唯一写者)
|
||||
*/
|
||||
int (*clear_req)(uint8_t a_log_id);
|
||||
/*
|
||||
* 函数功能:查某类记录"清除请求"的状态
|
||||
* 入口参数:log_id - 记录源编号 uint8_t
|
||||
* 返回值:UI_DATA_CLEAR_ST_NONE / _OK / _WAIT / _FAIL 之一 int
|
||||
* 限定条件:真机实现 = syslog_clear_state(映射后 id),返回值直通不改码。
|
||||
* ⚠ WAIT 是正常态(删除在 defaultTask 的 syslog_drain() 尾部落地);
|
||||
* 模拟器端同步删除,永不出现 WAIT —— 页面两种都要能显示
|
||||
*/
|
||||
int (*clear_state)(uint8_t a_log_id);
|
||||
} ui_data_maint_ops_t;
|
||||
|
||||
/*
|
||||
* 清除状态(与 App/log/sys_log.h 的 SYSLOG_CLEAR_NONE/OK/WAIT/FAIL **逐值相等**;
|
||||
* 适配层直通、不做映射表,等值由主机单测在同一 TU 里逐项核对钉住,
|
||||
* 同 UI_W_SOE_T_* 的先例——等值只由测试钉,不由文档转述)
|
||||
*/
|
||||
#define UI_DATA_CLEAR_ST_NONE 0 /* 从未请求过清除 */
|
||||
#define UI_DATA_CLEAR_ST_OK 1 /* 已清空:同拍该记录条数必为 0 */
|
||||
#define UI_DATA_CLEAR_ST_WAIT (-1) /* 请求排队中(正常态,不是失败) */
|
||||
#define UI_DATA_CLEAR_ST_FAIL (-2) /* 落地失败:原记录一条没少,可再发一次请求 */
|
||||
|
||||
/*
|
||||
* 函数功能:注入装置维护执行侧函数表(启动期一次;先于 ui_menu_init)
|
||||
* 入口参数:ops - 函数表(六个成员指针都不得为 NULL;生命周期由注入方保证,
|
||||
* 本层只存指针不拷贝内容) const ui_data_maint_ops_t *
|
||||
* 返回值:0 - 已注入;-1 - ops 为 NULL 或某成员为 NULL(存量不动,页面按
|
||||
* "维护能力不可用"渲染,不崩不猜) int
|
||||
* 限定条件:uiTask 启动期(与 ui_w_records_bind 同一处);传 NULL = 撤销注入
|
||||
* 函数说明:与 ui_data_param_drain() 的"每次调用时传入"不同,本表是**常驻注入**:
|
||||
* 维护动作由用户按键触发、无固定节拍,若也走"每次传表"就得在 uiTask
|
||||
* 主循环里每轮挂一次,而那一轮并没有任何东西要落定(空转一次函数调用
|
||||
* 换不来任何判据)。撤销窗口只在测试里存在
|
||||
*/
|
||||
int ui_data_maint_bind(const ui_data_maint_ops_t *a_ops);
|
||||
|
||||
/*
|
||||
* 函数功能:查询维护通道是否已注入
|
||||
* 入口参数:无
|
||||
* 返回值:1 - 已注入;0 - 未注入 uint8_t
|
||||
* 限定条件:无(只读单字)
|
||||
* 函数说明:页面用它决定"这页能不能操作"——未注入时按钮一律拒并按 LOCK 显示,
|
||||
* 比默默吞掉按键好(现场能立刻看出是装配缺了一步,而不是设备坏了)
|
||||
*/
|
||||
uint8_t ui_data_maint_ready(void);
|
||||
|
||||
/*
|
||||
* 函数功能:请求设置墙钟(时钟设置页"保存"行的唯一出口)
|
||||
* 入口参数:unix_epoch - 由 ui_data_clock_to_epoch() 合成、且已整组校验过的秒 uint32_t
|
||||
* 返回值:0 - 执行侧受理(终态由执行侧的回读校验给出);-1 - 未注入或执行侧失败;
|
||||
* -2 - 参数无效(本层不判日期合法性,那是
|
||||
* ui_data_clock_to_epoch() 的活,到这儿只判 0 哨兵) int
|
||||
* 限定条件:uiTask 上下文(页面按键钩子)
|
||||
*/
|
||||
int ui_data_maint_clock_set(uint32_t a_unix_epoch);
|
||||
|
||||
/*
|
||||
* 函数功能:请求复归全部保护元件(强制复归页的执行出口)
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 已受理;-1 - 未注入或执行侧拒收 int
|
||||
* 限定条件:uiTask 上下文;⚠ 操作记录**不在本函数里投**——"这是本地面板发起的"
|
||||
* 这一事实只有页面知道,由页面在返 0 之后显式调
|
||||
* ui_data_maint_oper_note() 投(拒收时不得投,屏上留痕与记录留痕
|
||||
* 必须同一条判据)
|
||||
*/
|
||||
int ui_data_maint_reset_req(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取当前动作元件数(复归页"保护动作 N"行的数据源;屏上用字须已入子集)
|
||||
* 入口参数:无
|
||||
* 返回值:元件数;未注入返回 0(= 显示"无元件动作",与真值 0 同形,
|
||||
* 但页脚/态标另有 LOCK 提示装配未接好) uint32_t
|
||||
* 限定条件:页面绘制路径可直调(一次函数指针调用,无 I/O、无锁)
|
||||
* 函数说明:本函数是"动作位复归可证"的**唯一观测量**:复归请求受理后,
|
||||
* 页面按帧重读,N 从 >0 变 0 就是屏上可见的证据;复归不掉只可能是
|
||||
* 测量仍在越限(元件重新 pickup),不是请求丢了
|
||||
*/
|
||||
uint32_t ui_data_maint_active_count(void);
|
||||
|
||||
/*
|
||||
* 函数功能:投一条操作记录留痕(页面在本地动作受理后调)
|
||||
* 入口参数:desc - 描述串(ASCII,长度受执行侧 SYSLOG_DESC_MAX 约束;NULL = 什么都不做)
|
||||
* const char *
|
||||
* 返回值:0 - 已投递;-1 - 未注入或 desc 为 NULL int
|
||||
* 限定条件:uiTask 上下文;投递本身不阻塞、可能因队列满被丢弃(执行侧口径,
|
||||
* 与 syslog_post_quiet 一致——本层不加"投递失败"的第二套显示语义:
|
||||
* 留痕丢了不等于操作没发生,把记录通道故障显示成操作失败更坏)
|
||||
*/
|
||||
int ui_data_maint_oper_note(const char *a_desc);
|
||||
|
||||
/*
|
||||
* 函数功能:请求清除某类记录(清除记录页的执行出口)
|
||||
* 入口参数:log_id - 记录源编号(UI_W_REC_LOG_*) uint8_t
|
||||
* 返回值:0 - 已受理;-1 - 未注入 / 编号越界 / 执行侧拒绝 int
|
||||
* 限定条件:uiTask 上下文;"受理"≠"已清除",终态读 ui_data_maint_clear_state(),
|
||||
* 事实复核读记录条数(ui_w_records 的同一枚 reader)
|
||||
*/
|
||||
int ui_data_maint_clear_req(uint8_t a_log_id);
|
||||
|
||||
/*
|
||||
* 函数功能:查某类记录的清除状态(清除记录页值列的数据源)
|
||||
* 入口参数:log_id - 记录源编号 uint8_t
|
||||
* 返回值:UI_DATA_CLEAR_ST_NONE / _OK / _WAIT / _FAIL;未注入或编号越界返回 NONE
|
||||
* ("从没请求过",页面据此显示条数而不是结果词) int
|
||||
* 限定条件:页面绘制路径可直调;返回值直通执行侧、不做二次加工(同
|
||||
* ui_data_param_state() 的理由:两处解释同一件事必然漂移)
|
||||
*/
|
||||
int ui_data_maint_clear_state(uint8_t a_log_id);
|
||||
|
||||
/* ── 时钟字段换算(本地域,无时区偏移;"无效值拒收"的唯一判据处)─────────── */
|
||||
|
||||
/*
|
||||
* 年份上下限(两条硬约束的交集,不是拍脑袋的整数区间):
|
||||
* 下限 2000:SD2506 RTC 年字段是相对值 0~99(+2000,见
|
||||
* Drivers/BSP/SD2506/sd2506.h 的 sd2506_time_t.year 注释),
|
||||
* 2000 以下写不进 RTC
|
||||
* 上限 2038:墙钟 epoch 是 uint32_t 秒,上界 0x7FFFFFFF = 2038-01-19 03:14:07
|
||||
* (App/util/sys_clock.h 的 sys_clock_set_unix 取值域一致)
|
||||
*/
|
||||
#define UI_DATA_CLOCK_YEAR_MIN 2000u
|
||||
#define UI_DATA_CLOCK_YEAR_MAX 2038u
|
||||
#define UI_DATA_CLOCK_EPOCH_MAX 0x7FFFFFFFu
|
||||
#define UI_DATA_CLOCK_TS_LEN 19u /* "YYYY-MM-DD HH:MM:SS"(与 rt.clock 同串型) */
|
||||
|
||||
/* 时钟日历字段(本地域;数值就是屏上显示的那六个数,不含任何时区语义) */
|
||||
typedef struct {
|
||||
uint16_t year; /* UI_DATA_CLOCK_YEAR_MIN..UI_DATA_CLOCK_YEAR_MAX */
|
||||
uint8_t month; /* 1..12 */
|
||||
uint8_t day; /* 1..该月真实天数(闰年由本层判,不交给调用方) */
|
||||
uint8_t hour; /* 0..23 */
|
||||
uint8_t minute; /* 0..59 */
|
||||
uint8_t second; /* 0..59 */
|
||||
} ui_data_clock_t;
|
||||
|
||||
/*
|
||||
* 函数功能:取某年某月的天数(含闰年 2 月)
|
||||
* 入口参数:year - 年 uint16_t month - 月 1..12 uint8_t
|
||||
* 返回值:28 / 29 / 30 / 31;月份越界返回 0(调用方据此判非法,不用第二条错误码) uint8_t
|
||||
* 限定条件:闰年规则 = (4 年 && !100 年) || 400 年(与 App/util/sys_clock.c
|
||||
* 的 unix_to_sd2506 / sd2506_to_unix 同一判据,两侧都是完整格里历规则;
|
||||
* 2100 年不在允许年域内,不需要讨论那个例外)
|
||||
*/
|
||||
uint8_t ui_data_clock_days_in_month(uint16_t a_year, uint8_t a_month);
|
||||
|
||||
/*
|
||||
* 函数功能:日历字段 → 本地域 epoch 秒(**同时是这组字段的合法性判据**)
|
||||
* 入口参数:p_in - 六个字段 const ui_data_clock_t *
|
||||
* p_out - 秒 uint32_t *
|
||||
* 返回值:0 - 合法并已输出;-1 - 参数为 NULL;-2 - 字段组合非法(年/月/日/时/分/秒
|
||||
* 任一越界,含 2 月 30 日、非闰年 2 月 29 日、月 13、时 24 等);
|
||||
* -3 - 组合合法但超出取值域(epoch > UI_DATA_CLOCK_EPOCH_MAX) int
|
||||
* 限定条件:无(纯整数,零依赖,可在主机单测跑全表)
|
||||
* 函数说明:日的当月真实上限**在这里**判,不在字段编辑器里判——编辑器只有静态
|
||||
* min/max(日一律 1..31),把闰年规则抄一份进编辑器就是第二真源。
|
||||
* ⚠ 不含时区偏移:本工程墙钟的 epoch 域本身就是本地时间
|
||||
* (App/util/sys_clock.h 的 sys_clock_set_unix 注明"秒,本地时间",
|
||||
* 其正/反换算两头都没有 ±8 运算;App/task/ui_task.c 的 SOE 时标注记
|
||||
* 已把"别顺手补 8 小时"钉过一次)。这里加 28800 的结果是"屏上输 08:00、
|
||||
* 存进去 16:00",且只在真机上错,模拟器照样自洽
|
||||
*/
|
||||
int ui_data_clock_to_epoch(const ui_data_clock_t *p_in, uint32_t *p_out);
|
||||
|
||||
/*
|
||||
* 函数功能:本地域 epoch 秒 → 日历字段
|
||||
* 入口参数:unix_epoch - 秒 uint32_t p_out - 输出 ui_data_clock_t *
|
||||
* 返回值:0 - 已输出;-1 - p_out 为 NULL;-2 - 超出允许年域(< 2000-01-01 或
|
||||
* > UI_DATA_CLOCK_EPOCH_MAX) int
|
||||
* 限定条件:与 ui_data_clock_to_epoch() 互为反函数(同一套格里历规则,
|
||||
* 往返必等;这条往返性是主机单测的断言之一,不是注释里的口头保证)
|
||||
*/
|
||||
int ui_data_clock_from_epoch(uint32_t a_unix_epoch, ui_data_clock_t *p_out);
|
||||
|
||||
/*
|
||||
* 函数功能:解析 "YYYY-MM-DD HH:MM:SS" 串为日历字段
|
||||
* 入口参数:ts - 待解析串(快照 rt.clock 的形状) const char *
|
||||
* p_out - 输出 ui_data_clock_t *
|
||||
* 返回值:0 - 成功;-1 - 参数 NULL;-2 - 串型不对(长度非 19、分隔位不是
|
||||
* '-'/' '/':'、出现非数字);-2 同时覆盖"型对但值非法"
|
||||
* (解析出的组合再过 to_epoch()) int
|
||||
* 限定条件:时钟无效时快照里是**空串**(ui_data.h:149 的口径),空串走 -2;
|
||||
* 调用方据此显示"无时钟"而不是 0000-00-00
|
||||
*/
|
||||
int ui_data_clock_parse(const char *a_ts, ui_data_clock_t *p_out);
|
||||
|
||||
/*
|
||||
* 函数功能:日历字段 → "YYYY-MM-DD HH:MM:SS"(与 ui_data_clock_parse 同串型)
|
||||
* 入口参数:p_in - 字段 const ui_data_clock_t *
|
||||
* buf - 输出缓冲 char * len - 缓冲容量(含 NUL;不足则整串不写) uint32_t
|
||||
* 返回值:0 - 已写出;-1 - 参数无效或容量不足 int
|
||||
* 限定条件:不判字段合法性(调用方刚从 parse/to_epoch 那条链出来);
|
||||
* 手写十进制、不引 stdio——本层是零 LVGL 零 App 的叶子,拉进
|
||||
* lv_snprintf/stdio 就等于给主机单测加依赖
|
||||
*/
|
||||
int ui_data_clock_format(const ui_data_clock_t *p_in, char *a_buf, uint32_t a_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+142
-104
@@ -1,11 +1,19 @@
|
||||
/*
|
||||
* 模块名称:UI 动作确认页(操作页共享实现)实现
|
||||
* 模块功能:见 ui_page_action.h 头注释(确认链状态机 + 投递回报 + 表单页自绘)
|
||||
* 模块功能:见 ui_page_action.h 头注释——本文件是那三个钩子的实现体:
|
||||
* init 复位确认链、draw 画问句/态标/观测行/键提示四行、key 走状态机并
|
||||
* 只投递不阻塞。实例由页面跳板持有,本文件不再有模块单例,也不建描述符
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道B task-5 三组件之一)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-3:删模块单例 s_act/s_desc 与
|
||||
* ui_page_action_open()(全仓零调用者),改出
|
||||
* ui_page_action_init/draw/key 三个收实例指针的
|
||||
* 钩子;新增只读拒绝(need_modify → 态标 LOCK、
|
||||
* 键提示 ESC=返回)与可选观测行(watch_label +
|
||||
* watch_cb);返回值只留 uint8_t 分发标志
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -20,24 +28,19 @@
|
||||
/* 私有宏定义区 */
|
||||
|
||||
#define AC_PROMPT_Y 44 /* 问句/结果行 放置 y(内容区上三分之一) */
|
||||
#define AC_STAGE_Y 66 /* 态标行("二次确认" / "ERR")放置 y */
|
||||
#define AC_STAGE_Y 66 /* 态标行("二次确认" / "ERR" / "LOCK")放置 y */
|
||||
#define AC_WATCH_Y 88 /* 可选观测行放置 y(实例没配观测行时这行留空) */
|
||||
#define AC_HINT_Y 110 /* 键提示行 放置 y */
|
||||
|
||||
/* 固定文案(用字均已入字体子集:二/次/确/认/取/消/返/回/越/界 及 ASCII) */
|
||||
/* 固定文案(用字均已入字体子集:二/次/确/认/取/消/返/回/越/界 及 ASCII;
|
||||
* 道F task-3 起新增的"只读"两词沿用同一批用字,未扩字库) */
|
||||
#define AC_STAGE2_TXT "二次确认"
|
||||
#define AC_HINT_DO "ENTER=确认 ESC=取消"
|
||||
#define AC_HINT_BACK "ENTER=返回"
|
||||
#define AC_HINT_RO "ESC=返回" /* 只读:ENTER 无效,提示必须跟着改口 */
|
||||
#define AC_ERR_TXT "ERR"
|
||||
|
||||
/* 私有变量定义区 */
|
||||
|
||||
/*
|
||||
* 模块单例:内容页框架全局单页(s_open 互斥),同一时刻至多一张操作页,
|
||||
* 故钩子经"当前实例指针"取上下文即可成立(头注已载此约束;批 2 各跳板
|
||||
* 各自 static 实例、先后开页互不冲突)
|
||||
*/
|
||||
static ui_page_action_t *s_act; /* 当前实例(NULL = 未在用) */
|
||||
static content_page_def_t s_desc; /* 模板描述符(开页时按实例填充) */
|
||||
#define AC_RO_TXT "LOCK" /* 只读进入的态标(词表与整定保存行一致,
|
||||
都是 ASCII,见 UI_数据供给设计说明.md §6.3) */
|
||||
|
||||
/* 私有函数声明区 */
|
||||
|
||||
@@ -77,123 +80,158 @@ static void ac_draw_centered(lv_layer_t *a_layer, const char *a_text,
|
||||
ld.text_static = 1u; /* Flash 字面量:不拷内存 */
|
||||
ld.color = UI_COL_FG;
|
||||
ld.ofs_x = (UI_SCREEN_W -
|
||||
(int32_t)lv_text_get_width(a_text, (uint32_t)strlen(a_text), LV_FONT_DEFAULT, 0)) / 2;
|
||||
(int32_t)lv_text_get_width(a_text, (uint32_t)strlen(a_text), LV_FONT_DEFAULT, 0)) / 2;
|
||||
ld.ofs_y = 0;
|
||||
lv_draw_label(a_layer, &ld, &t);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:自绘(框架内置区块之后:问句/态标/键提示 三行居中)
|
||||
* 入口参数:layer - 绘制层 lv_layer_t *
|
||||
* clip - 重绘区 const lv_area_t *
|
||||
* 函数功能:开页复位确认链(框架 init_cb 的实现体)
|
||||
* 入口参数:a_inst - 操作页实例 ui_page_action_t *
|
||||
* 返回值:无
|
||||
* 限定条件:经描述符 draw_cb 挂接,页面打开中(s_act 有效)
|
||||
* 限定条件:内容页框架开页链内调用(页面描述符的 init_cb 转发到此)
|
||||
* 函数说明:stage 每次进页归零是**判据**不是收尾清洁:上一轮按到"二次确认"就
|
||||
* ESC 退出、下一轮进来看都不看再按 ENTER,如果 stage 跨次保留就直接
|
||||
* 执行了——破坏性动作不接受这种路径。err 一并清,否则上一次的投递
|
||||
* 失败会挂在新一页的态标行上(用户以为新页一进来就出错)
|
||||
*/
|
||||
static void ac_draw_cb(lv_layer_t *layer, const lv_area_t *clip) {
|
||||
if ((s_act == NULL) || (s_act->prompt == NULL)) {
|
||||
void ui_page_action_init(ui_page_action_t *a_inst) {
|
||||
if (a_inst == NULL) {
|
||||
return; /* 防御:跳板没填实例(编译期就该发现,运行期只保不崩) */
|
||||
}
|
||||
a_inst->stage = 0u;
|
||||
a_inst->err = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:只读进入判定(本次开页是否"没有修改权限")
|
||||
* 入口参数:a_inst - 操作页实例 const ui_page_action_t *
|
||||
* 返回值:1 = 本页需要修改权限而本次只有查看权限;0 = 其余 uint8_t
|
||||
* 限定条件:框架绘制/按键链内(开页时权限已装载,is_editable 读到的就是本次的)
|
||||
* 函数说明:判据只在这一处——绘制与按键两侧各自判一次,早晚在"屏上能按但按不动"
|
||||
* 这种半套状态上分家
|
||||
*/
|
||||
static uint8_t ui_page_action_is_readonly(const ui_page_action_t *a_inst) {
|
||||
if (a_inst->need_modify == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
return (ui_content_page_is_editable() == false) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:自绘(框架内置区块之后:问句 / 态标 / 可选观测行 / 键提示,四行居中)
|
||||
* 入口参数:a_inst - 操作页实例 ui_page_action_t *
|
||||
* a_layer - 绘制层 lv_layer_t *
|
||||
* a_clip - 重绘区 const lv_area_t *
|
||||
* 返回值:无
|
||||
* 限定条件:经页面描述符 draw_cb 挂接;实例或 prompt 为 NULL 时空操作
|
||||
* 函数说明:行 2 的优先级是刻意的——只读(LOCK) > 二次确认 > 投递失败(ERR):
|
||||
* 没权限时画"二次确认"会让人继续按,比不画更坏
|
||||
*/
|
||||
void ui_page_action_draw(ui_page_action_t *a_inst, lv_layer_t *a_layer,
|
||||
const lv_area_t *a_clip) {
|
||||
if ((a_inst == NULL) || (a_inst->prompt == NULL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 行 1:问句(stage 2 换成功结果行) */
|
||||
if (s_act->stage == 2u) {
|
||||
if (s_act->done_text != NULL) {
|
||||
ac_draw_centered(layer, s_act->done_text, AC_PROMPT_Y, clip);
|
||||
/* 行 1:问句(stage 2 换成功结果行;done_text=NULL 时根本走不到 stage 2) */
|
||||
if (a_inst->stage == 2u) {
|
||||
if (a_inst->done_text != NULL) {
|
||||
ac_draw_centered(a_layer, a_inst->done_text, AC_PROMPT_Y, a_clip);
|
||||
}
|
||||
}
|
||||
else {
|
||||
ac_draw_centered(layer, s_act->prompt, AC_PROMPT_Y, clip);
|
||||
} else {
|
||||
ac_draw_centered(a_layer, a_inst->prompt, AC_PROMPT_Y, a_clip);
|
||||
}
|
||||
|
||||
/* 行 2:态标(二次确认 / 投递失败 ERR) */
|
||||
if (s_act->stage == 1u) {
|
||||
ac_draw_centered(layer, (s_act->err != 0u) ? AC_ERR_TXT : AC_STAGE2_TXT,
|
||||
AC_STAGE_Y, clip);
|
||||
/* 行 2:态标(只读 / 二次确认 / 投递失败) */
|
||||
if (ui_page_action_is_readonly(a_inst) != 0u) {
|
||||
ac_draw_centered(a_layer, AC_RO_TXT, AC_STAGE_Y, a_clip);
|
||||
} else if (a_inst->stage == 1u) {
|
||||
ac_draw_centered(a_layer,
|
||||
(a_inst->err != 0u) ? AC_ERR_TXT : AC_STAGE2_TXT,
|
||||
AC_STAGE_Y, a_clip);
|
||||
} else {
|
||||
/* stage 0/2:这里没有要说的话——留空行比硬凑一个词诚实 */
|
||||
}
|
||||
|
||||
/* 行 3:键提示 */
|
||||
ac_draw_centered(layer, (s_act->stage == 2u) ? AC_HINT_BACK : AC_HINT_DO,
|
||||
AC_HINT_Y, clip);
|
||||
/* 行 3:可选观测行(复归页用它把"保护动作 N"钉在确认同一屏上:
|
||||
* N 从 >0 变 0 就是"动作位真的复归了"的屏上证据,不用翻页也不用接表) */
|
||||
if ((a_inst->watch_label != NULL) && (a_inst->watch_cb != NULL)) {
|
||||
char watch[UI_BUF_STRLEN];
|
||||
|
||||
(void)lv_snprintf(watch, sizeof(watch), "%s %u", a_inst->watch_label,
|
||||
(unsigned int)a_inst->watch_cb());
|
||||
/* 观测行是拼出来的栈串,不是 Flash 字面量:本函数同帧内绘制完即弃 */
|
||||
ac_draw_centered(a_layer, watch, AC_WATCH_Y, a_clip);
|
||||
}
|
||||
|
||||
/* 行 4:键提示(只读时 ENTER 无效,提示必须跟着改口——屏上说"ENTER=确认"
|
||||
* 而按下去没反应,是现场投诉最多的"设备坏了"误判来源) */
|
||||
if (ui_page_action_is_readonly(a_inst) != 0u) {
|
||||
ac_draw_centered(a_layer, AC_HINT_RO, AC_HINT_Y, a_clip);
|
||||
} else {
|
||||
ac_draw_centered(a_layer,
|
||||
(a_inst->stage == 2u) ? AC_HINT_BACK : AC_HINT_DO,
|
||||
AC_HINT_Y, a_clip);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按键处理(确认链状态机)
|
||||
* 入口参数:key - LVGL 键值 lv_key_t
|
||||
* 返回值:true = 已消费;false = 交回框架(其余键忽略)
|
||||
* 限定条件:经描述符 key_cb 挂接(key_intercept=0:LEFT 直接关页 = 取消,
|
||||
* 不进本函数;ESC 恒关页框架保证,同样不进本函数)
|
||||
* 函数说明:ENTER:stage0 -> stage1(二次确认);stage1 -> 投递 ok_cb:
|
||||
* 接受且有 done_text -> stage2 回报,接受且无 done_text -> 直接关页,
|
||||
* 拒收 -> err=1 留在 stage1;stage2 -> 关页返回
|
||||
* 函数功能:按键处理(确认链状态机;框架 key_cb 的实现体)
|
||||
* 入口参数:a_inst - 操作页实例 ui_page_action_t *
|
||||
* a_key - LVGL 键值 lv_key_t
|
||||
* 返回值:1 = 已消费(框架跳过内置语义);0 = 未消费(其余键交回框架)
|
||||
* (键分发标志非错误通道,取 uint8_t——bsp-err/err-type)
|
||||
* 限定条件:经页面描述符 key_cb 挂接;LEFT/ESC 不进本函数(框架恒关页 = 取消,
|
||||
* 这是"任何一页都退得出去"的框架保证,本组件不得拦截)
|
||||
* 函数说明:ENTER——stage0 转二次确认;stage1 调 ok_cb 投递:受理且有 done_text
|
||||
* 转 stage2 回报、受理且无 done_text 直接关页、拒收置 err 留在 stage1
|
||||
* (再 ENTER 重走一次投递,不自动重试:连点两次就投两次,
|
||||
* 清除记录这类动作要的是"我确实在两下之内只发了一次");
|
||||
* stage2 关页返回——回报行必须让用户自己确认看到了才走,
|
||||
* 成功即关页的形态只在 done_text=NULL(没什么可回报的)时才成立。
|
||||
* 只读进入:先放行 stage2 的返回支(已完成的页总得关得掉),
|
||||
* 其余 ENTER 一律消费但不推进、不投递
|
||||
*/
|
||||
static bool ac_key_cb(lv_key_t key) {
|
||||
if (key != LV_KEY_ENTER) {
|
||||
return false;
|
||||
uint8_t ui_page_action_key(ui_page_action_t *a_inst, lv_key_t a_key) {
|
||||
bool ok = false;
|
||||
|
||||
if ((a_inst == NULL) || (a_inst->ok_cb == NULL)) {
|
||||
return 0u; /* 防御:配置不全的实例不该出现在这里 */
|
||||
}
|
||||
if (s_act == NULL) {
|
||||
return false; /* 防御 */
|
||||
if (a_key != LV_KEY_ENTER) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
if (s_act->stage == 0u) {
|
||||
s_act->stage = 1u;
|
||||
s_act->err = 0u;
|
||||
if (a_inst->stage == 2u) {
|
||||
ui_content_page_close();
|
||||
return 1u; /* 已回报:ENTER = 收页回菜单 */
|
||||
}
|
||||
|
||||
if (ui_page_action_is_readonly(a_inst) != 0u) {
|
||||
return 1u; /* 只读:吃掉按键,态标行已在说 LOCK,不做任何动作 */
|
||||
}
|
||||
|
||||
if (a_inst->stage == 0u) {
|
||||
a_inst->stage = 1u;
|
||||
a_inst->err = 0u;
|
||||
ui_content_page_invalidate();
|
||||
return true;
|
||||
return 1u;
|
||||
}
|
||||
|
||||
if (s_act->stage == 1u) {
|
||||
bool ok = false;
|
||||
|
||||
if (s_act->ok_cb != NULL) {
|
||||
ok = s_act->ok_cb(); /* 只投递请求,不等待执行结果 */
|
||||
}
|
||||
if (!ok) {
|
||||
s_act->err = 1u;
|
||||
ui_content_page_invalidate();
|
||||
return true; /* ERR 显示,再 ENTER 重试 */
|
||||
}
|
||||
if (s_act->done_text == NULL) {
|
||||
ui_content_page_close(); /* 成功即关页回菜单(无回报行形态) */
|
||||
return true;
|
||||
}
|
||||
s_act->stage = 2u;
|
||||
s_act->err = 0u;
|
||||
/* stage 1:第二次 ENTER = 真执行(只投递,不等执行结果) */
|
||||
ok = a_inst->ok_cb();
|
||||
if (!ok) {
|
||||
a_inst->err = 1u;
|
||||
ui_content_page_invalidate();
|
||||
return true;
|
||||
return 1u; /* ERR 显示,下一次 ENTER 重新走投递 */
|
||||
}
|
||||
|
||||
/* stage 2:已回报,ENTER 返回 */
|
||||
ui_content_page_close();
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:开操作页(装载实例 + 复位 + 框架开页)
|
||||
*/
|
||||
bsp_err_t ui_page_action_open(ui_page_action_t *a_inst, uint8_t a_perm) {
|
||||
if ((a_inst == NULL) || (a_inst->ok_cb == NULL) || (a_inst->prompt == NULL)) {
|
||||
return BSP_ERR_PARAM;
|
||||
if (a_inst->done_text == NULL) {
|
||||
ui_content_page_close(); /* 成功即关页(无回报行形态) */
|
||||
return 1u;
|
||||
}
|
||||
if (ui_content_page_is_open() != false) {
|
||||
return BSP_ERR_STATE; /* 框架单页互斥的显式化(失败原因对跳板可判) */
|
||||
}
|
||||
|
||||
s_act = a_inst;
|
||||
s_act->stage = 0u; /* 每次进入固定从一次确认开始 */
|
||||
s_act->err = 0u;
|
||||
|
||||
(void)memset(&s_desc, 0, sizeof(s_desc));
|
||||
s_desc.id = "action";
|
||||
s_desc.title = (a_inst->title != NULL) ? a_inst->title : a_inst->prompt;
|
||||
s_desc.hide_deco = 1u; /* 表单/操作页形态:无浅 V(用户密钥页先例) */
|
||||
s_desc.hide_footer = 1u; /* 无页脚页码 */
|
||||
s_desc.selectable = 0u; /* 无行列表 */
|
||||
s_desc.key_intercept = 0u; /* LEFT 不拦截:关页 = 取消 */
|
||||
s_desc.draw_cb = ac_draw_cb;
|
||||
s_desc.key_cb = ac_key_cb;
|
||||
|
||||
if (ui_content_page_open(&s_desc, a_perm) == false) {
|
||||
s_act = NULL; /* 开页失败不残留实例(钩子不会再被调) */
|
||||
return BSP_ERR_NO_SPACE;
|
||||
}
|
||||
return BSP_OK;
|
||||
a_inst->stage = 2u;
|
||||
a_inst->err = 0u;
|
||||
ui_content_page_invalidate();
|
||||
return 1u;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
/*
|
||||
* 模块名称:UI 动作确认页(操作页共享实现,ui_page_action)
|
||||
* 模块功能:可复用的"确认 -> 二次确认 -> 执行回报"操作页构建器——
|
||||
* 强制复归 / 清除记录 / 手动录波等叶子共用同一页实现,
|
||||
* 各自以一份实例(标题/问句/结果行/执行回调)开页;
|
||||
* 执行回调只投递请求不阻塞(跨任务交接由页面跳板实现),
|
||||
* 模块功能:可复用的"确认 -> 二次确认 -> 执行回报"操作页**钩子三件套**——
|
||||
* 强制复归 / 手动录波等破坏性叶子共用同一份确认链实现,各自持一份
|
||||
* 实例(标题 / 问句 / 结果行 / 执行回调 / 可选观测行);
|
||||
* 执行回调只投递请求不阻塞(跨任务交接由执行侧落定),
|
||||
* 成功经 done_text 回报后再 ENTER 返回,done_text=NULL 成功即关页;
|
||||
* 投递失败显示 ERR,下一次 ENTER 重新走二次确认
|
||||
* 投递失败显示 ERR,下一次 ENTER 重新走二次确认;
|
||||
* 只读权限(查看密码进入)可整页拒执行并显式说"没权限"(need_modify)
|
||||
* 适用范围:Drivers/BSP/UI(表单页形态:无浅 V 装饰、无页脚——用户密钥页
|
||||
* 跟随实机先例;ESC 恒关页 = 取消,LEFT 亦关页 = 取消)
|
||||
* 说明:同一时刻仅一张操作页在用(内容页框架本就全局单页),模块以
|
||||
* static 单例描述符 + 当前实例指针承载钩子上下文;页面跳板
|
||||
* (批 2 各 ui_page_xxx)持 ui_page_action_t 实例并在自己的入口
|
||||
* 调 ui_page_action_open()
|
||||
* 说明:本组件**不建描述符也不开页**(v1.1 起的形态):内容页框架按描述符
|
||||
* **指针**开页,而描述符只能由"一叶一模块"的页面文件导出(真源 =
|
||||
* ui_menu_tree.txt 的 #page 注记 + 生成物 ui_page_registry.c)——
|
||||
* 旧形态"组件自己 open()"决定了它永远不可能被注册表绑定(构建器不是
|
||||
* 描述符,全仓零调用者即其化石证据)。改成 init/draw/key 三个收实例的
|
||||
* 钩子后:确认链仍只有一份实现,而页面跳板薄到只剩"一份实例 + 一枚
|
||||
* 描述符"(见 ui_page_reset.c),多个操作页各自持实例、互不串态
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
@@ -19,6 +23,14 @@
|
||||
* 页面扩充详细方案.md §3.4 装置维护操作页 +
|
||||
* §4.2 ui_page_action bind(title, prompt, ok_cb)
|
||||
* 二次确认、ok_cb 只投递不阻塞)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-3(装置维护 3 页):改成钩子三件套
|
||||
* 形态(init/draw/key 收实例指针),删掉模块单例
|
||||
* 与 ui_page_action_open()——构建器开不了注册表
|
||||
* 绑定的页(框架按描述符指针开页),旧形态全仓
|
||||
* 零调用者;新增 need_modify(只读拒绝执行并显
|
||||
* LOCK)与可选观测行(watch_label + watch_cb,
|
||||
* 复归页用它把"动作位复归"画在同一屏上);
|
||||
* 错误通道由 bsp_err_t 改为无返回 + uint8_t 分发标志
|
||||
*/
|
||||
#ifndef UI_PAGE_ACTION_H
|
||||
#define UI_PAGE_ACTION_H
|
||||
@@ -30,39 +42,76 @@ extern "C" {
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "bsp_err.h" /* bsp_err_t(开页结果错误通道——BSP 公开 API 约定) */
|
||||
#include "lvgl.h" /* lv_key_t / lv_layer_t:钩子三件套的形参类型(本组件不建对象) */
|
||||
|
||||
/* 执行回调:只投递请求(事件位/消息队列),立即返回;
|
||||
* 返回值:true = 投递被接受(页面转成功回报),false = 拒收(页面显示 ERR) */
|
||||
typedef bool (*ui_page_action_ok_fn)(void);
|
||||
|
||||
/* 操作页实例(配置字段 Flash 指针 + 运行态);
|
||||
* 用法:页面跳板 static 一份、花括号初始化配置,开页经 ui_page_action_open */
|
||||
/* 操作页实例(配置字段 = Flash 指针与只读回调,运行态 = 本模块写的两个字节);
|
||||
* 用法:页面跳板 static 一份、花括号初始化配置,把 init/draw/key 三个钩子直接
|
||||
* 转进本组件(v1.1 起本组件不再自己开页——内容页框架按描述符指针开页,
|
||||
* 跳板就是那枚描述符的属主,见 ui_page_reset.c) */
|
||||
typedef struct {
|
||||
const char *title; /* Flash:页标题(框架头部居中) */
|
||||
const char *prompt; /* Flash:确认问句(如"确认复归所有保护?"——
|
||||
问句用字须已入字体子集) */
|
||||
/* 页标题不在本结构里:描述符(连 title)由页面跳板 own —— 见 ui_page_reset.c;
|
||||
* 两处各留一份标题就是两个真源,改一处屏上另一处不变那种 */
|
||||
const char *prompt; /* Flash:确认问句(如"确认复归保护?"——
|
||||
问句用字须已入字体子集,否则显示空白方块) */
|
||||
const char *done_text; /* Flash:成功后结果行(如"已请求复归");
|
||||
NULL = 成功后直接关页回菜单 */
|
||||
ui_page_action_ok_fn ok_cb; /* 必填:执行投递 */
|
||||
ui_page_action_ok_fn ok_cb; /* 必填:执行投递(只投递,不等待执行结果) */
|
||||
uint8_t need_modify; /* 1 = 只读权限(查看密码进入)时拒绝执行:
|
||||
两次 ENTER 都不投递、态标位改显 LOCK。
|
||||
破坏性动作必须置 1——只靠"屏上看着像能按"
|
||||
拦不住人,必须落在键路上 */
|
||||
const char *watch_label; /* Flash:可选观测行的前缀(NULL = 不画该行),
|
||||
如"保护动作"。配合 watch_cb 让"复归前 N>0、
|
||||
复归后 N=0"在同一屏上看得见 */
|
||||
uint32_t (*watch_cb)(void); /* 可选:观测行取值(NULL = 不画该行;与
|
||||
watch_label 要么都给要么都不给) */
|
||||
uint8_t stage; /* 运行态:0 一次确认 / 1 二次确认 / 2 完成回报 */
|
||||
uint8_t err; /* 运行态:1 = 上轮投递被拒(显示 ERR) */
|
||||
} ui_page_action_t;
|
||||
|
||||
/*
|
||||
* 函数功能:开操作页(装载实例 + 复位确认态 + 经内容页框架开页)
|
||||
* 入口参数:inst - 操作页实例(配置字段须已填好;ok_cb 不得为 NULL)
|
||||
* 函数功能:开页复位确认链(接框架的 init_cb;v1.1 起本组件不再自己开页)
|
||||
* 入口参数:inst - 操作页实例(ok_cb 不得为 NULL;配置字段由跳板在编译期填好)
|
||||
* ui_page_action_t *
|
||||
* perm - 授予权限(ui_perm_t;由跳板决定传什么权限,
|
||||
* 组件不拦权限只拦 ok_cb 为空)
|
||||
* 返回值:BSP_OK = 已开页;BSP_ERR_PARAM = 参数无效;
|
||||
* BSP_ERR_STATE = 已有页面打开(框架单页互斥);
|
||||
* BSP_ERR_NO_SPACE = 框架开页失败(池不足等) bsp_err_t
|
||||
* 限定条件:uiTask 上下文;每次进入从一次确认开始(不跨次保留 stage)
|
||||
* 函数说明:键语义——ENTER 推进确认链(0->1->执行->2),ESC/LEFT 关页取消;
|
||||
* stage=2 时 ENTER 关页返回
|
||||
* 返回值:无
|
||||
* 限定条件:内容页框架开页链内调用(uiTask);实例为 NULL 时空操作(防御)
|
||||
* 函数说明:每次进入固定从一次确认开始——"上一次按到一半"跨次保留下来就是
|
||||
* 第二次进页一按 ENTER 直接执行,那是操作票事故的形状,不是效率优化
|
||||
*/
|
||||
bsp_err_t ui_page_action_open(ui_page_action_t *a_inst, uint8_t a_perm);
|
||||
void ui_page_action_init(ui_page_action_t *a_inst);
|
||||
|
||||
/*
|
||||
* 函数功能:自绘(接框架的 draw_cb:问句 / 态标 / 可选观测行 / 键提示,四行居中)
|
||||
* 入口参数:inst - 操作页实例 ui_page_action_t *
|
||||
* layer - 绘制层 lv_layer_t *
|
||||
* clip - 重绘区 const lv_area_t *
|
||||
* 返回值:无
|
||||
* 限定条件:框架绘制链末尾调用(本组件只画自己那几行,标题/装饰/页脚归框架);
|
||||
* 实例或 prompt 为 NULL 时空操作
|
||||
* 函数说明:态标行优先级——只读且需修改权限 > 二次确认 > 投递失败 ERR;
|
||||
* 只读那条排最前是刻意的:现场最常见的误读是"我按了没反应",
|
||||
* 把"没权限"显出来才不会让人反复按
|
||||
*/
|
||||
void ui_page_action_draw(ui_page_action_t *a_inst, lv_layer_t *a_layer,
|
||||
const lv_area_t *a_clip);
|
||||
|
||||
/*
|
||||
* 函数功能:按键处理(接框架的 key_cb:确认链状态机)
|
||||
* 入口参数:inst - 操作页实例 ui_page_action_t *
|
||||
* key - LVGL 键值 lv_key_t
|
||||
* 返回值:1 = 已消费(框架跳过内置语义);0 = 未消费(交回框架)
|
||||
* (键分发标志非错误通道,取 uint8_t 不用 bool——
|
||||
* bsp-err/err-type 口径,同 ui_w_editor_key)
|
||||
* 限定条件:经描述符 key_cb 挂接;LEFT/ESC 不进本函数(框架恒关页 = 取消)
|
||||
* 函数说明:ENTER——stage0 转二次确认;stage1 调 ok_cb 投递:受理且有 done_text
|
||||
* 转 stage2 回报、受理且无 done_text 直接关页、拒收置 err 留在 stage1;
|
||||
* stage2 关页返回。只读且 need_modify 时任何 ENTER 都不推进也不投递
|
||||
*/
|
||||
uint8_t ui_page_action_key(ui_page_action_t *a_inst, lv_key_t a_key);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* 模块名称:UI Page clear (清除记录内容页)
|
||||
* 模块功能:装置维护 → 清除记录 叶子(node 27,#P 受密码保护)的内容页——
|
||||
* 七行清单(运行 / 自检 / 事故 / 保护告警 / 保护启动 / 操作 / 遥控记录),
|
||||
* 值列 = 该类记录当前条数(0 条显"无记录"),逐行独立清除;
|
||||
* 行内两段式确认:第一次 ENTER 该行值列变"确认?",第二次 ENTER 才投递,
|
||||
* 之后该行按清除状态显 WAIT / OK / ERR,直到光标移开才收回;
|
||||
* 条数只走 ui_w_records 的**同一枚注入 reader**(裸计数入口
|
||||
* ui_w_records_count_of),删除只发请求(执行侧 = syslog_request_clear),
|
||||
* 真反馈 = 状态机 + 下一帧条数归零,不是"按了就算成功";
|
||||
* 只读进入(查看密码)整页不投递;
|
||||
* 本文件零 App include、零文件系统触点(既不调 MCU 专属的
|
||||
* syslog_clear_apply(),也绝不自己 f_unlink / 碰卷锁——defaultTask 是
|
||||
* 记录唯一写者,uiTask 在 IWDG required_mask 内,给它一条最坏数百 ms
|
||||
* 的存储路径就是拿误复位当代价)
|
||||
* 为什么不做"一键清全部":七类记录分属不同事件面(运行/自检 vs 事故/告警/启动 vs
|
||||
* 操作/遥控审计),一次点击清光七类 = 一次误操作毁掉全部审计面;
|
||||
* 逐行清已完全覆盖判据(删后重建、计数归零、不存在时幂等)
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-26
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-26 DTU 项目组 创建(道F task-3;判据 docs/任务规划/UI补齐.md
|
||||
* §2 第 3 行「清除记录…不允许只做没有实际效果的按钮」;
|
||||
* 删除 API 与请求/落地分离 = task-4,口径真源
|
||||
* App/log/README.md 与 App/log/sys_log.h)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include <string.h>
|
||||
|
||||
#include "ui_page_clear.h"
|
||||
|
||||
#include "ui_content_page.h"
|
||||
#include "ui_data.h"
|
||||
#include "ui_tokens.h"
|
||||
#include "ui_w_records.h"
|
||||
|
||||
/* 私有宏定义区 */
|
||||
|
||||
#define CL_ROW_N UI_W_REC_LOG_N /* 行数 = 记录源个数(两端映射表长度同源) */
|
||||
#define CL_NONE 0xFFu /* "无行"哨兵(行号上限 7,绝不撞) */
|
||||
#define CL_TXT_NONE "无记录" /* 0 条(用字已在子集,与两条记录页同一词) */
|
||||
#define CL_TXT_CONFIRM "确认?" /* 第一段确认('?' 是 ASCII) */
|
||||
/* 结果词沿用全 ASCII 一排:WAIT 是正常态(MCU 端删除在 defaultTask 的
|
||||
* syslog_drain() 尾部落地),模拟器端同步删除永不出现 WAIT —— 两端都要能显示 */
|
||||
#define CL_TXT_WAIT "WAIT"
|
||||
#define CL_TXT_OK "OK"
|
||||
#define CL_TXT_ERR "ERR"
|
||||
|
||||
/* 私有函数声明区 */
|
||||
|
||||
static const char *cl_name_of(uint16_t a_row);
|
||||
static uint8_t cl_id_of(uint16_t a_row);
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
/* 行名表(Flash);顺序 = UI_W_REC_LOG_* 编号序,与两端映射表同一套下标 */
|
||||
static const char *const k_cl_name[CL_ROW_N] = {
|
||||
"运行记录", "自检记录", "事故记录", "保护告警", "保护启动", "操作记录", "遥控记录"
|
||||
};
|
||||
static const uint8_t k_cl_id[CL_ROW_N] = {
|
||||
UI_W_REC_LOG_RUN, UI_W_REC_LOG_SELFTEST, UI_W_REC_LOG_ACCIDENT, UI_W_REC_LOG_ALARM,
|
||||
UI_W_REC_LOG_START, UI_W_REC_LOG_OPER, UI_W_REC_LOG_YK
|
||||
};
|
||||
|
||||
static uint8_t s_confirm; /* 待确认行(第一次 ENTER 已按下),CL_NONE = 无 */
|
||||
static uint8_t s_note; /* 已投递、要显示清除结果的行,CL_NONE = 无 */
|
||||
static uint8_t s_err; /* 投递被拒的行,CL_NONE = 无 */
|
||||
static uint16_t s_sel_seen; /* 上一次绘制时的选中行(移开光标即收回三个锁存) */
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:取行名
|
||||
* 入口参数:row - 行号 uint16_t
|
||||
* 返回值:名(Flash 字面量;越界回空串,不越表读) const char *
|
||||
* 限定条件:无
|
||||
*/
|
||||
static const char *cl_name_of(uint16_t a_row) {
|
||||
if (a_row >= CL_ROW_N) {
|
||||
return "";
|
||||
}
|
||||
return k_cl_name[a_row];
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取行对应的记录源编号
|
||||
* 入口参数:row - 行号 uint16_t
|
||||
* 返回值:UI_W_REC_LOG_*;越界回 0(首号,调用方已在越界时不调本函数) uint8_t
|
||||
* 限定条件:无
|
||||
* 函数说明:编号不透明——本页不知道也不该知道"3 号是 alarm.log",映射在两端
|
||||
* 适配层(App/task/ui_task.c 与 simulator/main.c),加一类记录时
|
||||
* 本表加一行、两端各加一条映射,屏上自动多一行
|
||||
*/
|
||||
static uint8_t cl_id_of(uint16_t a_row) {
|
||||
if (a_row >= CL_ROW_N) {
|
||||
return (uint8_t)UI_W_REC_LOG_RUN;
|
||||
}
|
||||
return k_cl_id[a_row];
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:开页复位(框架 init_cb)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:框架开页链内(此时本次授予权限已装载)
|
||||
* 函数说明:三个锁存全部归无——清除状态是**按 id 的全局单值**(syslog 侧
|
||||
* 上电以来一直是 OK),开页直读会把"昨天清过操作记录"显示成"这次
|
||||
* 清成功了"(契约 §6.3 第 3 条同一件事:状态是全局的,页面必须锁存)
|
||||
*/
|
||||
static void pg_init(void) {
|
||||
s_confirm = CL_NONE;
|
||||
s_note = CL_NONE;
|
||||
s_err = CL_NONE;
|
||||
s_sel_seen = ui_content_page_selected_row();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:逐行取数(框架 refresh;值列按"结果 > 确认 > 条数"优先级)
|
||||
* 入口参数:a_row / a_name / a_value - 契约见 ui_content_page.h
|
||||
* 返回值:无
|
||||
* 限定条件:框架绘制链内
|
||||
* 函数说明:已投递那一行的词来自 ui_data_maint_clear_state()(执行侧直通,
|
||||
* FAIL 也照实显 ERR:原文件一个字节没少,可以再发一次请求);
|
||||
* 没投递的行每帧现算条数——清 run.log 之后 sys_log 会往**操作记录**
|
||||
* 补一条留痕,所以"操作记录"那行会从 0 变成 1,这是真的,页面不缓存
|
||||
* "我以为归零了"(清 oper.log 自己时刻意不补,App/log/README.md 已载)
|
||||
*/
|
||||
static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) {
|
||||
uint8_t id;
|
||||
uint16_t n;
|
||||
|
||||
(void)memset(a_value, 0, UI_BUF_STRLEN);
|
||||
id = cl_id_of(a_row);
|
||||
(void)memcpy(a_name, cl_name_of(a_row), strlen(cl_name_of(a_row)) + 1u);
|
||||
|
||||
if ((uint8_t)a_row == s_note) {
|
||||
int st = ui_data_maint_clear_state(id);
|
||||
if (st == UI_DATA_CLEAR_ST_OK) {
|
||||
(void)memcpy(a_value, CL_TXT_OK, sizeof(CL_TXT_OK));
|
||||
} else if (st == UI_DATA_CLEAR_ST_WAIT) {
|
||||
(void)memcpy(a_value, CL_TXT_WAIT, sizeof(CL_TXT_WAIT));
|
||||
} else {
|
||||
(void)memcpy(a_value, CL_TXT_ERR, sizeof(CL_TXT_ERR));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((uint8_t)a_row == s_err) {
|
||||
(void)memcpy(a_value, CL_TXT_ERR, sizeof(CL_TXT_ERR));
|
||||
return;
|
||||
}
|
||||
if ((uint8_t)a_row == s_confirm) {
|
||||
(void)memcpy(a_value, CL_TXT_CONFIRM, sizeof(CL_TXT_CONFIRM));
|
||||
return;
|
||||
}
|
||||
n = ui_w_records_count_of(id); /* 现算:不留"我以为归零了"的缓存 */
|
||||
if (n == 0u) {
|
||||
(void)memcpy(a_value, CL_TXT_NONE, sizeof(CL_TXT_NONE));
|
||||
return;
|
||||
}
|
||||
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%u", (unsigned int)n);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:自绘(框架 draw_cb)——顺带承担"光标移开就收回锁存"的观测
|
||||
* 入口参数:a_layer / a_clip - 契约见 ui_content_page.h
|
||||
* 返回值:无
|
||||
* 限定条件:框架绘制链内
|
||||
* 函数说明:为什么在 draw 里看选中行:↑↓ 移选是框架内置的(本页 key_cb 对
|
||||
* 非 ENTER 键一律交回框架,看不到它们),而每次移选框架都会
|
||||
* invalidate 并重绘——draw 是唯一必然发生、且总在状态变化之后的观测点。
|
||||
* 语义 = "用户已经看过程序结果 / 已经放弃这次确认",本页不再有
|
||||
* "上一次的结果挂在另一行上"的显示
|
||||
*/
|
||||
static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) {
|
||||
uint16_t sel = ui_content_page_selected_row();
|
||||
|
||||
(void)a_layer;
|
||||
(void)a_clip;
|
||||
if (sel != s_sel_seen) {
|
||||
s_sel_seen = sel;
|
||||
s_confirm = CL_NONE;
|
||||
s_note = CL_NONE;
|
||||
s_err = CL_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按键处理(行内两段式确认)
|
||||
* 入口参数:a_key - LVGL 键值 lv_key_t
|
||||
* 返回值:true = 本页已消费;false = 交回框架(↑↓ 移选、←/ESC 关页 = 取消) bool
|
||||
* 限定条件:框架按键链内;描述符 edit_capable=1
|
||||
* 函数说明:只读进入一律交回框架(本页没有只读可做的动作,也没有保存行可显
|
||||
* LOCK——清单页没有态标位,加了反而像"可以按");
|
||||
* 第一次 ENTER 只是把该行标成待确认(不做任何投递),第二次才投;
|
||||
* 投递被拒(含维护通道未注入)时置 s_err,光标一动就收回
|
||||
*/
|
||||
static bool pg_key(lv_key_t a_key) {
|
||||
uint16_t sel;
|
||||
|
||||
if (a_key != LV_KEY_ENTER) {
|
||||
return false;
|
||||
}
|
||||
if (ui_content_page_is_editable() == false) {
|
||||
return false; /* 只读:清除是修改类动作,一条都不投 */
|
||||
}
|
||||
sel = ui_content_page_selected_row();
|
||||
if (sel >= CL_ROW_N) {
|
||||
return true; /* 越界防御:不投任何编号 */
|
||||
}
|
||||
|
||||
if ((uint8_t)sel != s_confirm) {
|
||||
s_confirm = (uint8_t)sel; /* 第一段:只标记,不投递 */
|
||||
s_note = CL_NONE;
|
||||
s_err = CL_NONE;
|
||||
ui_content_page_invalidate();
|
||||
return true;
|
||||
}
|
||||
|
||||
s_confirm = CL_NONE;
|
||||
s_err = CL_NONE;
|
||||
if (ui_data_maint_clear_req(cl_id_of(sel)) == 0) {
|
||||
s_note = (uint8_t)sel; /* 已受理:接下来按状态机显 WAIT/OK/ERR */
|
||||
} else {
|
||||
s_err = (uint8_t)sel;
|
||||
}
|
||||
ui_content_page_invalidate();
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=clear 绑定引用)
|
||||
* 七行 1 页(rows_per_page = UI_LIST_ROWS(8) > 7 ⇒ 不翻页);key_intercept=0:
|
||||
* 本页没有"编辑到一半"的态要保,← 与 ESC 一律关页 = 取消
|
||||
*/
|
||||
const content_page_def_t ui_page_clear_desc = {
|
||||
.id = "clear",
|
||||
.title = "清除记录",
|
||||
.selectable = 1u,
|
||||
.edit_capable = 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,
|
||||
.row_count = CL_ROW_N,
|
||||
.init_cb = pg_init,
|
||||
.refresh = pg_refresh,
|
||||
.draw_cb = pg_draw,
|
||||
.key_cb = pg_key,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 模块名称:UI Page clear (清除记录内容页)
|
||||
* 模块功能:清除记录叶子的内容页声明——向框架 ui_content_page 提供页面描述符
|
||||
* (条数走 ui_w_records 的同一枚注入 reader、删除只发请求(syslog_request_clear 语义在 App 侧适配层))
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-26
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-26 DTU 项目组 创建(道F task-3「装置维护 3 页」,判据 =
|
||||
* docs/任务规划/UI补齐.md §2 第 3 行;形态 =
|
||||
* docs/UI设计/页面扩充详细方案.md §3.4)
|
||||
*/
|
||||
#ifndef UI_PAGE_CLEAR_H
|
||||
#define UI_PAGE_CLEAR_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "ui_content_page.h"
|
||||
|
||||
/* 页面描述符(定义在 ui_page_clear.c;生成物按菜单树 #page=clear 引用本符号) */
|
||||
extern const content_page_def_t ui_page_clear_desc;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* UI_PAGE_CLEAR_H */
|
||||
@@ -0,0 +1,388 @@
|
||||
/*
|
||||
* 模块名称:UI Page clock (时钟设置内容页)
|
||||
* 模块功能:装置维护 → 时钟设置 叶子(node 24,#P 受密码保护)的内容页——
|
||||
* 六字段表单(年/月/日/时/分/秒)+ 保存行(仅可编辑态才有),
|
||||
* 预填取自 ui_data 实时帧的 clock[20] + clock_ok(本文件不 include
|
||||
* 任何 App 头,尤其**不碰 sys_clock**:模拟器没链 sys_clock,页面里
|
||||
* 用一次就是"MCU 编得过、模拟器链接期爆"),字段编辑复用 ui_w_editor
|
||||
* (逐位 ±1、左右移光标、ENTER 提交、越界拒退并提示),
|
||||
* 保存走 ui_data_maint_clock_set() 注入转发——执行侧真机实现
|
||||
* = sys_clock_set_unix() + 回读复核,返 0 才是终态;
|
||||
* 无效值拒收:六字段先过 ui_data_clock_to_epoch() 整组校验
|
||||
* (含 2 月 30 日、非闰年 2 月 29 日这类"每段都合法、组合非法"),
|
||||
* 不过则**一个字节都不写**、保存行显 ERR;
|
||||
* 墙钟无效(clock_ok=0 或串解析失败):六行值列一律 "--" + 标题右侧
|
||||
* "无时钟"徽标,**绝不显示也绝不预填 1970**(口径见下方 CK_INVAL 注)
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-26
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-26 DTU 项目组 创建(道F task-3;判据 docs/任务规划/UI补齐.md
|
||||
* §2 第 3 行「时钟设置写入 sys_clock_set_unix()
|
||||
* 并回读」;形态 = 页面扩充详细方案.md §3.4 表单 6 字段)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include <string.h>
|
||||
|
||||
#include "ui_page_clock.h"
|
||||
|
||||
#include "ui_data.h"
|
||||
#include "ui_tokens.h"
|
||||
#include "ui_w_editor.h"
|
||||
|
||||
/* 私有宏定义区 */
|
||||
|
||||
#define CK_FIELD_N 6u /* 字段行数:年月日时分秒 */
|
||||
#define CK_ROW_COUNT_MAX 7u /* 最多行数:6 字段 + 1 保存行 */
|
||||
#define CK_SAVE_ROW CK_FIELD_N /* 保存行下标(仅可编辑态存在) */
|
||||
#define CK_BADGE_X 7 /* 徽标左边界:与行名同一列 */
|
||||
#define CK_BADGE_Y 124 /* 徽标上边界:清单最后一行(108..121)与页脚带(140+)
|
||||
之间的空带,实测三条链都没占过它 */
|
||||
#define CK_NO_CLOCK_TXT "无时钟" /* 徽标(用字已在字库) */
|
||||
#define CK_INVAL_VAL "--" /* 无效墙钟的字段值列——**不是 1970**:
|
||||
记录侧那条 "1970-01-01 08:00:00" 是
|
||||
sys_log 写死占位串(App/log/sys_log_mcu.c
|
||||
落盘时 sys_clock_get_str 返 NULL 才用),
|
||||
把它抄来预填等于让用户"照抄保存",
|
||||
一次点击就把 1970 写进 RTC —— 比不显示更坏 */
|
||||
|
||||
/* 保存行值列词表:与整定保存行、网口设置保存行同一排 ASCII 词。
|
||||
* 没有 WAIT 这一档:维护通道是**同步转发**,返回值就是执行侧带回读结论的终态,
|
||||
* 屏上挂一个永远等不到的 WAIT 比不挂更让人困惑(契约 §6.3 第 2 条同一口径) */
|
||||
#define CK_ST_ENTER "ENTER"
|
||||
#define CK_ST_OK "OK"
|
||||
#define CK_ST_ERR "ERR"
|
||||
|
||||
/*
|
||||
* 本地这条操作记录的描述串:ASCII 18 字节 < SYSLOG_DESC_MAX(28)。
|
||||
* 与远端 "clock set by 104"(App/task/iec104_serv.c)对称——面板与主站各一条、
|
||||
* 串不同,查"这装置什么时候被谁对过时"才分得开。只在保存成功那条腿投。
|
||||
*/
|
||||
#define CK_OPER_NOTE "clock set by panel"
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
/* 字段区间:日的"当月真实上限"不在这里判(编辑器只有静态 min/max),
|
||||
* 组合合法性统一由 ui_data_clock_to_epoch() 在保存时把关——少一处第二判据 */
|
||||
static const uint16_t k_ck_min[CK_FIELD_N] = {
|
||||
(uint16_t)UI_DATA_CLOCK_YEAR_MIN, 1u, 1u, 0u, 0u, 0u
|
||||
};
|
||||
static const uint16_t k_ck_max[CK_FIELD_N] = {
|
||||
(uint16_t)UI_DATA_CLOCK_YEAR_MAX, 12u, 31u, 23u, 59u, 59u
|
||||
};
|
||||
static const char *const k_ck_name[CK_FIELD_N] = {
|
||||
"年", "月", "日", "时", "分", "秒"
|
||||
};
|
||||
|
||||
/* 实时帧缓存:只在发布序号变过时整帧读(§5.4 行缓存纪律;本层不做逐行整帧拷贝) */
|
||||
static ui_data_rt_t s_rt;
|
||||
static uint32_t s_rt_seq;
|
||||
|
||||
static uint16_t s_val[CK_FIELD_N]; /* 工作值(编辑提交后落在这里;保存前不写任何数据源) */
|
||||
static uint8_t s_have; /* 1 = 最近一次预填来自一帧有效时标;0 = 无有效时标 */
|
||||
static uint8_t s_dirty; /* 1 = 用户改过工作值还没落钟:帧不得覆盖用户输入,
|
||||
值列也不写 "--"(否则编辑到一半被 200ms 节拍冲掉) */
|
||||
static uint8_t s_editing; /* 1 = 正在编辑(被编辑行 = 当前选中行) */
|
||||
static uint8_t s_st; /* 保存态锁存:0 未投过 / 1 OK / 2 ERR */
|
||||
static uint8_t s_ed_row; /* 被编辑行号(仅 s_editing=1 时有意义) */
|
||||
|
||||
static ui_w_editor_t s_ed; /* 数值编辑器实例(本页独占,勿跨页共用) */
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:跟随发布序号取实时帧,并把时标拆成六个工作值
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask(框架绘制/按键链内)
|
||||
* 函数说明:解析失败(空串 = 墙钟无效;或快照里出现非本工程口径的串)一律
|
||||
* s_have=0 → 六行显 "--" + 徽标;**不猜**、不回填 0、不回填 1970
|
||||
*/
|
||||
static void ck_sync(void) {
|
||||
uint32_t seq = ui_data_rt_seq();
|
||||
ui_data_clock_t f;
|
||||
|
||||
if ((seq != s_rt_seq) && (ui_data_rt(&s_rt) == 0)) {
|
||||
s_rt_seq = seq;
|
||||
}
|
||||
if ((s_editing != 0u) || (s_dirty != 0u)) {
|
||||
return; /* 用户正在改:200 ms 一帧的节拍不得冲掉他还没落钟的输入 */
|
||||
}
|
||||
if ((s_rt.clock_ok == 0u) || (ui_data_clock_parse(s_rt.clock, &f) != 0)) {
|
||||
s_have = 0u;
|
||||
return;
|
||||
}
|
||||
/* 无未落钟改动 ⇒ 值列跟着墙钟走:保存成功后新时标进帧,这就是"回读校验
|
||||
* 真读回来了"的屏上证据,不必页面自己造一份"我以为写进去了"的显示 */
|
||||
s_val[0] = f.year;
|
||||
s_val[1] = f.month;
|
||||
s_val[2] = f.day;
|
||||
s_val[3] = f.hour;
|
||||
s_val[4] = f.minute;
|
||||
s_val[5] = f.second;
|
||||
s_have = 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取当前可编辑性(本次进入是否有修改权限)
|
||||
* 入口参数:无
|
||||
* 返回值:1 = 可编辑;0 = 只读 uint8_t
|
||||
* 限定条件:框架开页之后(init_cb 起,is_editable 读到的就是本次进入的权限)
|
||||
* 函数说明:只读进入连保存行都不给(同整定四页的只读分支)——一个按不动的
|
||||
* 保存行只是徒增"是不是设备坏了"的误判面
|
||||
*/
|
||||
static uint8_t ck_editable(void) {
|
||||
return (ui_content_page_is_editable() != false) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:总行数(框架 count_cb)
|
||||
* 入口参数:无
|
||||
* 返回值:6(只读)或 7(可编辑:6 字段 + 保存行) uint16_t
|
||||
* 限定条件:框架绘制/翻页链内
|
||||
*/
|
||||
static uint16_t pg_count(void) {
|
||||
return (uint16_t)(CK_FIELD_N + ck_editable());
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:逐行取数(框架 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) {
|
||||
uint16_t sel = ui_content_page_selected_row();
|
||||
|
||||
(void)memset(a_value, 0, UI_BUF_STRLEN);
|
||||
|
||||
if (a_row >= CK_FIELD_N) {
|
||||
(void)memcpy(a_name, "保存", sizeof("保存"));
|
||||
(void)memcpy(a_value, (s_st == 0u) ? CK_ST_ENTER
|
||||
: ((s_st == 1u) ? CK_ST_OK : CK_ST_ERR),
|
||||
((s_st == 0u) ? sizeof(CK_ST_ENTER)
|
||||
: ((s_st == 1u) ? sizeof(CK_ST_OK) : sizeof(CK_ST_ERR))));
|
||||
return;
|
||||
}
|
||||
|
||||
(void)memcpy(a_name, k_ck_name[a_row], strlen(k_ck_name[a_row]) + 1u);
|
||||
|
||||
if ((s_editing != 0u) && (sel == a_row)) {
|
||||
ui_w_editor_format(&s_ed, a_value, UI_BUF_STRLEN); /* 编辑串与光标同规则 */
|
||||
return;
|
||||
}
|
||||
if ((s_have == 0u) && (s_dirty == 0u) && (s_editing == 0u)) {
|
||||
(void)memcpy(a_value, CK_INVAL_VAL, sizeof(CK_INVAL_VAL));
|
||||
return; /* 墙钟无效且用户还没动手:显 "--",不显 1970、也不显 0 */
|
||||
}
|
||||
if (a_row == 0u) {
|
||||
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%04u", (unsigned int)s_val[0]);
|
||||
} else {
|
||||
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%02u", (unsigned int)s_val[a_row]);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:页脚带靠左画"无时钟"徽标(墙钟无效的屏上表达)
|
||||
* 入口参数:a_layer / a_clip - 契约见 ui_content_page.h
|
||||
* 返回值:无
|
||||
* 限定条件:框架绘制链内;只在 s_have=0 时落墨
|
||||
* 函数说明:UI_COL_FG、不做反显——反显在 1bpp 下表示"选中",徽标不是可选中项;
|
||||
* 左边界/所在带见下方注释(实测过标题带会让"无时钟"和标题连成一句)
|
||||
*/
|
||||
static void ck_draw_badge(lv_layer_t *a_layer, const lv_area_t *a_clip) {
|
||||
lv_area_t area;
|
||||
lv_draw_label_dsc_t ld;
|
||||
const char *txt = CK_NO_CLOCK_TXT;
|
||||
|
||||
if ((s_have != 0u) || (s_dirty != 0u)) {
|
||||
return; /* 已有有效时标;或用户正在填一个新时间(此时钟确实还没建立,但
|
||||
屏上已经在看他输入的数,再压一个徽标只是抢注意力) */
|
||||
}
|
||||
/* 位置实测两次才定下来,两次踩的坑都记在这里(都是"看起来放得下"):
|
||||
* ① 标题带右侧(整定页"未标定"徽标那条线,右界 141):本标题"时钟设置"4 个字
|
||||
* 居中占到 x≈104,徽标 36 px 正好贴在它后面,一屏读成"时钟设置无时钟"一句
|
||||
* 话——整定页能那么放是因为它最长标题只有 3 个字(到 x≈98)。
|
||||
* ② 页脚带左侧 x=7:页脚"第 1 页,共 1 页"**不是居中的**,实测从 x=6 起排,
|
||||
* 徽标直接压在"第 1 页"上(像素比对里表现为两边都缺一块)。
|
||||
* 现落点 = 清单最后一行(108..121)与页脚带(140 起)之间的空带:谁都不压,
|
||||
* 且仍在"这一屏说的是时钟"的视线内 */
|
||||
area.x1 = CK_BADGE_X;
|
||||
/* 该版本 lv_text_get_width:长度必须传 strlen(传 -1/0 会死循环或返回 0) */
|
||||
area.x2 = CK_BADGE_X
|
||||
+ (int32_t)lv_text_get_width(txt, (uint32_t)strlen(txt), LV_FONT_DEFAULT, 0);
|
||||
area.y1 = CK_BADGE_Y;
|
||||
area.y2 = CK_BADGE_Y + UI_TEXT_BAND_H;
|
||||
if ((area.x1 > a_clip->x2) || (a_clip->x1 > area.x2) ||
|
||||
(area.y1 > a_clip->y2) || (a_clip->y1 > area.y2)) {
|
||||
return;
|
||||
}
|
||||
lv_draw_label_dsc_init(&ld);
|
||||
ld.base.layer = a_layer;
|
||||
ld.text = txt;
|
||||
ld.text_static = 1u;
|
||||
ld.color = UI_COL_FG;
|
||||
ld.ofs_x = 0;
|
||||
ld.ofs_y = 0;
|
||||
lv_draw_label(a_layer, &ld, &area);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:自绘(框架 draw_cb 末尾:编辑器光标反显 + 无时钟徽标)
|
||||
* 入口参数:a_layer / a_clip - 契约见 ui_content_page.h
|
||||
* 返回值:无
|
||||
* 限定条件:框架绘制链内
|
||||
*/
|
||||
static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) {
|
||||
if (s_editing != 0u) {
|
||||
uint16_t sel = ui_content_page_selected_row();
|
||||
int32_t row_top;
|
||||
|
||||
/* 本页 ≤7 行、rows_per_page = UI_LIST_ROWS(8) ⇒ 恒第 1 页、窗口首行 = 0;
|
||||
* 两个变量都只在编辑态有意义,就声明在编辑态这块里(cppcheck variableScope)*/
|
||||
row_top = UI_LIST_TOP + ((int32_t)sel * UI_ROW_PITCH);
|
||||
ui_w_editor_draw(&s_ed, a_layer, a_clip, row_top, 0, UI_ROW_BAND_H, UI_LIST_RIGHT_X);
|
||||
}
|
||||
ck_draw_badge(a_layer, a_clip);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:保存——整组校验通过后投递墙钟设置,成功后补投本地操作记录
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:可编辑态、保存行 ENTER;uiTask 上下文
|
||||
* 函数说明:三步顺序即判据:① to_epoch 整组校验(组合非法 → 一个字节都不写,
|
||||
* 屏上 ERR,光标留在页面上,用户改的就是那一行不必重新找);
|
||||
* ② 注入转发(真机 = sys_clock_set_unix + 回读复核;**sys_clock_set()
|
||||
* 本身恒返 0**,RTC 写失败只打日志,所以回读是唯一真判据);
|
||||
* ③ 只有返 0 才投操作记录——给一次没落地的对时留审计记录,
|
||||
* 比少一条记录坏得多
|
||||
* 结果落在 s_st 上并**锁存**:状态是全局单值,直读会让用户动一下光标
|
||||
* 就看到别处留下的旧结果(契约 §6.3 第 3 条同一条理由)
|
||||
*/
|
||||
static void ck_save(void) {
|
||||
ui_data_clock_t f;
|
||||
uint32_t epoch = 0u;
|
||||
int rc;
|
||||
|
||||
f.year = (uint16_t)s_val[0];
|
||||
f.month = (uint8_t)s_val[1];
|
||||
f.day = (uint8_t)s_val[2];
|
||||
f.hour = (uint8_t)s_val[3];
|
||||
f.minute = (uint8_t)s_val[4];
|
||||
f.second = (uint8_t)s_val[5];
|
||||
|
||||
if (ui_data_clock_to_epoch(&f, &epoch) != 0) {
|
||||
s_st = 2u; /* 组合非法:一条都不投 */
|
||||
return;
|
||||
}
|
||||
rc = ui_data_maint_clock_set(epoch);
|
||||
if (rc == 0) {
|
||||
s_st = 1u;
|
||||
s_dirty = 0u; /* 已落钟:值列交还给实时帧,回读的新时标自己会上屏 */
|
||||
(void)ui_data_maint_oper_note(CK_OPER_NOTE);
|
||||
} else {
|
||||
s_st = 2u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:开页重置运行态(框架 init_cb)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:框架开页链内(此时本次授予权限已装载,ck_editable 读得到真值)
|
||||
*/
|
||||
static void pg_init(void) {
|
||||
s_rt_seq = 0u;
|
||||
s_have = 0u;
|
||||
s_dirty = 0u;
|
||||
s_st = 0u;
|
||||
s_editing = 0u;
|
||||
s_ed_row = 0u;
|
||||
ui_w_editor_reset(&s_ed);
|
||||
(void)memset(s_val, 0, sizeof(s_val));
|
||||
ck_sync();
|
||||
if (s_have == 0u) {
|
||||
/* 无有效时标:起编值取各字段下限(不是 0、也不是 1970)——用户从
|
||||
* 2000-01-01 00:00:00 起步改,比从一片 0 里猜要少按十几次键 */
|
||||
s_val[0] = (uint16_t)UI_DATA_CLOCK_YEAR_MIN;
|
||||
s_val[1] = 1u;
|
||||
s_val[2] = 1u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按键转发(框架 key_cb)
|
||||
* 入口参数:a_key - LVGL 键值 lv_key_t
|
||||
* 返回值:true = 本页已消费;false = 交回框架(↑↓ 移选、← 关页) bool
|
||||
* 限定条件:框架按键链内;描述符 edit_capable=1 + key_intercept=1
|
||||
* 函数说明:编辑中的 ←→ 是"移数字光标",必须先给页面(key_intercept=1 的用途),
|
||||
* 否则框架会把 ← 当成关页,编辑到一半就跳出去;ESC 恒不到这里
|
||||
* (框架保证任何一页退得出去);只读进入 ENTER 一律忽略
|
||||
*/
|
||||
static bool pg_key(lv_key_t a_key) {
|
||||
uint16_t sel = ui_content_page_selected_row();
|
||||
|
||||
if (s_editing != 0u) {
|
||||
if (ui_w_editor_key(&s_ed, a_key) == 0u) {
|
||||
return false; /* 编辑器不认的键交回框架 */
|
||||
}
|
||||
if (ui_w_editor_is_editing(&s_ed) == false) {
|
||||
/* 编辑器已提交(ENTER 且范围通过):落工作值,作废上一次的保存结果 */
|
||||
if (s_ed_row < CK_FIELD_N) {
|
||||
s_val[s_ed_row] = (uint16_t)ui_w_editor_value(&s_ed);
|
||||
s_dirty = 1u; /* 工作值与墙钟从此两回事,别再让帧覆盖它 */
|
||||
}
|
||||
s_editing = 0u;
|
||||
s_st = 0u; /* 改了任何一位,上一次的保存结果就作废显示 */
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a_key != LV_KEY_ENTER) {
|
||||
return false; /* ↑↓ 等交回框架内置移选 */
|
||||
}
|
||||
if (ck_editable() == 0u) {
|
||||
return false; /* 只读:不编辑、不保存(保存行本来也不在屏上) */
|
||||
}
|
||||
if (sel >= CK_FIELD_N) {
|
||||
ck_save();
|
||||
ui_content_page_invalidate();
|
||||
return true;
|
||||
}
|
||||
/* 进字段编辑:初值取当前工作值;无有效时标时从下限起编 */
|
||||
s_ed_row = (uint8_t)sel;
|
||||
s_editing = 1u;
|
||||
ui_w_editor_begin(&s_ed, (int32_t)s_val[sel], (int32_t)k_ck_min[sel],
|
||||
(int32_t)k_ck_max[sel], 0u);
|
||||
ui_content_page_invalidate();
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=clock 绑定引用)
|
||||
* 列表页骨架(6~7 行 1 页,几何令牌同整定四页)+ 可编辑(edit_capable,
|
||||
* 还要本次授予修改权限)+ ← 拦截(编辑中移数字光标)。
|
||||
* band_h 必须 >0:selectable 页选中行是黑字,没有白带就是黑底黑字看不见选中行
|
||||
*/
|
||||
const content_page_def_t ui_page_clock_desc = {
|
||||
.id = "clock",
|
||||
.title = "时钟设置",
|
||||
.selectable = 1u,
|
||||
.edit_capable = 1u,
|
||||
.key_intercept = 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,
|
||||
.row_count = CK_ROW_COUNT_MAX,
|
||||
.init_cb = pg_init,
|
||||
.count_cb = pg_count,
|
||||
.refresh = pg_refresh,
|
||||
.draw_cb = pg_draw,
|
||||
.key_cb = pg_key,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 模块名称:UI Page clock (时钟设置内容页)
|
||||
* 模块功能:时钟设置叶子的内容页声明——向框架 ui_content_page 提供页面描述符
|
||||
* (字段编辑走 ui_w_editor、预填走 ui_data 实时帧、写方向只经 ui_data_maint_* 注入转发)
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-26
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-26 DTU 项目组 创建(道F task-3「装置维护 3 页」,判据 =
|
||||
* docs/任务规划/UI补齐.md §2 第 3 行;形态 =
|
||||
* docs/UI设计/页面扩充详细方案.md §3.4)
|
||||
*/
|
||||
#ifndef UI_PAGE_CLOCK_H
|
||||
#define UI_PAGE_CLOCK_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "ui_content_page.h"
|
||||
|
||||
/* 页面描述符(定义在 ui_page_clock.c;生成物按菜单树 #page=clock 引用本符号) */
|
||||
extern const content_page_def_t ui_page_clock_desc;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* UI_PAGE_CLOCK_H */
|
||||
@@ -10,10 +10,13 @@
|
||||
#include "ui_page_registry.h"
|
||||
|
||||
#include "ui_page_ac.h"
|
||||
#include "ui_page_clear.h"
|
||||
#include "ui_page_clock.h"
|
||||
#include "ui_page_dc.h"
|
||||
#include "ui_page_device_info.h"
|
||||
#include "ui_page_net_cfg.h"
|
||||
#include "ui_page_password.h"
|
||||
#include "ui_page_reset.h"
|
||||
#include "ui_page_runlog.h"
|
||||
#include "ui_page_selftest.h"
|
||||
#include "ui_page_soe.h"
|
||||
@@ -36,6 +39,9 @@ const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = {
|
||||
{ 14u, &ui_page_view_set_desc }, /* node 14 定值 */
|
||||
{ 15u, &ui_page_view_word_desc }, /* node 15 控制字 */
|
||||
{ 16u, &ui_page_view_plate_desc }, /* node 16 软压板 */
|
||||
{ 24u, &ui_page_clock_desc }, /* node 24 时钟设置 */
|
||||
{ 25u, &ui_page_reset_desc }, /* node 25 强制复归 */
|
||||
{ 27u, &ui_page_clear_desc }, /* node 27 清除记录 */
|
||||
{ 30u, &ui_page_net_cfg_desc }, /* node 30 网口设置 */
|
||||
{ 33u, &ui_page_soe_desc }, /* node 33 SOE记录 */
|
||||
{ 39u, &ui_page_selftest_desc }, /* node 39 自检记录 */
|
||||
|
||||
@@ -25,7 +25,7 @@ typedef struct {
|
||||
const content_page_def_t *desc; /* 页面描述符(Flash const) */
|
||||
} ui_page_entry_t;
|
||||
|
||||
#define UI_PAGE_MAP_COUNT 16u /* 绑定了内容页的叶子数 */
|
||||
#define UI_PAGE_MAP_COUNT 19u /* 绑定了内容页的叶子数 */
|
||||
|
||||
extern const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT];
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 模块名称:UI Page reset (强制复归内容页)
|
||||
* 模块功能:装置维护 → 强制复归 叶子(node 25,#P 受密码保护)的内容页——
|
||||
* 复用 ui_page_action 共享确认链(一次确认 → 二次确认 → 执行回报),
|
||||
* 并在同一屏画一行"保护动作 N"(N = 当前处于动作态的保护元件个数):
|
||||
* 复归前 N > 0、复归请求被 protectTask 消费后 N 归 0,这个变化就是
|
||||
* "动作位真的复归了"的屏上证据,不用翻页、不用外接表;
|
||||
* 执行只做两件事:投复归请求 + 补一条本地操作记录留痕;
|
||||
* 只读进入(查看密码)整页拒执行并显式说"没权限"(LOCK)——
|
||||
* 破坏性动作不给"看着像能按"的第二次机会;
|
||||
* 本文件零 App include、零存储触点(分层方向由注入表守住)
|
||||
* 为什么这么薄:确认链的形态(含二次确认与回报)是共享件,四个维护/操作类叶子
|
||||
* 共用一份实现;本文件只把"这一页问什么、执行什么、观测什么"三项
|
||||
* 配置接进框架的描述符骨架(同 ui_page_runlog 之于 ui_w_records)
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-26
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-26 DTU 项目组 创建(道F task-3;判据 docs/任务规划/UI补齐.md
|
||||
* §2 第 3 行「强制复归确认后动作位复归且有 SOE」)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include "ui_page_reset.h"
|
||||
|
||||
#include "ui_content_page.h"
|
||||
#include "ui_data.h"
|
||||
#include "ui_page_action.h"
|
||||
|
||||
/* 私有宏定义区 */
|
||||
|
||||
/*
|
||||
* 本地这条操作记录的描述串:ASCII 20 字节 < SYSLOG_DESC_MAX(28)。
|
||||
* 与远端那条(App/protocol/iec104/iec104_pts.c 的 "force reset")**刻意不同串**——
|
||||
* 操作记录是审计面,两条一样就再也分不出"是主站下发的还是现场有人按面板",
|
||||
* 而那正是查遥控事故时第一个要问的问题。
|
||||
* 为什么由页面投而不是由 prot_elm_request_reset() 投:那条函数在 algo 层,
|
||||
* 在那里投会把 sys_log.h 拽进算法层(并把记录通道的依赖摊到 test/host/prot_dim
|
||||
* 的 include 路径上);而"这是本地发起的"这个事实只有页面知道(口径真源
|
||||
* App/log/README.md 五类记录表 + task-10 结论)。
|
||||
*/
|
||||
#define RS_OPER_NOTE "force reset by panel"
|
||||
|
||||
/* 私有函数声明区(静态初始化器要引用下面两个函数的地址,故先声明) */
|
||||
|
||||
static bool rs_ok(void);
|
||||
static uint32_t rs_watch(void);
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
/*
|
||||
* 本页操作页实例(配置全为 Flash 字面量与函数地址;运行态两字节由组件写)。
|
||||
* 字段顺序 = ui_page_action_t 声明序(ARMCC v5 = C99 要求指定顺序一致)。
|
||||
*/
|
||||
static ui_page_action_t s_act = {
|
||||
"确认复归保护?", /* prompt:问句用字已在字库(确/认/复/归/保/护 + '?') */
|
||||
"已请求复归", /* done_text:回报"请求已受理",不谎称"已复归完成" */
|
||||
rs_ok, /* ok_cb */
|
||||
1u, /* need_modify:只读进入一律拒执行 */
|
||||
"保护动作", /* watch_label(用字全在既有子集内;曾拟"动作元件",
|
||||
其中"元"不在子集 —— 见本行下方注释与终报) */
|
||||
rs_watch, /* watch_cb */
|
||||
0u, /* stage(运行态初值) */
|
||||
0u /* err (运行态初值) */
|
||||
};
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:执行投递——请求复归,受理成功后补投本地操作记录
|
||||
* 入口参数:无
|
||||
* 返回值:true = 已受理(组件转成功回报);false = 拒收(组件显示 ERR) bool
|
||||
* 限定条件:由 ui_page_action_key 在第二次 ENTER 时调用(uiTask 上下文)
|
||||
* 函数说明:两条腿的次序即判据——**先请求后留痕**:请求被拒时一条记录都不投,
|
||||
* 给一次没发生过的操作写审计记录,比少一条记录坏得多(同
|
||||
* App/task/iec104_serv.c 对时留痕"只在真改成功之后才投"那条纪律)。
|
||||
* 维护通道未注入(装配缺一半)也走拒收这条腿:屏上显 ERR,
|
||||
* 比"按了像成功其实什么都没发生"诚实
|
||||
*/
|
||||
static bool rs_ok(void) {
|
||||
if (ui_data_maint_reset_req() != 0) {
|
||||
return false;
|
||||
}
|
||||
(void)ui_data_maint_oper_note(RS_OPER_NOTE);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取当前处于动作态的保护元件数(观测行 "保护动作 N" 的 N)
|
||||
* 入口参数:无
|
||||
* 返回值:元件数(未注入时 0) uint32_t
|
||||
* 限定条件:绘制路径可直调(一次转发,零 I/O、零锁)
|
||||
* 函数说明:⚠ 台架判据的前提要写清:产品构建现处"未标定闭锁"
|
||||
* (App/task/protect_task.c 停走判据),闭锁态下元件本就是复位态,
|
||||
* "N 从 >0 变 0"这一眼要等两点标定解卡后才成立;解卡前该判据
|
||||
* 退化为"页面受理 + 操作记录落一条",不得把闭锁态报成复归无效
|
||||
*/
|
||||
static uint32_t rs_watch(void) {
|
||||
return ui_data_maint_active_count();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:开页复位确认链(框架 init_cb)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:框架开页链内调用
|
||||
* 函数说明:复位由组件做(stage/err 是它的运行态),本页不重复清一遍
|
||||
*/
|
||||
static void pg_init(void) {
|
||||
ui_page_action_init(&s_act);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:自绘(框架 draw_cb;四行居中全由组件画)
|
||||
* 入口参数:a_layer / a_clip - 契约见 ui_content_page.h
|
||||
* 返回值:无
|
||||
* 限定条件:框架绘制链末尾调用
|
||||
*/
|
||||
static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) {
|
||||
ui_page_action_draw(&s_act, a_layer, a_clip);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按键转发(框架 key_cb)
|
||||
* 入口参数:a_key - LVGL 键值 lv_key_t
|
||||
* 返回值:true = 本页已消费;false = 交回框架 bool
|
||||
* 限定条件:框架按键链内
|
||||
* 函数说明:确认链的判定全在组件里(含只读拒执行),本页只如实透传返回值,
|
||||
* 不在这层"顺手"加判据——两处各判一次早晚分家
|
||||
*/
|
||||
static bool pg_key(lv_key_t a_key) {
|
||||
return (ui_page_action_key(&s_act, a_key) != 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=reset 绑定引用)
|
||||
* 表单/操作页形态:无浅 V、无页脚、无行列表(用户密钥页与网口设置页先例);
|
||||
* edit_capable=1 是**能不能改**的那半,另半(本次进入授予的权限)由
|
||||
* ui_content_page_is_editable() 给——两半都在,组件才能判出"只读";
|
||||
* key_intercept=0:LEFT 不拦截,关页 = 取消(本链没有"编辑到一半"的状态要保)
|
||||
*/
|
||||
const content_page_def_t ui_page_reset_desc = {
|
||||
.id = "reset",
|
||||
.title = "强制复归",
|
||||
.hide_deco = 1u,
|
||||
.hide_footer = 1u,
|
||||
.selectable = 0u,
|
||||
.key_intercept = 0u,
|
||||
.edit_capable = 1u,
|
||||
.init_cb = pg_init,
|
||||
.draw_cb = pg_draw,
|
||||
.key_cb = pg_key,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 模块名称:UI Page reset (强制复归内容页)
|
||||
* 模块功能:强制复归叶子的内容页声明——向框架 ui_content_page 提供页面描述符
|
||||
* (确认链与观测行全部走 ui_page_action 共享实现,本模块零 App include、零存储触点)
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-26
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-26 DTU 项目组 创建(道F task-3「装置维护 3 页」,判据 =
|
||||
* docs/任务规划/UI补齐.md §2 第 3 行;形态 =
|
||||
* docs/UI设计/页面扩充详细方案.md §3.4)
|
||||
*/
|
||||
#ifndef UI_PAGE_RESET_H
|
||||
#define UI_PAGE_RESET_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "ui_content_page.h"
|
||||
|
||||
/* 页面描述符(定义在 ui_page_reset.c;生成物按菜单树 #page=reset 引用本符号) */
|
||||
extern const content_page_def_t ui_page_reset_desc;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* UI_PAGE_RESET_H */
|
||||
@@ -31,6 +31,26 @@ void ui_w_records_bind(const ui_rec_reader_t *a_reader) {
|
||||
s_reader = a_reader;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按记录源编号查条数(不经行缓存的裸计数)
|
||||
* 入口参数:log_id - 记录源编号(UI_W_REC_LOG_*) uint8_t
|
||||
* 返回值:条数;未注入 reader 或编号未知一律 0,即空态,调用方按"无记录"显示
|
||||
* uint16_t
|
||||
* 限定条件:uiTask 上下文(记录侧绘制/按键链)
|
||||
* 函数说明:为什么开这个口子——清除记录页要在 7 行上分行显示各类条数(点完
|
||||
* "清除"之后条数归零就是它唯一的真反馈),而 reader 是本组件私有
|
||||
* 的注入表;不开放一个只读入口,页面就只能自己去注第二份 reader,
|
||||
* 那正好把 task-6 刚削掉的分层反向边又接回来(口径见
|
||||
* docs/UI设计/UI_数据供给设计说明.md §7.2/§7.3)。
|
||||
* 本函数不带行缓存:缓存属于"某一条记录的字段",与条数无关
|
||||
*/
|
||||
uint16_t ui_w_records_count_of(uint8_t a_log_id) {
|
||||
if (s_reader == NULL) {
|
||||
return 0u;
|
||||
}
|
||||
return s_reader->count(a_log_id);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:总行数(动态;每条记录 2 行,空日志 = 单条伪行"无记录")
|
||||
* 入口参数:a_inst - 记录页实例 ui_w_records_t *
|
||||
|
||||
@@ -27,11 +27,25 @@ extern "C" {
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* 记录源不透明编号:UI 侧只认"0 号 / 1 号日志",App 侧适配层映射到 syslog_id_t
|
||||
/* 记录源不透明编号:UI 侧只认"N 号日志",App 侧适配层映射到 syslog_id_t
|
||||
* (映射表见 App/task/ui_task.c 与 simulator/main.c 的绑定处;新增记录页 =
|
||||
* 这里加编号 + 两端适配层各加一条映射) */
|
||||
* 这里加编号 + 两端适配层各加一条映射)
|
||||
* ⚠ **顺序契约(Lead 裁定 2026-10-26,task-6 起 7 条)**:本表顺序必须逐字等于
|
||||
* App/log/sys_log.h 的 syslog_id_t 枚举序(RUN/SELFTEST/ACCIDENT/ALARM/START/
|
||||
* OPER/YK),且**只准尾部追加**——枚举序就是两端 s_paths[]/s_caps[] 的下标,
|
||||
* 中间插值会安静地把事故写进告警文件(记录侧已有 TC-SYSLOG-130/131/131b 那条
|
||||
* 机械防线,UI 侧对应的那条 = test/host/ui_data 的 TC-UIDATA-024 逐项核对 +
|
||||
* 两端适配层里的编译期数组守卫;编号"不透明"指的是**页面不得依赖**这个相等,
|
||||
* 不是允许两者各数各的:适配层始终显式映射,从不把本编号强转成 syslog_id_t)。
|
||||
* 追加之外的任何改动(换序/删号/复用号)都会让已落盘的记录被读成别的类别。 */
|
||||
#define UI_W_REC_LOG_RUN 0u /* 0 号:运行记录(MCU /log/run.log) */
|
||||
#define UI_W_REC_LOG_SELFTEST 1u /* 1 号:自检记录(MCU /log/selftest.log) */
|
||||
#define UI_W_REC_LOG_ACCIDENT 2u /* 2 号:事故记录(/log/accid.log,跳闸型动作) */
|
||||
#define UI_W_REC_LOG_ALARM 3u /* 3 号:保护告警(/log/alarm.log,告警型动作) */
|
||||
#define UI_W_REC_LOG_START 4u /* 4 号:保护启动(/log/start.log,picked 上升沿) */
|
||||
#define UI_W_REC_LOG_OPER 5u /* 5 号:操作记录(/log/oper.log,人工/远端操作留痕) */
|
||||
#define UI_W_REC_LOG_YK 6u /* 6 号:遥控记录(/log/yk.log,104 选择/执行/拒绝) */
|
||||
#define UI_W_REC_LOG_N 7u /* 编号个数(上界;两端映射表的数组长度,不是合法编号) */
|
||||
|
||||
/* 读取缓冲容量(UI 侧定义;必须 >= sys_log 契约下限:时间戳 19+NUL、描述 28+NUL。
|
||||
* sys_log 侧 SYSLOG_TS_LEN/SYSLOG_DESC_MAX 变化时这里同步加宽——容量不足时
|
||||
@@ -91,6 +105,19 @@ typedef struct {
|
||||
*/
|
||||
void ui_w_records_bind(const ui_rec_reader_t *a_reader);
|
||||
|
||||
/*
|
||||
* 函数功能:按记录源编号查条数(不经行缓存的裸计数;清除记录页逐行显示用)
|
||||
* 入口参数:log_id - 记录源编号(UI_W_REC_LOG_*) uint8_t
|
||||
* 返回值:条数;未注入 reader 或编号未知一律 0,即空态,调用方按"无记录"显示
|
||||
* uint16_t
|
||||
* 限定条件:uiTask 上下文(绘制/按键链)
|
||||
* 函数说明:reader 是本组件私有的注入表,不开放只读入口的话,想按行显示条数的
|
||||
* 页面只能自己去注第二份 reader——那正好把 task-6 刚削掉的分层反向边
|
||||
* 接回来(口径 docs/UI设计/UI_数据供给设计说明.md §7.2/§7.3);
|
||||
* 本函数不带行缓存(缓存属于"某条记录的字段",与条数无关)
|
||||
*/
|
||||
uint16_t ui_w_records_count_of(uint8_t a_log_id);
|
||||
|
||||
/*
|
||||
* 函数功能:总行数(动态;每条记录 2 行,空日志 = 单条伪行"无记录")
|
||||
* 入口参数:a_inst - 记录页实例 ui_w_records_t *
|
||||
|
||||
@@ -3606,6 +3606,21 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\UI\ui_page_yx.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>ui_page_clear.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\UI\ui_page_clear.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>ui_page_clock.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\UI\ui_page_clock.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>ui_page_reset.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\UI\ui_page_reset.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
|
||||
@@ -111,6 +111,9 @@ set(UI_SOURCES
|
||||
${UI_DIR}/ui_page_view_set.c
|
||||
${UI_DIR}/ui_page_view_word.c
|
||||
${UI_DIR}/ui_page_yx.c
|
||||
${UI_DIR}/ui_page_clear.c
|
||||
${UI_DIR}/ui_page_clock.c
|
||||
${UI_DIR}/ui_page_reset.c
|
||||
)
|
||||
|
||||
# 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程)
|
||||
|
||||
+141
-15
@@ -147,14 +147,20 @@ static void sim_time_cb(char *buf, size_t len) {
|
||||
* 入口参数:log_id - UI 记录源编号(UI_W_REC_LOG_*) uint8_t
|
||||
* 返回值:记录条数;未知编号返回 0(页面空态) uint16_t
|
||||
*/
|
||||
/* 镜像 App/task/ui_task.c 的 s_log_map:两端 UI 编号 → syslog_id_t 表同源同长;
|
||||
* 顺序契约(== 枚举序、只准尾追加)在真机侧有编译期守卫,宿主侧由
|
||||
* test/host/ui_data TC-UIDATA-024 与下面的数组长度同时把住 */
|
||||
static const syslog_id_t s_sim_log_map[UI_W_REC_LOG_N] = {
|
||||
SYSLOG_RUN, SYSLOG_SELFTEST, SYSLOG_ACCIDENT, SYSLOG_ALARM,
|
||||
SYSLOG_START, SYSLOG_OPER, SYSLOG_YK
|
||||
};
|
||||
typedef char sim_log_map_len_check_t[(UI_W_REC_LOG_N == (uint8_t)SYSLOG_COUNT) ? 1 : -1];
|
||||
|
||||
static uint16_t sim_rec_count(uint8_t log_id) {
|
||||
if (log_id == UI_W_REC_LOG_RUN) {
|
||||
return (uint16_t)syslog_count(SYSLOG_RUN);
|
||||
if (log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||||
return 0u;
|
||||
}
|
||||
if (log_id == UI_W_REC_LOG_SELFTEST) {
|
||||
return (uint16_t)syslog_count(SYSLOG_SELFTEST);
|
||||
}
|
||||
return 0u;
|
||||
return (uint16_t)syslog_count(s_sim_log_map[log_id]);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -164,16 +170,103 @@ static uint16_t sim_rec_count(uint8_t log_id) {
|
||||
*/
|
||||
static int sim_rec_get(uint8_t log_id, uint32_t idx, char *ts, uint32_t ts_len,
|
||||
char *desc, uint32_t desc_len) {
|
||||
if (log_id == UI_W_REC_LOG_RUN) {
|
||||
return syslog_get_record(SYSLOG_RUN, idx, ts, (size_t)ts_len, desc, (size_t)desc_len);
|
||||
if (log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||||
return -1;
|
||||
}
|
||||
if (log_id == UI_W_REC_LOG_SELFTEST) {
|
||||
return syslog_get_record(SYSLOG_SELFTEST, idx, ts, (size_t)ts_len, desc, (size_t)desc_len);
|
||||
}
|
||||
return -1;
|
||||
return syslog_get_record(s_sim_log_map[log_id], idx, ts, (size_t)ts_len, desc,
|
||||
(size_t)desc_len);
|
||||
}
|
||||
|
||||
static const ui_rec_reader_t s_rec_reader = { sim_rec_count, sim_rec_get };
|
||||
|
||||
/*
|
||||
* ======================= 道F task-3:装置维护写通道假件(镜像 ui_task.c)
|
||||
*
|
||||
* 形状与真机完全同一张函数表(六件),语义差一处如实记在下面:清除请求在宿主端
|
||||
* 是同步落地的(simulator/sim/sim_syslog.c:本调用内就删完,状态直接 OK),
|
||||
* 真机则先 WAIT 再等 defaultTask 落 —— 页面对两种时序都要显示正确,
|
||||
* 所以清除记录页的值词表里 WAIT 与 OK 都留了位置(黄金参考只覆盖得到 OK)。
|
||||
*/
|
||||
|
||||
static void sim_publish_data_frames(void); /* 前置声明:本文件下方定义 */
|
||||
|
||||
static int s_noclock; /* --noclock:把本机装成"墙钟无效" */
|
||||
static uint32_t s_sim_epoch; /* 面板设过的墙钟(0 = 没设过) */
|
||||
static uint32_t s_sim_active = 1u; /* 假"动作态元件数":1 → 复归请求后 0 */
|
||||
static char s_sim_note[32]; /* 最近一条面板留痕(页面上看不到,
|
||||
但自检/排障时是唯一的取证处) */
|
||||
|
||||
/*
|
||||
* 函数功能:假"设置墙钟"——校验后落内存并立即重发实时帧
|
||||
* 入口参数:a_epoch - Unix 秒 uint32_t
|
||||
* 返回值:0 - 已生效;-1 - 越界(与真机同一判据) int
|
||||
* 限定条件:uiTask 等价上下文(注键链内)
|
||||
* 函数说明:真机的"回读证据"来自 SD2506 回读 + 快照层 200ms 重发;模拟器没有
|
||||
* RTC 也没有周期发布(无头模式连 200ms 节拍都不走),这里用"当场重发
|
||||
* 一帧"补上那半条时间轴——与 sim_param_pump() 同一个理由。不这么做的话,
|
||||
* 保存成功但屏幕上的时间纹丝不动,黄金参考就会把"页面显示旧值"误当成
|
||||
* "回读失败",而真机上这个组合根本不可能出现(那就是假故障)
|
||||
*/
|
||||
static int sim_maint_clock_set(uint32_t a_epoch) {
|
||||
if ((a_epoch == 0u) || (a_epoch > (uint32_t)UI_DATA_CLOCK_EPOCH_MAX)) {
|
||||
return -1;
|
||||
}
|
||||
s_sim_epoch = a_epoch;
|
||||
sim_publish_data_frames();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 函数功能:假"强制复归"——把动作态元件数清零(真机由 protectTask 下一拍清) */
|
||||
static int sim_maint_reset_req(void) {
|
||||
s_sim_active = 0u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 函数功能:假"动作态元件数"(复归页观测行 "保护动作 N") */
|
||||
static uint32_t sim_maint_active_count(void) {
|
||||
return s_sim_active;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:假"面板留痕"——真投进宿主端的 /log/oper.log(不是只记账)
|
||||
* 入口参数:a_desc - 描述串 const char *
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 等价上下文
|
||||
* 函数说明:留痕必须**真的落盘**,否则清除记录页上"清完 oper.log 之后操作记录
|
||||
* 又从 0 变回 1"这条事实在模拟器里永远看不见,黄金参考也就白拍
|
||||
*/
|
||||
static void sim_maint_oper_note(const char *a_desc) {
|
||||
if (a_desc == NULL) {
|
||||
return;
|
||||
}
|
||||
(void)snprintf(s_sim_note, sizeof(s_sim_note), "%s", a_desc);
|
||||
syslog_post(SYSLOG_OPER, a_desc); /* 该 API 无格式串形态(本头就是 const char *desc) */
|
||||
}
|
||||
|
||||
/* 函数功能:假"请求清除记录"(宿主端同步落地,返回即已是终态) */
|
||||
static int sim_maint_clear_req(uint8_t a_log_id) {
|
||||
if (a_log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||||
return -1;
|
||||
}
|
||||
return (syslog_request_clear(s_sim_log_map[a_log_id]) == 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* 函数功能:假"清除状态"(值直通,与真机同一不改写口径) */
|
||||
static int sim_maint_clear_state(uint8_t a_log_id) {
|
||||
if (a_log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||||
return UI_DATA_CLEAR_ST_NONE;
|
||||
}
|
||||
return syslog_clear_state(s_sim_log_map[a_log_id]);
|
||||
}
|
||||
|
||||
static const ui_data_maint_ops_t s_maint_ops = {
|
||||
sim_maint_clock_set,
|
||||
sim_maint_reset_req,
|
||||
sim_maint_active_count,
|
||||
sim_maint_oper_note,
|
||||
sim_maint_clear_req,
|
||||
sim_maint_clear_state
|
||||
};
|
||||
/*
|
||||
* 函数功能:SOE 记录假源——固定 3 条(镜像 App/task/ui_task.c 真机绑定的形态)
|
||||
* 入口参数:无
|
||||
@@ -327,8 +420,24 @@ static void sim_publish_data_frames(void) {
|
||||
rt.yx_dp = 0x0FULL;
|
||||
rt.freq_valid = 0u; /* eng=0 期间幅值门恒失效 → 无新鲜读数 */
|
||||
rt.freq_hz = 0.0f;
|
||||
rt.clock_ok = 1u;
|
||||
(void)snprintf(rt.clock, sizeof(rt.clock), "%s", "2026-10-25 08:00:00");
|
||||
/* 墙钟两态(道F task-3):--noclock 把这台机器装成"RTC 异常/未对时"——只有
|
||||
* 这样才照得到时钟设置页的 "--" + "无时钟"徽标形态;面板设过时以面板值为准,
|
||||
* 这就是页面上"回读证据"的唯一来源(真机那边是 SD2506 回读 + 快照层重发) */
|
||||
rt.clock_ok = (s_noclock == 0) ? 1u : 0u;
|
||||
if (s_noclock != 0) {
|
||||
rt.clock[0] = 0; /* 无效时标 = 空串(页面据此显 "--",绝不显 1970) */
|
||||
} else if (s_sim_epoch != 0u) {
|
||||
ui_data_clock_t ck;
|
||||
|
||||
if (ui_data_clock_from_epoch(s_sim_epoch, &ck) == 0) {
|
||||
(void)ui_data_clock_format(&ck, rt.clock, sizeof(rt.clock));
|
||||
} else {
|
||||
rt.clock[0] = 0;
|
||||
rt.clock_ok = 0u;
|
||||
}
|
||||
} else {
|
||||
(void)snprintf(rt.clock, sizeof(rt.clock), "%s", "2026-10-25 08:00:00");
|
||||
}
|
||||
(void)ui_data_publish_rt(&rt);
|
||||
|
||||
/* 定值帧:出厂默认 + 封存(幂等),再按 prot_set 现状发一帧(见上面两个函数) */
|
||||
@@ -470,6 +579,10 @@ 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);
|
||||
/* 装置维护写通道(道F task-3):与真机同一张表形状,假件见上方实现 */
|
||||
if (ui_data_maint_bind(&s_maint_ops) != 0) {
|
||||
printf("[sim] UI maint bind: FAIL\n");
|
||||
}
|
||||
ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页假源(形态镜像真机绑定) */
|
||||
/* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页,
|
||||
* 页面任何时刻打开都拿得到帧,不必等装配节拍) */
|
||||
@@ -885,10 +998,19 @@ static int run_logtest(void) {
|
||||
* 函数功能:模拟器入口(解析模式开关 → 窗口模式或无头自检)
|
||||
* 入口参数:argc - 参数个数 int
|
||||
* argv - 参数数组(--screenshot / --keys / --standby / --cfgtest /
|
||||
* --logtest / --idle) char **
|
||||
* --logtest / --idle / --noclock) char **
|
||||
* 返回值:0 - 正常退出或自检 PASS;非 0 - 失败 int
|
||||
*/
|
||||
int main(int argc, char **argv) {
|
||||
/* 形态旗标必须先扫(道F task-3):--keys / --standby 分支命中即 return,
|
||||
* 放在它们后面的循环根本走不到 —— 这是旗标看似加了、实际只在无参窗口模式
|
||||
* 生效的那类"看起来做了"的坑,写在这里省一个人重新踩 */
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--noclock") == 0) {
|
||||
s_noclock = 1; /* 把这台机器装成"墙钟无效"(RTC 异常/未对时) */
|
||||
}
|
||||
}
|
||||
|
||||
/* 截图/自检模式:--screenshot / --keys / --standby / --cfgtest / --logtest */
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--screenshot") == 0 && (i + 1) < argc) {
|
||||
@@ -953,6 +1075,10 @@ 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);
|
||||
/* 装置维护写通道(道F task-3):与真机同一张表形状,假件见上方实现 */
|
||||
if (ui_data_maint_bind(&s_maint_ops) != 0) {
|
||||
printf("[sim] UI maint bind: FAIL\n");
|
||||
}
|
||||
ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页假源(形态镜像真机绑定) */
|
||||
/* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页,
|
||||
* 页面任何时刻打开都拿得到帧,不必等装配节拍) */
|
||||
|
||||
@@ -85,12 +85,113 @@ PAGES = [
|
||||
("netcfg_ro", "ddddde de e"), # 查看密码 000 → 只读态
|
||||
("netcfg_edit", "ddddde de uruuruuue e"), # 编辑模式:IP 首字节光标反显
|
||||
("netcfg_save", "ddddde de uruuruuue dddd e"), # 保存行 ENTER → 值列 OK
|
||||
# ---- 道F task-3 装置维护三页(根菜单第 5 项 = 4 下;子项 0/1/3 = 时钟/复归/清除)
|
||||
# 键序读法与上面同源:e 进子菜单后再 e 打开叶子(叶子带 #P → 先落密码页),
|
||||
# "uruuruuue" = 修改口令 123(可编辑态),单一个 e = 查看口令 000(只读态)。
|
||||
# 空格是分组占位:模拟器键映射把空格当可读分隔跳过(simulator/main.c 键映射)
|
||||
("clock", "dddd ee uruuruuue"),
|
||||
("clock_ro", "dddd ee e"),
|
||||
("clock_edit", "dddd ee uruuruuue e"),
|
||||
("clock_save", "dddd ee uruuruuue ddddd e u e d e"), # 秒行 +1 → 保存
|
||||
("clock_noclock", "dddd ee uruuruuue"), # 同 clock 键序 + --noclock
|
||||
("reset", "dddd ede uruuruuue"),
|
||||
("reset_2nd", "dddd ede uruuruuue e"),
|
||||
("reset_done", "dddd ede uruuruuue ee"),
|
||||
("reset_ro", "dddd ede e"),
|
||||
("clear", "dddd eddde uruuruuue"),
|
||||
("clear_2nd", "dddd eddde uruuruuue e"),
|
||||
("clear_done", "dddd eddde uruuruuue ee"),
|
||||
("clear_ro", "dddd eddde e"),
|
||||
]
|
||||
|
||||
# 文本级断言表:页名 -> [(名称, 期望串, y 带, 是否反显)]
|
||||
# 只断言确定的内容;标签与值分列断言(两列 x 不同,不能拼成一串)。
|
||||
# 覆盖本工程新增/易缺字的关键文案(网口设置/地址/掩码/逗号/保存/OK 等)。
|
||||
TEXTS = {
|
||||
# ---- 道F task-3 装置维护三页。行槽沿用列表页口径(首行 24+14i),
|
||||
# 操作页四行 y = 44/66/88/110(Drivers/BSP/UI/ui_page_action.c 的 AC_*_Y)。
|
||||
# 只断言"这一屏此刻该有什么",不拿参考图当断言(参考图会跟着实现一起错)
|
||||
"clock": [
|
||||
("标题", "时钟设置", (0, 24), False),
|
||||
("r0名", "年", (24, 38), True), ("r0值", "2026", (24, 38), True),
|
||||
("r1名", "月", (38, 52), False), ("r1值", "10", (38, 52), False),
|
||||
("r2名", "日", (52, 66), False), ("r2值", "25", (52, 66), False),
|
||||
("r3名", "时", (66, 80), False), ("r3值", "08", (66, 80), False),
|
||||
("r4名", "分", (80, 94), False), ("r4值", "00", (80, 94), False),
|
||||
("r5名", "秒", (94, 108), False), ("r5值", "00", (94, 108), False),
|
||||
("r6名", "保存", (108, 122), False), ("r6值", "ENTER", (108, 122), False),
|
||||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||||
],
|
||||
"clock_ro": [
|
||||
("标题", "时钟设置", (0, 24), False),
|
||||
("r0名", "年", (24, 38), True), ("r0值", "2026", (24, 38), True),
|
||||
("r5名", "秒", (94, 108), False), ("r5值", "00", (94, 108), False),
|
||||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||||
],
|
||||
"clock_edit": [
|
||||
("标题", "时钟设置", (0, 24), False),
|
||||
("r0名", "年", (24, 38), True),
|
||||
],
|
||||
"clock_save": [
|
||||
("标题", "时钟设置", (0, 24), False),
|
||||
("r0名", "年", (24, 38), False), ("r0值", "2026", (24, 38), False),
|
||||
("r5名", "秒", (94, 108), False), ("r5值改后", "01", (94, 108), False),
|
||||
("r6名", "保存", (108, 122), True), ("r6值", "OK", (108, 122), True),
|
||||
],
|
||||
"clock_noclock": [
|
||||
("标题", "时钟设置", (0, 24), False),
|
||||
("无时钟徽标", "无时钟", (122, 140), False),
|
||||
("页脚未被徽标压住", "第 1 页,共 1 页", (140, 160), False),
|
||||
("r0名", "年", (24, 38), True), ("r0值空", "--", (24, 38), True),
|
||||
("r6名", "保存", (108, 122), False),
|
||||
],
|
||||
"reset": [
|
||||
("标题", "强制复归", (0, 24), False),
|
||||
("问句", "确认复归保护?", (38, 60), False),
|
||||
("观测行", "保护动作", (82, 102), False),
|
||||
("键提示", "ENTER=确认 ESC=取消", (104, 126), False),
|
||||
],
|
||||
"reset_2nd": [
|
||||
("标题", "强制复归", (0, 24), False),
|
||||
("问句", "确认复归保护?", (38, 60), False),
|
||||
("二次确认", "二次确认", (60, 82), False),
|
||||
("观测行", "保护动作", (82, 102), False),
|
||||
],
|
||||
"reset_done": [
|
||||
("标题", "强制复归", (0, 24), False),
|
||||
("结果行", "已请求复归", (38, 60), False),
|
||||
("观测行归零", "保护动作", (82, 102), False),
|
||||
("返回提示", "ENTER=返回", (104, 126), False),
|
||||
],
|
||||
"reset_ro": [
|
||||
("标题", "强制复归", (0, 24), False),
|
||||
("问句", "确认复归保护?", (38, 60), False),
|
||||
("只读态标", "LOCK", (60, 82), False),
|
||||
("只读提示", "ESC=返回", (104, 126), False),
|
||||
],
|
||||
"clear": [
|
||||
("标题", "清除记录", (0, 24), False),
|
||||
("r0名", "运行记录", (24, 38), True),
|
||||
("r1名", "自检记录", (38, 52), False),
|
||||
("r3名", "保护告警", (66, 80), False),
|
||||
("r5名", "操作记录", (94, 108), False),
|
||||
("r6名", "遥控记录", (108, 122), False),
|
||||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||||
],
|
||||
"clear_2nd": [
|
||||
("标题", "清除记录", (0, 24), False),
|
||||
("确认问", "确认?", (24, 38), True),
|
||||
],
|
||||
"clear_done": [
|
||||
("标题", "清除记录", (0, 24), False),
|
||||
("结果", "OK", (24, 38), True),
|
||||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||||
],
|
||||
"clear_ro": [
|
||||
("标题", "清除记录", (0, 24), False),
|
||||
("r0名", "运行记录", (24, 38), True),
|
||||
("r6名", "遥控记录", (108, 122), False),
|
||||
],
|
||||
# v2.0(道E task-15 回炉):6 行 Flash 字面量 -> 8 行真值;标签去全角冒号(要给值列
|
||||
# 让出 MAC 的 102 px),行槽 = 38 + 13i。值全部来自模拟器固定帧(simulator/main.c 的
|
||||
# sim_publish_data_frames)与 net_cfg 出厂副本 —— 逐行断言等于把这两处口径钉在像素侧。
|
||||
@@ -383,6 +484,53 @@ TEXTS = {
|
||||
# · 徽标只属于整定态(可改值但判据停走),不属于只读态,也不属于四个查看页;
|
||||
# · 只读态没有"保存"这件事(无 commit → 组件不画保存行)。
|
||||
ABSENT = {
|
||||
# ---- 道F task-3:反向断言钉的是"规矩本身",不依赖参考图 ----
|
||||
# 1970 那条是本页最值钱的反向断言:记录侧的占位串一旦漏进时钟页,
|
||||
# 用户一次"照抄保存"就把 1970 写进 RTC(口径见 ui_page_clock.c 的 CK_INVAL 注)
|
||||
"clock_noclock": [
|
||||
("无效时标不显 1970", "1970", (0, 160), False),
|
||||
("无效时标不显旧帧值", "2026", (0, 160), False),
|
||||
("无效时标不显 08:00", "08:00", (0, 160), False),
|
||||
# 徽标压页脚这件事**不用反向断言钉**:实测本脚本的字形匹配把 y 带判得
|
||||
# 比字面宽(徽标在 125..136、页脚在 144..155,写 ("无时钟", (140,160)) 反
|
||||
# 而报"出现了")——拿它当"位置不重叠"的证据会假红。真正的防线在 TEXTS 那
|
||||
# 一侧:"页脚未被徽标压住"这条正向断言在压字的版本上实测过不了(就是
|
||||
# 上一轮那次 FAIL),它才是这条位置规矩的机器防线。
|
||||
],
|
||||
"clock": [
|
||||
("有效时标不该挂无时钟徽标", "无时钟", (122, 140), False),
|
||||
],
|
||||
"clock_ro": [
|
||||
("只读页没有保存这件事", "保存", (0, 140), False),
|
||||
],
|
||||
"clock_save": [
|
||||
("保存成功后保存行不再提示 ENTER", "ENTER", (108, 122), True),
|
||||
],
|
||||
# 操作页 hide_footer=1:页脚/页码都不该出现(出现 = 描述符没关掉内置区块)
|
||||
"reset": [
|
||||
("操作页无页脚", "第 1 页", (138, 160), False),
|
||||
("未确认前不投递", "已请求复归", (0, 160), False),
|
||||
],
|
||||
"reset_2nd": [
|
||||
("停在二次确认就没投递", "已请求复归", (0, 160), False),
|
||||
("操作页无页脚", "第 1 页", (138, 160), False),
|
||||
],
|
||||
"reset_done": [
|
||||
("回报后问句让位", "确认复归保护?", (38, 60), False),
|
||||
],
|
||||
"reset_ro": [
|
||||
("只读不进二次确认", "二次确认", (0, 160), False),
|
||||
("只读一条都不投", "已请求复归", (0, 160), False),
|
||||
("只读不显示动作结果", "OK", (60, 82), False),
|
||||
],
|
||||
"clear": [
|
||||
("没按过就不该有确认问", "确认?", (0, 160), False),
|
||||
("没投递就不该有结果词", "OK", (0, 140), False),
|
||||
],
|
||||
"clear_ro": [
|
||||
("只读不进确认态", "确认?", (0, 160), False),
|
||||
("只读不投递", "WAIT", (0, 160), False),
|
||||
],
|
||||
"sparam_ro": [
|
||||
("只读页无未标定徽标", "未标定", (0, 24), False),
|
||||
("只读页无保存行", "保存", (0, 140), False),
|
||||
@@ -416,7 +564,15 @@ STRIDE = W // 8 # I1:20 字节/行
|
||||
POOL_AFTER_ROUNDTRIP = 5448
|
||||
|
||||
|
||||
def run_sim(seq, out, cwd):
|
||||
# 每页额外的模拟器旗标(道F task-3):clock_noclock 需要一台"墙钟无效"的机器,
|
||||
# 而 rt 帧只有 --noclock 这一条路能装出来(真机侧对应"RTC 异常/未对时")。
|
||||
# 不为它另造一套假数据:假的是"时钟有没有"这件事,页面与显示链全是真的。
|
||||
EXTRA = {
|
||||
"clock_noclock": ["--noclock"],
|
||||
}
|
||||
|
||||
|
||||
def run_sim(seq, out, cwd, extra=()):
|
||||
"""在指定工作目录里跑一次无头截图。
|
||||
|
||||
模拟器按**工作目录**读写 `sim_config.ini`:目录里残留一份配置(尤其
|
||||
@@ -424,7 +580,7 @@ def run_sim(seq, out, cwd):
|
||||
拒绝保存 → 网口设置页保存行显示 ERR → 与"工厂默认态"的黄金参考对不上。
|
||||
因此一律在空临时目录里跑,回到"无配置文件 = 工厂默认"这一确定态。
|
||||
"""
|
||||
r = subprocess.run([str(EXE), "--keys", seq, out], capture_output=True,
|
||||
r = subprocess.run([str(EXE), "--keys", seq, out] + list(extra), capture_output=True,
|
||||
cwd=str(cwd))
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError("lvgl_sim --keys %r rc=%d: %s"
|
||||
@@ -589,7 +745,7 @@ def main():
|
||||
shots = {}
|
||||
for name, seq in PAGES:
|
||||
shot = os.path.join(tmp, "shot_%s.bin" % name)
|
||||
run_sim(seq, shot, tmp)
|
||||
run_sim(seq, shot, tmp, EXTRA.get(name, ()))
|
||||
shots[name] = open(shot, "rb").read()
|
||||
|
||||
n_pos = 0
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -60,6 +60,21 @@
|
||||
* 已生效的前几项不回滚(prot_set 无回滚接口,谎称整批未写更坏)
|
||||
* TC-UIDATA-021 v2 帧扩展:UI_DATA_VER == 2、既有字段偏移逐字节见证(只允许尾
|
||||
* 追加)、cal_block 两种态(未标定/已标定)整帧往返
|
||||
* ── 以下 5 组 = 装置维护通道与时钟换算(道F task-3,
|
||||
* docs/任务规划/UI补齐.md §2 第 3 行「装置维护 3 页」的地基)──
|
||||
* TC-UIDATA-022 时钟换算与"无效值拒收":12 条锚点正向换算 + 反函数互逆 +
|
||||
* **时区反证**(不得含 28800 偏移)+ 逐月天数(闰/平)+ 非法组合
|
||||
* 逐条 -2 + epoch 0/越域反算 -2(1970 绝不被反算出来)
|
||||
* TC-UIDATA-023 时标串 parse/format:正常串、空串(无效时钟)、记录侧占位串
|
||||
* "1970-01-01 08:00:00" 必须解析失败、分隔位/长度/尾字符/型对值非法
|
||||
* TC-UIDATA-024 跨层编号顺序契约:UI_W_REC_LOG_* 逐字等于 syslog_id_t 枚举序 +
|
||||
* UI 侧缓冲容量与 SYSLOG 契约同宽 + UI_DATA_CLEAR_ST_* 与
|
||||
* SYSLOG_CLEAR_* 逐值相等 + 面板两条留痕串各自不超描述上限
|
||||
* ("与远端串不同"不在这里判:字面量互比是恒真空断言,被
|
||||
* test-cppcheck 抓过,真判据落在 TC-UIDATA-026s)
|
||||
* TC-UIDATA-025 维护通道未注入防御:六口安全返回 + 残表整表拒绝且不覆盖好表
|
||||
* TC-UIDATA-026 注入后转发言行:epoch 0 哨兵不下发、reset_req 自行投操作记录=0 次
|
||||
* (谁触发谁投,防"远端 + 本地"双投)、clear 状态码直通不改码
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
@@ -84,6 +99,8 @@
|
||||
#include "ui_w_soe.c"
|
||||
#include "soe_rec.h" /* soe_type_t:事件类型编号真源 */
|
||||
#include "prot_elm.h" /* prot_elm_id_t:元件编号真源 */
|
||||
#include "sys_log.h" /* syslog_id_t / SYSLOG_CLEAR_*:记录编号与清除状态真源 */
|
||||
#include "ui_w_records.h" /* UI_W_REC_LOG_*:UI 侧记录源编号(同一 TU 里核对顺序契约) */
|
||||
|
||||
/* ===================== 断言框架 ===================== */
|
||||
|
||||
@@ -886,6 +903,396 @@ static void tc_param_frame_v2(void)
|
||||
CHECK(got.cal_block == 0u, "TC-UIDATA-021j zero-initialized frame means not blocked");
|
||||
}
|
||||
|
||||
/* ============ 装置维护写通道与时钟换算(道F task-3,UI 补齐 §2 第 3 行)============ */
|
||||
|
||||
/*
|
||||
* 维护执行侧桩件:只回答"哪个口被调了几次、最后一次收到什么",不做任何业务判断。
|
||||
* 计数而不是布尔:多处偷投同一条操作记录(本道最怕的"远端 + 本地各投一次")
|
||||
* 只有靠次数才照得出来。
|
||||
*/
|
||||
static int s_stub_clock_rc;
|
||||
static int s_stub_clock_calls;
|
||||
static uint32_t s_stub_clock_last;
|
||||
static int s_stub_reset_rc;
|
||||
static int s_stub_reset_calls;
|
||||
static uint32_t s_stub_active;
|
||||
static int s_stub_note_calls;
|
||||
static char s_stub_note_last[32];
|
||||
static int s_stub_clear_calls;
|
||||
static uint8_t s_stub_clear_last_id;
|
||||
static int s_stub_clear_state_calls;
|
||||
static uint8_t s_stub_clear_state_id;
|
||||
static int s_stub_clear_state_rc;
|
||||
|
||||
static int stub_clock_set(uint32_t a_epoch)
|
||||
{
|
||||
s_stub_clock_calls++;
|
||||
s_stub_clock_last = a_epoch;
|
||||
return s_stub_clock_rc;
|
||||
}
|
||||
|
||||
static int stub_reset_req(void)
|
||||
{
|
||||
s_stub_reset_calls++;
|
||||
return s_stub_reset_rc;
|
||||
}
|
||||
|
||||
static uint32_t stub_active_count(void)
|
||||
{
|
||||
return s_stub_active;
|
||||
}
|
||||
|
||||
static void stub_oper_note(const char *a_desc)
|
||||
{
|
||||
s_stub_note_calls++;
|
||||
if (a_desc != NULL) {
|
||||
(void)strncpy(s_stub_note_last, a_desc, sizeof(s_stub_note_last) - 1u);
|
||||
s_stub_note_last[sizeof(s_stub_note_last) - 1u] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int stub_clear_req(uint8_t a_id)
|
||||
{
|
||||
s_stub_clear_calls++;
|
||||
s_stub_clear_last_id = a_id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int stub_clear_state(uint8_t a_id)
|
||||
{
|
||||
s_stub_clear_state_calls++;
|
||||
s_stub_clear_state_id = a_id;
|
||||
return s_stub_clear_state_rc;
|
||||
}
|
||||
|
||||
static void stub_reset_all(void)
|
||||
{
|
||||
s_stub_clock_rc = 0;
|
||||
s_stub_clock_calls = 0;
|
||||
s_stub_clock_last = 0u;
|
||||
s_stub_reset_rc = 0;
|
||||
s_stub_reset_calls = 0;
|
||||
s_stub_active = 0u;
|
||||
s_stub_note_calls = 0;
|
||||
s_stub_note_last[0] = '\0';
|
||||
s_stub_clear_calls = 0;
|
||||
s_stub_clear_last_id = 0u;
|
||||
s_stub_clear_state_calls = 0;
|
||||
s_stub_clear_state_id = 0u;
|
||||
s_stub_clear_state_rc = UI_DATA_CLEAR_ST_NONE;
|
||||
}
|
||||
|
||||
static const ui_data_maint_ops_t s_maint_stub = {
|
||||
stub_clock_set, stub_reset_req, stub_active_count,
|
||||
stub_oper_note, stub_clear_req, stub_clear_state
|
||||
};
|
||||
|
||||
/* 故意缺一条(clear_state 为 NULL)的残表:注入必须整表拒绝,不能半途接住 */
|
||||
static const ui_data_maint_ops_t s_maint_broken = {
|
||||
stub_clock_set, stub_reset_req, stub_active_count,
|
||||
stub_oper_note, stub_clear_req, NULL
|
||||
};
|
||||
|
||||
/* 日历用例:字段 → 期望秒(期望值由独立实现(Python/JS 格里历)算出,不自我复制) */
|
||||
typedef struct {
|
||||
uint16_t y;
|
||||
uint8_t mo;
|
||||
uint8_t d;
|
||||
uint8_t h;
|
||||
uint8_t mi;
|
||||
uint8_t s;
|
||||
uint32_t epoch;
|
||||
} clock_case_t;
|
||||
|
||||
static const clock_case_t k_clock_cases[] = {
|
||||
{ 2000u, 1u, 1u, 0u, 0u, 0u, 946684800u }, /* 年域下限(RTC 年字段起点) */
|
||||
{ 2000u, 2u, 29u, 0u, 0u, 0u, 951782400u }, /* 千禧闰年 2/29 */
|
||||
{ 2000u, 3u, 1u, 0u, 0u, 0u, 951868800u }, /* 闰日次日:月序跨界 */
|
||||
{ 2001u, 2u, 28u, 23u, 59u, 59u, 983404799u }, /* 平年 2 月末 + 当日最大秒 */
|
||||
{ 2004u, 2u, 29u, 12u, 0u, 0u, 1078056000u }, /* 4 年闰规则 */
|
||||
{ 2016u,12u, 31u, 0u, 0u, 0u, 1483142400u }, /* 年末 */
|
||||
{ 2020u, 6u, 15u, 6u, 30u, 45u, 1592202645u }, /* 任意中段 */
|
||||
{ 2024u, 2u, 29u, 12u, 34u, 56u, 1709210096u }, /* 近一个闰日 */
|
||||
{ 2025u, 2u, 28u, 0u, 0u, 1u, 1740700801u }, /* 平年 2/28 */
|
||||
{ 2026u,10u, 25u, 8u, 0u, 0u, 1792915200u }, /* 模拟器固定帧那一刻 */
|
||||
{ 2037u,12u, 31u, 23u, 59u, 59u, 2145916799u }, /* 上界前一天 */
|
||||
{ 2038u, 1u, 19u, 3u, 14u, 7u, 2147483647u } /* uint32 秒域上界 = 0x7FFFFFFF */
|
||||
};
|
||||
|
||||
/*
|
||||
* TC-UIDATA-022 时钟换算与"无效值拒收"
|
||||
* a) 12 条锚点正向换算(期望值来自独立格里历实现,不是本函数的自我复述)
|
||||
* b) 反函数逐条回原文(互逆性:屏上显示的时间 = 存进去的时间,不多不少一小时)
|
||||
* c) **时区反证**:2026-10-25 08:00:00 必须是 1792915200,而不是它 +28800;
|
||||
* 本工程墙钟 epoch 域就是本地时间,谁在这里补了 8 小时,模拟器照样自洽、
|
||||
* 只有真机上"屏上输 08:00、存进去 16:00"(App/task/ui_task.c 的 SOE 时标注记
|
||||
* 是同一条纪律的另一处防线)
|
||||
* d) 逐月天数(含闰年 2 月 29 / 平年 2 月 28 / 出域月返 0)
|
||||
* e) 非法组合逐条拒(2 月 30、非闰 2 月 29、月 0/13、日 0/32、时 24、分秒 60、
|
||||
* 年 1999/2039),且 -2 与"越能力上界"的 -3 可分
|
||||
* f) 边界外秒域反算(epoch 0 与 0x80000000 都拒;0 是记录侧占位起点,绝不能
|
||||
* 被反算成 1970 显示到屏上)
|
||||
*/
|
||||
static void tc_clock_convert(void)
|
||||
{
|
||||
ui_data_clock_t t;
|
||||
uint32_t e;
|
||||
uint32_t i;
|
||||
int rc;
|
||||
int ok_fwd = 1;
|
||||
int ok_rev = 1;
|
||||
int n = (int)(sizeof(k_clock_cases) / sizeof(k_clock_cases[0]));
|
||||
|
||||
for (i = 0u; i < (uint32_t)n; i++) {
|
||||
(void)memset(&t, 0, sizeof(t));
|
||||
t.year = k_clock_cases[i].y;
|
||||
t.month = k_clock_cases[i].mo;
|
||||
t.day = k_clock_cases[i].d;
|
||||
t.hour = k_clock_cases[i].h;
|
||||
t.minute = k_clock_cases[i].mi;
|
||||
t.second = k_clock_cases[i].s;
|
||||
e = 0u;
|
||||
rc = ui_data_clock_to_epoch(&t, &e);
|
||||
if ((rc != 0) || (e != k_clock_cases[i].epoch)) {
|
||||
ok_fwd = 0;
|
||||
printf(" case %u: rc=%d got=%lu want=%lu\n", (unsigned)i, rc,
|
||||
(unsigned long)e, (unsigned long)k_clock_cases[i].epoch);
|
||||
}
|
||||
(void)memset(&t, 0xFF, sizeof(t));
|
||||
rc = ui_data_clock_from_epoch(k_clock_cases[i].epoch, &t);
|
||||
if ((rc != 0) || (t.year != k_clock_cases[i].y) || (t.month != k_clock_cases[i].mo) ||
|
||||
(t.day != k_clock_cases[i].d) || (t.hour != k_clock_cases[i].h) ||
|
||||
(t.minute != k_clock_cases[i].mi) || (t.second != k_clock_cases[i].s)) {
|
||||
ok_rev = 0;
|
||||
printf(" rev case %u: rc=%d got=%u-%u-%u %u:%u:%u\n", (unsigned)i, rc,
|
||||
(unsigned)t.year, (unsigned)t.month, (unsigned)t.day,
|
||||
(unsigned)t.hour, (unsigned)t.minute, (unsigned)t.second);
|
||||
}
|
||||
}
|
||||
CHECK(ok_fwd, "TC-UIDATA-022a fields to epoch over 12 anchors");
|
||||
CHECK(ok_rev, "TC-UIDATA-022b epoch to fields is the exact inverse");
|
||||
|
||||
/* 时区反证:本函数不得含任何时区偏移 */
|
||||
(void)memset(&t, 0, sizeof(t));
|
||||
t.year = 2026u; t.month = 10u; t.day = 25u; t.hour = 8u; t.minute = 0u; t.second = 0u;
|
||||
e = 0u;
|
||||
(void)ui_data_clock_to_epoch(&t, &e);
|
||||
CHECK(e == 1792915200u, "TC-UIDATA-022c no timezone shift baked in (local epoch domain)");
|
||||
CHECK(e != (1792915200u + 28800u), "TC-UIDATA-022c2 must not be plus 8 hours");
|
||||
|
||||
/* 逐月天数(闰/平两年 + 出域月) */
|
||||
{
|
||||
static const uint8_t k_leap[12] = { 31u,29u,31u,30u,31u,30u,31u,31u,30u,31u,30u,31u };
|
||||
static const uint8_t k_flat[12] = { 31u,28u,31u,30u,31u,30u,31u,31u,30u,31u,30u,31u };
|
||||
int ok_dim = 1;
|
||||
for (i = 1u; i <= 12u; i++) {
|
||||
if (ui_data_clock_days_in_month(2000u, (uint8_t)i) != k_leap[i - 1u]) ok_dim = 0;
|
||||
if (ui_data_clock_days_in_month(2025u, (uint8_t)i) != k_flat[i - 1u]) ok_dim = 0;
|
||||
}
|
||||
CHECK(ok_dim, "TC-UIDATA-022d days in month (leap 2000 / flat 2025)");
|
||||
CHECK(ui_data_clock_days_in_month(2000u, 0u) == 0u, "TC-UIDATA-022d2 month 0 means invalid");
|
||||
CHECK(ui_data_clock_days_in_month(2000u, 13u) == 0u, "TC-UIDATA-022d3 month 13 means invalid");
|
||||
}
|
||||
|
||||
/* 非法组合逐条:一律 -2(输入错),不是 -3(能力边界) */
|
||||
{
|
||||
static const clock_case_t k_bad[] = {
|
||||
{ 2025u, 2u, 29u, 0u, 0u, 0u, 0u }, /* 非闰年 2/29 */
|
||||
{ 2026u, 2u, 30u, 0u, 0u, 0u, 0u }, /* 2 月 30 日 */
|
||||
{ 2026u, 4u, 31u, 0u, 0u, 0u, 0u }, /* 4 月无 31 日 */
|
||||
{ 2026u, 0u, 1u, 0u, 0u, 0u, 0u }, /* 月 0 */
|
||||
{ 2026u,13u, 1u, 0u, 0u, 0u, 0u }, /* 月 13 */
|
||||
{ 2026u,10u, 0u, 0u, 0u, 0u, 0u }, /* 日 0 */
|
||||
{ 2026u,10u, 32u, 0u, 0u, 0u, 0u }, /* 日 32 */
|
||||
{ 2026u,10u, 25u,24u, 0u, 0u, 0u }, /* 时 24 */
|
||||
{ 2026u,10u, 25u, 0u,60u, 0u, 0u }, /* 分 60 */
|
||||
{ 2026u,10u, 25u, 0u, 0u,60u, 0u }, /* 秒 60 */
|
||||
{ 1999u, 1u, 1u, 0u, 0u, 0u, 0u }, /* 年 < RTC 年域 */
|
||||
{ 2039u, 1u, 1u, 0u, 0u, 0u, 0u } /* 年 > uint32 秒域 */
|
||||
};
|
||||
int ok_bad = 1;
|
||||
for (i = 0u; i < (uint32_t)(sizeof(k_bad) / sizeof(k_bad[0])); i++) {
|
||||
/* 逐字段搬(两种结构体尺寸不同,靠强转按字节拷是给自己埋雷) */
|
||||
t.year = k_bad[i].y;
|
||||
t.month = k_bad[i].mo;
|
||||
t.day = k_bad[i].d;
|
||||
t.hour = k_bad[i].h;
|
||||
t.minute = k_bad[i].mi;
|
||||
t.second = k_bad[i].s;
|
||||
rc = ui_data_clock_to_epoch(&t, &e);
|
||||
if (rc != -2) {
|
||||
ok_bad = 0;
|
||||
printf(" bad case %u: rc=%d (want -2)\n", (unsigned)i, rc);
|
||||
}
|
||||
}
|
||||
CHECK(ok_bad, "TC-UIDATA-022e invalid field combos rejected with -2");
|
||||
CHECK(ui_data_clock_to_epoch(NULL, &e) == -1, "TC-UIDATA-022e2 NULL input is -1");
|
||||
CHECK(ui_data_clock_to_epoch(&t, NULL) == -1, "TC-UIDATA-022e3 NULL output is -1");
|
||||
}
|
||||
|
||||
/* 反算越界:0 是"墙钟无效"的占位起点,绝不能显示成 1970-01-01 */
|
||||
CHECK(ui_data_clock_from_epoch(0u, &t) == -2, "TC-UIDATA-022f epoch 0 rejected (never show 1970)");
|
||||
CHECK(ui_data_clock_from_epoch(946684799u, &t) == -2, "TC-UIDATA-022f2 epoch below 2000 rejected");
|
||||
CHECK(ui_data_clock_from_epoch(0x80000000u, &t) == -2, "TC-UIDATA-022f3 epoch above uint32 domain");
|
||||
CHECK(ui_data_clock_from_epoch(0u, NULL) == -1, "TC-UIDATA-022f4 NULL output is -1");
|
||||
}
|
||||
|
||||
/*
|
||||
* TC-UIDATA-023 时标串 parse / format
|
||||
* 快照 rt.clock 的串型(19 字符)是页面预填的唯一来源;无效时钟时它是空串,
|
||||
* 记录侧的占位串 "1970-01-01 08:00:00" 同样必须解析失败——否则页面会把
|
||||
* 占位串当真实时间预填,用户照抄保存就等于把 1970 写进 RTC。
|
||||
*/
|
||||
static void tc_clock_string(void)
|
||||
{
|
||||
ui_data_clock_t t;
|
||||
char buf[24];
|
||||
uint32_t e;
|
||||
|
||||
(void)memset(&t, 0, sizeof(t)); /* 解析失败按契约不写输出,先给个确定的初值 */
|
||||
e = 0u;
|
||||
|
||||
CHECK(ui_data_clock_parse("2026-10-25 08:00:00", &t) == 0, "TC-UIDATA-023a parse well-formed ts");
|
||||
if (t.year == 2026u && t.month == 10u && t.day == 25u && t.hour == 8u) {
|
||||
(void)ui_data_clock_to_epoch(&t, &e);
|
||||
CHECK(e == 1792915200u, "TC-UIDATA-023b parsed ts maps to same epoch as fields");
|
||||
} else {
|
||||
CHECK(0, "TC-UIDATA-023b parsed fields wrong");
|
||||
}
|
||||
|
||||
CHECK(ui_data_clock_parse("", &t) == -2, "TC-UIDATA-023c empty ts (clock invalid) is -2");
|
||||
CHECK(ui_data_clock_parse("1970-01-01 08:00:00", &t) == -2,
|
||||
"TC-UIDATA-023d record placeholder ts must not parse (no 1970 on screen)");
|
||||
CHECK(ui_data_clock_parse("2026-10-25T08:00:00", &t) == -2, "TC-UIDATA-023e wrong separator");
|
||||
CHECK(ui_data_clock_parse("2026-10-25 08:00:0", &t) == -2, "TC-UIDATA-023f short ts");
|
||||
CHECK(ui_data_clock_parse("2026-10-25 08:00:001", &t) == -2, "TC-UIDATA-023g trailing junk");
|
||||
CHECK(ui_data_clock_parse("2O26-10-25 08:00:00", &t) == -2, "TC-UIDATA-023h non digit year");
|
||||
CHECK(ui_data_clock_parse("2026-02-30 00:00:00", &t) == -2, "TC-UIDATA-023i shaped but invalid day");
|
||||
CHECK(ui_data_clock_parse("20260101 00:00:00", &t) == -2, "TC-UIDATA-023j missing dashes");
|
||||
CHECK(ui_data_clock_parse(NULL, &t) == -1, "TC-UIDATA-023k NULL ts is -1");
|
||||
CHECK(ui_data_clock_parse("2026-10-25 08:00:00", NULL) == -1, "TC-UIDATA-023l NULL out is -1");
|
||||
|
||||
(void)memset(buf, 0x7A, sizeof(buf));
|
||||
CHECK(ui_data_clock_format(&t, buf, sizeof(buf)) == 0, "TC-UIDATA-023m format ok into 24B buffer");
|
||||
CHECK(strcmp(buf, "2026-10-25 08:00:00") == 0, "TC-UIDATA-023n formatted string round-trips");
|
||||
(void)memset(buf, 0x7A, sizeof(buf));
|
||||
CHECK(ui_data_clock_format(&t, buf, 19u) == -1, "TC-UIDATA-023o capacity 19 refused (need 20)");
|
||||
CHECK(buf[0] == (char)0x7A, "TC-UIDATA-023p refused format writes nothing");
|
||||
CHECK(ui_data_clock_format(NULL, buf, sizeof(buf)) == -1, "TC-UIDATA-023q NULL in is -1");
|
||||
}
|
||||
|
||||
/*
|
||||
* TC-UIDATA-024 跨层编号顺序契约(Lead 裁定:UI 侧编号逐字等于 syslog_id_t 枚举序,
|
||||
* 只准尾部追加)+ 清除状态码逐值相等 + 墙钟字段容量与串型一致。
|
||||
* 适配层始终显式映射(从不强转),这条断言钉的是"两套编号今天确实相等"这件事
|
||||
* 本身——枚举中间插值会安静地把事故读成告警,只有在这里才会响。
|
||||
*/
|
||||
static void tc_record_codes(void)
|
||||
{
|
||||
CHECK(UI_W_REC_LOG_RUN == (uint8_t)SYSLOG_RUN, "TC-UIDATA-024a RUN order");
|
||||
CHECK(UI_W_REC_LOG_SELFTEST == (uint8_t)SYSLOG_SELFTEST, "TC-UIDATA-024b SELFTEST order");
|
||||
CHECK(UI_W_REC_LOG_ACCIDENT == (uint8_t)SYSLOG_ACCIDENT, "TC-UIDATA-024c ACCIDENT order");
|
||||
CHECK(UI_W_REC_LOG_ALARM == (uint8_t)SYSLOG_ALARM, "TC-UIDATA-024d ALARM order");
|
||||
CHECK(UI_W_REC_LOG_START == (uint8_t)SYSLOG_START, "TC-UIDATA-024e START order");
|
||||
CHECK(UI_W_REC_LOG_OPER == (uint8_t)SYSLOG_OPER, "TC-UIDATA-024f OPER order");
|
||||
CHECK(UI_W_REC_LOG_YK == (uint8_t)SYSLOG_YK, "TC-UIDATA-024g YK order");
|
||||
CHECK(UI_W_REC_LOG_N == (uint8_t)SYSLOG_COUNT, "TC-UIDATA-024h count matches sentinel (append only below)");
|
||||
CHECK(UI_DATA_CLOCK_TS_LEN == SYSLOG_TS_LEN, "TC-UIDATA-024i ts field length same contract on both sides");
|
||||
CHECK(UI_W_REC_TS_LEN == (SYSLOG_TS_LEN + 1u), "TC-UIDATA-024j UI ts buffer covers SYSLOG_TS_LEN + NUL");
|
||||
CHECK(UI_W_REC_DESC_LEN == (SYSLOG_DESC_MAX + 1u), "TC-UIDATA-024k UI desc buffer covers max + NUL");
|
||||
CHECK(UI_DATA_CLEAR_ST_NONE == SYSLOG_CLEAR_NONE, "TC-UIDATA-024l clear NONE value");
|
||||
CHECK(UI_DATA_CLEAR_ST_OK == SYSLOG_CLEAR_OK, "TC-UIDATA-024m clear OK value");
|
||||
CHECK(UI_DATA_CLEAR_ST_WAIT == SYSLOG_CLEAR_WAIT, "TC-UIDATA-024n clear WAIT value");
|
||||
CHECK(UI_DATA_CLEAR_ST_FAIL == SYSLOG_CLEAR_FAIL, "TC-UIDATA-024o clear FAIL value");
|
||||
/* 操作记录描述上限:本地/远端两条留痕串都必须装得下,超了会被投递侧静默截断 */
|
||||
CHECK(strlen("force reset by panel") <= (size_t)SYSLOG_DESC_MAX,
|
||||
"TC-UIDATA-024p panel reset note fits desc limit");
|
||||
CHECK(strlen("clock set by panel") <= (size_t)SYSLOG_DESC_MAX,
|
||||
"TC-UIDATA-024q panel clock note fits desc limit");
|
||||
/* 这里曾写 CHECK(strcmp("force reset by panel", "force reset") != 0, ...):
|
||||
* 两个字面量互比 = 恒真,test-cppcheck 一眼抓出(钉口径的断言永不可能失败,
|
||||
* 比根本没有断言更坏——它让人以为这条已经被管住了)。同一判据挪到
|
||||
* TC-UIDATA-026s:拿实际落进执行侧缓冲的串去比远端那条文案 */
|
||||
}
|
||||
|
||||
/*
|
||||
* TC-UIDATA-025 维护通道未注入时的防御:六个口一律安全返回、不崩、ready 返 0;
|
||||
* 残表(某成员 NULL)整表拒绝且不覆盖已生效的好表——半途接住会在"按下才崩"的位置崩。
|
||||
*/
|
||||
static void tc_maint_not_bound(void)
|
||||
{
|
||||
(void)ui_data_maint_bind(NULL);
|
||||
stub_reset_all();
|
||||
|
||||
CHECK(ui_data_maint_ready() == 0u, "TC-UIDATA-025a not bound before bind");
|
||||
CHECK(ui_data_maint_clock_set(1792915200u) == -1, "TC-UIDATA-025b clock_set not bound is -1");
|
||||
CHECK(ui_data_maint_reset_req() == -1, "TC-UIDATA-025c reset_req not bound is -1");
|
||||
CHECK(ui_data_maint_active_count() == 0u, "TC-UIDATA-025d active_count not bound is 0");
|
||||
CHECK(ui_data_maint_oper_note("x") == -1, "TC-UIDATA-025e oper_note not bound is -1");
|
||||
CHECK(ui_data_maint_clear_req(UI_W_REC_LOG_RUN) == -1, "TC-UIDATA-025f clear_req not bound is -1");
|
||||
CHECK(ui_data_maint_clear_state(UI_W_REC_LOG_RUN) == UI_DATA_CLEAR_ST_NONE,
|
||||
"TC-UIDATA-025g clear_state not bound is NONE");
|
||||
CHECK(ui_data_maint_bind(&s_maint_broken) == -1, "TC-UIDATA-025h incomplete table refused");
|
||||
CHECK(ui_data_maint_ready() == 0u, "TC-UIDATA-025i refused table left channel unbound");
|
||||
CHECK(ui_data_maint_bind(&s_maint_stub) == 0, "TC-UIDATA-025j complete table accepted");
|
||||
CHECK(ui_data_maint_ready() == 1u, "TC-UIDATA-025k bound after accept");
|
||||
CHECK(ui_data_maint_bind(&s_maint_broken) == -1, "TC-UIDATA-025l broken table refused again");
|
||||
CHECK(ui_data_maint_ready() == 1u, "TC-UIDATA-025m refusal keeps the good table active");
|
||||
}
|
||||
|
||||
/*
|
||||
* TC-UIDATA-026 注入后的转发言行(页面侧全部判据的地基):
|
||||
* - clock_set(0) 是本层保留的"未初始化"哨兵,必须挡在下发之前(桩计数为 0)
|
||||
* - reset_req **不得**自己投操作记录:谁触发谁投,这条留痕属页面;
|
||||
* 在转发层偷投 = 同一次操作两个真源,也正是"远端那条已投过"会被重复投的形态
|
||||
* - clear_state 返回值直通不改码(WAIT/FAIL 都是正常显示态)
|
||||
* - oper_note(NULL) 不投递、不崩
|
||||
*/
|
||||
static void tc_maint_forwarding(void)
|
||||
{
|
||||
(void)ui_data_maint_bind(&s_maint_stub);
|
||||
stub_reset_all();
|
||||
|
||||
CHECK(ui_data_maint_clock_set(0u) == -2, "TC-UIDATA-026a epoch 0 sentinel refused locally");
|
||||
CHECK(s_stub_clock_calls == 0, "TC-UIDATA-026b sentinel never reaches executor");
|
||||
s_stub_clock_rc = 0;
|
||||
CHECK(ui_data_maint_clock_set(1792915200u) == 0, "TC-UIDATA-026c clock_set forwarded");
|
||||
CHECK((s_stub_clock_calls == 1) && (s_stub_clock_last == 1792915200u),
|
||||
"TC-UIDATA-026d executor got the exact epoch");
|
||||
s_stub_clock_rc = -1;
|
||||
CHECK(ui_data_maint_clock_set(1792915200u) == -1, "TC-UIDATA-026e executor failure propagates");
|
||||
|
||||
CHECK(ui_data_maint_reset_req() == 0, "TC-UIDATA-026f reset accepted");
|
||||
CHECK(s_stub_reset_calls == 1, "TC-UIDATA-026g executor reset called once");
|
||||
CHECK(s_stub_note_calls == 0, "TC-UIDATA-026h forwarding layer must NOT post the oper record");
|
||||
(void)ui_data_maint_oper_note("force reset by panel");
|
||||
CHECK((s_stub_note_calls == 1) && (strcmp(s_stub_note_last, "force reset by panel") == 0),
|
||||
"TC-UIDATA-026i page-side note reaches executor verbatim");
|
||||
/* 面板这条与远端 YK11 那条(App/protocol/iec104/iec104_pts.c 投的
|
||||
* "force reset")必须是不同串,否则审计面再也分不出"主站下发的"还是
|
||||
* "现场有人按了面板"。比的是执行侧缓冲里的实际内容,不是两个常量互比 */
|
||||
CHECK(strcmp(s_stub_note_last, "force reset") != 0,
|
||||
"TC-UIDATA-026s posted note differs from the remote YK11 wording");
|
||||
CHECK(ui_data_maint_oper_note(NULL) == -1, "TC-UIDATA-026j NULL note refused");
|
||||
CHECK(s_stub_note_calls == 1, "TC-UIDATA-026k NULL note not posted");
|
||||
s_stub_reset_rc = -1;
|
||||
CHECK(ui_data_maint_reset_req() == -1, "TC-UIDATA-026l rejected reset propagates");
|
||||
|
||||
s_stub_active = 3u;
|
||||
CHECK(ui_data_maint_active_count() == 3u, "TC-UIDATA-026m active count forwarded (proof of reset)");
|
||||
|
||||
CHECK((ui_data_maint_clear_req(UI_W_REC_LOG_YK) == 0) && (s_stub_clear_last_id == UI_W_REC_LOG_YK),
|
||||
"TC-UIDATA-026n clear req id passed through unchanged");
|
||||
s_stub_clear_state_rc = UI_DATA_CLEAR_ST_WAIT;
|
||||
CHECK(ui_data_maint_clear_state(UI_W_REC_LOG_OPER) == UI_DATA_CLEAR_ST_WAIT,
|
||||
"TC-UIDATA-026o WAIT passed through (normal state, not failure)");
|
||||
s_stub_clear_state_rc = UI_DATA_CLEAR_ST_FAIL;
|
||||
CHECK(ui_data_maint_clear_state(UI_W_REC_LOG_OPER) == UI_DATA_CLEAR_ST_FAIL,
|
||||
"TC-UIDATA-026p FAIL passed through unchanged");
|
||||
CHECK(s_stub_clear_state_id == UI_W_REC_LOG_OPER, "TC-UIDATA-026q state queried the asked id");
|
||||
CHECK(s_stub_clear_state_calls == 2, "TC-UIDATA-026r state queried once per call");
|
||||
}
|
||||
|
||||
/* ===================== main ===================== */
|
||||
|
||||
int main(void)
|
||||
@@ -930,6 +1337,16 @@ int main(void)
|
||||
tc_param_apply_fail();
|
||||
printf("-- param frame v2 (cal_block) --\n");
|
||||
tc_param_frame_v2();
|
||||
printf("-- clock convert --\n");
|
||||
tc_clock_convert();
|
||||
printf("-- clock ts string --\n");
|
||||
tc_clock_string();
|
||||
printf("-- record id order contract --\n");
|
||||
tc_record_codes();
|
||||
printf("-- maint channel: not bound --\n");
|
||||
tc_maint_not_bound();
|
||||
printf("-- maint channel: forwarding --\n");
|
||||
tc_maint_forwarding();
|
||||
|
||||
printf("\n=== UI Data Snapshot Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
|
||||
+3
-3
@@ -40,10 +40,10 @@
|
||||
遥信防抖 #P
|
||||
双点遥信 #P
|
||||
装置维护
|
||||
时钟设置 #P
|
||||
强制复归 #P
|
||||
时钟设置 #P #page=clock
|
||||
强制复归 #P #page=reset
|
||||
手动录波 #P
|
||||
清除记录 #P
|
||||
清除记录 #P #page=clear
|
||||
通讯参数
|
||||
通讯设置 #P
|
||||
网口设置 #P #page=net_cfg
|
||||
|
||||
Reference in New Issue
Block a user