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