/* * 模块名称: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 #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, };