Files
STM32F4-Base/Drivers/BSP/UI/ui_page_clock.c
T
wandering1 ae09c5737a docs: 删任务规划两份已完成文档,数据可信内容迁入算法层/硬件平台/登记簿
任务规划目录从 5 个文件收敛到 3 个:只留导航(README)、待办(登记簿)、
暂缓专项(暂缓-RS485)。

删除的两个文件都不再有现行读者:
- UI补齐.md 执行清单 2026-10-28 清空、上板判据 2026-11-06 台架轮已复核;
  叶数口径迁 docs/UI设计/菜单页面设计.md §3.9,三条内容页硬规矩迁
  docs/UI设计/内容页架构.md §7.1,两条未闭环待办迁登记簿 §7/§8。
- 数据可信.md 不是完成记录,而是被约 130 处代码头注释引用的设计约束,
  因此按内容分三处落位后删除,不整篇丢弃。

数据可信内容的新归属(旧节号 → 新家):
- §2/§4 设计口径 → docs/算法/采集与算法层设计说明.md 新增 §13(13 小节),
  §7 前置决策表 → §13.12 已定案的前置决策。
- §3 台阶与波次退出判据 → docs/硬件平台/ADC通道映射说明.md 新增 §9。
- §5 任务卡 → docs/任务规划/登记簿.md §7(拆成变比确认 / 两点标定两行)。

引用改写 34 文件 77 处路径引用 + 14 文件 24 处昵称引用 + 双重节号 35 处,
覆盖代码头注释、测试、生成器与文档;归档区历史路径改纯文本提及。
四个 SVG 生成器文案同步并重跑,产物随脚本同批提交。

同批处理两件事:
- 9 个 UTF-16 LE 证据文件转 UTF-8。end-of-file-fixer 按字节判末尾换行,
  UTF-16 的换行是 0A 00、末字节 00,钩子会追加裸 0x0a 使文件变奇数长度,
  8 个已实际损坏。转码后逐文件比对 HEAD 原文验证正文零丢失。
- 修回 App/cfg/cfg_file.c 被剥掉的 UTF-8 BOM(补 3 字节,未整文件重写)。

验证:/keil-build 0 错误 0 警告(Code=255160);check_doc_links --all
--sections-strict 221 篇全通过、无法判定 0;pre-commit 全量全 Passed。
2026-11-09 16:13:57 +08:00

389 lines
17 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称: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
* §7.1 第 2 条「时钟设置写入 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,
};