Files
STM32F4-Base/Drivers/BSP/UI/ui_page_sntp.c
T
super_agent_bot ad7338365c
CI / 静态门禁 (pre-commit) (push) Failing after 6m14s
CI / 主机侧单测 (MSVC) (push) Successful in 54s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 4m35s
CI / 主机侧单测 (MSVC) (pull_request) Successful in 47s
feat(sntp): 板端 SNTP 校时接入——coreSNTP 序列化器 + [sntp] 段 + 设置页(#73)
- vendored coreSNTP v2.0.0 序列化器(MIT,仅 serializer+LICENSE,零堆),
  48 字节报文编解码在库,节奏与准入判据在本方(RFC 4330 §10 调研口径)
- App/sync/time_sntp.c 挂 timeSyncTask 的 8888 事件循环,等待切片 ≤50 ms,
  不饿任务狗与 IWDG;懒建 UDP socket,代际变化/禁用/死错误码即关闭归还
- 防伪 = OriginTimestamp 逐位等于发出时 T1(含低 16 位随机小数位);
  mode/stratum/闰秒指示/KoD 按 RFC 拒收;DENY·RSTR 换服务器、RATE 只退避
- 主备轮换 + 失败指数退避(5 s 起、60 s 封顶)+ 首轮 1~5 分钟随机错峰;
  成功恢复 poll_min 周期(1~1440 分,出厂默认 60,默认不启用不发包)
- 钟面 = 本地时间:UTC+8 固定偏移落 sys_clock_set_unix(§1.1 时基裁定,
  第三个写入口,装置仍不存时区);|offset|<2 s 只记日志不步进,冷启动或
  更大偏差步进并回写 SD2506,留痕 [SNTP] step 与 syslog OPER
- 首个校验通过的同步置 SYS_EVT_TIMESYNC_READY(就绪位,消费方按需 NoClear)
- [sntp] 配置段(enable/server1/server2/poll_min)属主 sntp_cfg.c,
  注册在 net_cfg_load();UI 页 31「SNTP 设置」(密码叶):启用开关 +
  主/备服务器 octet 编辑 + 周期(分) + 保存,复用 net_cfg_save_request 链
- 测试与门禁:test/host/sntp 新套件(报文/防伪/KoD/退避周期/阶跃域/配置
  注册表)、net_cfg 与 time_sync 套件补 fake、黄金参考 66→74 张零漂移、
  字符集 228 字、Keil 0 错 0 警(RO +5,848 B 含字体)、覆盖率基线回填
- 文档:墙钟维护说明 §16、网络配置说明 [sntp] 节、配置文件设计说明变更记录、
  产品需求 FR-TIME-3 与 8.3/9.1.2 台账、UI 补齐规划/UI补齐 31 叶口径、
  登记簿销项并登记「SNTP 校时真机判据」等台架(PC 时间源 192.168.31.161/.13)
2026-11-04 16:20:22 +08:00

432 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 SNTP (SNTP设置内容页)
* 模块功能:SNTP设置叶子节点的内容页数据 + 行为——5 行列表(启用/主服务器/
* 备服务器/周期(分) + 保存行,5 行每页共 1 页);
* 行槽顶 = 24+14i、选中白带 x=2..156 高 13、名 x=4 / 值 x=54;
* 值取自 sntp_cfg RAM 副本([sntp] 段,defaultTask 上电自 Flash
* 加载,net_cfg_load 统一注册);
* 编辑模式:ENTER 进选中编辑行(地址行 = 字节光标反显,周期行 =
* 整值反显),↑↓ 调值、←→ 移字节光标 / 粗调周期、ENTER 确认退出;
* 启用行为开关行:可编辑态 ENTER 直接切换(无字节光标);
* 权限:edit_capable=1——查看密码 → 只读(编辑/保存键无效),
* 修改密码 → 可编辑;保存行 ENTER → 投递保存请求
* (net_cfg_save_request 整档链:属主任务 defaultTask 写盘;
* [sntp] 消费方每轮直读 RAM 副本 + 网络代次重建自动收口,
* 无需独立 apply 通道);保存行值随交接状态显示
* ENTER/WAIT/OK/ERR;
* 向框架 ui_content_page 提供页面描述符(零 LVGL 对象)
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-11-04
* 修改记录:
* v1.0 2026-11-04 王建锋 创建初始版本(板端 SNTP 客户端接入,#57 调研结论;
* 布局/编辑语义复刻 ui_page_net_cfg.c 先例)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_page_sntp.h"
#include "net_cfg.h" /* net_cfg_fmt_ipv4 + 整档保存请求链(save request/state) */
#include "sntp_cfg.h"
#include "ui_tokens.h"
/* 私有宏定义区 */
/*
* 布局取值:行骨架用 ui_tokens.h 的列表页令牌(UI_LIST_TOP / UI_ROW_PITCH /
* UI_ROW_BAND_H / UI_BUF_STRLEN),本文件只留本页私有的两列 x 与行数
* (与网口设置页同栅格:值列 ≥54,IPv4 定长 15 字符 90px 止于 143,在
* 选中白带 x=2..156 内)。
*/
#define SN_ROW_COUNT 5u /* 1 开关行 + 2 地址行 + 1 周期行 + 1 保存行 */
#define SN_EN_ROW 0u /* 启用(开关行) */
#define SN_ADDR_ROW_MIN 1u /* 地址行范围 1..2(主/备服务器) */
#define SN_ADDR_ROW_MAX 2u
#define SN_POLL_ROW 3u /* 周期行(分钟) */
#define SN_SAVE_ROW 4u /* 保存行下标 */
#define SN_LEFT_X 4 /* 左列 x(名) */
#define SN_RIGHT_X 54 /* 右列 x(值) */
/* 周期编辑步进与合法带(与 sntp_cfg.h getter 带一致;带外读回 60 自愈) */
#define SN_POLL_MIN 1u
#define SN_POLL_MAX 1440u
#define SN_POLL_BIG_STEP 60u
/* 静态变量定义区 */
/* 页面运行态(每次开页经 init_cb 重置;约 2 B) */
static uint8_t s_editing; /* 1 = 编辑中(被编辑行 = 当前选中行) */
static uint8_t s_cursor; /* 地址行编辑活动字节(0..3) */
/* 函数定义区 */
/*
* 函数功能:判断两区域是否相交(与 ui_content_page.c / ui_page_net_cfg.c 同实现)
* 入口参数:a - 区域 1 const lv_area_t *
* b - 区域 2 const lv_area_t *
* 返回值:true - 相交;false - 不相交
* 限定条件:无
*/
static bool sn_areas_intersect(const lv_area_t *a, const lv_area_t *b) {
int32_t x1 = (a->x1 > b->x1) ? a->x1 : b->x1;
int32_t y1 = (a->y1 > b->y1) ? a->y1 : b->y1;
int32_t x2 = (a->x2 < b->x2) ? a->x2 : b->x2;
int32_t y2 = (a->y2 < b->y2) ? a->y2 : b->y2;
return (x1 <= x2 && y1 <= y2);
}
/*
* 函数功能:生成指定地址行的值串(与 refresh 值列同源,编辑光标定位与行文本一致)
* 入口参数:row - 地址行下标(1..2) uint16_t
* p_buf - 输出缓冲(≥ UI_BUF_STRLEN) char *
* 返回值:无
* 限定条件:p_buf 非空
* 函数说明:"192.168.001.100"(每段 3 位补零定长,字节 i 恒在字符 4i 起),
* 与网口设置页 IP 行同格式(net_cfg_fmt_ipv4)
*/
static void sn_fmt_addr(uint16_t row, char *p_buf) {
const sntp_cfg_t *cfg = sntp_cfg_get();
const uint8_t *field = (row == SN_ADDR_ROW_MIN) ? cfg->server_pri : cfg->server_bak;
net_cfg_fmt_ipv4(field, p_buf, UI_BUF_STRLEN);
}
/*
* 函数功能:动态行刷新(逐行取名/值)
* 入口参数:row - 行下标(0..4) uint16_t
* name - 名缓冲(≥ UI_BUF_STRLEN,框架栈缓冲) char *
* value - 值缓冲(≥ UI_BUF_STRLEN,框架栈缓冲) char *
* 返回值:无
* 限定条件:页面打开(框架绘制链内逐行调用)
* 函数说明:周期行显示 RAM 副本原值(不做带外回显换算——带外值经
* sntp_cfg_poll_min() 读回 60 属消费侧自愈口径,UI 忠实显示
* 用户写入值,避免"保存后数字自己变了");
* 保存行值随保存交接状态(net_cfg_save_state)显示 ENTER/WAIT/OK/ERR
*/
static void sn_refresh(uint16_t row, char *name, char *value) {
const sntp_cfg_t *cfg = sntp_cfg_get();
name[0] = '\0';
value[0] = '\0';
switch (row) {
case SN_EN_ROW:
(void)memcpy(name, "启用", sizeof("启用"));
if (cfg->enable != 0u) {
(void)memcpy(value, "开启", sizeof("开启"));
}
else {
(void)memcpy(value, "关闭", sizeof("关闭"));
}
break;
case SN_ADDR_ROW_MIN:
(void)memcpy(name, "主服务器", sizeof("主服务器"));
sn_fmt_addr(SN_ADDR_ROW_MIN, value);
break;
case SN_ADDR_ROW_MAX:
(void)memcpy(name, "备服务器", sizeof("备服务器"));
sn_fmt_addr(SN_ADDR_ROW_MAX, value);
break;
case SN_POLL_ROW:
(void)memcpy(name, "周期(分)", sizeof("周期(分)"));
(void)lv_snprintf(value, UI_BUF_STRLEN, "%u", (unsigned)cfg->poll_min);
break;
case SN_SAVE_ROW:
(void)memcpy(name, "保存", sizeof("保存"));
switch (net_cfg_save_state()) {
case NET_CFG_SAVE_ST_WAIT:
(void)memcpy(value, "WAIT", sizeof("WAIT"));
break;
case NET_CFG_SAVE_ST_OK:
(void)memcpy(value, "OK", sizeof("OK"));
break;
case NET_CFG_SAVE_ST_FAIL:
(void)memcpy(value, "ERR", sizeof("ERR"));
break;
default:
(void)memcpy(value, "ENTER", sizeof("ENTER"));
break;
}
break;
default:
break;
}
}
/*
* 函数功能:切换启用开关(只改 RAM 副本)
* 入口参数:无
* 返回值:无
* 限定条件:可编辑权限(key_cb 已把门)
*/
static void sn_toggle_enable(void) {
sntp_cfg_t cfg = *sntp_cfg_get();
cfg.enable = (cfg.enable != 0u) ? 0u : 1u;
sntp_cfg_set(&cfg);
}
/*
* 函数功能:调整被编辑地址行的活动字节(0..255 环绕,实时落 RAM 副本)
* 入口参数:row - 地址行下标(1..2) uint16_t
* delta - 调整方向(>0 = +1,<0 = -1) int8_t
* 返回值:无
* 限定条件:编辑模式(s_cursor 有效)
* 函数说明:只改 RAM 副本;写盘须显式经保存行投递 net_cfg_save_request
* (net_cfg_adjust_octet 同口径)
*/
static void sn_adjust_octet(uint16_t row, int8_t delta) {
sntp_cfg_t cfg = *sntp_cfg_get();
uint8_t *field = (row == SN_ADDR_ROW_MIN) ? cfg.server_pri : cfg.server_bak;
field[s_cursor] = (uint8_t)((field[s_cursor] + ((delta > 0) ? 1u : 255u)) % 256u);
sntp_cfg_set(&cfg);
}
/*
* 函数功能:调整周期(分钟)值(实时落 RAM 副本)
* 入口参数:step - 步长(有符号;±1 在 1..1440 带内环绕,±60 触带钳位) int16_t
* 返回值:无
* 限定条件:编辑模式(选中周期行)
*/
static void sn_adjust_poll(int16_t step) {
sntp_cfg_t cfg = *sntp_cfg_get();
int32_t v = (int32_t)cfg.poll_min;
if (v < (int32_t)SN_POLL_MIN || v > (int32_t)SN_POLL_MAX) {
v = (int32_t)SNTP_CFG_DEF_POLL_MIN; /* 带外起点先落回默认再步进 */
}
if (step == 1) {
v = (v >= (int32_t)SN_POLL_MAX) ? (int32_t)SN_POLL_MIN : (v + 1);
}
else if (step == -1) {
v = (v <= (int32_t)SN_POLL_MIN) ? (int32_t)SN_POLL_MAX : (v - 1);
}
else {
v += step; /* ±60 粗调:钳带不环绕(大步进环绕会跨带跳变,失序) */
if (v < (int32_t)SN_POLL_MIN) {
v = (int32_t)SN_POLL_MIN;
}
if (v > (int32_t)SN_POLL_MAX) {
v = (int32_t)SN_POLL_MAX;
}
}
cfg.poll_min = (uint16_t)v;
sntp_cfg_set(&cfg);
}
/*
* 函数功能:页面自绘(编辑模式:选中行值内活动区域反显)
* 入口参数:layer - 绘制层 lv_layer_t *
* clip - 重绘区 const lv_area_t *
* 返回值:无
* 限定条件:页面打开(框架绘制链末尾调用)
* 函数说明:非编辑模式无附加绘制(行/带/页脚全由框架);地址行编辑 = 活动
* 字节反显(同网口设置页),周期行编辑 = 整值反显;
* 1bpp:black=暗底 white=亮字
*/
static void sn_draw_cb(lv_layer_t *layer, const lv_area_t *clip) {
char value[UI_BUF_STRLEN];
lv_area_t box;
uint32_t start;
uint32_t seg_len;
int32_t slot_y;
int32_t x0;
int32_t w;
uint16_t sel;
if (s_editing == 0u || ui_content_page_is_editable() == false) {
return;
}
sel = ui_content_page_selected_row();
if (sel == SN_ADDR_ROW_MIN || sel == SN_ADDR_ROW_MAX) {
sn_fmt_addr(sel, value);
start = (uint32_t)s_cursor * 4u; /* 字节 i 字符偏移 4i(%03u + 点) */
seg_len = 3u;
}
else if (sel == SN_POLL_ROW) {
(void)lv_snprintf(value, UI_BUF_STRLEN, "%u",
(unsigned)sntp_cfg_get()->poll_min);
start = 0u;
seg_len = (uint32_t)strlen(value); /* 周期行:整值反显 */
}
else {
return; /* 防御:开关行/保存行不进编辑态,无光标可画 */
}
slot_y = UI_LIST_TOP + (int32_t)sel * UI_ROW_PITCH;
x0 = SN_RIGHT_X + (int32_t)lv_text_get_width(value, start, LV_FONT_DEFAULT, 0);
w = (int32_t)lv_text_get_width(value + start, seg_len, LV_FONT_DEFAULT, 0);
box.x1 = x0 - 1;
box.x2 = x0 + w;
box.y1 = slot_y;
box.y2 = slot_y + UI_ROW_BAND_H - 1;
if (!sn_areas_intersect(&box, clip)) {
return; /* 编辑光标不在重绘区:跳过(culling) */
}
/* 反显:黑底白字(与选中行白带黑字相反,方能在带内凸显活动段) */
lv_draw_rect_dsc_t fd;
lv_draw_rect_dsc_init(&fd);
fd.base.layer = layer;
fd.bg_color = UI_COL_BG;
fd.bg_opa = LV_OPA_COVER;
lv_draw_rect(layer, &fd, &box);
{
lv_draw_label_dsc_t ld;
char seg[8]; /* 段文本:地址 3 字符 / 周期 ≤4 字符,+ '\0' */
/* 只绘制"段文本"本身(防 LVGL 按 coords 宽折行溢出,见网口设置页同注释) */
memcpy(seg, value + start, seg_len);
seg[seg_len] = '\0';
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = seg;
ld.text_static = 0u; /* 栈缓冲(同帧有效) */
ld.color = UI_COL_FG;
ld.ofs_x = 1; /* box.x1 = x0-1,+1 = x0(与框架行文本原点一致) */
ld.ofs_y = 0; /* 与行文本垂直对齐(text_dy = 0) */
lv_draw_label(layer, &ld, &box);
}
}
/*
* 函数功能:页面按键处理(列表页 + 编辑模式)
* 入口参数:key - LVGL 键值 lv_key_t
* 返回值:true - 页已处理(框架跳过内置语义);false - 未处理
* 限定条件:页面打开;经框架键分发调用(uiTask 上下文)
* 函数说明: 1. 编辑模式(地址行/周期行):地址行 ↑↓ 调活动字节(0..255
* 环绕)、←→ 移字节光标(首↔末环绕);周期行 ↑↓ ±1 分带内环绕、
* ←→ ±60 分钳带;ENTER 确认退出编辑(值已实时落 RAM 副本);
* ESC 恒关页(框架保证,未保存调整留在 RAM,下次开页重显、
* 不写盘、重启失效——网口设置页同口径)
* 2. 正常模式:ENTER 启用行 → 直接切换开关;ENTER 地址行/周期行 →
* 进编辑模式;ENTER 保存行 → 投递整档保存请求
* net_cfg_save_request(属主任务 defaultTask 写盘,[sntp] 段
* 随档落盘;消费方每轮直读 RAM 副本,网络代次重建自动收口);
* ↑↓ 落框架移选、← 落框架关页;只读态 ENTER 一律忽略
*/
static bool sn_key_cb(lv_key_t key) {
uint16_t sel = ui_content_page_selected_row();
if (s_editing != 0u) {
if (sel < SN_ADDR_ROW_MIN || sel > SN_POLL_ROW) {
s_editing = 0u; /* 防御:编辑中选中行越界(编辑态移选被拦截,不应发生) */
return false;
}
switch (key) {
case LV_KEY_UP:
if (sel == SN_POLL_ROW) {
sn_adjust_poll(1);
}
else {
sn_adjust_octet(sel, 1);
}
ui_content_page_invalidate();
return true;
case LV_KEY_DOWN:
if (sel == SN_POLL_ROW) {
sn_adjust_poll(-1);
}
else {
sn_adjust_octet(sel, -1);
}
ui_content_page_invalidate();
return true;
case LV_KEY_LEFT:
if (sel == SN_POLL_ROW) {
sn_adjust_poll(-(int16_t)SN_POLL_BIG_STEP);
}
else {
s_cursor = (uint8_t)((s_cursor + 3u) % 4u); /* 光标左移,首绕末 */
}
ui_content_page_invalidate();
return true;
case LV_KEY_RIGHT:
if (sel == SN_POLL_ROW) {
sn_adjust_poll((int16_t)SN_POLL_BIG_STEP);
}
else {
s_cursor = (uint8_t)((s_cursor + 1u) % 4u); /* 光标右移,末绕首 */
}
ui_content_page_invalidate();
return true;
case LV_KEY_ENTER:
s_editing = 0u; /* 确认(值已在 RAM 副本);退出编辑 */
ui_content_page_invalidate();
return true;
default:
return false; /* ESC 框架恒关页;其余键落内置忽略 */
}
}
if (key == LV_KEY_ENTER && ui_content_page_is_editable() != false) {
if (sel == SN_EN_ROW) {
sn_toggle_enable();
ui_content_page_invalidate();
return true;
}
if (sel >= SN_ADDR_ROW_MIN && sel <= SN_POLL_ROW) {
s_editing = 1u;
s_cursor = 0u;
ui_content_page_invalidate();
return true;
}
if (sel == SN_SAVE_ROW) {
(void)net_cfg_save_request(); /* 投递整档保存请求:属主任务写盘 */
ui_content_page_invalidate();
return true;
}
}
return false;
}
/*
* 函数功能:重置页面运行态
* 入口参数:无
* 返回值:无
* 限定条件:框架 ui_content_page_open 内调用(开页后一次)
* 函数说明:页面模块静态态不随屏/容器建毁自动复位,须自重置(编辑态/光标)
*/
static void sn_init_cb(void) {
s_editing = 0u;
s_cursor = 0u;
}
/*
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=sntp 绑定引用):
* 列表页(5 行 1 页,动态值)+ 可编辑(edit_capable,还须修改权限)
* + ← 拦截(编辑模式移字节光标/粗调周期;ESC 恒不可拦截)
*/
const content_page_def_t ui_page_sntp_desc = {
.id = "sntp",
.title = "SNTP设置",
.selectable = 1u,
.key_intercept = 1u,
.edit_capable = 1u,
.top_y = UI_LIST_TOP,
.pitch = UI_ROW_PITCH,
.band_h = UI_ROW_BAND_H,
.text_dy = 0,
.left_x = SN_LEFT_X,
.right_x = SN_RIGHT_X,
.rows_per_page = SN_ROW_COUNT,
.row_count = SN_ROW_COUNT,
.refresh = sn_refresh,
.init_cb = sn_init_cb,
.draw_cb = sn_draw_cb,
.key_cb = sn_key_cb,
};