Files
STM32F4-Base/Drivers/BSP/UI/ui_page_clock.c
T
wandering1 a98d4e06e8 UI 其余五个实时页接入框架自动失效(按内容变化节拍分档取值)
- 描述符各加一行(机制 task-2 已建,本轮零框架代码改动):dc 300ms(电气量与
  ac 同账);yx 500ms(遥信变位按 1s 内上屏到位,比 300 档少 40% 稳态重绘);
  clock 1000ms(快变项只有墙钟秒尾);device_info / runreport 1000ms 并把新鲜度
  源换到通道二 .auto_seq = ui_data_info_seq(info 与 rt 同拍 200ms 发布)
  ——各页取值账目在该页描述符注释
- docs/UI设计:供给说明 §5.5 改六页全接终态(用户裁定 2026-11-13);内容页架构
  字段表/内存表/变更记录同步;自动化测试流程补 rtlive 覆盖行;页面扩充方案
  追加 74 张现势注(带日期史实行冻结不动)
- .dsh/skills/lvgl-simulator:开关表加 --rtlive 行、黄金参考计数 66→74 同步

验证:模拟器 0错0警;菜单 8 状态 + 内容页 74 张 + rtlive PASS(lead 独立复跑
RC=0,池水位 5368/5448B 均不漂移——timer 开页创建关页销毁、分配归还);
Keil 0错0警且 Code/RO/RW/ZI 与 task-2 基线逐字节一致(只改常量值);BOM 全补回。
2026-11-13 14:51:49 +08:00

396 lines
18 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,
/* 自动失效轮询(机制见 ui_content_page.h 的 auto_ms 注;口径见供给说明 §5.5):
* 本页快变项只有墙钟秒尾——rt 帧 200ms 一发,但秒每 1s 才变,轮询快于 1s
* 只会把同一秒多画几遍(seq 随帧走,每拍都触发整页重绘)。取 1000ms:
* 1000 = 5 × 发布节拍 = 50 × LVGL 整刷周期 20ms;最坏屏上滞后 200 + 1000 =
* 1200ms,屏显是墙钟字符串(非自走计数),迟到的是展示不是时刻,不累积漂移。
* 编辑态无碍:失效只按当前叠层状态重绘,不动编辑数据 */
.auto_ms = 1000u,
};