diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e62d0e1..f8fa2ca7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -672,7 +672,7 @@ repos: # 疑似 MSBuild 节点争用;此后 3 次干净树连跑均绿。判据:单次失败先复跑一次再看, # 连续两次失败才当真(不许直接 --no-verify 绕过)。 - id: ui-golden - name: UI 像素回归(菜单 8 状态 + 内容页 66 张 黄金参考) + name: UI 像素回归(菜单 8 状态 + 内容页 74 张 黄金参考) entry: python scripts/check_ui_golden.py language: system files: ^(Drivers/BSP/UI/|App/task/ui_task\.(c|h)$|simulator/(tests|sim|src)/|simulator/main\.c$|simulator/CMakeLists\.txt$|simulator/lv_conf\.h$|scripts/check_ui_golden\.py$|\.pre-commit-config\.yaml$) @@ -682,7 +682,7 @@ repos: # 背景:大窗实测——靠预置键序盲走到深页时,9 帧里只有 3 帧真是它声称的态; # KEY 的 ACK 是空应答、不携带状态,所以**画面是唯一事实源**。判据脚本 # scripts/board_ui_nav.py 据此改成闭环:每按一键重新认屏再决定下一个键。 - # 本钩子跑它的**快检档** --selftest-quick:只核"认屏认得对"(接线当时 18 张、现 66 张黄金参考 + + # 本钩子跑它的**快检档** --selftest-quick:只核"认屏认得对"(接线当时 18 张、现 74 张黄金参考 + # 真机留档帧,目录不在版本控制里、缺就整段跳过),秒级、无构建产物依赖。 # ⚠ 不挂完整 --selftest:第三段闭环需要 lvgl_sim.exe(构建产物,新工作树里没有) # —— 正是同一天 mem-numbers 缺 .map 那课的形状;脚本对缺 exe 报 FAIL 并给出 diff --git a/App/app/sys_events.h b/App/app/sys_events.h index 1785f919..b4e47f59 100644 --- a/App/app/sys_events.h +++ b/App/app/sys_events.h @@ -27,7 +27,7 @@ extern osEventFlagsId_t sys_evtHandle; #define SYS_EVT_NET_READY (1u << 0) /* netTask:net_init + PHY 稳定完成 */ #define SYS_EVT_STORAGE_READY (1u << 1) /* defaultTask:FTL/FatFS 挂载完成 */ #define SYS_EVT_FTP_READY (1u << 2) /* ftpTask:lftpd_start 完成后置位;暂无消费者(预留) */ -#define SYS_EVT_TIMESYNC_READY (1u << 3) /* 预留:timeSyncTask 在跑但不置本位,生产者消费者都没有 */ +#define SYS_EVT_TIMESYNC_READY (1u << 3) /* SNTP 首个校验通过的同步完成(App/sync/time_sntp.c 置位;就绪位,osFlagsNoClear 消费) */ #define SYS_EVT_NET_RECONFIG (1u << 4) /* UI:请求立即应用新网口配置(netTask 消费后重建网络) */ #define SYS_EVT_CFG_SAVE (1u << 5) /* UI:网口配置保存请求(defaultTask = 配置引擎属主,消费并写盘) */ #define SYS_EVT_CONSOLE_TX (1u << 6) /* 控制台单写者:生产者入环形缓冲后置位,defaultTask 消费并发送 diff --git a/App/sync/README.md b/App/sync/README.md index 4ca6e284..7f533950 100644 --- a/App/sync/README.md +++ b/App/sync/README.md @@ -8,15 +8,17 @@ | 文件 | 作用 | |---|---| | `time_sync.c/h` | **时间同步任务**:等待网络就绪 → 与时间源交互 → 校准墙钟 | +| `time_sntp.c/h` | **SNTP 校时客户端**(2026-11-04 落地):挂在 time_sync 主循环里的 UDP/123 轮询,编解码用 `Lib/coreSNTP/` 序列化器,配置读 `[sntp]` 段(属主 `../../Drivers/BSP/NETCFG/sntp_cfg.c`);单测 `test/host/sntp/` | ## 工作流 ``` timeSyncTask 启动 ↓ osEventFlagsWait(SYS_EVT_NET_READY, ...|osFlagsNoClear) // 状态位不可消费! -time_sync 主循环 → 与时间源通信 → sys_clock 校准 - ↓ -置 SYS_EVT_TIMESYNC_READY +time_sync 主循环 → TCP :8888 被动接收("TIME"+u32 → sys_clock 校准) + ↘ 每轮顺带 time_sntp_run(gen):到点主动向 [sntp] 服务器发 48 B 查询 + (单轮等待切片 ≤50 ms),校验通过按策略步进/只记日志, + 首个通过的同步置 SYS_EVT_TIMESYNC_READY ``` > ⚠️ 等待**就绪位**(`SYS_EVT_NET_READY`)必须 `osFlagsNoClear`——它是状态位, diff --git a/App/sync/time_sntp.c b/App/sync/time_sntp.c new file mode 100644 index 00000000..943a6d81 --- /dev/null +++ b/App/sync/time_sntp.c @@ -0,0 +1,625 @@ +/* + * 模块名称:Time SNTP — 板端 SNTP 客户端(状态机实现) + * 模块功能:见 time_sntp.h。节拍由 timeSyncTask 主循环驱动(每 ≤200ms 一次), + * 报文编解码用 vendor coreSNTP serializer(仅 serializer 层—— + * client 层要求自带 transport 接口与定时器,本仓 UDP 已队列化在 + * net_socket,节奏循环留在任务里,见 #57 调研 §4.1) + * 适用平台:STM32407ZGTx + CH395F;模拟器/主机单测经 stub 编译 + * 作者:王建锋 + * 创建日期:2026-11-04 + * 修改记录: + * v1.0 2026-11-04 王建锋 创建初始版本(SNTP 客户端接入,#57 调研结论: + * RFC 4330 §10 节奏、§5.3 防欺骗判据、§5.5 步进策略、 + * 钟面 = 本地时间(UTC+8 固定,§1.1 裁定)) + */ + +#include + +#include "time_sntp.h" + +#include "core_sntp_serializer.h" +#include "net_socket.h" +#include "sntp_cfg.h" +#include "sys_clock.h" +#include "sys_events.h" +#include "sys_log.h" + +/* 深调用链模块(与 time_sync.c 同任务栈预算口径,见其文件头注释): + * 日志行缓冲收到 128 B(默认 192) */ +#define DBG_LINE_BUF_MAX 128U +#define DBG_TAG "[SNTP]" +#include "dbg_log.h" + +/* 私有宏定义区 */ + +#define SNTP_SERVER_PORT 123U +/* 应答超时:基础 5s,失败逐次翻倍封顶 60s(RFC 4330 §10:间隔递增; + * 翻倍节奏 = "超时即退避",下一次请求最早在 timeout_ms 之后发出) */ +#define SNTP_RESP_TIMEOUT_BASE_MS 5000U +#define SNTP_RESP_TIMEOUT_MAX_MS 60000U +/* 首次请求随机散布 1~5 分钟(RFC 4330 §10 避免同批设备上电撞服务器) */ +#define SNTP_FIRST_DELAY_MIN_MS 60000U +#define SNTP_FIRST_DELAY_SPAN_MS 240000U +/* 等应答期的单次收片粒度(看门狗 100ms 抽样窗约束,同 time_sync.c 第 8b 轮口径) */ +#define SNTP_RECV_SLICE_MS 50U +/* 步进策略(调研 §5.5):|钟差| < 2s 只记录不步进(防 RTC 秒抖动来回拨针) */ +#define SNTP_STEP_DEADBAND_MS 2000 +/* 内部钟面 = 本地时间(#56 裁定 §1.1:sys_clock 存"钟面秒",104 写 UTC 的 + * 8 小时差已有文档口径)——SNTP 给的是 UTC,落钟面前加固定东八区偏移; + * 全仓无时区存储,本常量与 UI 手工对时 / 8888 推送同源 */ +#define SNTP_TZ_LOCAL_OFFSET_SECS 28800U +/* 合理性下限:2020-01-01 UTC;上限 = sys_clock 钟面域 0x7FFFFFFF 扣除时区 */ +#define SNTP_UNIX_MIN_VALID 1577836800U +#define SNTP_UNIX_MAX_UTC (0x7FFFFFFFUL - SNTP_TZ_LOCAL_OFFSET_SECS) +/* 收包缓冲:48B 报文 + 富余(>48 的扩展按 BSD 语义截断,serializer 只读前 48) */ +#define SNTP_RX_BUF_SIZE 64U + +/* 状态机 */ +#define SNTP_ST_IDLE 0 /* 停用 / 从未启用 */ +#define SNTP_ST_WAIT_DUE 1 /* 等下一次请求窗口 */ +#define SNTP_ST_SEND 2 /* 本轮应发请求 */ +#define SNTP_ST_WAIT_RESP 3 /* 等应答(每次 run 收一个 ≤50ms 片) */ + +/* 静态变量定义区 */ + +static struct { + uint8_t state; /* SNTP_ST_* */ + int fd; /* UDP socket(-1 = 未开) */ + uint32_t gen; /* 打开 socket 时的网络代次 */ + uint32_t due_ms; /* 下一次请求的 tick 锚点(回绕安全比较) */ + uint32_t timeout_ms; /* 当前应答超时(失败翻倍,成功复位) */ + uint32_t deadline_ms; /* 本轮应答截止 tick */ + uint8_t srv; /* 0 = 主 server1,1 = 备 server2 */ + uint8_t no_srv_logged; /* "未配置服务器"只警一次(配置态,不是故障) */ + uint8_t ready_posted; /* SYS_EVT_TIMESYNC_READY 已投(一次性) */ + uint32_t sync_count; /* 校验通过次数(含死区内未步进) */ + int32_t last_off_ms; /* 最近一次钟差(int64 截断,诊断用) */ + SntpTimestamp_t t1; /* 本轮请求时间(serializer 要求跨收发保存) */ + uint8_t pkt[SNTP_PACKET_BASE_SIZE]; /* 请求报文(48B,静态省栈) */ +} s_ctx; + +static uint32_t s_rng; /* xorshift32:散布/防欺骗随机数(非密钥用途) */ +static uint8_t s_rng_seeded; + +/* 私有函数区 */ + +/* + * 函数功能:PRNG 播种一次(芯片 96 位 UID 折叠 ^ tick ^ 当前钟面) + * 入口参数:无 + * 返回值:无 + * 限定条件:任务上下文 + * 函数说明:F407 软件上无可用硬件 RNG 通路(HAL_RNG 在 hal_conf 关闭), + * 此处只需要"不可预测的 16 位低分数 + 上电散布",UID 折叠足够; + * 非 MCU 构建(宿主单测/模拟器)不碰 0x1FFF7A10,用固定盐—— + * 防欺骗强度依赖的是应答 echo 回 T1(含随机低 16 位)的攻击者 + * 不可知性,真机上由 UID+tick 提供 + */ +static void s_rng_seed_once(void) { + uint32_t seed; + + if (s_rng_seeded != 0u) { + return; + } +#ifdef STM32F407XX + { + const volatile uint32_t *uid = (const volatile uint32_t *)0x1FFF7A10UL; + seed = uid[0] ^ (uid[1] * 2654435761UL) ^ uid[2]; + } +#else + seed = 0x9E3779B9UL; +#endif + seed ^= (uint32_t)xTaskGetTickCount(); + seed ^= sys_clock_get(); + if (seed == 0u) { + seed = 0xA5A5A5A5u; + } + s_rng = seed; + s_rng_seeded = 1u; +} + +/* + * 函数功能:xorshift32 随机数 + * 入口参数:无 + * 返回值:伪随机 uint32(播种前兜底返回黄金比例常数,不会为 0) uint32_t + */ +static uint32_t s_rng_next(void) { + uint32_t x = s_rng; + + if (x == 0u) { + x = 0x9E3779B9u; + } + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + s_rng = x; + return x; +} + +/* + * 函数功能:回绕安全的"到点"判断 + * 入口参数:now - 当前 tick;due - 目标 tick uint32_t + * 返回值:1 - 已到/已过;0 - 未到 uint8_t + */ +static uint8_t s_tick_due(uint32_t now, uint32_t due) { + return ((int32_t)(now - due) >= 0) ? 1u : 0u; +} + +/* + * 函数功能:IPv4 四字节是否全零(未配置) + * 入口参数:p_ip - 4 字节 const uint8_t * + * 返回值:1 - 全零;0 - 已配置 uint8_t + */ +static uint8_t s_ip_is_zero(const uint8_t *p_ip) { + return ((p_ip[0] | p_ip[1] | p_ip[2] | p_ip[3]) == 0u) ? 1u : 0u; +} + +/* + * 函数功能:取当前应使用的服务器 IP(按 s_ctx.srv 选主/备,缺位自动落另一侧) + * 入口参数:无 + * 返回值:4 字节 IP 指针;主备都未配置返回 NULL const uint8_t * + */ +static const uint8_t *s_server_ip(const sntp_cfg_t *p_cfg) { + const uint8_t *pri = p_cfg->server_pri; + const uint8_t *bak = p_cfg->server_bak; + + if (s_ctx.srv == 0u) { + if (s_ip_is_zero(pri) == 0u) { + return pri; + } + s_ctx.srv = 1u; + } + if (s_ip_is_zero(bak) == 0u) { + return bak; + } + if (s_ip_is_zero(pri) == 0u) { + s_ctx.srv = 0u; + return pri; + } + return NULL; +} + +/* + * 函数功能:主备切换(仅两侧都配置时有效;失败退避时调用) + * 入口参数:p_cfg - 配置副本 const sntp_cfg_t * + * 返回值:无 + */ +static void s_server_toggle(const sntp_cfg_t *p_cfg) { + if ((s_ip_is_zero(p_cfg->server_pri) == 0u) && + (s_ip_is_zero(p_cfg->server_bak) == 0u)) { + s_ctx.srv = (s_ctx.srv == 0u) ? 1u : 0u; + } +} + +/* + * 函数功能:当前 UTC 秒(钟面有效才返回) + * 入口参数:p_utc - 输出 UTC 秒 int64_t * + * 返回值:1 - 钟面有效;0 - 未初始化/早于合理性下限 uint8_t + */ +static uint8_t s_utc_now(int64_t *p_utc) { + uint32_t face = sys_clock_get(); + + if ((face == UINT32_MAX) || + ((uint64_t)face < (uint64_t)SNTP_UNIX_MIN_VALID + SNTP_TZ_LOCAL_OFFSET_SECS)) { + return 0u; + } + *p_utc = (int64_t)face - (int64_t)SNTP_TZ_LOCAL_OFFSET_SECS; + return 1u; +} + +/* + * 函数功能:取当前 SNTP 时间戳(1900 纪元秒 + 毫秒分数) + * 入口参数:p_ts - 输出 SntpTimestamp_t * + * 返回值:无 + * 函数说明:钟面无效(冷启动)时用"纪元差 + 开机秒 + 随机扰动"构造非零且 + * 每次上电唯一的时间戳——serializer 只要求 T1 非零防欺骗,冷启动 + * 路径的成功处理不使用 clockOffset(直接采用服务器 T3),乱值无害 + */ +static void s_ts_now(SntpTimestamp_t *p_ts) { + int64_t utc; + uint32_t ms; + + ms = (uint32_t)xTaskGetTickCount(); + if (s_utc_now(&utc) != 0u) { + /* uint32 加和天然回绕 = SNTP era 进位语义(2036 年后仍成立) */ + p_ts->seconds = (uint32_t)(utc + (int64_t)SNTP_TIME_AT_UNIX_EPOCH_SECS); + } + else { + p_ts->seconds = SNTP_TIME_AT_UNIX_EPOCH_SECS + (ms / 1000u) + (s_rng_next() & 0xFFFFu); + } + p_ts->fractions = (ms % 1000u) * (1000u * SNTP_FRACTION_VALUE_PER_MICROSECOND); +} + +/* + * 函数功能:打开并绑定 UDP socket(客户端侧动态端口) + * 入口参数:gen - 当前网络代次 uint32_t + * 返回值:0 - 成功;-1 - 失败(fd 保持 -1) int + * 函数说明:UDP bind 恒开芯片服务器形态 Socket(PR #74 后语义), + * 端口 0 = 动态分配;发送用带目的地址的 net_sendto,不经 + * net_udp_set_peer 轻切换(每包显式对端,源过滤未证不依赖) + */ +static int s_socket_open(uint32_t gen) { + struct net_sockaddr_in addr; + int s; + + s = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + if (s < 0) { + DBG_ERROR("net_socket FAIL errno=%d", net_get_errno()); + return -1; + } + (void)memset(&addr, 0, sizeof(addr)); + addr.sin_family = NET_AF_INET; + addr.sin_port = net_htons(0u); /* 0 = 动态本地端口 */ + addr.sin_addr.s_addr = NET_INADDR_ANY; + + if (net_bind(s, (struct net_sockaddr *)&addr, (int)sizeof(addr)) != 0) { + DBG_ERROR("udp bind FAIL errno=%d", net_get_errno()); + net_close(s); + return -1; + } + s_ctx.fd = s; + s_ctx.gen = gen; + return 0; +} + +/* + * 函数功能:关闭 UDP socket(代次变化 / 配置停用 / 死 fd 时调用) + * 入口参数:无 + * 返回值:无 + */ +static void s_socket_close(void) { + if (s_ctx.fd >= 0) { + net_close(s_ctx.fd); + s_ctx.fd = -1; + } +} + +/* + * 函数功能:一次失败收口——超时翻倍(封顶)并重排下一次请求 + * 入口参数:now - 当前 tick uint32_t + * 返回值:无 + * 函数说明:状态回 WAIT_DUE;socket 保留(轻问题不重开,代次变化才重开) + */ +static void s_fail_backoff(uint32_t now) { + uint32_t t = s_ctx.timeout_ms * 2u; + + if (t > SNTP_RESP_TIMEOUT_MAX_MS) { + t = SNTP_RESP_TIMEOUT_MAX_MS; + } + s_ctx.timeout_ms = t; + s_ctx.due_ms = now + t; + s_ctx.state = SNTP_ST_WAIT_DUE; +} + +/* + * 函数功能:int64 毫秒钟差截断到 int32(日志/诊断口) + * 入口参数:off - 毫秒钟差 int64_t + * 返回值:截断后的 int32 毫秒 int32_t + */ +static int32_t s_off_clamp(int64_t off) { + if (off > 2147483647LL) { + return 2147483647; + } + if (off < -2147483647LL) { + return -2147483647; + } + return (int32_t)off; +} + +/* + * 函数功能:校验通过的应答落地——步进策略 + 节奏复位 + 就绪事件 + * 入口参数:p_resp - 解析出的应答数据;now - 当前 tick uint32_t + * 返回值:无 + * 函数说明:见文件头步进策略注释;**任何校验通过都算一次成功同步** + * (含死区内未步进),复位退避并按 poll_min 重排下一轮; + * 首个成功投 SYS_EVT_TIMESYNC_READY(状态位,不消费); + * 每次实际步进落一条 quiet 操作记录("clock set by SNTP", + * 与 8888/104 同口径,失败不落痕) + */ +static void s_handle_success(const SntpResponseData_t *p_resp, uint32_t now) { + int64_t utc; + int64_t new_utc; + int32_t off_ms = s_off_clamp(p_resp->clockOffsetMs); + uint8_t clock_valid; + + s_ctx.sync_count++; + s_ctx.last_off_ms = off_ms; + s_ctx.timeout_ms = SNTP_RESP_TIMEOUT_BASE_MS; + s_ctx.due_ms = now + (uint32_t)sntp_cfg_poll_min() * 60000u; + s_ctx.state = SNTP_ST_WAIT_DUE; + + if ((s_ctx.ready_posted == 0u) && (sys_evtHandle != NULL)) { + (void)osEventFlagsSet(sys_evtHandle, SYS_EVT_TIMESYNC_READY); + s_ctx.ready_posted = 1u; + } + + clock_valid = s_utc_now(&utc); + if (clock_valid != 0u) { + int64_t off = p_resp->clockOffsetMs; + + if ((off <= (int64_t)SNTP_STEP_DEADBAND_MS) && + (off >= -(int64_t)SNTP_STEP_DEADBAND_MS)) { + /* 死区:本地钟足够准,不拨针也不写 RTC(防 SD2506 无谓磨损) */ + DBG_INFO("off=%ldms within deadband, no step", (long)off_ms); + return; + } + /* 四元组中点估计:把"服务器秒"折算到本地应有的 UTC 秒 */ + { + int64_t off_s = (off >= 0) ? ((off + 500) / 1000) : ((off - 500) / 1000); + new_utc = utc + off_s; + } + } + else { + /* 冷启动(钟面未初始化/早于 2020):钟差不可信,直接采用服务器 T3 */ + uint32_t t3_secs = 0u; + uint32_t t3_us = 0u; + + if (Sntp_ConvertToUnixTime(&p_resp->serverTime, &t3_secs, &t3_us) != SntpSuccess) { + DBG_ERROR("unix convert FAIL"); + return; + } + new_utc = (int64_t)t3_secs; + } + + if ((new_utc < (int64_t)SNTP_UNIX_MIN_VALID) || (new_utc > (int64_t)SNTP_UNIX_MAX_UTC)) { + DBG_ERROR("target unix out of domain, skip step"); + return; + } + + (void)sys_clock_set_unix((uint32_t)(new_utc + (int64_t)SNTP_TZ_LOCAL_OFFSET_SECS)); + syslog_post_quiet(SYSLOG_OPER, "clock set by SNTP"); + DBG_INFO("step offset=%ldms face=%lu", (long)off_ms, + (unsigned long)(uint32_t)(new_utc + (int64_t)SNTP_TZ_LOCAL_OFFSET_SECS)); +} + +/* + * 函数功能:收包校验 + 反欺骗判据 + 解析落地(WAIT_RESP 态的单包处理) + * 入口参数:p_buf - 载荷;len - 长度;p_src - 源地址;now - tick uint32_t + * 返回值:1 - 本轮已收口(成功或按失败处理,调用方可停止收片); + * 0 - 该包丢弃,继续等到 deadline uint8_t + * 函数说明:判据顺序 = 长度 → 源地址(IP+端口必须 = 本轮请求目的)→ + * stratum≤15 → LI≠3(服务器未同步告警态)→ serializer 校验 + * (mode=4、origin 与 T1 含随机低 16 位精确匹配、时间戳非零、 + * KoD 分类)。调研 §5.3:LI=3 / stratum>15 serializer 不拦, + * 必须在这里补;KoD 与畸形应答按失败退避(KoD 语义里 + * ChangeServer 换服务器,RATE 保留当前服务器) + */ +static uint8_t s_handle_packet(const uint8_t *p_buf, int len, + const struct net_sockaddr_in *p_src, + const uint8_t *p_srv_ip, uint32_t now) { + const sntp_cfg_t *cfg = sntp_cfg_get(); + SntpResponseData_t resp; + SntpTimestamp_t t4; + SntpStatus_t st; + + if (len < (int)SNTP_PACKET_BASE_SIZE) { + DBG_INFO("WARN: short resp len=%d, drop", len); + return 0u; + } + if ((memcmp(&p_src->sin_addr.s_addr, p_srv_ip, 4u) != 0) || + (p_src->sin_port != net_htons(SNTP_SERVER_PORT))) { + /* 防欺骗第一道:应答源必须是我们刚发过请求的那个 IP:123 */ + DBG_INFO("WARN: resp from unexpected src, drop"); + return 0u; + } + if ((p_buf[1] & 0x7FU) > 15u) { + DBG_INFO("WARN: stratum=%u invalid, drop", (unsigned)(p_buf[1] & 0x7FU)); + s_fail_backoff(now); + return 1u; + } + if ((p_buf[0] >> 6) == 3u) { + /* LI=3:服务器自己没同步,时间不可用;再等多久也一样 → 直接失败 */ + DBG_INFO("WARN: server not synced (LI=3)"); + s_server_toggle(cfg); + s_fail_backoff(now); + return 1u; + } + + s_ts_now(&t4); + st = Sntp_DeserializeResponse(&s_ctx.t1, &t4, p_buf, (size_t)len, &resp); + + switch (st) { + case SntpSuccess: + s_handle_success(&resp, now); + return 1u; + + case SntpRejectedResponseChangeServer: + case SntpRejectedResponseOtherCode: + DBG_INFO("WARN: kiss-of-death code=%lu, switch server", + (unsigned long)resp.rejectedResponseCode); + s_server_toggle(cfg); + s_fail_backoff(now); + return 1u; + + case SntpRejectedResponseRetryWithBackoff: + DBG_INFO("WARN: kiss-of-death RATE, backoff"); + s_fail_backoff(now); + return 1u; + + case SntpInvalidResponse: + default: + /* originate 不匹配 / 时间戳为零 / mode 错 = 伪造或损坏:丢弃继续等 */ + DBG_INFO("WARN: invalid resp st=%d, drop", (int)st); + return 0u; + } +} + +/* + * 函数功能:发起一轮请求(SEND 态执行体:选服务器 → 开 socket → 序列化 → 发送) + * 入口参数:net_gen - 网络代次;now - 当前 tick uint32_t + * 返回值:无 + * 函数说明:成功 → 状态进 WAIT_RESP;任一失败 → 主备切换(适用时)+ + * s_fail_backoff 退避重排;服务器未配置 → 停在 WAIT_DUE 只警一次, + * 等下一次 run(配置补齐即恢复,不产生退避——没发过请求不算失败) + */ +static void s_send_round(uint32_t net_gen, uint32_t now) { + const sntp_cfg_t *cfg = sntp_cfg_get(); + const uint8_t *srv_ip; + struct net_sockaddr_in dst; + + srv_ip = s_server_ip(cfg); + if (srv_ip == NULL) { + if (s_ctx.no_srv_logged == 0u) { + DBG_INFO("WARN: enabled but no server configured"); + s_ctx.no_srv_logged = 1u; + } + return; + } + s_ctx.no_srv_logged = 0u; + + if (s_ctx.fd < 0) { + if (s_socket_open(net_gen) != 0) { + s_fail_backoff(now); + return; + } + } + + s_ts_now(&s_ctx.t1); + if (Sntp_SerializeRequest(&s_ctx.t1, s_rng_next(), s_ctx.pkt, + sizeof(s_ctx.pkt)) != SntpSuccess) { + /* T1 为 0 才会失败(ts 构造保证非零)——真到这儿是构造性 bug */ + DBG_ERROR("serialize FAIL"); + s_fail_backoff(now); + return; + } + + (void)memset(&dst, 0, sizeof(dst)); + dst.sin_family = NET_AF_INET; + dst.sin_port = net_htons(SNTP_SERVER_PORT); + (void)memcpy(&dst.sin_addr.s_addr, srv_ip, 4u); + + if (net_sendto(s_ctx.fd, s_ctx.pkt, (int)sizeof(s_ctx.pkt), 0, + (struct net_sockaddr *)&dst, (int)sizeof(dst)) + != (int)sizeof(s_ctx.pkt)) { + DBG_ERROR("sendto FAIL errno=%d", net_get_errno()); + s_server_toggle(cfg); + s_fail_backoff(now); + return; + } + + s_ctx.deadline_ms = now + s_ctx.timeout_ms; + s_ctx.state = SNTP_ST_WAIT_RESP; + /* 发送轮不占收片时间,立即让出回主循环 */ +} + +/* 函数定义区 */ + +/** + * 函数功能:SNTP 客户端单步驱动——由 timeSyncTask 主循环每轮调用一次 + * 入口参数:net_gen - 当前网络代次(time_sync 循环持有) uint32_t + * 返回值:无 + * 限定条件:仅 timeSyncTask 上下文;单次调用最多停留一片 SNTP_RECV_SLICE_MS, + * 看门狗喂点由调用方节奏保证;配置热读 [sntp] RAM 副本,无需重启 + */ +void time_sntp_run(uint32_t net_gen) { + const sntp_cfg_t *cfg; + uint32_t now = (uint32_t)xTaskGetTickCount(); + + s_rng_seed_once(); + cfg = sntp_cfg_get(); + + /* 网络代次变化:旧 socket 随芯片复位失效,弃掉重排首轮散布 */ + if (net_gen != s_ctx.gen) { + s_socket_close(); + s_ctx.gen = net_gen; + s_ctx.state = SNTP_ST_IDLE; + s_ctx.no_srv_logged = 0u; + } + + if (cfg->enable == 0u) { + if (s_ctx.state != SNTP_ST_IDLE) { + s_socket_close(); + s_ctx.state = SNTP_ST_IDLE; + } + return; + } + + if (s_ctx.state == SNTP_ST_IDLE) { + s_ctx.state = SNTP_ST_WAIT_DUE; + s_ctx.timeout_ms = SNTP_RESP_TIMEOUT_BASE_MS; + s_ctx.due_ms = now + SNTP_FIRST_DELAY_MIN_MS + + (s_rng_next() % SNTP_FIRST_DELAY_SPAN_MS); + } + + switch (s_ctx.state) { + case SNTP_ST_WAIT_DUE: + if (s_tick_due(now, s_ctx.due_ms) != 0u) { + s_send_round(net_gen, now); + } + return; + + case SNTP_ST_SEND: + s_send_round(net_gen, now); + return; + + case SNTP_ST_WAIT_RESP: { + uint8_t rxbuf[SNTP_RX_BUF_SIZE]; + struct net_sockaddr_in src; + const uint8_t *srv_ip; + int src_len = (int)sizeof(src); + int n; + + (void)memset(&src, 0, sizeof(src)); + n = net_recvfrom_to(s_ctx.fd, rxbuf, (int)sizeof(rxbuf), + SNTP_RECV_SLICE_MS, + (struct net_sockaddr *)&src, &src_len); + if (n > 0) { + srv_ip = s_server_ip(cfg); + if (srv_ip == NULL) { + s_fail_backoff(now); + return; + } + (void)s_handle_packet(rxbuf, n, &src, srv_ip, now); + return; + } + if (n == -2) { + /* 本片等满无数据:看总截止 */ + if (s_tick_due(now, s_ctx.deadline_ms) != 0u) { + DBG_INFO("WARN: resp timeout, tmo=%lu ms", (unsigned long)s_ctx.timeout_ms); + s_server_toggle(cfg); + s_fail_backoff(now); + } + return; + } + if (n == -1) { + int e = net_get_errno(); + + DBG_ERROR("recvfrom FAIL errno=%d", e); + if ((e == NET_ERR_BADF) || (e == NET_ERR_CONNRESET) || + (e == NET_ERR_INVAL)) { + /* fd 已死(重建取消/非法):关掉等代次/下轮重开 */ + s_socket_close(); + s_fail_backoff(now); + } + /* 其余瞬时错误:继续等到 deadline,超时自然收口 */ + return; + } + /* n == 0:UDP 语义下不应出现(EOF 是 TCP 概念),防御收口 */ + s_fail_backoff(now); + return; + } + + default: + s_ctx.state = SNTP_ST_IDLE; + return; + } +} + +/** + * 函数功能:累计校验通过的同步次数(诊断口) + * 入口参数:无 + * 返回值:同步成功次数 uint32_t + * 限定条件:任意上下文只读 + */ +uint32_t time_sntp_sync_count(void) { + return s_ctx.sync_count; +} + +/** + * 函数功能:最近一次校验通过应答的毫秒钟差(诊断口,int64 截断值) + * 入口参数:无 + * 返回值:(T2-T1+T3-T4)/2 毫秒,截断到 int32;从未成功为 0 int32_t + * 限定条件:任意上下文只读;仅日志/排障用,不作判据 + */ +int32_t time_sntp_last_offset_ms(void) { + return s_ctx.last_off_ms; +} diff --git a/App/sync/time_sntp.h b/App/sync/time_sntp.h new file mode 100644 index 00000000..d4b285c3 --- /dev/null +++ b/App/sync/time_sntp.h @@ -0,0 +1,53 @@ +/* + * 模块名称:Time SNTP — 板端 SNTP 客户端(UDP 48 字节报文) + * 模块功能:按 [sntp] 段配置向主/备 SNTP 服务器发送 RFC 4330 请求、校验应答 + * (originate 精确匹配 + 源地址 + mode/stratum/LI/KoD)、计算钟差并 + * 按阈值步进本地墙钟(sys_clock_set_unix,钟面 = 本地时间 UTC+8), + * 首次成功后置 SYS_EVT_TIMESYNC_READY;报文编解码用 vendor 的 + * coreSNTP serializer(Lib/coreSNTP,MIT) + * 适用平台:STM32F407ZGTx + CH395F(timeSyncTask 内运行,模拟器不编) + * 作者:王建锋 + * 创建日期:2026-11-04 + */ + +#ifndef TIME_SNTP_H +#define TIME_SNTP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* + * 函数功能:SNTP 客户端节拍入口(幂等、非阻塞——除等待应答期一次 ≤50ms 收片) + * 入口参数:net_gen - 调用方本轮读取的网络代次(net_get_generation()) uint32_t + * 返回值:无 + * 限定条件:**仅 timeSyncTask 上下文**调用(看门狗心跳由调用方在片间喂, + * 本模块单次调用最长驻留 = 一个 50ms 收片,满足 100ms 抽样窗约束) + * 函数说明:内部状态机 停用 → 到期 → 发送 → 等应答 → 步进/退避;配置 + * (sntp_cfg_get())每轮现读,代次变化自动弃旧 Socket 重开 + */ +void time_sntp_run(uint32_t net_gen); + +/* + * 函数功能:取累计成功同步次数(诊断/测试锚点) + * 入口参数:无 + * 返回值:成功次数(含死区内的"未步进但校验通过") uint32_t + * 限定条件:无 + */ +uint32_t time_sntp_sync_count(void); + +/* + * 函数功能:取最近一次校验通过应答的钟差(诊断锚点) + * 入口参数:无 + * 返回值:钟差毫秒,截断到 int32;正 = 本地钟落后服务器;从未成功 = 0 int32_t + * 限定条件:无 + */ +int32_t time_sntp_last_offset_ms(void); + +#ifdef __cplusplus +} +#endif + +#endif /* TIME_SNTP_H */ diff --git a/App/sync/time_sync.c b/App/sync/time_sync.c index 82a524b0..19931ed5 100644 --- a/App/sync/time_sync.c +++ b/App/sync/time_sync.c @@ -19,10 +19,15 @@ * 等待(500/2000/200ms)统一走 time_sync_delay_fed() * 按 ≤50ms 分片、每片喂心跳——消除"心跳滑出两个连续 * 100ms 抽样窗"的节拍抖动(第 7 轮台架 snap=0x17) + * v1.4 2026-11-04 DTU 项目组 SNTP 客户端接入(#57 调研落地):accept 轮询 + * 片内驱动 time_sntp_run()(48B UDP 请求/应答状态机 + * 在 App/sync/time_sntp.c,节奏/退避/步进策略见该文件); + * 本任务循环结构与 8888 语义不变 */ #include #include "time_sync.h" +#include "time_sntp.h" #include "net_socket.h" #include "sys_clock.h" #include "sys_events.h" @@ -272,6 +277,10 @@ void time_sync_task(void *argument) { ls = -1; break; } + /* SNTP 客户端节拍:accept 轮询片内驱动(等待应答期单次 + * 驻留 ≤50ms 收片,心跳仍由下面的分片延时喂,100ms 抽样窗 + * 不破)。代次变化由 time_sntp_run 内部自理(弃旧 socket)。 */ + time_sntp_run(gen); time_sync_delay_fed(200U); } } while (conn < 0); diff --git a/Drivers/BSP/NETCFG/net_cfg.c b/Drivers/BSP/NETCFG/net_cfg.c index e39d6028..07a23644 100644 --- a/Drivers/BSP/NETCFG/net_cfg.c +++ b/Drivers/BSP/NETCFG/net_cfg.c @@ -32,6 +32,8 @@ #include "ftp_cfg.h" +#include "sntp_cfg.h" + /* 深调用链模块(net_cfg_load/save 在 defaultTask 启动链与保存链上): * 日志行缓冲收到 128 B(默认 192),本模块最长行渲染后 < 128 → 不截断 */ #define DBG_LINE_BUF_MAX 128U @@ -80,6 +82,16 @@ static const cfg_key_def_t s_net_keys[] = { { "mac", CFG_VAL_MAC, offsetof(net_cfg_t, mac), 6u }, }; +/* [sntp] 段键表(sntp_cfg_t 布局内偏移;数据本体在 sntp_cfg.c——分层门禁 + * layer-deps 要求段描述符的组装集中在本文件:NETCFG 里只有 net_cfg.c / + * ftp_cfg.c 在反向依赖基线上,[sntp] 是新增模块,不再往基线添条目) */ +static const cfg_key_def_t s_sntp_keys[] = { + { "enable", CFG_VAL_UINT, offsetof(sntp_cfg_t, enable), 1u }, + { "server1", CFG_VAL_IP4, offsetof(sntp_cfg_t, server_pri), 4u }, + { "server2", CFG_VAL_IP4, offsetof(sntp_cfg_t, server_bak), 4u }, + { "poll_min", CFG_VAL_UINT, offsetof(sntp_cfg_t, poll_min), 2u }, +}; + /* 函数定义区 */ /* @@ -122,6 +134,35 @@ static void s_net_register(void) { s_done = 1u; } +/* + * 函数功能:向 cfg_file 引擎注册 [sntp] 段(幂等) + * 入口参数:无 + * 返回值:无 + * 限定条件:load/save 前调用(net_cfg_load 内统一编排) + * 函数说明:数据本体与工厂默认由 sntp_cfg.c 出(sntp_cfg_data/defaults); + * 无变更回调——SNTP 客户端每轮直读 RAM 副本,保存后的"立即生效" + * 走既有 SYS_EVT_NET_RECONFIG(网络重建 → 代次变化 → 客户端重开 + * Socket)链,本段无需单独挂接 + */ +static void s_sntp_register(void) { + static uint8_t s_done; + cfg_section_reg_t reg; + + if (s_done != 0u) { + return; + } + (void)memset(®, 0, sizeof(reg)); + reg.name = "sntp"; + reg.p_data = sntp_cfg_data(); + reg.p_defaults = sntp_cfg_defaults(); + reg.data_size = sizeof(sntp_cfg_t); + reg.p_keys = s_sntp_keys; + reg.key_count = 4u; + reg.p_cb = NULL; + (void)cfg_file_register(®); + s_done = 1u; +} + /* * 函数功能:判断 6 字节 MAC 是否全零 * 入口参数:p_mac - MAC 指针(不可为 NULL) const uint8_t * @@ -148,8 +189,10 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { */ int net_cfg_load(void) { s_net_register(); - /* [ftp] 段一并注册(所有受管段必须先注册、后 load,引擎按注册表解析) */ + /* [ftp] / [sntp] 段一并注册(所有受管段必须先注册、后 load,引擎按注册表解析); + * [sntp] 描述符在本文件组装——见 s_sntp_register 分层门禁注 */ ftp_cfg_register(); + s_sntp_register(); /* 旧档 NCFG 迁移已于 2026-10-10 删除(net_cfg_legacy 移除;理由见文件头 v1.3)。 * 若卷上仍残留 /cfg/netcfg.bin:不读、不删、不迁移 → 配置回落工厂默认 */ diff --git a/Drivers/BSP/NETCFG/sntp_cfg.c b/Drivers/BSP/NETCFG/sntp_cfg.c new file mode 100644 index 00000000..0d5ec7a0 --- /dev/null +++ b/Drivers/BSP/NETCFG/sntp_cfg.c @@ -0,0 +1,103 @@ +/* + * 模块名称:SNTP 配置核心 (SNTP Configuration Core) + * 模块功能:RAM 配置副本 + 工厂默认 + 读写出口(INI [sntp] 段: + * enable/server1/server2/poll_min);加载/保存编排复用 [network] + * 段的既有链路(net_cfg_load/save + UI 保存行请求链)。段描述符 + * 由 net_cfg.c 的 s_sntp_register() 组装注册——分层门禁 layer-deps + * 要求本模块不 include 配置引擎头(App/cfg/cfg_file.h),只留 + * 数据本体与访问器 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-11-04 + * 修改记录: + * v1.0 2026-11-04 王建锋 创建初始版本(SNTP 客户端接入,#57 调研结论: + * 出厂默认关闭、服务器 IPv4 字面量、周期 1~1440 分钟) + * v1.1 2026-11-04 王建锋 分层门禁口径改造:注册编排移出本模块(见 .h 同名注) + */ + +/* 头文件包含区 */ +#include + +#include "sntp_cfg.h" + +/* 静态变量定义区 */ + +/* RAM 配置副本:以工厂默认值静态初始化(落 .data),自启动即有效—— + * 不依赖 cfg_file_load 是否成功(与 net_cfg s_cfg 同一口径)。 + * 命名带 s_sntp_ 前缀:net_cfg.c 与本文件会在同一 TU 里被主机单测并入 + * (test/host/net_cfg),裸 s_cfg/s_defaults 会同名重定义 */ +static sntp_cfg_t s_sntp_cfg = { + .enable = SNTP_CFG_DEF_ENABLE, + .server_pri = SNTP_CFG_DEF_SERVER, + .server_bak = SNTP_CFG_DEF_SERVER, + .poll_min = SNTP_CFG_DEF_POLL_MIN, +}; + +/* 工厂默认(引擎注册 p_defaults 指向;与 s_sntp_cfg 初值共用宏单一来源) */ +static const sntp_cfg_t s_sntp_defaults = { + .enable = SNTP_CFG_DEF_ENABLE, + .server_pri = SNTP_CFG_DEF_SERVER, + .server_bak = SNTP_CFG_DEF_SERVER, + .poll_min = SNTP_CFG_DEF_POLL_MIN, +}; + +/* 函数定义区 */ + +/* + * 函数功能:取 [sntp] 段 RAM 副本(引擎注册专用写入口) + * 入口参数:无 + * 返回值:RAM 配置副本指针(恒非空) sntp_cfg_t * + * 限定条件:仅 net_cfg.c 组段描述符用;业务读走 sntp_cfg_get(), + * UI 编辑走 sntp_cfg_set() + */ +sntp_cfg_t *sntp_cfg_data(void) { + return &s_sntp_cfg; +} + +/* + * 函数功能:取工厂默认值(引擎注册 p_defaults 指向) + * 入口参数:无 + * 返回值:工厂默认结构指针(只读,恒非空) const sntp_cfg_t * + * 限定条件:仅 net_cfg.c 组段描述符用 + */ +const sntp_cfg_t *sntp_cfg_defaults(void) { + return &s_sntp_defaults; +} + +/* + * 函数功能:取 [sntp] 段 RAM 副本(只读) + * 入口参数:无 + * 返回值:RAM 配置副本指针(恒非空) const sntp_cfg_t * + * 限定条件:无 + */ +const sntp_cfg_t *sntp_cfg_get(void) { + return &s_sntp_cfg; +} + +/* + * 函数功能:更新 RAM 副本(不写盘) + * 入口参数:p_new - 新配置(NULL 忽略) const sntp_cfg_t * + * 返回值:无 + * 限定条件:UI 编辑路径(net_cfg_set 同口径:整结构体一次拷贝, + * 写盘走 net_cfg_save_request 整档链,属主 defaultTask) + */ +void sntp_cfg_set(const sntp_cfg_t *p_new) { + if (p_new != NULL) { + (void)memcpy(&s_sntp_cfg, p_new, sizeof(s_sntp_cfg)); + } +} + +/* + * 函数功能:取轮询周期(带外自愈读出口) + * 入口参数:无 + * 返回值:合法范围内 = 存储值;0 或 >1440 = 工厂默认 60 uint16_t + * 限定条件:无 + */ +uint16_t sntp_cfg_poll_min(void) { + uint16_t v = s_sntp_cfg.poll_min; + + if ((v < SNTP_CFG_POLL_MIN_MIN) || (v > SNTP_CFG_POLL_MIN_MAX)) { + return SNTP_CFG_DEF_POLL_MIN; + } + return v; +} diff --git a/Drivers/BSP/NETCFG/sntp_cfg.h b/Drivers/BSP/NETCFG/sntp_cfg.h new file mode 100644 index 00000000..64febd83 --- /dev/null +++ b/Drivers/BSP/NETCFG/sntp_cfg.h @@ -0,0 +1,97 @@ +/* + * 模块名称:SNTP 配置([sntp] 段) + * 模块功能:板端 SNTP 客户端配置——RAM 副本 + 工厂默认 + 读写出口 + * ([sntp] 段:enable / server1 / server2 / poll_min);段描述符 + * 由 net_cfg.c 在 net_cfg_load 里组好交引擎(分层门禁 layer-deps: + * 本模块不 include 引擎头);消费者(App/sync/time_sntp.c)与 UI 页 + * 经 sntp_cfg_get() 只读访问,配置引擎属主仍为 defaultTask + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-11-04 + * 修改记录: + * v1.0 2026-11-04 王建锋 创建初始版本(SNTP 客户端接入,#57 调研结论) + * v1.1 2026-11-04 王建锋 分层门禁口径:sntp_cfg_register 改为 + * sntp_cfg_data/sntp_cfg_defaults 数据口, + * 段描述符编排移 net_cfg.c(本模块不再依赖引擎头) + */ + +#ifndef SNTP_CFG_H +#define SNTP_CFG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* 轮询周期合法范围(分钟);范围外的存储值读取时回落工厂默认(同 [comm] + * 频段自愈口径,见 iec104_comm.c 带外值自愈) */ +#define SNTP_CFG_POLL_MIN_MIN 1u +#define SNTP_CFG_POLL_MIN_MAX 1440u + +/* 工厂默认:enable=0(出厂不启用,调研 §6 产品口径)、服务器留空 + * (0.0.0.0 = 未配置,客户端跳过该服务器)、poll_min=60(1 小时) */ +#define SNTP_CFG_DEF_ENABLE 0u +#define SNTP_CFG_DEF_SERVER { 0u, 0u, 0u, 0u } +#define SNTP_CFG_DEF_POLL_MIN 60u + +typedef struct { + uint8_t enable; /* 1 = 启用 SNTP 客户端;0 = 停用(默认) */ + uint8_t server_pri[4]; /* 主 SNTP 服务器 IPv4(全零 = 未配置) */ + uint8_t server_bak[4]; /* 备 SNTP 服务器 IPv4(全零 = 无备) */ + uint16_t poll_min; /* 成功后的轮询周期(分钟,1~1440) */ +} sntp_cfg_t; + +/* + * 函数功能:取 [sntp] 段 RAM 副本(引擎注册专用写入口) + * 入口参数:无 + * 返回值:RAM 配置副本指针(恒非空,静态常驻) sntp_cfg_t * + * 限定条件:仅供 net_cfg.c 组 [sntp] 段描述符交配置引擎(net_cfg_load 编排); + * 分层门禁 layer-deps:本模块不 include cfg_file.h,段注册集中在 + * net_cfg.c(同层基线文件)完成 + */ +sntp_cfg_t *sntp_cfg_data(void); + +/* + * 函数功能:取工厂默认值(引擎注册 p_defaults 指向) + * 入口参数:无 + * 返回值:工厂默认结构指针(只读,恒非空) const sntp_cfg_t * + * 限定条件:同上,net_cfg.c 组段描述符专用 + */ +const sntp_cfg_t *sntp_cfg_defaults(void); + +/* + * 函数功能:取 [sntp] 段 RAM 副本(只读) + * 入口参数:无 + * 返回值:恒非 NULL(静态常驻,load 前初值 = 工厂默认) const sntp_cfg_t * + * 限定条件:无 + * 函数说明:defaultTask poll 重载瞬间可能读到撕裂值——IPv4 4 字节 / uint16 + * 在本平台是单条 LDR 指令天然原子,enable 单字节同理;即便撕裂, + * SNTP 客户端最坏多用一次旧值,下一轮自愈(配置变更本就罕见) + */ +const sntp_cfg_t *sntp_cfg_get(void); + +/* + * 函数功能:更新 RAM 副本(不写盘) + * 入口参数:p_new - 新配置(NULL 忽略) const sntp_cfg_t * + * 返回值:无 + * 限定条件:UI 编辑路径专用(net_cfg_set 同口径);写盘须走 + * net_cfg_save_request 整档链(属主 defaultTask) + */ +void sntp_cfg_set(const sntp_cfg_t *p_new); + +/* + * 函数功能:取轮询周期(带外自愈读出口) + * 入口参数:无 + * 返回值:合法范围内 = 存储值;0 或 >1440 = 工厂默认 60 uint16_t + * 限定条件:无 + * 函数说明:不改写 RAM 副本(UI 显示的仍是用户写的原值,避免"保存后 + * 数字自己变了"的困惑);消费方(time_sntp)用本口而非裸字段 + */ +uint16_t sntp_cfg_poll_min(void); + +#ifdef __cplusplus +} +#endif + +#endif /* SNTP_CFG_H */ diff --git a/Drivers/BSP/UI/ui_font_cn_12.c b/Drivers/BSP/UI/ui_font_cn_12.c index ac0a6a4e..19887f95 100644 --- a/Drivers/BSP/UI/ui_font_cn_12.c +++ b/Drivers/BSP/UI/ui_font_cn_12.c @@ -437,6 +437,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x9f, 0x52, 0x2a, 0x7d, 0x48, 0xa9, 0x11, 0x26, 0x60, + /* U+52A1 "务" */ + 0x10, 0x7, 0xf1, 0x44, 0x7, 0x3, 0x19, 0x90, + 0xc2, 0x3, 0xfc, 0x10, 0x82, 0x10, 0x82, 0x21, + 0x80, + /* U+52A8 "动" */ 0x1, 0xe, 0x20, 0x4, 0x1, 0xff, 0x92, 0x42, 0x50, 0x4a, 0x89, 0x89, 0x3f, 0x44, 0x8, 0x82, @@ -515,6 +520,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xc8, 0x9, 0x7d, 0x28, 0xa5, 0xf5, 0x0, 0xc0, 0x70, + /* U+5668 "器" */ + 0x7b, 0xc9, 0x49, 0xef, 0x2, 0x40, 0x45, 0xff, + 0xc2, 0x81, 0x8c, 0xc0, 0x6f, 0x79, 0x29, 0x3d, + 0xe0, + /* U+56DE "回" */ 0xff, 0xe0, 0x18, 0x6, 0x79, 0x92, 0x64, 0x99, 0xe6, 0x1, 0x80, 0x7f, 0xf8, 0x4, @@ -549,6 +559,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x52, 0xaa, 0x76, 0x40, 0xcc, 0x57, 0x35, 0x81, 0x10, + /* U+5907 "备" */ + 0x10, 0x7, 0xf1, 0x44, 0x7, 0x3, 0x19, 0x80, + 0xcf, 0xe1, 0x24, 0x3f, 0x84, 0x90, 0xfe, 0x10, + 0x40, + /* U+590D "复" */ 0x3f, 0xe8, 0x2, 0xff, 0x10, 0x23, 0xfc, 0x40, 0x8f, 0xf0, 0x80, 0x3f, 0xca, 0x10, 0x3c, 0x38, @@ -800,6 +815,10 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x3f, 0xc8, 0x12, 0x4, 0xff, 0x20, 0x48, 0x13, 0xfc, 0x81, 0x40, 0x50, 0x18, 0x1c, + /* U+670D "服" */ + 0x7b, 0xe9, 0x45, 0x28, 0xbd, 0x64, 0xa0, 0x97, + 0xde, 0xaa, 0x55, 0x4a, 0x49, 0x56, 0x6c, 0x80, + /* U+671F "期" */ 0x48, 0x9, 0x3f, 0xf4, 0xa4, 0x97, 0x9e, 0x92, 0x5e, 0x4a, 0x4f, 0xfd, 0x20, 0x25, 0x24, 0xc3, @@ -1515,24 +1534,24 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 898, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 915, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 932, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 949, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 963, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 979, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 995, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1012, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1028, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1045, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1061, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1}, - {.bitmap_index = 1073, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1089, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 949, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 966, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 980, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 996, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1012, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1029, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1045, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1062, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1078, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1}, + {.bitmap_index = 1090, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1106, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1123, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1140, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1157, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1174, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1191, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1205, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1222, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1191, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1208, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1225, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 1239, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1256, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1273, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1540,29 +1559,29 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 1307, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1324, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1341, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1358, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1374, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1391, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1408, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1358, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1375, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1392, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1409, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1425, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1442, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1459, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1473, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1490, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1507, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1459, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1476, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1493, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1510, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 1524, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1541, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1558, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1575, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1591, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1608, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1625, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1639, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1656, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1673, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1687, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1704, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1721, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1575, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1592, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1609, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1626, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1642, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1659, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1676, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1690, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1707, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1724, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 1738, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1755, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1772, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1583,132 +1602,136 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 2027, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2044, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2061, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2078, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2094, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 2107, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2124, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2140, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2157, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2171, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2188, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2205, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2222, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2239, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2256, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2273, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2290, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2307, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2324, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2341, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2358, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2375, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2392, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2408, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2424, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2441, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2458, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2475, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2492, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2509, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2526, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2543, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2560, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2577, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2594, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2611, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2628, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2645, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2662, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2679, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2693, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2710, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2727, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2744, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 2759, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2776, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2793, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2810, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2078, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2095, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2112, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2129, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2145, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 2158, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2175, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2191, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2208, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2222, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2238, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2255, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2272, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2289, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2306, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2323, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2340, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2357, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2374, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2391, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2408, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2425, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2442, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2459, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2475, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2491, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2508, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2525, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2542, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2559, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2576, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2593, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2610, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2627, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2644, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2661, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2678, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2695, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2712, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2729, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2746, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2760, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2777, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2794, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2811, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 2826, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2843, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2859, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2876, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2843, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2860, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2877, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2893, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2910, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2927, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2944, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2961, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2978, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2995, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3012, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3029, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3046, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3063, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3080, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3097, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3114, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3131, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3148, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3162, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3179, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3196, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3213, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3230, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3247, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3264, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3281, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3298, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3315, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3332, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3349, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3366, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3383, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3400, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3417, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3434, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3451, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3468, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3485, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3502, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3519, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3536, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3553, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3570, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3587, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3604, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3621, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3638, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3655, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3672, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3689, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3706, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3723, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3740, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2910, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2926, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2943, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2960, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2977, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2994, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3011, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3028, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3045, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3062, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3079, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3096, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3113, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3130, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3147, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3164, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3181, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3198, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3215, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 3229, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3246, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3263, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3280, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3297, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3314, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3331, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3348, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3365, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3382, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3399, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3416, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3433, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3450, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3467, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3484, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3501, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3518, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3535, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3552, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3569, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3586, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3603, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3620, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3637, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3654, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3671, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3688, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3705, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3722, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3739, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3756, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3773, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3790, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3807, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3824, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3841, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3858, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3875, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3892, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3907, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3922, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3937, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3954, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3971, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3988, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 4005, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 4022, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3807, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3823, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3840, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3857, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3874, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3891, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3908, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3925, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3942, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3959, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 3974, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 3989, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 4004, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4021, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 4038, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 4055, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 4072, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 4089, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 4106, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 4123, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 4140, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 4141, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 4143, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */ + {.bitmap_index = 4089, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4105, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4122, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4139, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4156, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4173, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4190, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4207, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 4208, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 4210, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */ }; /*--------------------- @@ -1719,31 +1742,32 @@ static const uint16_t unicode_list_1[] = { 0x0, 0x1, 0x2, 0x2ca0, 0x2ca9, 0x2caa, 0x2cad, 0x2cdb, 0x2d2b, 0x2d2c, 0x2d44, 0x2d96, 0x2ded, 0x2dee, 0x2dfc, 0x2e7d, 0x2e81, 0x2edc, 0x3005, 0x300c, 0x3011, 0x3013, 0x3039, 0x309a, - 0x30a6, 0x30d6, 0x30ea, 0x30ed, 0x3148, 0x31b6, 0x31da, 0x322b, - 0x323f, 0x3262, 0x326c, 0x3271, 0x3276, 0x3283, 0x328f, 0x3297, - 0x32a8, 0x32cc, 0x32cf, 0x32ea, 0x3308, 0x357e, 0x3580, 0x3594, - 0x35d0, 0x35e0, 0x362b, 0x367f, 0x37ad, 0x37b6, 0x37d1, 0x39f0, - 0x39f7, 0x39f8, 0x3a3a, 0x3a3e, 0x3a66, 0x3c92, 0x3cc7, 0x3cd8, - 0x3d14, 0x3d2f, 0x3d46, 0x3d96, 0x3da0, 0x3da2, 0x3daf, 0x3db5, - 0x3dda, 0x3df2, 0x3df3, 0x3df5, 0x3ea1, 0x3edb, 0x3f02, 0x3f0f, - 0x40d7, 0x40eb, 0x4135, 0x4136, 0x4144, 0x4145, 0x420e, 0x4229, - 0x4245, 0x4247, 0x4249, 0x436d, 0x43d6, 0x43de, 0x43e5, 0x43e8, - 0x4410, 0x4414, 0x4480, 0x4485, 0x4496, 0x44de, 0x45a0, 0x45a8, - 0x45bf, 0x45ca, 0x45cc, 0x45da, 0x461f, 0x4685, 0x46a7, 0x46c1, - 0x46d7, 0x4760, 0x48c1, 0x49c0, 0x49c1, 0x4a03, 0x4a1b, 0x4a55, - 0x4ae2, 0x4b00, 0x4b82, 0x4be1, 0x4beb, 0x4c28, 0x4ca5, 0x4cc9, - 0x4f59, 0x50e7, 0x50e8, 0x5156, 0x5177, 0x518c, 0x53c8, 0x53d5, - 0x53ec, 0x5571, 0x5594, 0x55ab, 0x5685, 0x56a1, 0x570c, 0x570e, - 0x57da, 0x5872, 0x58ab, 0x5937, 0x596b, 0x5979, 0x598f, 0x59cc, - 0x5b1b, 0x5b9b, 0x5d5f, 0x5d68, 0x5d7f, 0x5d8d, 0x5d94, 0x5df1, - 0x5e0e, 0x5fba, 0x608a, 0x6122, 0x614f, 0x61a3, 0x6217, 0x66ec, - 0x6765, 0x6a06, 0x6a41, 0x6a44, 0x6a4f, 0x6a50, 0x6a5e, 0x6a82, - 0x6a85, 0x6a8f, 0x6a97, 0x6a9b, 0x6bbf, 0x6bc5, 0x6c2a, 0x6c93, - 0x6e0f, 0x6e1d, 0x6e33, 0x6e67, 0x6e70, 0x6e71, 0x6e74, 0x6ea0, - 0x6ea1, 0x6eba, 0x6ef3, 0x6f05, 0x6f88, 0x6fed, 0x7067, 0x706d, - 0x706f, 0x733f, 0x7345, 0x73a1, 0x73b9, 0x741f, 0x7488, 0x748d, - 0x7494, 0x7498, 0x74d2, 0x7504, 0x753c, 0x7596, 0x7602, 0x7715, - 0x7731, 0x7828, 0x7836, 0x792c, 0x7d78, 0xddac, 0xddba + 0x30a6, 0x30d6, 0x30ea, 0x30ed, 0x3141, 0x3148, 0x31b6, 0x31da, + 0x322b, 0x323f, 0x3262, 0x326c, 0x3271, 0x3276, 0x3283, 0x328f, + 0x3297, 0x32a8, 0x32cc, 0x32cf, 0x32ea, 0x3308, 0x3508, 0x357e, + 0x3580, 0x3594, 0x35d0, 0x35e0, 0x362b, 0x367f, 0x37a7, 0x37ad, + 0x37b6, 0x37d1, 0x39f0, 0x39f7, 0x39f8, 0x3a3a, 0x3a3e, 0x3a66, + 0x3c92, 0x3cc7, 0x3cd8, 0x3d14, 0x3d2f, 0x3d46, 0x3d96, 0x3da0, + 0x3da2, 0x3daf, 0x3db5, 0x3dda, 0x3df2, 0x3df3, 0x3df5, 0x3ea1, + 0x3edb, 0x3f02, 0x3f0f, 0x40d7, 0x40eb, 0x4135, 0x4136, 0x4144, + 0x4145, 0x420e, 0x4229, 0x4245, 0x4247, 0x4249, 0x436d, 0x43d6, + 0x43de, 0x43e5, 0x43e8, 0x4410, 0x4414, 0x4480, 0x4485, 0x4496, + 0x44de, 0x45a0, 0x45a8, 0x45ad, 0x45bf, 0x45ca, 0x45cc, 0x45da, + 0x461f, 0x4685, 0x46a7, 0x46c1, 0x46d7, 0x4760, 0x48c1, 0x49c0, + 0x49c1, 0x4a03, 0x4a1b, 0x4a55, 0x4ae2, 0x4b00, 0x4b82, 0x4be1, + 0x4beb, 0x4c28, 0x4ca5, 0x4cc9, 0x4f59, 0x50e7, 0x50e8, 0x5156, + 0x5177, 0x518c, 0x53c8, 0x53d5, 0x53ec, 0x5571, 0x5594, 0x55ab, + 0x5685, 0x56a1, 0x570c, 0x570e, 0x57da, 0x5872, 0x58ab, 0x5937, + 0x596b, 0x5979, 0x598f, 0x59cc, 0x5b1b, 0x5b9b, 0x5d5f, 0x5d68, + 0x5d7f, 0x5d8d, 0x5d94, 0x5df1, 0x5e0e, 0x5fba, 0x608a, 0x6122, + 0x614f, 0x61a3, 0x6217, 0x66ec, 0x6765, 0x6a06, 0x6a41, 0x6a44, + 0x6a4f, 0x6a50, 0x6a5e, 0x6a82, 0x6a85, 0x6a8f, 0x6a97, 0x6a9b, + 0x6bbf, 0x6bc5, 0x6c2a, 0x6c93, 0x6e0f, 0x6e1d, 0x6e33, 0x6e67, + 0x6e70, 0x6e71, 0x6e74, 0x6ea0, 0x6ea1, 0x6eba, 0x6ef3, 0x6f05, + 0x6f88, 0x6fed, 0x7067, 0x706d, 0x706f, 0x733f, 0x7345, 0x73a1, + 0x73b9, 0x741f, 0x7488, 0x748d, 0x7494, 0x7498, 0x74d2, 0x7504, + 0x753c, 0x7596, 0x7602, 0x7715, 0x7731, 0x7828, 0x7836, 0x792c, + 0x7d78, 0xddac, 0xddba }; /*Collect the unicode lists and glyph_id offsets*/ @@ -1754,12 +1778,12 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY }, { - .range_start = 9654, .range_length = 1, .glyph_id_start = 319, + .range_start = 9654, .range_length = 1, .glyph_id_start = 323, .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY }, { .range_start = 8544, .range_length = 56763, .glyph_id_start = 96, - .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 223, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 227, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Drivers/BSP/UI/ui_page_registry.c b/Drivers/BSP/UI/ui_page_registry.c index 5ee11d0b..9e079834 100644 --- a/Drivers/BSP/UI/ui_page_registry.c +++ b/Drivers/BSP/UI/ui_page_registry.c @@ -26,6 +26,7 @@ #include "ui_page_runlog.h" #include "ui_page_runreport.h" #include "ui_page_selftest.h" +#include "ui_page_sntp.h" #include "ui_page_soe.h" #include "ui_page_start.h" #include "ui_page_view_param.h" @@ -58,6 +59,7 @@ const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = { { 27u, &ui_page_clear_desc }, /* node 27 清除记录 */ { 29u, &ui_page_comm_desc }, /* node 29 通讯设置 */ { 30u, &ui_page_net_cfg_desc }, /* node 30 网口设置 */ + { 31u, &ui_page_sntp_desc }, /* node 31 SNTP设置 */ { 33u, &ui_page_soe_desc }, /* node 33 SOE记录 */ { 34u, &ui_page_accident_desc }, /* node 34 事故记录 */ { 35u, &ui_page_oper_desc }, /* node 35 操作记录 */ diff --git a/Drivers/BSP/UI/ui_page_registry.h b/Drivers/BSP/UI/ui_page_registry.h index c7091e81..fd1e76ae 100644 --- a/Drivers/BSP/UI/ui_page_registry.h +++ b/Drivers/BSP/UI/ui_page_registry.h @@ -25,7 +25,7 @@ typedef struct { const content_page_def_t *desc; /* 页面描述符(Flash const) */ } ui_page_entry_t; -#define UI_PAGE_MAP_COUNT 30u /* 绑定了内容页的叶子数 */ +#define UI_PAGE_MAP_COUNT 31u /* 绑定了内容页的叶子数 */ extern const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT]; diff --git a/Drivers/BSP/UI/ui_page_sntp.c b/Drivers/BSP/UI/ui_page_sntp.c new file mode 100644 index 00000000..258ef71d --- /dev/null +++ b/Drivers/BSP/UI/ui_page_sntp.c @@ -0,0 +1,431 @@ +/* + * 模块名称: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, +}; diff --git a/Drivers/BSP/UI/ui_page_sntp.h b/Drivers/BSP/UI/ui_page_sntp.h new file mode 100644 index 00000000..d051d104 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_sntp.h @@ -0,0 +1,30 @@ +/* + * 模块名称:UI Page SNTP (SNTP设置内容页) + * 模块功能:SNTP设置叶子节点的内容页描述符(5 行列表:启用/主服务器/ + * 备服务器/周期(分) + 保存行;可编辑页:修改权限下 ENTER 进编辑、 + * 地址行 ↑↓ 调字节 ←→ 移光标、周期行 ↑↓ ±1 ←→ ±60、 + * 保存行 ENTER 投递整档保存请求,属主任务写盘) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-11-04 + * 修改记录: + * v1.0 2026-11-04 王建锋 创建初始版本(板端 SNTP 客户端接入) + */ + +#ifndef UI_PAGE_SNTP_H +#define UI_PAGE_SNTP_H + +#include "ui_content_page.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* 页面描述符(生成物 ui_page_registry.c 按菜单树 #page=sntp 注记绑定引用;.id = "sntp") */ +extern const content_page_def_t ui_page_sntp_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_SNTP_H */ diff --git a/Lib/coreSNTP/LICENSE b/Lib/coreSNTP/LICENSE new file mode 100644 index 00000000..9cf10627 --- /dev/null +++ b/Lib/coreSNTP/LICENSE @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Lib/coreSNTP/core_sntp_serializer.c b/Lib/coreSNTP/core_sntp_serializer.c new file mode 100644 index 00000000..c4c3a05e --- /dev/null +++ b/Lib/coreSNTP/core_sntp_serializer.c @@ -0,0 +1,845 @@ +/* + * coreSNTP + * Copyright (C) 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * @file core_sntp_serializer.c + * @brief Implementation of the Serializer API of the coreSNTP library. + */ + +/* Standard includes. */ +#include +#include +#include + +/* Include API header. */ +#include "core_sntp_serializer.h" + +#include "core_sntp_config_defaults.h" + +/** + * @brief The version of SNTP supported by the coreSNTP library by complying + * with the SNTPv4 specification defined in [RFC 4330](https://tools.ietf.org/html/rfc4330). + */ +#define SNTP_VERSION ( 4U ) + +/** + * @brief The bit mask for the "Mode" information in the first byte of an SNTP packet. + * The "Mode" field occupies bits 0-2 of the byte. + * @note Refer to the [RFC 4330 Section 4](https://tools.ietf.org/html/rfc4330#section-4) + * for more information. + */ +#define SNTP_MODE_BITS_MASK ( 0x07U ) + +/** + * @brief The value indicating a "client" in the "Mode" field of an SNTP packet. + * @note Refer to the [RFC 4330 Section 4](https://tools.ietf.org/html/rfc4330#section-4) + * for more information. + */ +#define SNTP_MODE_CLIENT ( 3U ) + +/** + * @brief The value indicating a "server" in the "Mode" field of an SNTP packet. + * @note Refer to the [RFC 4330 Section 4](https://tools.ietf.org/html/rfc4330#section-4) + * for more information. + */ +#define SNTP_MODE_SERVER ( 4U ) + +/** + * @brief The position of the least significant bit of the "Leap Indicator" field + * in first byte of an SNTP packet. The "Leap Indicator" field occupies bits 6-7 of the byte. + * @note Refer to the [RFC 4330 Section 4](https://tools.ietf.org/html/rfc4330#section-4) + * for more information. + */ +#define SNTP_LEAP_INDICATOR_LSB_POSITION ( 6 ) + +/** + * @brief Value of Stratum field in SNTP packet representing a Kiss-o'-Death message + * from server. + */ +#define SNTP_KISS_OF_DEATH_STRATUM ( 0U ) + +/** + * @brief The position of least significant bit of the "Version" information + * in the first byte of an SNTP packet. "Version" field occupies bits 3-5 of + * the byte. + * @note Refer to the [RFC 4330 Section 4](https://tools.ietf.org/html/rfc4330#section-4) + * for more information. + */ +#define SNTP_VERSION_LSB_POSITION ( 3 ) + +/** + * @brief The integer value of the Kiss-o'-Death ASCII code, "DENY", used + * for comparison with data in an SNTP response. + * @note Refer to [RFC 4330 Section 8](https://tools.ietf.org/html/rfc4330#section-8) + * for more information. + */ +#define KOD_CODE_DENY_UINT_VALUE ( 0x44454e59U ) + +/** + * @brief The integer value of the Kiss-o'-Death ASCII code, "RSTR", used + * for comparison with data in an SNTP response. + * @note Refer to [RFC 4330 Section 8](https://tools.ietf.org/html/rfc4330#section-8) + * for more information. + */ +#define KOD_CODE_RSTR_UINT_VALUE ( 0x52535452U ) + +/** + * @brief The integer value of the Kiss-o'-Death ASCII code, "RATE", used + * for comparison with data in an SNTP response. + * @note Refer to [RFC 4330 Section 8](https://tools.ietf.org/html/rfc4330#section-8) + * for more information. + */ +#define KOD_CODE_RATE_UINT_VALUE ( 0x52415445U ) + +/** + * @brief Macro to represent exactly half of the total number of seconds in an NTP era. + * As 32 bits are used to represent the "seconds" part of an SNTP timestamp, the half of + * the total range of seconds in an NTP era is 2^31 seconds, which represents ~68 years. + * + * @note This macro represents the edge case of calculating the client system clock-offset + * relative to server time as the library ASSUMES that the client and server times are within + * 2^31 seconds apart in duration. This is done to support calculating clock-offset for the + * cases when server and client systems are in adjacent NTP eras, which can occur when NTP time + * wraps around in 2036, and the relative NTP era presence of client and server times is + * determined based on comparing first order difference values between all possible NTP era + * configurations of the systems. + */ +#define CLOCK_OFFSET_MAX_TIME_DIFFERENCE ( ( ( ( int64_t ) INT32_MAX + 1 ) * 1000 ) ) + +/** + * @brief Macro to represent the total number of milliseconds that are represented in an + * NTP era period. This macro represents a duration of ~136 years. + * + * @note Rationale for calculation: The "seconds" part of an NTP timestamp is represented in + * unsigned 32 bit width, thus, the total range of seconds it represents is 2^32, + * i.e. (UINT32_MAX + 1). + */ +#define TOTAL_MILLISECONDS_IN_NTP_ERA ( ( ( int64_t ) UINT32_MAX + 1 ) * 1000 ) + +/** + * @brief Structure representing an SNTP packet header. + * For more information on SNTP packet format, refer to + * [RFC 4330 Section 4](https://tools.ietf.org/html/rfc4330#section-4). + * + * @note This does not include extension fields for authentication data + * for secure SNTP communication. Authentication data follows the + * packet header represented by this structure. + */ +typedef struct SntpPacket +{ + /** + * @brief Bits 6-7 leap indicator, bits 3-5 are version number, bits 0-2 are mode + */ + uint8_t leapVersionMode; + + /** + * @brief stratum + */ + uint8_t stratum; + + /** + * @brief poll interval + */ + uint8_t poll; + + /** + * @brief precision + */ + uint8_t precision; + + /** + * @brief root delay + */ + uint32_t rootDelay; + + /** + * @brief root dispersion + */ + uint32_t rootDispersion; + + /** + * @brief reference ID + */ + uint32_t refId; + + /** + * @brief reference time + */ + SntpTimestamp_t refTime; + + /** + * @brief origin timestamp + */ + SntpTimestamp_t originTime; + + /** + * @brief receive timestamp + */ + SntpTimestamp_t receiveTime; + + /** + * @brief transmit timestamp + */ + SntpTimestamp_t transmitTime; +} SntpPacket_t; + +/** + * @brief Utility macro to fill 32-bit integer in word-sized + * memory in network byte (or Big Endian) order. + * + * @param[out] pWordMemory Pointer to the word-sized memory in which + * the 32-bit integer will be filled. + * @param[in] data The 32-bit integer to fill in the @p wordMemory + * in network byte order. + * + * @note This utility ensures that data is filled in memory + * in expected network byte order, as an assignment operation + * (like *pWordMemory = word) can cause undesired side-effect + * of network-byte ordering getting reversed on Little Endian platforms. + */ +static void fillWordMemoryInNetworkOrder( uint32_t * pWordMemory, + uint32_t data ) +{ + uint8_t * pByteMemory = ( uint8_t * ) pWordMemory; + + assert( pWordMemory != NULL ); + + pByteMemory[ 0 ] = ( uint8_t ) ( data >> 24 ); + pByteMemory[ 1 ] = ( uint8_t ) ( ( data >> 16 ) & 0x000000FFU ); + pByteMemory[ 2 ] = ( uint8_t ) ( ( data >> 8 ) & 0x000000FFU ); + pByteMemory[ 3 ] = ( uint8_t ) ( ( data ) & 0x000000FFU ); +} + +/** + * @brief Utility macro to generate a 32-bit integer from memory containing + * integer in network (or Big Endian) byte order. + * @param[in] ptr Pointer to the memory containing 32-bit integer in network + * byte order. + * + * @return The host representation of the 32-bit integer in the passed word + * memory. + */ +static uint32_t readWordFromNetworkByteOrderMemory( const uint32_t * ptr ) +{ + const uint8_t * pMemStartByte = ( const uint8_t * ) ptr; + + assert( ptr != NULL ); + + return ( uint32_t ) ( ( ( ( uint32_t ) pMemStartByte[ 0 ] ) << 24 ) | + ( 0x00FF0000U & ( ( ( uint32_t ) pMemStartByte[ 1 ] ) << 16 ) ) | + ( 0x0000FF00U & ( ( ( uint32_t ) pMemStartByte[ 2 ] ) << 8 ) ) | + ( ( uint32_t ) pMemStartByte[ 3 ] ) ); +} + +/** + * @brief Utility to return absolute (or positively signed) value of an signed + * 64 bit integer. + * + * @param[in] value The integer to return the absolute value of. + * + * @return The absolute value of @p value. + */ +static int64_t absoluteOf( int64_t value ) +{ + return ( value >= 0 ) ? value : ( ( int64_t ) 0 - value ); +} + +/** + * @brief Utility to determine whether a timestamp represents a zero + * timestamp value. + * + * @note This utility is used to determine whether a timestamp value is + * invalid. According to the SNTPv4 specification, a zero timestamp value + * is considered invalid. + * + * @param[in] pTime The timestamp whose value is to be inspected for + * zero value. + * + * @return `true` if the timestamp is zero; otherwise `false`. + */ +static bool isZeroTimestamp( const SntpTimestamp_t * pTime ) +{ + bool isSecondsZero = ( pTime->seconds == 0U ) ? true : false; + bool isFractionsZero = ( pTime->fractions == 0U ) ? true : false; + + return( isSecondsZero && isFractionsZero ); +} + +/** + * @brief Utility to convert the "fractions" part of an SNTP timestamp to milliseconds + * duration of time. + * @param[in] fractions The fractions value. + * + * @return The milliseconds equivalent of the @p fractions value. + */ +static uint32_t fractionsToMs( uint32_t fractions ) +{ + return( fractions / ( 1000U * SNTP_FRACTION_VALUE_PER_MICROSECOND ) ); +} + +/** + * @brief Utility to safely calculate difference between server and client timestamps and + * return the difference in the resolution of milliseconds as a signed 64 bit integer. + * The calculated value represents the effective subtraction as ( @p serverTimeSec - @p clientTimeSec ). + * + * @note This utility SUPPORTS the cases of server and client timestamps being in different NTP eras, + * and ASSUMES that the server and client systems are within 68 years of each other. + * To handle the case of different NTP eras, this function calculates difference values for all + * possible combinations of NTP eras of server and client times (i.e. 1. both timestamps in same era, + * 2. server timestamp one era ahead, and 3. client timestamp being one era ahead), and determines + * the NTP era configuration by choosing the difference value of the smallest absolute value. + * + * @param[in] pServerTime The server timestamp. + * @param[in] pClientTime The client timestamp. + * + * @return The calculated difference between server and client times as a signed 64 bit integer. + */ +static int64_t safeTimeDifference( const SntpTimestamp_t * pServerTime, + const SntpTimestamp_t * pClientTime ) +{ + int64_t eraAdjustedDiff = 0; + + /* Convert the timestamps into 64 bit signed integer values of milliseconds. */ + int64_t serverTime = ( ( int64_t ) pServerTime->seconds * 1000 ) + ( int64_t ) fractionsToMs( pServerTime->fractions ); + int64_t clientTime = ( ( int64_t ) pClientTime->seconds * 1000 ) + ( int64_t ) fractionsToMs( pClientTime->fractions ); + + /* The difference between the 2 timestamps is calculated by determining the whether the timestamps + * are present in the same NTP era or adjacent NTP eras (i.e. the NTP timestamp overflow case). */ + + /* First, calculate the first order time difference assuming that server and client times + * are in the same NTP era. */ + int64_t diffWithNoEraAdjustment = serverTime - clientTime; + + /* Store the absolute value of the time difference which will be used for comparison with + * different cases of relative NTP era configuration of client and server times. */ + int64_t absSameEraDiff = absoluteOf( diffWithNoEraAdjustment ); + + /* If the absolute difference value is 2^31 seconds, it means that the server and client times are + * away by exactly half the range of SNTP timestamp "second" values representable in unsigned 32 bits. + * In such a case, irrespective of whether the 2 systems exist in the same or adjacent NTP eras, the + * time difference calculated between the systems will ALWAYS yield the same value when viewed from + * all NTP era configurations of the times. + * For such a case, we will ASSUME that the server time is AHEAD of client time, and thus, generate + * a positive clock-offset value. + */ + if( absSameEraDiff == CLOCK_OFFSET_MAX_TIME_DIFFERENCE ) + { + /* It does not matter whether server and client are in the same era for this + * special case as the difference value for both same and adjacent eras will yield + * the same absolute value of 2^31.*/ + eraAdjustedDiff = CLOCK_OFFSET_MAX_TIME_DIFFERENCE; + } + else + { + /* Determine if server time belongs to an NTP era different than the client time, and accordingly + * choose the 64 bit representation of the first order difference to account for the era. + * The logic for determining the relative era presence of the timestamps is by calculating the + * first order difference (of "Server Time - Client Time") for all the 3 different era combinations + * (1. both timestamps in same era, 2. server time one era ahead, 3. client time one era ahead) + * and choosing the NTP era configuration that has the smallest first order difference value. + */ + int64_t diffWithServerEraAdjustment = serverTime + TOTAL_MILLISECONDS_IN_NTP_ERA - + clientTime; /* This helps determine whether server is an + * era ahead of client time. */ + int64_t diffWithClientEraAdjustment = serverTime - + ( TOTAL_MILLISECONDS_IN_NTP_ERA + clientTime ); /* This helps determine whether server is an + * era behind of client time. */ + + /* Store the absolute value equivalents of all the time difference configurations + * for easier comparison to smallest value from them. */ + int64_t absServerEraAheadDiff = absoluteOf( diffWithServerEraAdjustment ); + int64_t absClientEraAheadDiff = absoluteOf( diffWithClientEraAdjustment ); + + /* Determine the correct relative era of client and server times by checking which era + * configuration of difference value represents the least difference. */ + if( ( absSameEraDiff <= absServerEraAheadDiff ) && ( absSameEraDiff <= absClientEraAheadDiff ) ) + { + /* Both server and client times are in the same era. */ + eraAdjustedDiff = diffWithNoEraAdjustment; + } + /* Check if server time is an NTP era ahead of client time. */ + else if( absServerEraAheadDiff < absSameEraDiff ) + { + /* Server time is in NTP era 1 while client time is in NTP era 0. */ + eraAdjustedDiff = diffWithServerEraAdjustment; + } + /* Now, we know that the client time is an era ahead of server time. */ + else + { + /* Server time is in NTP era 0 while client time is in NTP era 1. */ + eraAdjustedDiff = diffWithClientEraAdjustment; + } + } + + return eraAdjustedDiff; +} + +/** + * @brief Utility to calculate clock offset of system relative to the + * server using the on-wire protocol specified in the NTPv4 specification. + * For more information on on-wire protocol, refer to + * [RFC 5905 Section 8](https://tools.ietf.org/html/rfc5905#section-8). + * + * @note The following diagram explains the calculation of the clock + * offset: + * + * T2 T3 + * --------------------------------- <----- *SNTP/NTP server* + * /\ \ + * / \ + * Request* / \ *Response* + * / \/ + * --------------------------------- <----- *SNTP client* + * T1 T4 + * + * The four most recent timestamps, T1 through T4, are used to compute + * the clock offset of SNTP client relative to the server where: + * + * T1 = Client Request Transmit Time + * T2 = Server Receive Time (of client request) + * T3 = Server Response Transmit Time + * T4 = Client Receive Time (of server response) + * + * Clock Offset = T(NTP/SNTP server) - T(SNTP client) + * = [( T2 - T1 ) + ( T3 - T4 )] + * --------------------------- + * 2 + * + * @note Both NTPv4 and SNTPv4 specifications suggest calculating the + * clock offset value, if possible. As the timestamp format uses 64 bit + * integer and there exist 2 orders of arithmetic calculations on the + * timestamp values (subtraction followed by addition as shown in the + * diagram above), the clock offset for the system can be calculated + * ONLY if the value can be represented in 62 significant bits and 2 sign + * bits i.e. if the system clock is within 34 years (in the future or past) + * of the server time. + * + * @param[in] pClientTxTime The system time of sending the SNTP request. + * This is the same as "T1" in the above diagram. + * @param[in] pServerRxTime The server time of receiving the SNTP request + * packet from the client. This is the same as "T2" in the above diagram. + * @param[in] pServerTxTime The server time of sending the SNTP response + * packet. This is the same as "T3" in the above diagram. + * @param[in] pClientRxTime The system time of receiving the SNTP response + * from the server. This is the same as "T4" in the above diagram. + * @param[out] pClockOffset This will be filled with the calculated offset value + * of the system clock relative to the server time with the assumption that the + * system clock is within 68 years of server time. + */ +static void calculateClockOffset( const SntpTimestamp_t * pClientTxTime, + const SntpTimestamp_t * pServerRxTime, + const SntpTimestamp_t * pServerTxTime, + const SntpTimestamp_t * pClientRxTime, + int64_t * pClockOffset ) +{ + /* Variable for storing the first-order difference between timestamps. */ + int64_t firstOrderDiffSend = 0; + int64_t firstOrderDiffRecv = 0; + + assert( pClientTxTime != NULL ); + assert( pServerRxTime != NULL ); + assert( pServerTxTime != NULL ); + assert( pClientRxTime != NULL ); + assert( pClockOffset != NULL ); + + /* Perform first order difference of timestamps on the network send path i.e. T2 - T1. + * Note: The calculated difference value will always represent years in the range of + *[-68 years, +68 years]. */ + firstOrderDiffSend = safeTimeDifference( pServerRxTime, pClientTxTime ); + + /* Perform first order difference of timestamps on the network receive path i.e. T3 - T4. + * Note: The calculated difference value will always represent years in the range of + *[-68 years, +68 years]. */ + firstOrderDiffRecv = safeTimeDifference( pServerTxTime, pClientRxTime ); + + /* Now calculate the system clock-offset relative to server time as the average of the + * first order difference of timestamps in both directions of network path. + * Note: This will ALWAYS represent offset in the range of [-68 years, +68 years]. */ + *pClockOffset = ( firstOrderDiffSend + firstOrderDiffRecv ) / 2; +} + +/** + * @brief Parse a SNTP response packet by determining whether it is a rejected + * or accepted response to an SNTP request, and accordingly, populate the + * @p pParsedResponse parameter with the parsed data. + * + * @note If the server has rejected the request with the a Kiss-o'-Death message, + * then this function will set the associated rejection code in the output parameter + * while setting the remaining members to zero. + * If the server has accepted the time request, then the function will set the + * rejectedResponseCode member of the output parameter to #SNTP_KISS_OF_DEATH_CODE_NONE, + * and set the other the members with appropriate data extracted from the response + * packet. + * + * @param[in] pResponsePacket The SNTP response packet from server to parse. + * @param[in] pRequestTxTime The system time (in SNTP timestamp format) of + * sending the SNTP request to server. + * @param[in] pResponseRxTime The system time (in SNTP timestamp format) of + * receiving the SNTP response from server. + * @param[out] pParsedResponse The parameter that will be populated with data + * parsed from the response packet, @p pResponsePacket. + * + * @return This function returns one of the following: + * - #SntpSuccess if the server response does not represent a Kiss-o'-Death + * message. + * - #SntpRejectedResponseChangeServer if the server rejected with a code + * indicating that client cannot be retry requests to it. + * - #SntpRejectedResponseRetryWithBackoff if the server rejected with a code + * indicating that client should back-off before retrying request. + * - #SntpRejectedResponseOtherCode if the server rejected with a code + * other than "DENY", "RSTR" and "RATE". + */ +static SntpStatus_t parseValidSntpResponse( const SntpPacket_t * pResponsePacket, + const SntpTimestamp_t * pRequestTxTime, + const SntpTimestamp_t * pResponseRxTime, + SntpResponseData_t * pParsedResponse ) +{ + SntpStatus_t status = SntpSuccess; + + assert( pResponsePacket != NULL ); + assert( pResponseRxTime != NULL ); + assert( pParsedResponse != NULL ); + + /* Clear the output parameter memory to zero. */ + ( void ) memset( pParsedResponse, 0, sizeof( *pParsedResponse ) ); + + /* Determine if the server has accepted or rejected the request for time. */ + if( pResponsePacket->stratum == SNTP_KISS_OF_DEATH_STRATUM ) + { + /* Server has sent a Kiss-o'-Death message i.e. rejected the request. */ + + /* Extract the kiss-code sent by the server from the "Reference ID" field + * of the SNTP packet. */ + pParsedResponse->rejectedResponseCode = + readWordFromNetworkByteOrderMemory( &pResponsePacket->refId ); + + /* Determine the return code based on the Kiss-o'-Death code. */ + switch( pParsedResponse->rejectedResponseCode ) + { + case KOD_CODE_DENY_UINT_VALUE: + case KOD_CODE_RSTR_UINT_VALUE: + status = SntpRejectedResponseChangeServer; + break; + + case KOD_CODE_RATE_UINT_VALUE: + status = SntpRejectedResponseRetryWithBackoff; + break; + + default: + status = SntpRejectedResponseOtherCode; + break; + } + } + else + { + /* Server has responded successfully to the time request. */ + + SntpTimestamp_t serverRxTime; + + /* Map of integer value to SntpLeapSecondInfo_t enumeration type for the + * "Leap Indicator" field in the first byte of an SNTP packet. + * Note: This map is used to not violate MISRA Rule 10.5 when directly + * converting an integer to enumeration type. + */ + const SntpLeapSecondInfo_t leapIndicatorTypeMap[] = + { + NoLeapSecond, + LastMinuteHas61Seconds, + LastMinuteHas59Seconds, + AlarmServerNotSynchronized + }; + + /* Set the Kiss-o'-Death code value to NULL as server has responded favorably + * to the time request. */ + pParsedResponse->rejectedResponseCode = SNTP_KISS_OF_DEATH_CODE_NONE; + + /* Fill the output parameter with the server time which is the + * "transmit" time in the response packet. */ + pParsedResponse->serverTime.seconds = + readWordFromNetworkByteOrderMemory( &pResponsePacket->transmitTime.seconds ); + pParsedResponse->serverTime.fractions = + readWordFromNetworkByteOrderMemory( &pResponsePacket->transmitTime.fractions ); + + /* Extract information of any upcoming leap second from the response. */ + pParsedResponse->leapSecondType = leapIndicatorTypeMap[ + ( pResponsePacket->leapVersionMode >> SNTP_LEAP_INDICATOR_LSB_POSITION ) ]; + + /* Store the "receive" time in SNTP response packet in host order. */ + serverRxTime.seconds = + readWordFromNetworkByteOrderMemory( &pResponsePacket->receiveTime.seconds ); + serverRxTime.fractions = + readWordFromNetworkByteOrderMemory( &pResponsePacket->receiveTime.fractions ); + + /* Calculate system clock offset relative to server time, if possible, within + * the 64 bit integer width of the SNTP timestamp. */ + calculateClockOffset( pRequestTxTime, + &serverRxTime, + &pParsedResponse->serverTime, + pResponseRxTime, + &pParsedResponse->clockOffsetMs ); + } + + return status; +} + + +SntpStatus_t Sntp_SerializeRequest( SntpTimestamp_t * pRequestTime, + uint32_t randomNumber, + void * pBuffer, + size_t bufferSize ) +{ + SntpStatus_t status = SntpSuccess; + + if( pRequestTime == NULL ) + { + status = SntpErrorBadParameter; + } + else if( pBuffer == NULL ) + { + status = SntpErrorBadParameter; + } + else if( bufferSize < SNTP_PACKET_BASE_SIZE ) + { + status = SntpErrorBufferTooSmall; + } + + /* Zero timestamps for client request time is not allowed to protect against + * attack spoofing server response containing zero value for "originate timestamp". + * Note: In SNTP/NTP communication, the "originate timestamp" of a valid server response + * matches the "transmit timestamp" in corresponding client request packet. */ + else if( isZeroTimestamp( pRequestTime ) == true ) + { + status = SntpErrorBadParameter; + } + else + { + /* MISRA Ref 11.5.1 [Void pointer assignment] */ + /* More details at: https://github.com/FreeRTOS/coreSNTP/blob/main/MISRA.md#rule-115 */ + /* coverity[misra_c_2012_rule_11_5_violation] */ + SntpPacket_t * pRequestPacket = ( SntpPacket_t * ) pBuffer; + + /* Fill the buffer with zero as most fields are zero for a standard SNTP + * request packet.*/ + ( void ) memset( pBuffer, 0, sizeof( SntpPacket_t ) ); + + /* Set the first byte of the request packet for "Version" and "Mode" fields */ + pRequestPacket->leapVersionMode = 0U /* Leap Indicator */ | + ( SNTP_VERSION << SNTP_VERSION_LSB_POSITION ) /* Version Number */ | + SNTP_MODE_CLIENT /* Mode */; + + + /* Add passed random number to non-significant bits of the fractions part + * of the transmit timestamp. + * This is suggested by the SNTPv4 (and NTPv4) specification(s) + * to protect against replay attacks. Refer to RFC 4330 Section 3 for + * more information. + * Adding random bits to the least significant 16 bits of the fractions + * part of the timestamp affects only ~15 microseconds of information + * (calculated as 0xFFFF * 232 picoseconds). + */ + pRequestTime->fractions = ( pRequestTime->fractions + | ( randomNumber >> 16 ) ); + + /* Update the request buffer with request timestamp in network byte order. */ + fillWordMemoryInNetworkOrder( &pRequestPacket->transmitTime.seconds, + pRequestTime->seconds ); + fillWordMemoryInNetworkOrder( &pRequestPacket->transmitTime.fractions, + pRequestTime->fractions ); + } + + return status; +} + + +SntpStatus_t Sntp_DeserializeResponse( const SntpTimestamp_t * pRequestTime, + const SntpTimestamp_t * pResponseRxTime, + const void * pResponseBuffer, + size_t bufferSize, + SntpResponseData_t * pParsedResponse ) +{ + SntpStatus_t status = SntpSuccess; + /* MISRA Ref 11.5.1 [Void pointer assignment] */ + /* More details at: https://github.com/FreeRTOS/coreSNTP/blob/main/MISRA.md#rule-115 */ + /* coverity[misra_c_2012_rule_11_5_violation] */ + const SntpPacket_t * pResponsePacket = ( const SntpPacket_t * ) pResponseBuffer; + + if( ( pRequestTime == NULL ) || ( pResponseRxTime == NULL ) || + ( pResponseBuffer == NULL ) || ( pParsedResponse == NULL ) ) + { + status = SntpErrorBadParameter; + } + else if( bufferSize < SNTP_PACKET_BASE_SIZE ) + { + status = SntpErrorBufferTooSmall; + } + + /* Zero timestamps for client request time is not allowed to protect against + * attack spoofing server response containing zero value for "originate timestamp". + * Note: In SNTP/NTP communication, the "originate timestamp" of a valid server response + * matches the "transmit timestamp" in corresponding client request packet. */ + else if( isZeroTimestamp( pRequestTime ) == true ) + { + status = SntpErrorBadParameter; + } + /* Check if the packet represents a server in the "Mode" field. */ + else if( ( pResponsePacket->leapVersionMode & SNTP_MODE_BITS_MASK ) != SNTP_MODE_SERVER ) + { + status = SntpInvalidResponse; + } + + /* Check if any of the timestamps in the response packet are zero, which is invalid. + * Note: This is done to protect against a nuanced server spoofing attack where if the + * SNTP client resets its internal state of "Client transmit timestamp" (OR "originate + * timestamp" from server perspective) to zero as a protection against replay attack, an + * an attacker with this knowledge of the client operation can spoof a server response + * containing the "originate timestamp" as zero. Thus, to protect against such attack, + * a server response packet with any zero timestamp is rejected. */ + else if( ( isZeroTimestamp( &pResponsePacket->originTime ) == true ) || + ( isZeroTimestamp( &pResponsePacket->receiveTime ) == true ) || + ( isZeroTimestamp( &pResponsePacket->transmitTime ) == true ) ) + { + status = SntpInvalidResponse; + } + + + /* Validate that the server has sent the client's request timestamp in the + * "originate" timestamp field of the response. */ + else if( ( pRequestTime->seconds != + readWordFromNetworkByteOrderMemory( &pResponsePacket->originTime.seconds ) ) || + ( pRequestTime->fractions != + readWordFromNetworkByteOrderMemory( &pResponsePacket->originTime.fractions ) ) ) + + { + status = SntpInvalidResponse; + } + else + { + /* As the response packet is valid, parse more information from it and + * populate the output parameter. */ + + status = parseValidSntpResponse( pResponsePacket, + pRequestTime, + pResponseRxTime, + pParsedResponse ); + } + + return status; +} + +SntpStatus_t Sntp_CalculatePollInterval( uint16_t clockFreqTolerance, + uint16_t desiredAccuracy, + uint32_t * pPollInterval ) +{ + SntpStatus_t status = SntpSuccess; + + if( ( clockFreqTolerance == 0U ) || ( desiredAccuracy == 0U ) || ( pPollInterval == NULL ) ) + { + status = SntpErrorBadParameter; + } + else + { + uint32_t exactIntervalForAccuracy = 0U; + uint8_t log2PollInterval = 0U; + + /* Calculate the poll interval required for achieving the exact desired clock accuracy + * with the following formulae: + * + * System Clock Drift Rate ( microseconds / second ) = Clock Frequency Tolerance (in PPM ) + * Maximum Clock Drift ( milliseconds ) = Desired Accuracy ( milliseconds ) + * + * Poll Interval ( seconds ) = Maximum Clock Drift + * --------------------------- + * System Clock Drift Rate + * + * = Maximum Drift ( milliseconds ) * 1000 ( microseconds / millisecond ) + * ------------------------------------------------------------------------ + * System Clock Drift Rate ( microseconds / second ) + * + * = Desired Accuracy * 1000 + * ------------------------------ + * Clock Frequency Tolerance + */ + exactIntervalForAccuracy = ( ( uint32_t ) desiredAccuracy * 1000U ) / clockFreqTolerance; + + /* Check if calculated poll interval value falls in the supported range of seconds. */ + if( exactIntervalForAccuracy == 0U ) + { + /* Poll interval value is less than 1 second, and is not supported by the function. */ + status = SntpZeroPollInterval; + } + else + { + /* To calculate the log 2 value of the exact poll interval value, first determine the highest + * bit set in the value. */ + while( exactIntervalForAccuracy != 0U ) + { + log2PollInterval++; + exactIntervalForAccuracy /= 2U; + } + + /* Convert the highest bit in the exact poll interval value to the nearest integer + * value lower or equal to the log2 of the exact poll interval value. */ + log2PollInterval--; + + /* Calculate the poll interval as the closest exponent of 2 value that achieves + * equal or higher accuracy than the desired accuracy. */ + *pPollInterval = ( ( ( uint32_t ) 1U ) << log2PollInterval ); + } + } + + return status; +} + +SntpStatus_t Sntp_ConvertToUnixTime( const SntpTimestamp_t * pSntpTime, + uint32_t * pUnixTimeSecs, + uint32_t * pUnixTimeMicrosecs ) +{ + SntpStatus_t status = SntpSuccess; + + if( ( pSntpTime == NULL ) || ( pUnixTimeSecs == NULL ) || ( pUnixTimeMicrosecs == NULL ) ) + { + status = SntpErrorBadParameter; + } + + if( status == SntpSuccess ) + { + /* Handle case when SNTP timestamp is in SNTP era 0 time range + * (i.e. time before 7 Feb 2036 6:28:15 UTC). */ + if( pSntpTime->seconds >= SNTP_TIME_AT_UNIX_EPOCH_SECS ) + { + *pUnixTimeSecs = pSntpTime->seconds - SNTP_TIME_AT_UNIX_EPOCH_SECS; + } + else + { + /* Handle case when timestamp represents date in SNTP era 1 + * (i.e. time from 7 Feb 2036 6:28:16 UTC onwards). */ + *pUnixTimeSecs = UNIX_TIME_SECS_AT_SNTP_ERA_1_SMALLEST_TIME + pSntpTime->seconds; + } + + /* Convert SNTP fractions to microseconds for UNIX time. */ + *pUnixTimeMicrosecs = pSntpTime->fractions / SNTP_FRACTION_VALUE_PER_MICROSECOND; + } + + return status; +} diff --git a/Lib/coreSNTP/include/core_sntp_config.h b/Lib/coreSNTP/include/core_sntp_config.h new file mode 100644 index 00000000..52905328 --- /dev/null +++ b/Lib/coreSNTP/include/core_sntp_config.h @@ -0,0 +1,24 @@ +/* + * 模块名称:coreSNTP 定制配置(本仓 glue 文件,非 vendor 原文) + * 模块功能:core_sntp_config_defaults.h 在**未**定义 SNTP_DO_NOT_USE_CUSTOM_CONFIG + * 时会包含本文件;本仓使用 vendor 库的 serializer 层,默认值即所需 + * ——日志宏 LogError/LogWarn/LogInfo/LogDebug 保持 defaults 的编译期 + * 空实现:serializer 的失败路径都已有应用层 DBG 判读(time_sntp.c + * 在调用点按返回码记录),库内再开日志只增栈/Flash 不增信息量。 + * 错误/丢弃类留痕统一放调用方(App/sync/time_sntp.c), + * 避免 vendor 文件与本仓日志栈耦合。 + * 适用平台:STM32F407ZGTx / 主机单测(test/host/sntp) + * 作者:王建锋 + * 创建日期:2026-11-04 + * 修改记录: + * v1.0 2026-11-04 王建锋 创建初始版本(SNTP 客户端接入 #57) + */ + +#ifndef CORE_SNTP_CONFIG_H +#define CORE_SNTP_CONFIG_H + +/* 本文件刻意不覆盖任何默认宏;如将来需要库内日志,在此映射: + * #define LogError(message) ... + * (注意 vendor 调用形态为 LogError(("..."))——参数自带外括号) */ + +#endif /* CORE_SNTP_CONFIG_H */ diff --git a/Lib/coreSNTP/include/core_sntp_config_defaults.h b/Lib/coreSNTP/include/core_sntp_config_defaults.h new file mode 100644 index 00000000..62d3364d --- /dev/null +++ b/Lib/coreSNTP/include/core_sntp_config_defaults.h @@ -0,0 +1,144 @@ +/* + * coreSNTP + * Copyright (C) 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * @file core_sntp_config_defaults.h + * @brief This file represents the default values for the configuration macros + * of the coreSNTP library. + * + * @note This file SHOULD NOT be modified. If custom values are needed for + * any configuration macro, a core_sntp_config.h file should be provided to + * the SNTP library to override the default values defined in this file. + * To build the library with the core_sntp_config.h file, make sure to + * not set the SNTP_DO_NOT_USE_CUSTOM_CONFIG preprocessor macro. + */ + +#ifndef CORE_SNTP_CONFIG_DEFAULTS_H_ +#define CORE_SNTP_CONFIG_DEFAULTS_H_ + +/* The macro definition for SNTP_DO_NOT_USE_CUSTOM_CONFIG is for Doxygen + * documentation only. */ + +/** + * @brief Define this macro to build the SNTP library without the custom config + * file core_sntp_config.h. + * + * Without the custom config, the SNTP library builds with + * default values of config macros defined in core_sntp_config_defaults.h file. + * + * If a custom config is provided, then SNTP_DO_NOT_USE_CUSTOM_CONFIG should not + * be defined. + */ +#ifdef DOXYGEN + #define SNTP_DO_NOT_USE_CUSTOM_CONFIG +#endif + +/* SNTP_DO_NOT_USE_CUSTOM_CONFIG allows building the SNTP library + * without a custom config. If a custom config is provided, the + * SNTP_DO_NOT_USE_CUSTOM_CONFIG macro should not be defined. */ +#ifndef SNTP_DO_NOT_USE_CUSTOM_CONFIG + #include "core_sntp_config.h" +#endif + +/** + * @brief Macro that is called in the SNTP library for logging "Error" level + * messages. + * + * To enable error level logging in the SNTP library, this macro should be mapped to the + * application-specific logging implementation that supports error logging. + * + * @note This logging macro is called in the SNTP library with parameters wrapped in + * double parentheses to be ISO C89/C90 standard compliant. For a reference + * POSIX implementation of the logging macros, refer to core_sntp_config.h files, and the + * logging-stack in demos folder of the + * [AWS IoT Embedded C SDK repository](https://github.com/aws/aws-iot-device-sdk-embedded-C). + * + * Default value: Error logging is turned off, and no code is generated for calls + * to the macro in the SNTP library on compilation. + */ +#ifndef LogError + #define LogError( message ) +#endif + +/** + * @brief Macro that is called in the SNTP library for logging "Warning" level + * messages. + * + * To enable warning level logging in the SNTP library, this macro should be mapped to the + * application-specific logging implementation that supports warning logging. + * + * @note This logging macro is called in the SNTP library with parameters wrapped in + * double parentheses to be ISO C89/C90 standard compliant. For a reference + * POSIX implementation of the logging macros, refer to core_sntp_config.h files, and the + * logging-stack in demos folder of the + * [AWS IoT Embedded C SDK repository](https://github.com/aws/aws-iot-device-sdk-embedded-C/). + * + * Default value: Warning logs are turned off, and no code is generated for calls + * to the macro in the SNTP library on compilation. + */ +#ifndef LogWarn + #define LogWarn( message ) +#endif + +/** + * @brief Macro that is called in the SNTP library for logging "Info" level + * messages. + * + * To enable info level logging in the SNTP library, this macro should be mapped to the + * application-specific logging implementation that supports info logging. + * + * @note This logging macro is called in the SNTP library with parameters wrapped in + * double parentheses to be ISO C89/C90 standard compliant. For a reference + * POSIX implementation of the logging macros, refer to core_sntp_config.h files, and the + * logging-stack in demos folder of the + * [AWS IoT Embedded C SDK repository](https://github.com/aws/aws-iot-device-sdk-embedded-C/). + * + * Default value: Info logging is turned off, and no code is generated for calls + * to the macro in the SNTP library on compilation. + */ +#ifndef LogInfo + #define LogInfo( message ) +#endif + +/** + * @brief Macro that is called in the SNTP library for logging "Debug" level + * messages. + * + * To enable debug level logging from SNTP library, this macro should be mapped to the + * application-specific logging implementation that supports debug logging. + * + * @note This logging macro is called in the SNTP library with parameters wrapped in + * double parentheses to be ISO C89/C90 standard compliant. For a reference + * POSIX implementation of the logging macros, refer to core_sntp_config.h files, and the + * logging-stack in demos folder of the + * [AWS IoT Embedded C SDK repository](https://github.com/aws/aws-iot-device-sdk-embedded-C/). + * + * Default value: Debug logging is turned off, and no code is generated for calls + * to the macro in the SNTP library on compilation. + */ +#ifndef LogDebug + #define LogDebug( message ) +#endif + +#endif /* ifndef CORE_SNTP_CONFIG_DEFAULTS_H_ */ diff --git a/Lib/coreSNTP/include/core_sntp_serializer.h b/Lib/coreSNTP/include/core_sntp_serializer.h new file mode 100644 index 00000000..f05dccba --- /dev/null +++ b/Lib/coreSNTP/include/core_sntp_serializer.h @@ -0,0 +1,521 @@ +/* + * coreSNTP + * Copyright (C) 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * @file core_sntp_serializer.h + * @brief API for serializing SNTP request packets and, and de-serializing SNTP + * response packets. + * This API layer adheres to the SNTPv4 specification defined in + * [RFC 4330](https://tools.ietf.org/html/rfc4330). + */ + +#ifndef CORE_SNTP_SERIALIZER_H_ +#define CORE_SNTP_SERIALIZER_H_ + +/* Standard include. */ +#include +#include + +/* *INDENT-OFF* */ +#ifdef __cplusplus + extern "C" { +#endif +/* *INDENT-ON* */ + +/** + * @ingroup sntp_constants + * @brief The base packet size of request and response of the (S)NTP protocol. + * @note This is the packet size without any authentication headers for security + * mechanism. If the application uses a security mechanism for communicating with + * an (S)NTP server, it can add authentication data after the SNTP packet is + * serialized with the @ref Sntp_SerializeRequest API function. + */ +#define SNTP_PACKET_BASE_SIZE ( 48U ) + +/** + * @ingroup sntp_constants + * @brief Number of SNTP timestamp fractions in 1 microsecond. + * + * The fraction's part of an SNTP timestamp is 32-bits wide, thereby, giving a + * resolution of 2^(-32) seconds ~ 232 picoseconds. + * + * @note The application can use this value to convert microseconds part of system + * time into SNTP timestamp fractions. For example, if the microseconds + * part of system time is n microseconds, the fractions value to be used for the + * SNTP timestamp part will be n * SNTP_FRACTION_VALUE_PER_MICROSECOND. + */ +#define SNTP_FRACTION_VALUE_PER_MICROSECOND ( 4295U ) + +/** + * @ingroup sntp_constants + * @brief The seconds part of SNTP time at the UNIX epoch time, which represents + * an offset of 70 years (in seconds) between SNTP epoch and UNIX epoch time. + * SNTP uses 1st Jan 1900 UTC as the epoch time, whereas UNIX standard uses + * 1st Jan 1970 UTC as the epoch time, thereby, causing an offset of 70 years + * between them. + * + * Difference of 70 years = ((70 * 365) + 17 leap days) * 24 * 3600 seconds + * + * @note If your system follows UNIX time, the application can use this value to + * convert seconds part of a system time to seconds part of the equivalent SNTP + * time. For example, if the seconds part of system time is n seconds, the seconds + * value to be used for the SNTP timestamp will be n + SNTP_TO_UNIX_OFFSET_SECS. + */ +#define SNTP_TIME_AT_UNIX_EPOCH_SECS ( 2208988800U ) + +/** + * @ingroup sntp_constants + * @brief The seconds value of SNTP timestamp for the largest UNIX time when + * using signed 32-bit integer for seconds. + * The largest time representable with a 32-bit signed integer in UNIX time + * is 19 Jan 2038 3h 14m 7s UTC. However, as the SNTP time overflows at + * 7 Feb 2036 6h 28m 16s UTC, therefore, the SNTP time for the largest UNIX time + * represents the time duration between the 2 timestamps. + * + * SNTP Time at Largest Time Duration in the range + * Signed 32-bit UNIX time = [7 Feb 2036 6:28:16, 19 Jan 2038 3:14:07] + */ +#define SNTP_TIME_AT_LARGEST_UNIX_TIME_SECS ( 61505151U ) + +/** + * @ingroup sntp_constants + * @brief The UNIX time (in seconds) at the smallest SNTP time in era 1, + * i.e. UNIX time at 7 Feb 2036 6:28:16 UTC/ + * + * Time Duration = 7 Feb 6:28:16 UTC (SNTP Era 1 Epoch) - + * 1 Jan 1970 0:0:0 UTC (UNIX epoch) + * = 66 years, 37 days, 6 hours, 28 minutes and 16 seconds + * = ((66 * 365) + 16 leap days) * 24 * 3600) + (6 * 3600) + * + (28 * 60) + 16 + */ +#define UNIX_TIME_SECS_AT_SNTP_ERA_1_SMALLEST_TIME ( 2085978496U ) + +/** + * @ingroup sntp_constants + * @brief The fixed-length of any Kiss-o'-Death message ASCII code sent + * in an SNTP server response. + * @note An SNTP server sends a Kiss-o'-Death message to reject a time request + * from the client. For more information on the Kiss-o'-Death codes, refer to the + * [SNTPv4 specification Section 8](https://tools.ietf.org/html/rfc4330#section-8). + */ +#define SNTP_KISS_OF_DEATH_CODE_LENGTH ( 4U ) + +/** + * @ingroup sntp_constants + * @brief The value for the #SntpResponseData_t.rejectedResponseCode member + * when that the server response packet does not contain a Kiss-o'-Death + * message, and therefore, does not have a "kiss code". + * The server sends a "kiss-code" only when it rejects an SNTP request + * with a Kiss-o'-Death message. + */ +#define SNTP_KISS_OF_DEATH_CODE_NONE ( 0U ) + +/** + * @ingroup sntp_enum_types + * @brief Enumeration of status codes that can be returned + * by the coreSNTP Library API. + */ +typedef enum SntpStatus +{ + /** + * @brief Successful operation of an SNTP API. + */ + SntpSuccess, + + /** + * @brief Invalid parameter passed to an API function. + */ + SntpErrorBadParameter, + + /** + * @brief Server sent a Kiss-o'-Death message to reject the request for time. + * This status can be returned by the @ref Sntp_ReceiveTimeResponse API. + */ + SntpRejectedResponse, + + /** + * @brief Server sent a Kiss-o'-Death message with non-retryable code (i.e. DENY or RSTR). + */ + SntpRejectedResponseChangeServer, + + /** + * @brief Server sent a Kiss-o'-Death message with a RATE code, which means that + * client should back-off before retrying. + */ + SntpRejectedResponseRetryWithBackoff, + + /** + * @brief Server sent a Kiss-o'-Death message with a code, specific to the server. + * Application can inspect the ASCII kiss-code from @ref Sntp_DeserializeResponse API. + */ + SntpRejectedResponseOtherCode, + + /** + * @brief Application provided insufficient buffer space for serializing + * or de-serializing an SNTP packet. + * The minimum size of an SNTP packet is #SNTP_PACKET_BASE_SIZE + * bytes. */ + SntpErrorBufferTooSmall, + + /** + * @brief Server response failed validation checks for expected data in SNTP packet. + */ + SntpInvalidResponse, + + /** + * @brief Poll interval value is under 1 second which cannot be calculated + * by @ref Sntp_CalculatePollInterval. + */ + SntpZeroPollInterval, + + /** + * @brief The user-defined DNS resolution interface, @ref SntpResolveDns_t, failed to resolve + * address for a time server. This status is returned by the @ref Sntp_SendTimeRequest API. + */ + SntpErrorDnsFailure, + + /** + * @brief Networking operation of sending or receiving SNTP packet through the user-defined UDP + * transport interface, @ref UdpTransportInterface_t, failed. + * This status is returned by either of @ref Sntp_SendTimeRequest OR @ref Sntp_ReceiveTimeResponse + * APIs. + */ + SntpErrorNetworkFailure, + + /** + * @brief Time server is not authenticated from the authentication data in its response. + * This status can be returned by the user-supplied definition of the + * @ref SntpValidateServerAuth_t authentication interface. + */ + SntpServerNotAuthenticated, + + /** + * @brief Failure from the user-supplied authentication interface, @ref SntpAuthenticationInterface_t, + * in either generating authentication data for SNTP request OR validating the authentication + * data in SNTP response from server. + */ + SntpErrorAuthFailure, + + /** + * @brief A timeout occurred in sending time request packet over the network to a server through the + * @ref Sntp_SendTimeRequest API. + */ + SntpErrorSendTimeout, + + /** + * @brief A timeout has occurred in receiving server response with the @ref Sntp_ReceiveTimeResponse + * API. + */ + SntpErrorResponseTimeout, + + /** + * @brief No SNTP packet for server response is received from the network by the + * @ref Sntp_ReceiveTimeResponse API. + */ + SntpNoResponseReceived, + + /** + * @brief The SNTP context passed to @ref Sntp_SendTimeRequest or @ref Sntp_ReceiveTimeResponse APIs is + * is uninitialized. + */ + SntpErrorContextNotInitialized +} SntpStatus_t; + +/** + * @ingroup sntp_enum_types + * @brief Enumeration for leap second information that an SNTP server can + * send its response to a time request. An SNTP server sends information about + * whether there is an upcoming leap second adjustment in the last day of the + * current month. + * + * @note A leap second is an adjustment made in atomic clock time because Earth's rotation + * can be inconsistent. Leap seconds are usually incorporated as an extra second insertion + * or second deletion in the last minute before midnight i.e. in the minute of 23h:59m UTC + * on the last day of June or December. For more information on leap seconds, refer to + * https://www.nist.gov/pml/time-and-frequency-division/leap-seconds-faqs. + */ +typedef enum SntpLeapSecondInfo +{ + NoLeapSecond = 0x00, /**< @brief There is no upcoming leap second adjustment. */ + LastMinuteHas61Seconds = 0x01, /**< @brief A leap second should be inserted in the last minute before midnight. */ + LastMinuteHas59Seconds = 0x02, /**< @brief A leap second should be deleted from the last minute before midnight. */ + AlarmServerNotSynchronized = 0x03 /**< @brief An alarm condition meaning that server's time is not synchronized + * to an upstream NTP (or SNTP) server. */ +} SntpLeapSecondInfo_t; + +/** + * @ingroup sntp_struct_types + * @brief Structure representing an SNTP timestamp. + * + * @note The SNTP timestamp uses 1st January 1900 0h 0m 0s Coordinated Universal Time (UTC) + * as the primary epoch i.e. the timestamp represents current time as the amount of time since + * the epoch time. + * Refer to the [SNTPv4 specification](https://tools.ietf.org/html/rfc4330#section-3) for more + * information of the SNTP timestamp format. + */ +typedef struct SntpTimestamp +{ + uint32_t seconds; /**< @brief Number of seconds since epoch time. */ + uint32_t fractions; /**< @brief The fractions part of the SNTP timestamp with resolution + * of 2^(-32) ~ 232 picoseconds. */ +} SntpTimestamp_t; + +/** + * @ingroup sntp_struct_types + * @brief Structure representing data parsed from an SNTP response from server + * as well as data of arithmetic calculations derived from the response. + */ +typedef struct SntpResponse +{ + /** + * @brief The timestamp sent by the server. + */ + SntpTimestamp_t serverTime; + + /** + * @brief The information of an upcoming leap second in the + * server response. + */ + SntpLeapSecondInfo_t leapSecondType; + + /** + * @brief If a server responded with Kiss-o'-Death message to reject + * time request, this is the fixed length ASCII code sequence for the + * rejection. + * + * The Kiss-o'-Death code is always #SNTP_KISS_OF_DEATH_CODE_LENGTH + * bytes long. + * + * @note If the server does not send a Kiss-o'-Death message in its + * response, this value will be #SNTP_KISS_OF_DEATH_CODE_NONE. + */ + uint32_t rejectedResponseCode; + + /** + * @brief The offset (in milliseconds) of the system clock relative to the server time + * calculated from timestamps in the client SNTP request and server SNTP response packets. + * If the the system time is BEHIND the server time, then the clock-offset value is > 0. + * If the system time is AHEAD of the server time, then the clock-offset value is < 0. + * + * @note This information can be used to synchronize the system clock with a "slew", + * "step" OR combination of the two clock correction methodologies depending on the degree + * of system clock drift (represented by the clock-offset) and the application's + * tolerance for system clock error. + * + * @note The library calculates the clock-offset value using the On-Wire + * protocol suggested by the NTPv4 specification. For more information, + * refer to https://tools.ietf.org/html/rfc5905#section-8. + * + * @note The library ASSUMES that the server and client systems are within + * ~68 years of each other clock, whether in the same NTP era or across adjacent + * NTP eras. Thus, the client and system times MUST be within ~68 years (or + * 2^31 seconds exactly) of each other for correct calculation of clock-offset. + * + * @note When the server and client times are exactly 2^31 (or INT32_MAX + 1 ) + * seconds apart, the library ASSUMES that the server time is ahead of the client + * time, and return the clock-offset value of INT32_MAX. + */ + int64_t clockOffsetMs; +} SntpResponseData_t; + + +/** + * @brief Serializes an SNTP request packet to use for querying a + * time server. + * + * This function will fill only #SNTP_PACKET_BASE_SIZE bytes of data in the + * passed buffer. + * + * @param[in, out] pRequestTime The current time of the system, expressed as time + * since the SNTP epoch (i.e. 0h of 1st Jan 1900 ). This time will be serialized + * in the SNTP request packet. The function will use this parameter to return the + * timestamp serialized in the SNTP request. To protect against attacks spoofing + * server responses, the timestamp MUST NOT be zero in value. + * @param[in] randomNumber A random number (generated by a True Random Generator) + * for use in the SNTP request packet to protect against replay attacks as suggested + * by SNTPv4 specification. For more information, refer to + * [RFC 4330 Section 3](https://tools.ietf.org/html/rfc4330#section-3). + * @param[out] pBuffer The buffer that will be populated with the serialized + * SNTP request packet. + * @param[in] bufferSize The size of the @p pBuffer buffer. It should be at least + * #SNTP_PACKET_BASE_SIZE bytes in size. + * + * @note It is recommended to use a True Random Generator (TRNG) to generate + * the random number. + * @note The application MUST save the @p pRequestTime value for de-serializing + * the server response with @ref Sntp_DeserializeResponse API. + * + * @return This function returns one of the following: + * - #SntpSuccess when serialization operation is successful. + * - #SntpErrorBadParameter if an invalid parameter is passed. + * - #SntpErrorBufferTooSmall if the buffer does not have the minimum size + * for serializing an SNTP request packet. + */ +/* @[define_sntp_serializerequest] */ +SntpStatus_t Sntp_SerializeRequest( SntpTimestamp_t * pRequestTime, + uint32_t randomNumber, + void * pBuffer, + size_t bufferSize ); +/* @[define_sntp_serializerequest] */ + +/** + * @brief De-serializes an SNTP packet received from a server as a response + * to a SNTP request. + * + * This function will parse only the #SNTP_PACKET_BASE_SIZE bytes of data + * in the passed buffer. + * + * @note If the server has sent a Kiss-o'-Death message to reject the associated + * time request, the API function will return the appropriate return code and, + * also, provide the ASCII code (of fixed length, #SNTP_KISS_OF_DEATH_CODE_LENGTH bytes) + * in the #SntpResponseData_t.rejectedResponseCode member of @p pParsedResponse parameter, + * parsed from the response packet. + * The application SHOULD respect the server rejection and take appropriate action + * based on the rejection code. + * If the server response represents an accepted SNTP client request, then the API + * function will set the #SntpResponseData_t.rejectedResponseCode member of + * @p pParsedResponse parameter to #SNTP_KISS_OF_DEATH_CODE_NONE. + * + * @note If the server has positively responded with its clock time, then this API + * function will calculate the clock-offset. For the clock-offset to be correctly + * calculated, the system clock MUST be within ~68 years (or 2^31 seconds) of the server + * time mentioned. This function supports clock-offset calculation when server and client + * timestamps are in adjacent NTP eras, with one system is in NTP era 0 (i.e. before 7 Feb 2036 + * 6h:28m:14s UTC) and another system in NTP era 1 (on or after 7 Feb 2036 6h:28m:14s UTC). + * + * @note In the special case when the server and client times are exactly 2^31 seconds apart, + * the library ASSUMES that the server time is ahead of the client time, and returns the + * positive clock-offset value of INT32_MAX seconds. + * + * @param[in] pRequestTime The system time used in the SNTP request packet + * that is associated with the server response. This MUST be the same as the + * time returned by the @ref Sntp_SerializeRequest API. To protect against attacks + * spoofing server responses, this timestamp MUST NOT be zero in value. + * @param[in] pResponseRxTime The time of the system, expressed as time since the + * SNTP epoch (i.e. 0h of 1st Jan 1900 ), at receiving SNTP response from server. + * This time will be used to calculate system clock offset relative to server. + * @param[in] pResponseBuffer The buffer containing the SNTP response from the + * server. + * @param[in] bufferSize The size of the @p pResponseBuffer containing the SNTP + * response. It MUST be at least #SNTP_PACKET_BASE_SIZE bytes + * long for a valid SNTP response. + * @param[out] pParsedResponse The information parsed from the SNTP response packet. + * If possible to calculate without overflow, it also contains the system clock + * offset relative to the server time. + * + * @return This function returns one of the following: + * - #SntpSuccess if the de-serialization operation is successful. + * - #SntpErrorBadParameter if an invalid parameter is passed. + * - #SntpErrorBufferTooSmall if the buffer does not have the minimum size + * required for a valid SNTP response packet. + * - #SntpInvalidResponse if the response fails sanity checks expected in an + * SNTP response. + * - #SntpRejectedResponseChangeServer if the server rejected with a code + * indicating that client cannot be retry requests to it. + * - #SntpRejectedResponseRetryWithBackoff if the server rejected with a code + * indicating that client should back-off before retrying request. + * - #SntpRejectedResponseOtherCode if the server rejected with a code that + * application can inspect in the @p pParsedResponse parameter. + */ +/* @[define_sntp_deserializeresponse] */ +SntpStatus_t Sntp_DeserializeResponse( const SntpTimestamp_t * pRequestTime, + const SntpTimestamp_t * pResponseRxTime, + const void * pResponseBuffer, + size_t bufferSize, + SntpResponseData_t * pParsedResponse ); +/* @[define_sntp_deserializeresponse] */ + +/** + * @brief Utility to calculate the poll interval of sending periodic time queries + * to servers to achieve a desired system clock accuracy for a given + * frequency tolerance of the system clock. + * + * For example, from the SNTPv4 specification, "if the frequency tolerance + * is 200 parts per million (PPM) and the required accuracy is one minute, + * the maximum timeout is about 3.5 days". In this example, the system + * clock frequency tolerance is 200 PPM and the desired accuracy is + * 60000 milliseconds (or 1 minute) for which this API function + * will return the maximum poll interval value as 2^18 seconds (or ~3 days). + * + * @note The poll interval returned is a power of 2, which is the + * standard way to represent the value. According to the SNTPv4 specification + * Best Practices, an SNTP client SHOULD NOT have a poll interval less than 15 seconds. + * https://tools.ietf.org/html/rfc4330#section-10. This API function DOES NOT + * support poll interval calculation less than 1 second. + * + * @param[in] clockFreqTolerance The frequency tolerance of system clock + * in parts per million (PPM) units. This parameter MUST be non-zero. + * @param[in] desiredAccuracy The acceptable maximum drift, in milliseconds, + * for the system clock. The maximum value (0xFFFF) represents ~1 minute of + * desired clock accuracy. This parameter MUST be non-zero. + * @param[out] pPollInterval This is filled with the poll interval, in seconds + * calculated as the closest power of 2 value that will achieve either the + * exact desired or higher clock accuracy @p desiredAccuracy, for the given clock + * frequency tolerance, @p clockFreqTolerance. + * + * @return Returns one of the following: + * - #SntpSuccess if calculation is successful. + * - #SntpErrorBadParameter for an invalid parameter passed to the function. + * - #SntpZeroPollInterval if calculated poll interval is less than 1 second. + */ +/* @[define_sntp_calculatepollinterval] */ +SntpStatus_t Sntp_CalculatePollInterval( uint16_t clockFreqTolerance, + uint16_t desiredAccuracy, + uint32_t * pPollInterval ); +/* @[define_sntp_calculatepollinterval] */ + + +/** + * @brief Utility to convert SNTP timestamp (that uses 1st Jan 1900 as the epoch) to + * UNIX timestamp (that uses 1st Jan 1970 as the epoch). + * + * Refer to (this image)[docs/doxygen/images/Ntp_To_Unix_Time.png]. + * + * @note The function also correctly handles SNTP era overflow (from 7 Feb 2036 6h 28m 16s, + * i.e., SNTP era 1) to ensure accurate conversion across SNTP eras. + * + * @param[in] pSntpTime The SNTP timestamp to convert to UNIX time. + * @param[out] pUnixTimeSecs This will be filled with the seconds part of the + * UNIX time equivalent of the SNTP time, @p pSntpTime. + * @param[out] pUnixTimeMicrosecs This will be filled with the microseconds part + * of the UNIX time equivalent of the SNTP time, @p pSntpTime. + * + * @return Returns one of the following: + * - #SntpSuccess if conversion to UNIX time is successful + * - #SntpErrorBadParameter if any of the passed parameters are NULL. + */ +/* @[define_sntp_converttounixtime] */ +SntpStatus_t Sntp_ConvertToUnixTime( const SntpTimestamp_t * pSntpTime, + uint32_t * pUnixTimeSecs, + uint32_t * pUnixTimeMicrosecs ); +/* @[define_sntp_converttounixtime] */ + + +/* *INDENT-OFF* */ +#ifdef __cplusplus + } +#endif +/* *INDENT-ON* */ + +#endif /* ifndef CORE_SNTP_SERIALIZER_H_ */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index 0ddd0d41..92aae0ef 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -337,7 +337,7 @@ --diag_suppress=1,68,111,188,191,546,1293 USE_HAL_DRIVER,STM32F407xx,NDEBUG - ../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/ScreenLink;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../Drivers/BSP/NETCFG;../App;../App/app;../App/algo;../App/cfg;../App/fs;../App/log;../App/sync;../App/task;../App/protocol/iec104;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test/board;../Lib/lvgl;../Drivers/BSP/UI;../Lib + ../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/ScreenLink;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../Drivers/BSP/NETCFG;../App;../App/app;../App/algo;../App/cfg;../App/fs;../App/log;../App/sync;../App/task;../App/protocol/iec104;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test/board;../Lib/lvgl;../Drivers/BSP/UI;../Lib;../Lib/coreSNTP/include @@ -952,6 +952,11 @@ 1 ..\App\sync\time_sync.c + + time_sntp.c + 1 + ..\App\sync\time_sntp.c + default_task.c 1 @@ -1688,6 +1693,11 @@ 1 ..\Drivers\BSP\NETCFG\ftp_cfg.c + + sntp_cfg.c + 1 + ..\Drivers\BSP\NETCFG\sntp_cfg.c + nand_ftl.c 1 @@ -2359,6 +2369,16 @@ + + Lib/coreSNTP + + + core_sntp_serializer.c + 1 + ..\Lib\coreSNTP\core_sntp_serializer.c + + + Test @@ -3716,6 +3736,11 @@ 1 ..\Drivers\BSP\UI\ui_page_wave_play.c + + ui_page_sntp.c + 1 + ..\Drivers\BSP\UI\ui_page_sntp.c + diff --git a/docs/UI设计/UI_数据供给设计说明.md b/docs/UI设计/UI_数据供给设计说明.md index 814b7360..e79f2bf5 100644 --- a/docs/UI设计/UI_数据供给设计说明.md +++ b/docs/UI设计/UI_数据供给设计说明.md @@ -694,7 +694,7 @@ typedef struct { | 4 | 未标定时页面明确标识(不把码值当伏特) | 目检 + 文本断言(`eng=0` 时带标记) | | 5 | 首帧前显示 `--` 而非 `0.000` | 模拟器:不 publish 直接开页,断言文本 | | 6 | 定值整定全链闭环 | 就地改 → 保存 → 重启 → 值保持 + 校验和自检通过 + 主站 C_RD 一致。**半收口(2026-10-26)**:模拟器侧「改 → 保存 → OK → 回首屏新值已在帧里」已由像素 + 文本断言覆盖(黄金用例 `sparam_save` / `sparam_landed`,越界侧 `sparam_oob`);**重启保持与主站 C_RD 一致只能上板**——模拟器没有 NAND,`apply()` 到 `prot_set_set_sealed()` 就是终点,这条差距已登记在 `docs/测试说明/专项验证/模拟器差距清单.md`;另注意 40 项里有 13 项 `comm_addr=0`(纯本地项),主站侧根本读不到,不能拿 C_RD 当它们的判据 | -| 7 | 像素回归稳定 | `ui-golden` 连续两次运行零差异。**已验(2026-10-26,当时下限为 27 页)**:27 页连跑两次逐字节一致(含整定 9 态)——**集合现已扩至 66 张**,**双次一致已验(2026-10-29:同一批 66 张连跑三次逐字节一致,池水位 5,448 B;菜单 8 状态连跑三次亦一致,5,368 B)**;轮询定时器不影响——无头模式不调 `lv_timer_handler()`(§9.2 注 6) | +| 7 | 像素回归稳定 | `ui-golden` 连续两次运行零差异。**已验(2026-10-26,当时下限为 27 页)**:27 页连跑两次逐字节一致(含整定 9 态)——**集合现已扩至 74 张**,**双次一致已验(2026-10-29:同一批 66 张连跑三次逐字节一致,池水位 5,448 B;菜单 8 状态连跑三次亦一致,5,368 B)**;轮询定时器不影响——无头模式不调 `lv_timer_handler()`(§9.2 注 6) | | 9 | 「写不进去」与「改了不动作」两条事实互不冒充 | 反向文本断言(`check_content_pages.py` 的 `ABSENT`:查看页与只读态不得出现「未标定」徽标与「保存」行)+ 台架:未制造该态——要它必须改坏 /prot_set.bin(破坏产品配置存储,用户裁定 2026-10-26 不做),故 LOCK 态目前只有代码级证据与三重反证(ENTER 被正常消费、遥信 YX31=OFF、开机 CORRUPT=0),**没有像素证据** | | 8 | 主机单测覆盖数据模块 | `test/host/ui_data` 全绿并挂 pre-commit 钩子 | diff --git a/docs/UI设计/UI_自动化测试流程.md b/docs/UI设计/UI_自动化测试流程.md index 2836aee0..5aa393b0 100644 --- a/docs/UI设计/UI_自动化测试流程.md +++ b/docs/UI设计/UI_自动化测试流程.md @@ -26,7 +26,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 | 步 | 脚本/命令 | 门禁内容 | 预期输出 | |---|---|---|---| | 1 | `simulator/tests/check_menu_rows.py` | 菜单 8 状态:选中行白带 / 非选行黑底白字 / 框体 1px 边框 / 往返像素一致(MENU-STATE-03==MENU-STATE-01、MENU-STATE-06==MENU-STATE-01、MENU-STATE-07==MENU-STATE-02)+ MENU-STATE-00 池基线 | `PASS: 8 状态全部断言通过(8 图,池基线 5368 B)` | -| 2 | `simulator/tests/check_content_pages.py` | ① 文本级断言(字形模板匹配,正向 444 条 + 反向 45 条,独立于 `ref/`)→ ② 66 张截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 66 内容页 + 往返池水位全部通过`(文本断言先于像素比对,失败即中止) | +| 2 | `simulator/tests/check_content_pages.py` | ① 文本级断言(字形模板匹配,正向 484 条 + 反向 45 条,独立于 `ref/`)→ ② 74 张截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 74 内容页 + 往返池水位全部通过`(文本断言先于像素比对,失败即中止) | | 3 | `keil-build` skill(`keil_build.py`) | MCU 端编译(UI 源码双端共用,改一端必须过另一端) | 退出码 0 = BUILD_OK | | 4 | `scripts/check_coding_standard.py` | 代码规范 V1.0 机械检查 | PASS | @@ -52,7 +52,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 | `POOL_AFTER_ROUNDTRIP` | check_content_pages.py | **5448 B** | 开页-关页往返后水位 = 基线 + 一次性残留 80 B | > 水位历史:4784 →(2026-09-14 待机模块常驻 +656)5440 / 5520 →(2026-10-11 裁剪 LVGL 控件类 −72)5368 / 5448。 -> 后一次漂移已核实为**有意且更优**:像素逐字节未变(**裁剪当时黄金参考只有 11 页**,在裁剪**前**生成、裁剪后全部一致;该集合现已扩至 66 张), +> 后一次漂移已核实为**有意且更优**:像素逐字节未变(**裁剪当时黄金参考只有 11 页**,在裁剪**前**生成、裁剪后全部一致;该集合现已扩至 74 张), > 且一次性残留仍是 80 B(5448 − 5368)→ 泄漏特征未变,只是 `lv_theme_apply` 不再为已不存在的控件类登记样式条目。 **漂移处理**:菜单/页面结构或样式变更导致门禁失败时,先确认新值合理(残留分解见 @@ -61,7 +61,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 ## 四、黄金参考(入库)与目检 PNG(本地产物) -`simulator/tests/ref/*.bin`(像素黄金参考,66 张 × 3200 B)**随仓库入库**;派生的 `.png` +`simulator/tests/ref/*.bin`(像素黄金参考,74 张 × 3200 B)**随仓库入库**;派生的 `.png` 目检图仍是本地产物(`.gitignore`)。入库的理由:比对是**逐字节**的,基线不入库时任何新克隆 或新工作树跑 `ui-golden` 都必然 FAIL(缺基准),这条门禁就只在"有老副本的机器"上真实存在。 可复现性实测(2026-10-22):两份各自独立构建的 `lvgl_sim`(不同构建目录、相隔 11 天的代码) @@ -154,7 +154,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 ### 7.3 用法与门禁档位 ```powershell -# 认屏快检:66 张黄金参考(+ 若有台架留档帧),秒级,不需要构建产物 +# 认屏快检:74 张黄金参考(+ 若有台架留档帧),秒级,不需要构建产物 & '.venv\Scripts\python.exe' scripts/board_ui_nav.py --selftest-quick # 完整自检:再加全绑定页闭环导航(需模拟器可执行文件,约 30 s) & '.venv\Scripts\python.exe' scripts/board_ui_nav.py --selftest diff --git a/docs/UI设计/mockups/31_SNTP设置_落地前_模拟器截图.png b/docs/UI设计/mockups/31_SNTP设置_落地前_模拟器截图.png new file mode 100644 index 00000000..c2e0a59d Binary files /dev/null and b/docs/UI设计/mockups/31_SNTP设置_落地前_模拟器截图.png differ diff --git a/docs/UI设计/mockups/31_SNTP设置_落地后_模拟器截图.png b/docs/UI设计/mockups/31_SNTP设置_落地后_模拟器截图.png new file mode 100644 index 00000000..d29fed63 Binary files /dev/null and b/docs/UI设计/mockups/31_SNTP设置_落地后_模拟器截图.png differ diff --git a/docs/UI设计/内容页架构.md b/docs/UI设计/内容页架构.md index d2cc0449..cec05f18 100644 --- a/docs/UI设计/内容页架构.md +++ b/docs/UI设计/内容页架构.md @@ -453,7 +453,7 @@ extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash ## 八、验证与回归 - **门禁**(`ui-verify` skill / `UI_自动化测试流程.md`): - 0) 模拟器构建 0 警告 → 1) 菜单 8 状态像素回归 + 池基线 → 2) 内容页文本断言 + 66 张黄金参考逐字节比对 + 0) 模拟器构建 0 警告 → 1) 菜单 8 状态像素回归 + 池基线 → 2) 内容页文本断言 + 74 张黄金参考逐字节比对 (网口设置 4 态 + 密码页 4 态等)+ 开页-关页往返水位 → 3) Keil 构建 0 错 0 警 → 4) 代码规范检查 - **门禁常量**(脚本头部):`MENU_POOL_BASELINE = 5368 B`、`POOL_AFTER_ROUNDTRIP = 5448 B` - **失败处置**:像素不一致 = 代码 bug(修代码,**禁止**刷参考掩盖);**有意**改视觉 → diff --git a/docs/UI设计/页面补齐规划.md b/docs/UI设计/页面补齐规划.md index 96974f4f..edcfff3f 100644 --- a/docs/UI设计/页面补齐规划.md +++ b/docs/UI设计/页面补齐规划.md @@ -81,7 +81,7 @@ | 26 | 手动录波 | 操作页(确认) | `wave_ring_request` 手动触发经 `ui_data_wave_ops_t` 注入 | ✅ 已落地(`ui_page_wave_manual.c`,2026-10-29 录波 UI 道;配套新增「录波回放」叶 node 43 = `ui_page_wave_play.c`,不在本表 27 叶之列) | | 27 | 清除记录 | 操作页(选类型 + 确认) | `sys_log`(**缺删除 API**) | 🔶 需补 API | | 29 | 通讯设置 | 可编辑列表(4~6 项) | 104 公共地址/端口/主站(当前固定) | 🔶 需 `comm_cfg` | -| 31 | SNTP设置 | 可编辑列表(3 项) | SNTP 客户端 | ❌ 未实现(产品口径亦未定) | +| 31 | SNTP设置 | 可编辑列表(3 项) | SNTP 客户端 | ✅ 已落地(`ui_page_sntp.c`,2026-11-04 板端 SNTP 接入立项;形态较原规划多 1 行 = 启用/主服务器/备服务器/周期(分) + 保存行,编辑语义复刻网口设置页) | | 33 | SOE记录 | 动态列表(每记录 2 行) | `/log/soe.log` 二进制 64B | ✅ 已落地(`ui_page_soe.c`) | | 34 | 事故记录 | 动态列表 | /log/accid.log(SYSLOG_ACCIDENT,写入点 protect_task.c) | ✅ 已落地(ui_page_accident.c,2026-10-28) | | 35 | 操作记录 | 动态列表 | /log/oper.log(SYSLOG_OPER,写入点 4 处:定值保存 / 复归 / 对时 / 清除记录) | ✅ 已落地(ui_page_oper.c,2026-10-28) | @@ -94,6 +94,7 @@ **2026-10-28 复核**:本表 27 叶中 **22 叶已落地**(本批 +6:记录 5 页与运行报告,见 docs/任务规划/UI补齐.md §2 与 docs/任务规划/登记簿.md §7);未落地的 5 叶 = 遥信类型 / 遥信防抖 / 双点遥信(无开入硬件)+ 手动录波(无录波模块)+ SNTP设置(产品口径未定),取舍与重开条件见 docs/任务规划/UI补齐.md §5。口径闭合:本表 22 叶 + 立项前就已绑定、不在本表内的 6 叶(装置信息 / 交流量 / 直流量 / 网口设置 / 自检记录 / 运行记录)= 现值已绑 28 叶(= UI_PAGE_MAP_COUNT);本表未落地 5 叶 = 菜单树未绑 5 叶(33 − 28)。复核方法:按缩进判叶子(有更深缩进后继者即非叶子),再按行内 #page= 数已绑。**2026-10-29 补充**:「手动录波」已随故障录波独立立项(docs/子系统设计/故障录波设计说明.md;原卡片登记簿 2026-10-30 销项并入该文 §10),上面那行数字仍是 2026-10-28 当日复核值,未随立项改动。 **2026-10-29 二补(录波 UI 道落地)**:本表「手动录波」转已落地(23 叶);菜单树另新增「录波回放」叶(node 43,不在本表立项快照内)——现值 43 节点 / 34 叶 / 已绑 30 / 未绑 4(= 遥信 3 + SNTP 1,UI_PAGE_MAP_COUNT = 30),口径复核方法与上一段相同 +**2026-11-04 复核(板端 SNTP 接入立项,#57 调研结论)**:本表「SNTP设置」转已落地(24 叶)——`ui_page_sntp.c` 绑 `#page=sntp`,[sntp] 段经 `net_cfg_load` 注册、SNTP 客户端在 timeSyncTask 轮询(`App/sync/time_sntp.c`);现值 43 节点 / 34 叶 / 已绑 31 / 未绑 3(= 遥信 3,UI_PAGE_MAP_COUNT = 31),口径复核方法与上一段相同 --- diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index 28e04408..bc78e0aa 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -324,6 +324,11 @@ flowchart LR - 实现:`timeSyncTask` 等 `NET_READY` → TCP listen :8888 → 收 PC 推送 → 写 RTC;30 s 无包则掉线重建监听。 - 判据:对时后设备墙钟更新、日志时间戳正确。 +**FR-TIME-3 SNTP 校时** ✅(2026-11-04) +- 需求:设备作 SNTP(RFC 4330)客户端从配置的 IPv4 服务器经 UDP/123 取时写回 RTC;使能、主/备服务器、轮询周期可配(UI「SNTP 设置」页或 `/config.ini` `[sntp]` 段,保存后随下一轮生效)。 +- 实现:`timeSyncTask` 复用 8888 事件循环(≤50 ms 等待切片,不饿任务狗与 IWDG);48 字节报文由 vendored coreSNTP 序列化器(`Lib/coreSNTP/`)编解码,节奏与判据在本方;防伪 = OriginTimestamp 须逐位等于发出的 T1(含随机小数位),mode / stratum / 闰秒指示 / Kiss-o'-Death 按 RFC 拒绝;主备轮换 + 失败指数退避(5 s 起、60 s 封顶)+ 首轮 1~5 分钟随机;成功时 |offset| < 2 s 只记日志不阶跃,更大偏差阶跃写回 SD2506——钟面 = 本地时间(UTC+8,与 8888 同一时基口径,取舍见 docs/任务规划/UI补齐.md §4 时基口径注);首个校验通过的同步置 `SYS_EVT_TIMESYNC_READY`。 +- 判据:主机单测 `test/host/sntp`(报文格式 / 伪造拒收 / KoD 轮换 / 退避与周期 / 阶跃域守卫);真机 = 使能并配置服务器后时钟与时间源一致、`[SNTP] step offset=` 日志在场(台架窗口统一做)。 + > **已知限制**:uint32 epoch → **2038 年**上限。 ### 5.4 配置引擎(FR-CFG)✅ @@ -613,7 +618,7 @@ flowchart LR 上电次数 + 本次复位原因快照)与 iec104_serv 的四个只读观测计数,经 info 帧下发 (口径 docs/UI设计/UI_数据供给设计说明.md §4.4)。 - 判据:五类记录各有写入点、可翻最近 20 条、空记录明确显示"无记录"、共用 reader——**软件面已满足** - (空态与四个范围标签由 66 张像素黄金钉住;模拟器侧这些文件没有写入方,只拍到空态)。 + (空态与四个范围标签由 74 张像素黄金钉住;模拟器侧这些文件没有写入方,只拍到空态)。 真记录翻页、掉电后保持、清除记录后归零、boot_count 自增与四态判定**待台架复核** (docs/任务规划/登记簿.md §7)。 @@ -1539,10 +1544,11 @@ pre-commit run --all-files > 图里的节点数、绑页与密码门读数由 `tools/arch_diagram/gen_menu_tree_svg.py` 现算自真值 `ui_menu_tree.txt`(改菜单树 → 同步脚本内 `NODE_SRC` 重跑);级联框在屏上怎么摆(几何)→ `docs/UI设计/菜单页面设计.md` §4.1。 **规模**:菜单节点 **9** 个(含根)+ 叶子节点 **34** 个 = **43** 节点;最大深度 **4 级(主界面 / 一级 / 二级 / 三级菜单)**。 -**绑定现状**(2026-10-29 复扫):**已绑内容页 30 叶 / 未绑 4 叶**(页面注册表 `UI_PAGE_MAP_COUNT` = 30, -与已绑叶数一致);未绑 4 叶 = 无开入硬件 3 + SNTP 1(取舍与重开条件见 +**绑定现状**(2026-11-04 复扫):**已绑内容页 31 叶 / 未绑 3 叶**(页面注册表 `UI_PAGE_MAP_COUNT` = 31, +与已绑叶数一致);未绑 3 叶 = 无开入硬件 3(取舍与重开条件见 docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 页 + 运行报告)已于 2026-10-28 落地绑页, -「手动录波」与「录波回放」两叶于 2026-10-29 随录波道绑页(判据见 docs/子系统设计/故障录波设计说明.md §4.1 与 §8.1)。 +「手动录波」与「录波回放」两叶于 2026-10-29 随录波道绑页(判据见 docs/子系统设计/故障录波设计说明.md §4.1 与 §8.1), +「SNTP 设置」叶于 2026-11-04 随 SNTP 客户端立项绑页(判据见 §5.3 FR-TIME-3 与 docs/网络/BSD_Socket_API_使用指南.md §6.1)。 **根页(主界面)7 个一级条目**:装置信息 / 实时数据 / 参数定值 / 三遥设置 / 装置维护 / 通讯参数 / 记录查询。 @@ -1576,7 +1582,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 ### 8.3 内容页 -**已绑 30 页**(按分组;逐页形态见 [`docs/UI设计/页面补齐规划.md`](../UI设计/页面补齐规划.md)): +**已绑 31 页**(按分组;逐页形态见 [`docs/UI设计/页面补齐规划.md`](../UI设计/页面补齐规划.md)): | 分组 | 页面 | 形态 | |---|---|---| @@ -1586,21 +1592,21 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | 参数定值 | 整定 4 页(参数 / 定值 / 控制字 / 软压板) | 可编辑(写值 + 置脏 + 保存反馈) | | 三遥设置 | 遥测死区 / 遥测系数 | 可编辑(运行参数段,`0x8030~0x804F`) | | 装置维护 | 时钟设置 / 强制复归 / 清除记录 / 手动录波 | 操作页(执行类必须出真反馈,不得"点了没效果";手动录波触发后回读同一份计数,判据见 docs/子系统设计/故障录波设计说明.md §8.2) | -| 通讯参数 | 通讯设置 / 网口设置 | 可编辑(`[comm]` / `[network]` 段,保存后按各自生效时机) | +| 通讯参数 | 通讯设置 / 网口设置 / SNTP 设置 | 可编辑(`[comm]` / `[network]` / `[sntp]` 段,保存后按各自生效时机;SNTP 页改后立即随下一轮生效,判据见 §5.3 FR-TIME-3) | | 记录查询 | SOE 记录 / 自检记录 / 运行记录 | 记录列表(共用 reader) | | 记录查询 | 事故记录 / 操作记录 / 保护告警 / 保护启动 / 遥控记录 | 记录列表(每记录 2 行、空态显「无记录」;写入点与判据见 App/log/README.md §五类记录) | | 记录查询 | 运行报告 | 只读列表 13 行分 2 页;四个数逐个挂范围标签(自电池周期 / 本次开机),跨开机累计按裁定不做 | | 记录查询 | 录波回放 | 单通道波形页(阶梯线自绘、8 路切换、单位徽标、削顶标记、空态与不可读态显式文案);显示契约见 docs/子系统设计/故障录波设计说明.md §8.1 | -**未绑 4 叶**(2026-10-28:记录 5 页与运行报告页已落地绑页,原「当前执行 6 项」清零;2026-10-29: -录波道落地,「手动录波」叶子随之一并绑页): -遥信类型 / 遥信防抖 / 双点遥信 = 等开入硬件(§9.1.1「出口与开入」);SNTP 设置 = 等 SNTP 对时(§9.1.2「SNTP 对时」)。 +**未绑 3 叶**(2026-10-28:记录 5 页与运行报告页已落地绑页,原「当前执行 6 项」清零;2026-10-29: +录波道落地,「手动录波」叶子随之一并绑页;2026-11-04:SNTP 客户端立项落地,「SNTP 设置」叶绑页): +遥信类型 / 遥信防抖 / 双点遥信 = 等开入硬件(§9.1.1「出口与开入」)。 - 内容页为**独立全屏屏**,由框架一次性自绘(**零 LVGL 子对象**),`ESC` 返回菜单屏。 - 菜单导航状态**不压层**,返回即原样还原;同一时刻至多一个内容页打开。 - 节点→页映射在逻辑层 `menu_content_page_for(node)`;未绑叶子进入空白终点框(不伪造数据)。 - 数据供给走**注入通道**(rt / par / info / 遥测死区与系数第四表),页面不直接触算法层; - 像素黄金用例当前 **66 张**(菜单 8 状态另计;2026-10-28 记录 5 页与运行报告 2 页落地 +7 张;2026-10-29 录波 UI 道 6 张:手动录波 3 态 + 录波回放 3 态)。 + 像素黄金用例当前 **74 张**(菜单 8 状态另计;2026-10-28 记录 5 页与运行报告 2 页落地 +7 张;2026-10-29 录波 UI 道 6 张:手动录波 3 态 + 录波回放 3 态;2026-11-04 SNTP 设置页 +8 张)。 ### 8.4 密码与权限 @@ -1664,7 +1670,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | **多规约栈** | 17 | 12 种 | 1 种(104) | +11 种(101 / 102 / 103 / CDT / ModBus / Whm_GB / 61850 / CAN + 子站侧 3 类) | | **国密算法** | 18 | `Comm/Ecc/` 全套 | 0 | SM2 / SM3 / ECDSA | | **软压板铺开** | 11 | 9 面(表容量 32) | 1 面 | +8 面 | -| **SNTP 对时** | 17 | 参考对时走 SNTP | 私有 TCP 8888 + 104 `C_CS` | SNTP 客户端 | +| **SNTP 对时** | 17 | 参考对时走 SNTP | **SNTP 客户端已落地**(2026-11-04:coreSNTP 序列化器 + `[sntp]` 配置 + UI 设置页 + timeSyncTask 接入,判据 §5.3 FR-TIME-3);另有私有 TCP 8888 + 104 `C_CS` | 台架真机对时判据(现场时间源,docs/任务规划/登记簿.md) | | **固有参数** | 1 | `8001H~800AH` 字符串类参数 | 未实现(无法短浮点上送) | 随送检点表设计 | | **遥测增强** | 4、5 | k=1~9 谐波方和根 + 4 点平均 | 仅基波轨 | 谐波 / 等周波调频 / 零序自产外接切换 三条算法轨 | | **屏板链路对齐** | 20 | **串口屏板通道**(**700000 8N1 / USART1 TTL 直连、无 DE**,帧 A 6 B 召唤、帧 B **4208 B** 应答:显存 3200 B + **状态灯 4 B 位掩码** + 4 B info)+ **7003 四命令**(INIT **16 B**,单色口径)+ 续传 / 拒绝握手 / 链路监视 / F1·F2 | **串口通道已交付**(4208 B 应答 / 状态灯掩码 / 键上行 / 双出口并发,屏板链路设计说明 §9.1);7003 四命令(INIT **8 B**);无续传、无 `bNeedFlash` 拒绝、无 1 分钟监视、键值 6 个 | 7003 报文口径 6 项(`参考产品复刻明细.md` §11 第 2~9 条);串口侧验收尾巴见屏板链路设计说明 §9.2 | diff --git a/docs/任务规划/UI补齐.md b/docs/任务规划/UI补齐.md index 494eff18..d816a185 100644 --- a/docs/任务规划/UI补齐.md +++ b/docs/任务规划/UI补齐.md @@ -8,20 +8,22 @@ ## 1. 目标与范围 -菜单树共 **34 叶**(节点行合计 43,缩进即层级):按行内 `#page=` 实测**已绑 30 叶、未绑 4 叶**, -页面映射表 `UI_PAGE_MAP_COUNT` = 30(与已绑叶数一致)。未绑 4 叶 = §5 明确不做的 **4 叶** -(遥信 3 / SNTP 1),与下面分类表逐项对得上;2026-10-29 录波 UI 道把「手动录波」从不做转为 -落地并新增「录波回放」叶,绑 +2(手动转绑 + 新叶)/ 叶 +1;2026-10-28 记录 5 页与运行报告页落地后, +菜单树共 **34 叶**(节点行合计 43,缩进即层级):按行内 `#page=` 实测**已绑 31 叶、未绑 3 叶**, +页面映射表 `UI_PAGE_MAP_COUNT` = 31(与已绑叶数一致)。未绑 3 叶 = §5 明确不做的 **遥信 3 叶**, +与下面分类表逐项对得上;2026-10-29 录波 UI 道把「手动录波」从不做转为 +落地并新增「录波回放」叶,绑 +2(手动转绑 + 新叶);2026-11-04 SNTP 客户端立项(Issue #57 拍板 +选型、#73 拍板节奏),SNTP 设置叶转绑定(+1);2026-10-28 记录 5 页与运行报告页落地后, 原「需补配置或写入方」的 6 叶缺口清零并按销项删行(该行内容见本节末与 §3)。 | 分类 | 数量 | 当前怎么处理 | |---|---:|---| | 无开入硬件 | 3 | 不做:遥信类型、遥信防抖、双点遥信 | -| 缺独立模块或产品定义 | 1 | 不做:SNTP 设置(手动录波已于 2026-10-29 随故障录波 UI 道落地转绑定,不再列不做) | + +(原「缺独立模块或产品定义:SNTP 设置」行已于 2026-11-04 随 SNTP 客户端立项转绑定销项;手动录波更早,2026-10-29 随故障录波 UI 道转绑。) 现有基础:`ui_w_editor` / `ui_w_setlist` / `ui_page_action` 已就绪;`ui_data` 已扩到**四通道** (rt / par / info + 遥测死区与系数用的第四张注入表);像素黄金用例与字符集门禁已存在,内容页黄金 -现有 **66 张**(菜单 8 状态另计;2026-10-29 录波 UI 道 +6 张:手动录波 3 态 + 录波回放 3 态)。**装置维护 3 页、遥测死区/系数 2 页、通讯设置页、记录 5 页与 +现有 **74 张**(菜单 8 状态另计;2026-10-29 录波 UI 道 +6 张:手动录波 3 态 + 录波回放 3 态;2026-11-04 SNTP 设置页 +8 张)。**装置维护 3 页、遥测死区/系数 2 页、通讯设置页、SNTP 设置页、记录 5 页与 运行报告页都是把这套基础接上真实消费者做出来的,不另发明一套机制。** 逐页形态见 `docs/UI设计/页面补齐规划.md`;数据契约见 `docs/UI设计/UI_数据供给设计说明.md`。本文件保留范围、完成判据与不做的取舍口径(执行清单已于 2026-10-28 清空)。 @@ -31,11 +33,11 @@ 1. **节点行** = `ui_menu_tree.txt` 里非空、非 `#` 开头的行(当前 **43** 行,含根「主界面」)。 2. **缩进即层级**(每级 2 空格;Tab 会被 `gen_menu.py` 直接拒绝):某节点行之后再没有更深缩进的行 = **叶** (当前 **34** 叶);有更深缩进后继者 = 菜单节点(当前 **9** 个)。等价于生成物口径 `ui_menu_tab_has_children(node) == false`。 -3. **绑定**:叶行行尾带 ` #page=` = 已绑(当前 **30** 叶);不带的 = 未绑(当前 **4** 叶)。 -4. **闭合**:`未绑 = 待做 + 不做` —— 当前待做 **0**(§2 清单已清空)+ 不做 **4**(遥信 3 / SNTP 1)= **4**, +3. **绑定**:叶行行尾带 ` #page=` = 已绑(当前 **31** 叶);不带的 = 未绑(当前 **3** 叶)。 +4. **闭合**:`未绑 = 待做 + 不做` —— 当前待做 **0**(§2 清单已清空)+ 不做 **3**(遥信 3)= **3**, 与上面的分类表、§5「当前不做」表逐项对得上。 5. **别拿 `grep -c '#page='` 当已绑叶数**:2026-10-28 实测它得 **29**,比真值多 1 —— 文件头第 5 行是规则说明里的 - ` #page=<模块名>` 示例行(不是节点行),必须先剔注释/示例行;剔完的 30 才等于生成物 `UI_PAGE_MAP_COUNT`。 + ` #page=<模块名>` 示例行(不是节点行),必须先剔注释/示例行;剔完的数才等于生成物 `UI_PAGE_MAP_COUNT`(现 31)。 复核命令(复用生成器自己的解析器 `tools/menu_gen/gen_menu.py` 的 `parse_tree`,不另写一套计数逻辑,免得与生成物脱钩): @@ -49,7 +51,7 @@ 三个已踩过的错算法(每种都同时给**当时**与**2026-10-28 重跑**的产物——错法不会自己失效,只是错数跟着已绑叶数一起漂): **把 42 个节点行当叶子**(多算 9 个菜单节点:当时已绑 21 叶 → 得「未绑 21」,2026-10-28 同一错法得「未绑 14」= 42 − 28); **`grep -c '#page='` 当时得 23**(当时已绑 22 叶,多的那 1 正是文件头注释第 5 行的 `#page=<模块名>` 示例字样,必须先剔注释行; -2026-10-28 同一命令得 29 = 当时已绑 28 + 该示例行 1;2026-10-29 现值 31 = 已绑 30 + 示例行 1);**按叶子名去重得 29**(`参数`/`定值`/`控制字`/`软压板` 在「整定」「查看」下各一份, +2026-10-28 同一命令得 29 = 当时已绑 28 + 该示例行 1;2026-10-29 当时值 31 = 已绑 30 + 示例行 1;2026-11-04 现值 32 = 已绑 31 + 示例行 1);**按叶子名去重得 29**(`参数`/`定值`/`控制字`/`软压板` 在「整定」「查看」下各一份, 4 对同名不能合并;2026-10-28 重跑仍是 29 = 33 叶 − 4 对同名)。 (21 / 23 两个错数分属 2026-10-27「已绑 21 叶」与「已绑 22 叶」两个时点的实测,只用来说明错法,不代表现值; 去重 29 与上面 grep 现值 29 **只是撞号**:一个是「合并同名叶」、一个是「不剔注释行」,都与真值 30(2026-10-29 现值)无关。) @@ -112,7 +114,6 @@ UTC 写入,后写者赢 ⇒ 装置墙钟与主机本地时刻恒差 8 小时 | 项目 | 不做原因 | 重开条件 | |---|---|---| | 遥信类型 / 防抖 / 双点 3 页 | V0.1 无开入硬件,配置没有消费者 | 开入固件立项后,与开入开出一起做 | -| SNTP 设置 | 当前使用 8888 私有对时协议,SNTP 链路与产品口径均未建立 | SNTP 客户端立项 | | net 层与其他结构债 | 当前只做 UI,避免两条主线互相拖慢 | UI 当前清单完成后,按 README §2 切换 | | RTC 时基统一 | 两套写入口径并存(面板与 8888 写本地钟面、104 写 UTC),恒差 8 小时;统一任一侧都要动 104 上行语义或 UI 判据 | 用户裁定时基方向后,作为跨模块变更单独立项 | | 主站地址白名单 | CH395F 单连接 socket 上「拒绝即 close+重 listen」等于给扫描风暴开门,且该值现在没有任何既有消费点 | 有真实消费场景或多连接后端立项时再评 | diff --git a/docs/任务规划/登记簿.md b/docs/任务规划/登记簿.md index 2c3d5d00..c7e57e41 100644 --- a/docs/任务规划/登记簿.md +++ b/docs/任务规划/登记簿.md @@ -67,6 +67,7 @@ | **网络负向场景台架验证** | 拔线、SPI 故障注入、远显压力条件 | PHY 拔线、芯片级 `net_init` 失败、短写压测 | 本文件 §1 | | **CI 远端闭环未验证** | runner 与第二台机器 | 验证远端两 job 闭环和不同工具路径 | 本文件 §4 | | **UI 六页真机判据(记录 5 页 + 运行报告)** | 台架:NAND 里有真实事件记录、SD2506 电池域可控、104 主站可连 | 上板复核六件事:真记录翻页与掉电后保持、清除记录后对应页归零、上电一次看 `boot_count` +1、拔 RTC 电池后 `seq_state` 落 RESTARTED、IWDG 复位后 `rst_cause` 落 IWDG、主站连一次看 `conn_up` +1(模拟器侧这些记录文件无写入方、无 `boot_stats`、无 104 协议栈,黄金只拍得到空态「无记录」与固定假数据的显示口径) | 六个页面模块文件头「台架待补」段 + `../测试说明/专项验证/模拟器差距清单.md` §2.3 开机账本一行 | +| **SNTP 校时真机判据** | 台架:板与 PC SNTP 服务器(192.168.31.161 / .13)同网段可达 | `[sntp]` 使能 + 配服务器后上板复核:`[SNTP] step offset=` 步进日志、钟面与时间源一致(本地 UTC+8 口径)、`poll_min` 周期到点重对、拔线后指数退避与主备轮换、恢复后回到周期 | `../子系统设计/系统墙钟时间维护说明.md` §16 | **送检顺序不可倒过来**:产品构建当前要求 UA/UN/IA 的标定 VALID 全部置位才走保护判据。 未完成标准源标定就送检,保护会表现为不动作。保持该闭锁,不用非标准源标定走捷径。 @@ -81,7 +82,6 @@ | **开入开出与遥信配置 3 页** | 固件和 V0.1 硬件均无可配开入对象;出口还涉及 GPIO 安全电平、保持/脉冲和回读自检 | 开入开出作为独立模块立项,再与 UI 页面同批 | | **故障录波** | 独立子系统,不是补一个页面——**2026-10-29 已立项**:设计与板级判据属主 docs/子系统设计/故障录波设计说明.md(§10;原卡片登记簿 2026-10-30 销项并入后删除) | 已解除暂缓;执行按该文 §10 顺序图,UI 道互斥 | | **104 文件传输(规约侧读文件)** | 录波取回走现成 FTP 已够用,规约侧文件服务是另一件事 | 送检或第三方交接需求明确后单独立项 | -| **SNTP 设置** | 当前对时走 8888 私有协议,SNTP 客户端与产品口径未定 | SNTP 客户端立项 | | **RS-485 整簇** | 用户决定暂缓 | `暂缓-RS485.md` | ## 9. 开放问题 diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 94319642..e59499d7 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 278,956 B (272.42 KB) | 1,048,576 B (1024 KB) | 26.6% | -| RW_IRAM1 | 94,624 B (92.41 KB) | 131,072 B (128 KB) | 72.2% | +| ER_IROM1 | 284,800 B (278.12 KB) | 1,048,576 B (1024 KB) | 27.2% | +| RW_IRAM1 | 94,736 B (92.52 KB) | 131,072 B (128 KB) | 72.3% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **281,008 B (274.4 KB)** | 1,048,576 B (1024 KB) | **26.8%** | +| **Flash (ROM 合计)** | **286,872 B (280.1 KB)** | 1,048,576 B (1024 KB) | **27.4%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index e641f994..ec1f9cd6 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,19 +15,19 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 278,956 B | 1,048,576 B | 26.6% | 参考 | +| ER_IROM1 (Flash) | 284,800 B | 1,048,576 B | 27.2% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 94,624 B | 131,072 B | 72.2% | PASS | +| RW_IRAM1 | 94,736 B | 131,072 B | 72.3% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| | 网络 | 13,958 B | 12 KB | ⚠ 超 1.6 KB | | 存储 | 7,072 B | 12 KB | OK | -| UI | 36,473 B | 16 KB | ⚠ 超 19.6 KB | +| UI | 36,475 B | 16 KB | ⚠ 超 19.6 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | -| 应用 | 26,333 B | 8 KB | ⚠ 超 17.7 KB | -| 其它 | 10,780 B | — | 参考 | +| 应用 | 26,433 B | 8 KB | ⚠ 超 17.8 KB | +| 其它 | 10,792 B | — | 参考 | - **门禁**:pre-commit `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)—— diff --git a/docs/子系统设计/系统墙钟时间维护说明.md b/docs/子系统设计/系统墙钟时间维护说明.md index f2a6387f..bf4e5631 100644 --- a/docs/子系统设计/系统墙钟时间维护说明.md +++ b/docs/子系统设计/系统墙钟时间维护说明.md @@ -299,7 +299,7 @@ uint32_t now = sys_clock_get(); /* Unix 秒,可用于 FatFS 文件时间、 |------|------|------------| | 时间范围 | 受 `uint32_t` 限制至 2038 | 改用 `uint64_t`/`time_t` 扩展 | | 运行期校准 | 30 天惰性再基准化已实现(§7):纯自我参照、零 I2C;**有意不**周期性重读 RTC 重锚——RTC 漂移(±20ppm ≈ ±1.7 s/天)远大于 Tick 漂移,重锚反而降低精度;时间纠偏由时间同步(§15)负责 | -| 网络校时 | 已实现(PC 推送 / TCP,见第 15 节) | SNTP/NTP 公网校时仍可作为后续扩展 | +| 网络校时 | 已实现两条:PC 推送 / TCP 8888(§15);**SNTP 客户端 / UDP 123(§16,2026-11-04 落地)** | NTP 公网源需现场路由,口径不变;104 `C_CS` 写入口径见 §1.1 末待决项 | | 时区 | **口径已定:内部一律本地钟面,不存时区**(用户裁定 2026-10-27,见 §1.1) | 「API 层加偏移参数」这条旧建议**作废**;唯一待决的是 104 上行是否要转 UTC(§1.1 末,送检前定) | ### 13.1 用户 SRAM 的占用分配(共享资源,用之前先在这里登记) @@ -334,6 +334,9 @@ SD2506 有 **70 字节电池保持用户 SRAM**(寄存器 `2CH~71H`,`Drivers | `App/app/app_main.c` | 初始化调用顺序:`sd2506_init()` → `boot_stats_init()`(用户 SRAM 上电账本,§13.1)→ `sys_clock_init()` | | `App/util/boot_stats.c` / `.h` | 用户 SRAM 上电账本 + 本次开机复位原因快照(占用见 §13.1;单测 `test/host/boot_stats/`) | | `App/sync/time_sync.c` / `time_sync.h` | 时间同步实现(TCP Server,`timeSyncTask`) | +| `App/sync/time_sntp.c` / `time_sntp.h` | SNTP 校时客户端(§16,同一任务内挂 8888 事件循环);单测 `test/host/sntp/` | +| `Drivers/BSP/NETCFG/sntp_cfg.c` / `.h` | `[sntp]` 配置段属主(注册在 `net_cfg_load()`) | +| `Lib/coreSNTP/` | vendored coreSNTP v2.0.0 序列化器(MIT,仅 serializer + LICENSE;48 字节报文编解码) | | `test/host/time_sync_server.py` | PC 端时间推送客户端(TCP) | --- @@ -392,6 +395,7 @@ for (;;) { - `net_socket()` 从 0 开始分配第一个空闲 Socket 控制块(`alloc_socket()`),并把硬件 Socket 号设为该下标;调用时的第三个参数(0)仅为协议占位,不影响分配。 - 当前启动顺序:FTP 任务(`NET_READY` + `STORAGE_READY` 屏障满足后,无魔法延时)先监听 → 占用一个 Socket(单连接模式下监听通道复用为数据通道);时间同步任务(`NET_READY` 后)后监听 → 占用另一个空闲 Socket。二者不冲突。 - 单连接模式允许**任意** Socket 作为"监听 + 数据复用"通道,且总共 8 个 Socket,FTP 与时间同步各用 1 个,余量充足。 +- SNTP 客户端(§16)另占 1 个 UDP Socket:懒创建(首次到点才 `net_socket`+`net_bind` 本地口 0 = 芯片动态分配),断网代际变化、`enable=0` 或内核错误码时关闭归还;不监听、不参与端口台账。 ### 15.6 客户端实现要点(避坑) @@ -415,3 +419,22 @@ PC 端 `test/host/time_sync_server.py` 发送 8 字节后**必须保持连接打 ``` PC 端 `python test/host/time_sync_server.py` 每 `interval` 秒推送一次,epoch 随电脑本地时间递增即表示同步正常。 + +--- + +## 16. SNTP 校时(客户端,2026-11-04 落地) + +- 模块:`App/sync/time_sntp.c/.h`(挂在 timeSyncTask 的 8888 事件循环里,等待切片 ≤50 ms,不饿任务狗); + 48 字节报文编解码 = vendored coreSNTP 序列化器(`Lib/coreSNTP/`,MIT,零堆),节奏与准入判据在本方。 +- 配置:`/config.ini` `[sntp]` 段(`enable` / `server1` / `server2` / `poll_min`,属主 `Drivers/BSP/NETCFG/sntp_cfg.c`, + 注册在 `net_cfg_load()`);现场改法 = UI「通讯参数 → SNTP 设置」页(密码叶)保存后随下一轮生效,或 FTP 改文件(4 s 落定)。 + 出厂默认**不启用**、服务器为空——不配不发任何 UDP 包。 +- 时钟面换算:SNTP 给的是 UTC,落库前加固定东八区偏移(`SNTP_TZ_LOCAL_OFFSET_SECS 28800`)写 `sys_clock_set_unix()` + (连带回写 SD2506)——这正是 §1.1 裁定「任何来源写入前先换算成本地钟面」的第三个写入口;装置仍不存时区。 +- 判据(RFC 4330 §10 口径):OriginTimestamp 逐位等于发出时的 T1(含低 16 位随机小数位,防伪造/防重放); + mode=4、stratum 1~15、闰秒指示 ≠3、服务器 Kiss-o'-Death 按 DENY/RSTR(换服务器)与 RATE(退避)分流; + 响应源地址必须等于请求目的地址。失败:超时指数退避(5 s 起、60 s 封顶)+ 主备轮换;成功:恢复 `poll_min` 周期。 +- 步进策略:|offset| < 2 s 只记日志不步进;≥ 2 s 或冷启动直接步进并留痕 `[SNTP] step offset=`、`syslog` OPER + 「clock set by SNTP」;首个校验通过的同步置 `SYS_EVT_TIMESYNC_READY`(就绪位,不覆盖后续步进语义)。 +- 与 8888 的关系:同一任务、同一 `sys_clock_set_unix` 收口,**后写者赢**维持现状(两路并存是部署选择,不是冲突缺陷)。 +- 主机单测:`test/host/sntp/`(报文格式、防伪拒收、KoD、退避与周期、阶跃域守卫、配置注册表)。 diff --git a/docs/子系统设计/网络配置说明.md b/docs/子系统设计/网络配置说明.md index 375d7173..622a1f61 100644 --- a/docs/子系统设计/网络配置说明.md +++ b/docs/子系统设计/网络配置说明.md @@ -197,6 +197,37 @@ write=1 [adc_cal] 9 键 / [adc_cal_ofs] 8 键 / [run_par] 9 键 / [comm] 2 键);段/键上限 的机器钉 = `test/host/cfg_file` TC-CONFIG-056/057。 +### 3.7 `[sntp]` 段:SNTP 校时参数(2026-11-04 新增,板端 SNTP 客户端接入,#57 调研结论) + +板端 SNTP 客户端的持久载体(消费方 = `App/sync/time_sntp.c`,挂在 timeSyncTask +既有循环上,48 字节报文走 PR #74 后的 UDP socket 通道;数据本体 = +`Drivers/BSP/NETCFG/sntp_cfg.h/.c`(纯段数据 + 读写出口,不依赖引擎头——分层门禁 +layer-deps 口径),段描述符由 `net_cfg.c` 的 `s_sntp_register()` 组装,注册编排 = +`net_cfg_load()` 统一调用——与 [ftp] 同路径,「先注册后 load」成立): + +| 键 | 含义 | 工厂默认 | getter 合法带(带外自愈回默认) | +|---|---|---|---| +| `enable` | 校时开关 | **0(出厂关闭)** | 0/1,非 1 一律视为关 | +| `server1` | 主 SNTP 服务器 | **0.0.0.0(留空)** | 点分 IPv4 字面量(引擎 `s_decode_ip4` 解析;**无 DNS**) | +| `server2` | 备 SNTP 服务器 | 0.0.0.0 | 同上;主服务器超时/拒绝(KoD DENY·RSTR)自动切换 | +| `poll_min` | 轮询周期(分钟) | 60 | 1 ~ 1440(带外读回 60,**不写回 RAM**,`sntp_cfg_poll_min()`) | + +- **零足迹口径**:出厂默认关闭 + 服务器留空 → 不开 socket、不发一个字节(与 + 「FTP 未配置不监听」一致);配了可达时服务器的现场才启用。 +- **生效链路**:页面保存行 → `SYS_EVT_CFG_SAVE` 整档外科落盘(引擎属主 defaultTask + 不变);客户端每轮直接读 RAM 副本,**无段回调、不新增事件位**。网络重建 + (代次变化)时 UDP socket 随芯片复位被弃用,客户端检测 `net_get_generation()` + 变化后关闭旧 fd 并**重新 1~5 分钟随机散布**首轮(RFC 4330 §10 口径)—— + 即 §4.2 自愈机制在 UDP 客户端侧的自然延伸(表内 TCP 三服务之外)。 +- **时钟面口径**:校验通过的服务器时间折算为 UTC 后**恒加 28800 s**(本地 + UTC+8,单一「本地钟面」口径,#56 裁决,见 + `docs/子系统设计/系统墙钟时间维护说明.md` §1.1;工程不存时区)再交 + `sys_clock_set_unix()`(内部含 SD2506 RTC 写回);`|offset| ≤ 2 s` 只记录 + 不拨针(防 RTC 无谓磨损)。冷启动(钟面无效)直接采用服务器 T3。 + 防欺骗/退避/KoD 细则 → `App/sync/time_sntp.c` 文件头 + + `test/host/sntp`(TC 命名即判据)。 +- 容量台账:本段上线后 `/config.ini` 受管段 = **7/8**(前 6 段见 §3.6,本段 4 键)。 + --- ## 4. 生效链路 @@ -302,7 +333,8 @@ ENTER 进入选中地址行 → `↑/↓` 调活动字节(0..255 **环绕**) | 文件 | 职责 | |---|---| -| `Drivers/BSP/NETCFG/net_cfg.h/.c` | 配置核心:RAM 副本 + 工厂默认 + 加载/保存编排 + MAC 影子 + IPv4 格式化([network] 段注册) | +| `Drivers/BSP/NETCFG/net_cfg.h/.c` | 配置核心:RAM 副本 + 工厂默认 + 加载/保存编排 + MAC 影子 + IPv4 格式化([network] 段注册;`net_cfg_load()` 一并注册 [ftp]/[sntp]) | +| `Drivers/BSP/NETCFG/sntp_cfg.h/.c` | [sntp] 段核心:RAM 副本 + 工厂默认 + 注册 + `poll_min` 带外自愈出口(`App/sync/time_sntp.c` 每轮只读) | | `App/cfg/cfg_file.h/.c` | 通用配置文件引擎(双端共享核心:解析/校验/外科保存/模板/沉降轮询) | | `App/cfg/cfg_file_store.h` | 存储后端接口(带路径的文件操作) | | `App/cfg/cfg_file_store_mcu.c` | MCU 后端:FatFS `/config.ini`(`/config.TMP` + `f_rename` 原子写) | diff --git a/docs/子系统设计/配置文件设计说明.md b/docs/子系统设计/配置文件设计说明.md index fc7d7561..6fdbc4b7 100644 --- a/docs/子系统设计/配置文件设计说明.md +++ b/docs/子系统设计/配置文件设计说明.md @@ -156,6 +156,7 @@ mac=00:00:00:00:00:00 | `App/util/sys_clock.c/.h` | 修改 | MCU | 新增 `sys_clock_get_time()`(日历时间获取器);改归 ④中间件层,自 `App/` 移入 `App/util/` | | `Drivers/BSP/NETCFG/net_cfg.h/.c` | 重构 | 双端 | `net_cfg_*` API 不变;load/save 走 cfg_file;注册 `[network]` 段 | | `App/protocol/iec104/iec104_comm.h/.c` | 新增(2026-10-26 道G task-8) | 双端(模拟器编译但无消费路径) | 配对模块注册 `[comm]` 段(104 ca/端口,ca 1..65535 / port 1024..65535 合法带 getter 自愈);内核前 `iec104_comm_init()` 注册(同 [adc_cal]/[run_par] 先注册后 load 契约);生效 = 改后重启(键表与理由 → `docs/子系统设计/网络配置说明.md` §3.6);**不走 scripts 白名单**——`.c` 按 new-source-file 口径同笔进 uvprojx + `simulator/CMakeLists.txt` | +| `Drivers/BSP/NETCFG/sntp_cfg.h/.c` | **新增**(2026-11-04 板端 SNTP 接入) | 双端(模拟器编译但无 sync 消费路径) | 注册 `[sntp]` 段(enable / server1 / server2 IPv4 字面量 / poll_min 1..1440 getter 带外自愈,iec104_comm 先例);`net_cfg_load()` 内与 [ftp] 同编排注册;消费方 `App/sync/time_sntp.c`(MCU 单端链接);键表与生效口径 → `docs/子系统设计/网络配置说明.md` §3.7;new-source-file 口径 uvprojx + `simulator/CMakeLists.txt` 双登记 | | `Drivers/BSP/NETCFG/net_cfg_legacy.c` | **已删除**(2026-10-10;曾于 P3 新增) | — | 旧 NCFG blob 解析 + 迁移;随"不保留旧档兼容"整体移除(见 §9.1) | | `Drivers/BSP/NETCFG/net_cfg_store.h` / `net_cfg_store_mcu.c` | **删除** | — | 由 `cfg_file_store.*` 取代 | | `simulator/sim/net_cfg_store_sim.c` | **删除** | 模拟器 | 由 `cfg_file_store_sim.c` 取代 | @@ -589,3 +590,4 @@ void adc_cfg_init(void) | 2026-10-04 | **P0-1 真机独立复跑 PASS(二次确认,代码零改动)**——`@build` 0/0(增量,无源文件新于镜像)→ `@test` **VERDICT PASS(rc=0)**:`[2026-10-04 00:01:21][CFG] load: OK (523B, heap free 8840B)` + `[NET_CFG] load: OK ip=192.168.1.100`,Forbidden `FAIL`/`ERROR`/`HardFault`/`[ERR]` 零命中,`selftest: no fault record`(先清掉 2026-10-02/03 故障注入窗口遗留的 67 条台账)→ ST-LINK 全片回读 `No difference found`(`0x08000000~0x08040F9B`)。**本次为 `待处理内容.md` §4.1「待验证」唯一未闭环项(P0-1)的收口**,该节现已清空 | | 2026-10-10 | v1.14:**删除旧档 NCFG 迁移(D10 撤销)**——`Drivers/BSP/NETCFG/net_cfg_legacy.{c,h}` 整体移除、`cfg_store_legacy_path()` 撤销(MCU / 模拟器后端与主机单测 `test/host/cfg_file/cfg_file_test.c` 同步删除),`net_cfg_load` 不再调用 `net_cfg_legacy_migrate()`;旧档 `/cfg/netcfg.bin` 若存在则被**忽略**(不读、不删、不迁移),配置回落工厂默认。理由:该一次性路径把整条配置写链挂进启动期调用图,defaultTask 静态最坏栈链 **+600 B**(该链整体 5312 → 2776 B,另三个杠杆见 `docs/内存管理/RAM使用分析与优化方案.md`),而本仓库不存在携带旧档的设备。连带影响:`App/util` 的 CRC-16/MODBUS 失去唯一调用者(现为零调用者);模拟器 `--cfgtest` 不再覆盖迁移场景;§2 D10 / §3.5 / §4.2 / §5.1 / §6.2 / §6.4 / §7 / §9 / §11.1 同步 | | 2026-10-26 | v1.15:**新增 `[comm]` 段(道G task-8,「UI 补齐」波次里的非 UI 前置项)**——`App/protocol/iec104/iec104_comm.h/.c` 配对模块(104 公共地址 / 监听端口 2 键,getter 合法带自愈,adc_cal 先例);内核前 `iec104_comm_init()` 注册([adc_cal] / [run_par] 同「先注册后 load」契约);生效 = **改后重启**(不挂回调、不新增事件位,区别于 [network] 免重启重建);段占位 5→6 / 8,键表与主站地址不做理由 → `docs/子系统设计/网络配置说明.md` §3.6;机器钉 = `test/host/cfg_file` TC-CONFIG-056/057(段/键上限)+ `test/host/iec104` TC-IEC104-031~033(模板/重载/带外/ca 端到端) +| 2026-11-04 | v1.16:**新增 `[sntp]` 段(板端 SNTP 客户端接入,#57 调研结论落地)**——`Drivers/BSP/NETCFG/sntp_cfg.h/.c`(enable / server1 / server2 / poll_min 4 键;出厂默认**关闭 + 服务器留空 + 60 分钟** = "未配置零网络足迹"口径;`poll_min` 带外自愈 1..1440 → 60 不写回 RAM);数据本体 = `Drivers/BSP/NETCFG/sntp_cfg.h/.c`(不依赖引擎头,分层门禁 layer-deps 口径),段描述符由 `net_cfg.c` 的 `s_sntp_register()` 组装,注册编排 = `net_cfg_load()`(与 [ftp] 同编排,先注册后 load);消费方 = `App/sync/time_sntp.c`(挂 timeSyncTask 既有循环,vendor `Lib/coreSNTP` serializer 做 48B 报文编解码/防欺骗校验);保存立即生效 = 每轮直读 RAM 副本 + 网络代次重建弃 UDP socket 重散布,**无段回调、不新增事件位**;段占位 6→7 / 8;键表与生效细则 → `docs/子系统设计/网络配置说明.md` §3.7;机器钉 = `test/host/net_cfg` TC-NETCFG-005/006b..d/007(段注册契约)+ `test/host/sntp`(RFC 4330 §10 节奏 / §5.3 防欺骗 / §5.5 步进策略 / poll 钳位) diff --git a/docs/测试说明/单元测试/主机单测覆盖率口径.md b/docs/测试说明/单元测试/主机单测覆盖率口径.md index b81a99bb..b242229a 100644 --- a/docs/测试说明/单元测试/主机单测覆盖率口径.md +++ b/docs/测试说明/单元测试/主机单测覆盖率口径.md @@ -736,7 +736,7 @@ uint16_t ch395f_link_change_count(void); | 类别 | 体量 | 为什么不做 | 那它靠什么把关 | |---|---|---|---| | 芯片驱动(寄存器/时序) | `ch395f.c`(801)、`gd5f2gq5ue.c`(689)、`sd2506.c`(264)、`tpafe5160.c`(157)、`rs485.c`(79) = **1,990 行** | 把 HAL 与寄存器假到底 ≈ **重写一遍驱动**,测的是自己的桩而不是硬件行为(SPI 分频必须 8、DMA 流、I2C 时序在桩里怎么编都"对") | 板级套件(`board-test-suite` skill)+ `/serial-boot-test` 真机日志 + 4 个 Fault 现场;陷阱逐条登记在 `docs/问题记录/CH395F_Trap_Records.md`、`docs/问题记录/GD5F2GQ5UE_Trap_Records.md`、`docs/问题记录/SD2506_Trap_Records.md` | -| UI 侧 | 17 个 `.c` = **1,942 行** | 与 PC 模拟器是**同一份源码**,行为回归已由 `ui-golden`(菜单 8 状态 + 内容页 66 张像素黄金参考)覆盖;再写单测是第三份重复投资 | `ui-verify` skill 五步 + `docs/UI设计/UI_自动化测试流程.md` | +| UI 侧 | 18 个 `.c` = **2,373 行**(2026-11-04 随 `ui_page_sntp.c` 从 17 个/1,942 行递增) | 与 PC 模拟器是**同一份源码**,行为回归已由 `ui-golden`(菜单 8 状态 + 内容页 74 张像素黄金参考)覆盖;再写单测是第三份重复投资 | `ui-verify` skill 五步 + `docs/UI设计/UI_自动化测试流程.md` | | 任务编排 | `default_task.c`(281)、`adc_task.c`(171)、`net_task.c`(123)、`protect_task.c`(90)、`ui_task.c`(71)、`rs485_task.c`(11) = **747 行** | 每个只有 4~5 个函数,主体是"按顺序调别人";真逻辑在被调模块里(那些已测或在 P1/P2)。给编排层写单测最容易写成"断言我调了几次"的自证 | 真机启动锚点计数 + IWDG `required_mask`(`/serial-boot-test`)+ `docs/架构设计/项目启动顺序管理.md` | | 内核前装配 | `app_main.c`(135) | 被测对象本身就是"`osKernelStart` 之前不许调内核 API"的时序,主机侧无法忠实复现(PC 上没有那个约束) | 同上 + `cubemx-survival` 审计 | diff --git a/docs/测试说明/流程与规范/测试开发标准流程.md b/docs/测试说明/流程与规范/测试开发标准流程.md index cf229d22..f5f1fbf3 100644 --- a/docs/测试说明/流程与规范/测试开发标准流程.md +++ b/docs/测试说明/流程与规范/测试开发标准流程.md @@ -373,7 +373,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 | 层 | 载体 | 抓住什么 | |---|---|---| | 文本层 | `check_content_pages.py` 字形模板匹配(310 条、覆盖当时 37 页,解析字体 cmap / glyph_dsc / bitmap) | 缺字显示空白 / 标签写错等**静默失败**(不依赖 `ref/`,先于像素比对运行、失败即中止防误刷参考) | -| 像素层 | 内容页黄金参考 66 张(64 页)+ 菜单 8 状态,与黄金参考逐字节比对 | 几何 / 样式 / 光标反显 / 整体外观 | +| 像素层 | 内容页黄金参考 74 张 + 菜单 8 状态,与黄金参考逐字节比对 | 几何 / 样式 / 光标反显 / 整体外观 | | 池水位层 | `MENU_POOL_BASELINE=5368 B` / `POOL_AFTER_ROUNDTRIP=5448 B` | LVGL 内存池泄漏 / 结构性漂移(量的是 `used`,与 `LV_MEM_SIZE` 总大小无关) | ### 5.3 黄金参考与字体纪律 diff --git a/docs/测试说明/流程与规范/自动化测试流程.md b/docs/测试说明/流程与规范/自动化测试流程.md index 2d7f1f24..0a5a1a35 100644 --- a/docs/测试说明/流程与规范/自动化测试流程.md +++ b/docs/测试说明/流程与规范/自动化测试流程.md @@ -119,7 +119,7 @@ - **初始化回归**:`App/task/ui_task.c` 已有 `UI port init: OK` / `UI menu init: OK` / `UI net sink: started` 打印(见 `run_boot_test.py` 命令示例);将其加入 `--required` 即可经串口自动回归 UI 初始化。 -- **屏幕内容回归**:由 LVGL 模拟器像素回归覆盖(文本级断言 + 66 张黄金参考 + 池水位门禁, +- **屏幕内容回归**:由 LVGL 模拟器像素回归覆盖(文本级断言 + 74 张黄金参考 + 池水位门禁, 见 `docs/UI设计/UI_自动化测试流程.md`);上机屏幕仍需人工目检(见 `测试开发标准流程.md` §1)。 ## 扩展用法 diff --git a/docs/门禁说明/README.md b/docs/门禁说明/README.md index 227c9da6..8790d2c4 100644 --- a/docs/门禁说明/README.md +++ b/docs/门禁说明/README.md @@ -80,7 +80,7 @@ | [`cubemx-survival.md`](cubemx-survival.md) | `cubemx-survival` | CubeMX 重生成会不会把定制静默抹掉(人工目检审计兜底) | `.ioc`、`Src/`、`Inc/` | 已写 | | [`test-suite-switch.md`](test-suite-switch.md) | `test-suite-switch` | 板级套件开关必须互斥;全注释 = 产品构建 | `test/board/test_config.h` | 已写 | | [`font-charset.md`](font-charset.md) | `font-charset` | UI 用到的汉字是否都在子集字体里(缺字 = 白框) | `Drivers/BSP/UI/**`、字体脚本 | 已写 | -| [`ui-golden.md`](ui-golden.md) | `ui-golden` | UI 像素回归:菜单 8 状态 + 内容页 66 张黄金参考图比对 | UI 源码、模拟器 | 已写 | +| [`ui-golden.md`](ui-golden.md) | `ui-golden` | UI 像素回归:菜单 8 状态 + 内容页 74 张黄金参考图比对 | UI 源码、模拟器 | 已写 | | [`bom-guard.md`](bom-guard.md) | `bom-guard` | 带 BOM 的文件被编辑工具剥掉 BOM(ARMCC 换解码 → 假语法错) | `Drivers/BSP/UI/**` 等名单内文件 | 已写 | | [`setview-check.md`](setview-check.md) | `setview-check` | 定值查看行表与 `prot_set` 枚举序 / `g_prot_set_meta` 上下限是否同一份口径 | `tools/setview_check/**`、`ui_w_setview.*` | 已写 | | (专篇待补,档位与限度正文在 [`../UI设计/UI_自动化测试流程.md`](../UI设计/UI_自动化测试流程.md) §7.3) | `uinav-selftest` | 板端 UI 导航器认屏快检(黄金参考帧分类;完整闭环档=合入前/上板前手工跑,快检不互替) | `scripts/board_ui_nav.py`、`simulator/tests/ref/*.bin` | 指针 | diff --git a/docs/门禁说明/ui-golden.md b/docs/门禁说明/ui-golden.md index c3df7578..3019917c 100644 --- a/docs/门禁说明/ui-golden.md +++ b/docs/门禁说明/ui-golden.md @@ -1,4 +1,4 @@ -# `ui-golden` — UI 像素回归(菜单 8 状态 + 内容页 66 张黄金参考) +# `ui-golden` — UI 像素回归(菜单 8 状态 + 内容页 74 张黄金参考) > 一句话:改 UI 源码时,把模拟器增量重编 + 两条 golden 回归串成一次提交门禁,**像素对不上就拦**。 > 它是包装器:真正的判据在被包装的两条脚本与参考图里。 @@ -11,7 +11,7 @@ ## 1. 它在查什么 - 菜单侧:整棵菜单树的行渲染结果(8 个状态)与参考图逐像素比对; -- 内容页侧:66 张黄金参考(= 66 个截图态条目,同一内容页的多个状态各一张:分页页多拍的第 2 屏与确认链/录波的各态都单独计条目),逐张与参考图比对; +- 内容页侧:74 张黄金参考(= 74 个截图态条目,同一内容页的多个状态各一张:分页页多拍的第 2 屏与确认链/录波的各态都单独计条目),逐张与参考图比对; - 参考图 `simulator/tests/ref/*.bin` **就是判据本身**。 ## 2. 为什么要它(有牙的实测) @@ -57,7 +57,7 @@ 1. **参考图即判据**:刷新参考图 = 修改判据。评审时必须盯 `simulator/tests/ref/` 的 diff; 一旦"顺手更新",此后所有回归都只在和错的基线比。**门禁无法阻止这件事**,只能靠评审。 -2. 只覆盖 8 个菜单状态 + 66 张内容页黄金参考:未列入的状态(密码错误态、超长文本、缺字渲染、 +2. 只覆盖 8 个菜单状态 + 74 张内容页黄金参考:未列入的状态(密码错误态、超长文本、缺字渲染、 无 SD 卡 / 挂载失败的降级显示)**不会有任何回归**。 3. 跑的是**模拟器那份配置**,不是固件那份 —— 两份 `lv_conf.h` 漂移时,回归结论不能外推到固件 (所以 `lv-conf-sync` 是本门有效性的前提)。 diff --git a/scripts/board_ui_nav.py b/scripts/board_ui_nav.py index e99cd8f8..a797f519 100644 --- a/scripts/board_ui_nav.py +++ b/scripts/board_ui_nav.py @@ -1247,7 +1247,7 @@ def selftest_board_frames(clf): def selftest_refs(clf): - """仓库黄金参考(66 张)逐帧分类自检:每张图的期望值 = 它的文件名。 + """仓库黄金参考(74 张)逐帧分类自检:每张图的期望值 = 它的文件名。 这一组是**语义正确的基线**(模拟器与板端同一份 UI 源码),用来把"分类器错了"与 "留档帧本来就是走歪的"分开——两类失败含义完全不同,混在一起就只能靠猜。 diff --git a/scripts/check_registration.py b/scripts/check_registration.py index 51139663..8207415c 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -105,6 +105,7 @@ SIM_EXEMPT = [ ("App/fs/vol_idle_sync.c", "MCU 存储属主后台空闲同步(FatFs disk_ioctl + 卷锁 + sys_events),主机侧纯逻辑单测覆盖于 test/host/vol_idle_sync/,模拟器无消费路径"), ("App/log/console_log.c", "MCU 控制台输出,模拟器无消费路径"), ("App/sync/time_sync.c", "对时任务(net+RTOS),MCU 专属"), + ("App/sync/time_sntp.c", "SNTP 校时客户端(net+RTOS+sys_clock 依赖,2026-11-04 立项落地),MCU 专属;主机侧纯逻辑单测覆盖于 test/host/sntp"), ("App/task/adc_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"), ("App/task/default_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"), ("App/task/iec104_serv.c", "固件 FreeRTOS 任务(104 上行,B4),模拟器不跑固件任务"), diff --git a/scripts/ram_baseline.py b/scripts/ram_baseline.py index 004c7cb0..03ef3d56 100644 --- a/scripts/ram_baseline.py +++ b/scripts/ram_baseline.py @@ -85,6 +85,7 @@ def subsystem_of(obj): "task_prof", "boot_stats", "vol_idle_sync", "sys_", "cfg_", "dbg_", "console_log", "task_watchdog", "sha256", "tpafe", "sd2506", "test_task", "time_sync", + "time_sntp", "default_task")): return "应用" return "其它" diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 14f77b59..adea5a7c 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -16,4 +16,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-30 246720 29836 2052 149708 276556 26.4 94336 72.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 2026-11-02 247548 29836 2052 149740 277384 26.5 94368 72.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 2026-11-03 249112 29836 2052 149996 278948 26.6 94624 72.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 -2026-11-04 249120 29836 2052 149996 278956 26.6 94624 72.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 +2026-11-04 254648 30152 2072 150088 284800 27.2 94736 72.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index 8074a293..079de918 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,25 +1,25 @@ { - "built_at": "2026-11-04T09:25:23+08:00", - "fingerprint": "2f112c1679d6145a49f70f6f208ade9a1e4fa573aad3cacdaa98b0ed2aac6855", - "fingerprint_files": 1468, + "built_at": "2026-11-04T16:13:16+08:00", + "fingerprint": "b17ef8ac89e3f05d556ab890b1e0cc29c45f77c5739449187de0e5d3e4314b3a", + "fingerprint_files": 1479, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "f10bb8df659e201efbacf04978c7442493fe94df", + "git_head": "912c16b75744cd0679b4f489696c02867d4b6f7a", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1793753528, + "map_mtime": 1793779933, "pct": { - "ER_IROM1": 26.6, + "ER_IROM1": 27.16, "RW_CCM": 87.89, - "RW_IRAM1": 72.19, + "RW_IRAM1": 72.28, "UNINIT_CCM": 18.75 }, "regions": { "ER_IROM1": { "max": 1048576, - "size": 278956 + "size": 284800 }, "RW_CCM": { "max": 65280, @@ -27,7 +27,7 @@ }, "RW_IRAM1": { "max": 131072, - "size": 94624 + "size": 94736 }, "UNINIT_CCM": { "max": 256, @@ -37,10 +37,10 @@ "schema": 2, "scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring", "sizes": { - "code": 249120, - "ro_data": 29836, - "rw_data": 2052, - "zi_data": 149996 + "code": 254648, + "ro_data": 30152, + "rw_data": 2072, + "zi_data": 150088 }, "threshold_pct": 92.0, "worktree_dirt_at_stamp": 0, diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index 327c933b..cae6e139 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -208,13 +208,21 @@ "lines_covered": 227, "lines_valid": 255 }, + "App/sync/time_sntp.c": { + "func_rate": 1.0, + "funcs_covered": 18, + "funcs_valid": 18, + "line_rate": 0.9153, + "lines_covered": 227, + "lines_valid": 248 + }, "App/sync/time_sync.c": { "func_rate": 1.0, "funcs_covered": 4, "funcs_valid": 4, "line_rate": 1.0, - "lines_covered": 100, - "lines_valid": 100 + "lines_covered": 101, + "lines_valid": 101 }, "App/task/iec104_serv.c": { "func_rate": 0.75, @@ -369,12 +377,20 @@ "lines_valid": 36 }, "Drivers/BSP/NETCFG/net_cfg.c": { - "func_rate": 0.9375, - "funcs_covered": 15, - "funcs_valid": 16, - "line_rate": 0.9643, - "lines_covered": 108, - "lines_valid": 112 + "func_rate": 0.9412, + "funcs_covered": 16, + "funcs_valid": 17, + "line_rate": 0.9685, + "lines_covered": 123, + "lines_valid": 127 + }, + "Drivers/BSP/NETCFG/sntp_cfg.c": { + "func_rate": 1.0, + "funcs_covered": 5, + "funcs_valid": 5, + "line_rate": 0.9474, + "lines_covered": 18, + "lines_valid": 19 }, "Drivers/BSP/ScreenLink/sb_link.c": { "func_rate": 1.0, @@ -560,9 +576,10 @@ }, "net_cfg": { "files": { - "Drivers/BSP/NETCFG/net_cfg.c": 112 + "Drivers/BSP/NETCFG/net_cfg.c": 127, + "Drivers/BSP/NETCFG/sntp_cfg.c": 19 }, - "lines_total": 112, + "lines_total": 146, "ok": true }, "net_events": { @@ -685,6 +702,14 @@ "lines_total": 114, "ok": true }, + "sntp": { + "files": { + "App/sync/time_sntp.c": 248, + "Drivers/BSP/NETCFG/sntp_cfg.c": 19 + }, + "lines_total": 267, + "ok": true + }, "soe": { "files": { "App/log/soe_rec.c": 87, @@ -724,9 +749,9 @@ }, "time_sync": { "files": { - "App/sync/time_sync.c": 100 + "App/sync/time_sync.c": 101 }, - "lines_total": 100, + "lines_total": 101, "ok": true }, "ui_data": { @@ -818,6 +843,7 @@ "App/protocol/iec104/iec104_comm.c", "App/protocol/iec104/iec104_link.c", "App/protocol/iec104/iec104_pts.c", + "App/sync/time_sntp.c", "App/sync/time_sync.c", "App/task/iec104_serv.c", "App/util/boot_stats.c", @@ -839,6 +865,7 @@ "Drivers/BSP/NET/net_udp.c", "Drivers/BSP/NETCFG/ftp_cfg.c", "Drivers/BSP/NETCFG/net_cfg.c", + "Drivers/BSP/NETCFG/sntp_cfg.c", "Drivers/BSP/ScreenLink/sb_link.c", "Drivers/BSP/ScreenLink/sb_proto.c", "Drivers/BSP/UI/ui_data.c", @@ -847,16 +874,16 @@ ], "totals": { "files_generated_excluded": 5, - "files_tested": 52, - "files_untested": 173, - "func_rate": 0.9266, - "funcs_covered": 619, - "funcs_valid": 668, - "line_rate": 0.9012, - "lines_covered": 8372, - "lines_valid": 9290, - "modules_ok": 47, - "modules_total": 47 + "files_tested": 54, + "files_untested": 177, + "func_rate": 0.9292, + "funcs_covered": 643, + "funcs_valid": 692, + "line_rate": 0.9018, + "lines_covered": 8633, + "lines_valid": 9573, + "modules_ok": 48, + "modules_total": 48 }, "untested_sources": [ { @@ -903,6 +930,10 @@ "code_lines": 264, "path": "Drivers/BSP/SD2506/sd2506.c" }, + { + "code_lines": 261, + "path": "Drivers/BSP/UI/ui_page_sntp.c" + }, { "code_lines": 251, "path": "Drivers/BSP/UI/ui_page_wave_play.c" @@ -1219,6 +1250,10 @@ "code_lines": 26, "path": "Drivers/BSP/UI/ui_port.h" }, + { + "code_lines": 25, + "path": "Drivers/BSP/NETCFG/sntp_cfg.h" + }, { "code_lines": 25, "path": "Drivers/BSP/bsp_err.h" @@ -1355,6 +1390,10 @@ "code_lines": 12, "path": "App/app/app_main.h" }, + { + "code_lines": 12, + "path": "App/sync/time_sntp.h" + }, { "code_lines": 12, "path": "App/task/iec104_serv.h" @@ -1427,6 +1466,10 @@ "code_lines": 11, "path": "Drivers/BSP/UI/ui_page_reset.h" }, + { + "code_lines": 11, + "path": "Drivers/BSP/UI/ui_page_sntp.h" + }, { "code_lines": 11, "path": "Drivers/BSP/UI/ui_page_soe.h" diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index e226f970..6a49a786 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -126,6 +126,7 @@ set(UI_SOURCES ${UI_DIR}/ui_page_yk.c ${UI_DIR}/ui_page_wave_manual.c ${UI_DIR}/ui_page_wave_play.c + ${UI_DIR}/ui_page_sntp.c ) # 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程) @@ -137,6 +138,7 @@ set(SYSLOG_SOURCES set(NETCFG_SOURCES ${NETCFG_DIR}/net_cfg.c ${NETCFG_DIR}/ftp_cfg.c + ${NETCFG_DIR}/sntp_cfg.c ${PROJECT_ROOT}/App/cfg/cfg_file.c sim/cfg_file_store_sim.c ) diff --git a/simulator/README.md b/simulator/README.md index be53fbd2..a5eece6c 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -53,7 +53,7 @@ simulator/ | `build_sim.bat` / `build_sim.ps1` | 一键构建入口:vswhere 探测本机 VS 大版本(16=2019 / 17=2022 / 18=2026,取已装最高)→ 映射 CMake 生成器;`build/` 缓存生成器与本机不符(换机/升级 VS)时自动删除重建(带重试,抗文件锁)。**构建入口一律走它,文档/命令中勿写死生成器版本**;`.bat` 仅负责绕过 PowerShell 执行策略并转交 `.ps1` | | `CMakeLists.txt` | 收集 LVGL 源码(`../Lib/lvgl`)+ 共享 UI 源码(`../Drivers/BSP/UI`)+ 通用配置文件引擎(`../App/cfg_file.c`、`../Drivers/BSP/NETCFG` 的 net_cfg;`net_cfg_legacy` 已于 2026-10-10 删除)+ `sim/` 平台层,链接预编译 SDL2,产出 `lvgl_sim.exe` | | `i1_to_png.py` | 把 `--screenshot` 产出的 I1(1bpp) 原始位图转成 PNG,便于离线比对设计稿。详见 §3.5 | -| `tests/` | 无头回归脚本(菜单 8 状态像素回归 + 内容页 66 张:文本断言 / 反向断言 / 黄金参考逐字节比对);`ref/shot_*.bin` **随仓库入库**(判据本体,改动等于改判据,评审须逐页过目),`ref/*.png` 是本地派生的目检图(`.gitignore` 挡掉)。详见 §3.6 | +| `tests/` | 无头回归脚本(菜单 8 状态像素回归 + 内容页 74 张:文本断言 / 反向断言 / 黄金参考逐字节比对);`ref/shot_*.bin` **随仓库入库**(判据本体,改动等于改判据,评审须逐页过目),`ref/*.png` 是本地派生的目检图(`.gitignore` 挡掉)。详见 §3.6 | > **共享代码不复制**:`ui_menu.c` / `ui_font_cn_12.c` / `ui_net_sink.c` 直接引用 `Drivers/BSP/UI/` 下的原件, > MCU 与 PC 两端永远同步。**不要**在本目录内复制 UI 源码。 @@ -222,7 +222,7 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI设计/UI_自动化 | 脚本 | 原理 | 失败输出 | |------|------|----------| | `check_menu_rows.py` | 键序注入(u/d/e/s)跑 8 个菜单状态,断言选中行白带、非选行黑底白字、框体边框、往返状态逐像素一致;MENU-STATE-00 池水位须等于脚本内 `MENU_POOL_BASELINE` | 未过断言清单 + 保留临时截图 | -| `check_content_pages.py` | ① 文本断言(解析字体字形,在截图 y 带内精确匹配期望串,不依赖 `ref/`)② **反向断言**(`ABSENT` 表:某串**不该**出现,如查看页/只读态不得出现「未标定」徽标与「保存」行——像素参考会被 `--update` 刷成"自证正确",反向断言不依赖参考图)③ **66 张**无头截图与 `tests/ref/shot_*.bin` **逐字节**比对(含 2026-10-26 新增的整定 9 态:进页 / 保存行 / 只读态 / 编辑态 / 越界 / 保存 OK / 落定后可读 / 二态回绕);另校验开页-关页往返池水位 = `POOL_AFTER_ROUNDTRIP` | 文本未命中项 / 反向断言被命中 / 差异像素数 + 双方 ink runs 对照 | +| `check_content_pages.py` | ① 文本断言(解析字体字形,在截图 y 带内精确匹配期望串,不依赖 `ref/`)② **反向断言**(`ABSENT` 表:某串**不该**出现,如查看页/只读态不得出现「未标定」徽标与「保存」行——像素参考会被 `--update` 刷成"自证正确",反向断言不依赖参考图)③ **74 张**无头截图与 `tests/ref/shot_*.bin` **逐字节**比对(含 2026-10-26 新增的整定 9 态:进页 / 保存行 / 只读态 / 编辑态 / 越界 / 保存 OK / 落定后可读 / 二态回绕);另校验开页-关页往返池水位 = `POOL_AFTER_ROUNDTRIP` | 文本未命中项 / 反向断言被命中 / 差异像素数 + 双方 ink runs 对照 | | `check_remo_sink.py` | 启动**窗口**模拟器(非无头,`CREATE_NEW_PROCESS_GROUP` 防管道阻塞),经 TCP 7003 发 LCDMEM 取 1bpp 帧;断言首帧=LCDMEM(无 INIT 污染)/位图 3200 字节/非平凡;复用 `i1_to_png` 出目检 PNG;与无头参考(`lvgl_sim --screenshot`)逐位比对 <2% 判 MATCH。守护"INIT 污染 / 快照卡死"两个历史缺陷 | 各判据失败原因 + `sim_err.txt` 尾部 + 保留 PNG(默认不删,`--clean` 才删) | - **黄金参考与目检 PNG**(`ref/*.bin` **入库**、`ref/*.png` 本地派生不入库):换机或想目检时—— diff --git a/simulator/tests/README.md b/simulator/tests/README.md index f0bdfa87..018ef2cb 100644 --- a/simulator/tests/README.md +++ b/simulator/tests/README.md @@ -11,7 +11,7 @@ | `check_content_pages.py` | **内容页回归**:各内容页渲染比对 | | `check_remo_sink.py` | **推流(RemoDispBus)回归**:接收端渲染比对 | | `regen_ref_png.py` | 由 `ref/*.bin` 批量出**目检 PNG**(不产生基准;基准由 `check_content_pages.py --update` 刷) | -| `ref/*.bin` | **像素黄金参考(入库)**:66 张 × 3200 B,160×160 单色打包(2026-10-29 录波 UI 道 +6 张) | +| `ref/*.bin` | **像素黄金参考(入库)**:74 张 × 3200 B,160×160 单色打包(2026-10-29 录波 UI 道 +6 张;2026-11-04 SNTP 设置页 +8 张) | ## 使用方式 diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py index 6ca5b2e3..0c9dcfa8 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -114,6 +114,17 @@ PAGES = [ ("netcfg_ro", "ddddde de e"), # 查看密码 000 → 只读态 ("netcfg_edit", "ddddde de uruuruuue e"), # 编辑模式:IP 首字节光标反显 ("netcfg_save", "ddddde de uruuruuue dddd e"), # 保存行 ENTER → 值列 OK + # ---- SNTP 设置页(node 31,通讯参数下第 3 项,#P 叶;[sntp] 工厂默认: + # enable=0 / 主备服务器 0.0.0.0 / 周期 60 分,见 Drivers/BSP/NETCFG/sntp_cfg.c) + # 导航同网口设置(ddddde 落通讯参数),再 dd 选到第 3 叶 SNTP设置。 + ("sntp", "ddddde dde uruuruuue"), # 首屏:工厂默认 + 保存行 ENTER + ("sntp_ro", "ddddde dde e"), # 查看密码 000 → 只读态 + ("sntp_edit", "ddddde dde uruuruuue d e"), # 主服务器编辑:首段字节光标反显 + ("sntp_oct", "ddddde dde uruuruuue d e r u e"), # 光标右移 + 段值 +1 → 000.001.000.000 + ("sntp_en", "ddddde dde uruuruuue e"), # 启用行 ENTER → 开启(开关行无编辑态) + ("sntp_poll", "ddddde dde uruuruuue ddd e"), # 周期行编辑:整值反显 + ("sntp_poll_big", "ddddde dde uruuruuue ddd e r e"), # ←→ ±60 粗调:60 → 120 分 + ("sntp_save", "ddddde dde uruuruuue e dddd e"), # 开启 + 保存行 ENTER → 值列 OK # ---- 道F task-3 装置维护三页(根菜单第 5 项 = 4 下;子项 0/1/3 = 时钟/复归/清除) # 键序读法与上面同源:e 进子菜单后再 e 打开叶子(叶子带 #P → 先落密码页), # "uruuruuue" = 修改口令 123(可编辑态),单一个 e = 查看口令 000(只读态)。 @@ -178,7 +189,7 @@ PAGES = [ # 计数范围/复位原因/主站接入 + 范围标签"本次开机",第 2 页钉 104 另外三枚(发送帧数 # 128 / 发送失败 0 / 接收字节 4096)与页脚真实页码 2/2。四枚假数据值互不相同是刻意的: # 行错位/串行只有"像素 + 逐行文本断言"两把尺子同时才照得出来(Lead 裁定 2026-10-28, - # 因此当时用例是 53+7=60 张,不是 53+6=59;现集合 66 张,见 PAGES 表)。 + # 因此当时用例是 53+7=60 张,不是 53+6=59;现集合 74 张,见 PAGES 表)。 ("runreport", "dddddde dddddddd e"), ("runreport_p2", "dddddde dddddddd e dddddddd"), # 页内再 8 下 → 第 2 页(选中落 r0=发送帧数) # ── 故障录波 UI 道(2026-10-29):手动录波三态 + 回放三态(真实波形/空态/坏段)── @@ -532,6 +543,55 @@ TEXTS = { ("r1名", "子网掩码", (38, 52), False), ("r4名", "保存", (80, 95), False), ], + # ---- SNTP 设置页:工厂默认 关闭 / 000.000.000.000 ×2 / 60 / ENTER。 + # 地址行为 net_cfg_fmt_ipv4 定长 15 字符(%03u 补零,与网口设置页同真源); + # 周期行是纯十进制(无补零);字节光标 = 黑底白字小段,编辑态文本断言 + # 只钉"名 + 选中带",被光标覆盖的段值改在退出编辑后的 sntp_oct 上钉全串。 + "sntp": [ + ("标题", "SNTP设置", (0, 24), False), + ("r0名", "启用", (24, 38), True), ("r0值", "关闭", (24, 38), True), + ("r1名", "主服务器", (38, 52), False), ("r1值", "000.000.000.000", (38, 52), False), + ("r2名", "备服务器", (52, 66), False), ("r2值", "000.000.000.000", (52, 66), False), + ("r3名", "周期(分)", (66, 80), False), ("r3值", "60", (66, 80), False), + ("r4名", "保存", (80, 95), False), ("r4值", "ENTER", (80, 95), False), + ("页脚", "第 1 页,共 1 页", (140, 160), False), + ], + "sntp_ro": [ + ("标题", "SNTP设置", (0, 24), False), + ("r0名", "启用", (24, 38), True), ("r0值", "关闭", (24, 38), True), + ("r1名", "主服务器", (38, 52), False), ("r1值", "000.000.000.000", (38, 52), False), + ("r3名", "周期(分)", (66, 80), False), ("r3值", "60", (66, 80), False), + ("r4名", "保存", (80, 95), False), + ("页脚", "第 1 页,共 1 页", (140, 160), False), + ], + "sntp_edit": [ + ("标题", "SNTP设置", (0, 24), False), + ("r1名", "主服务器", (38, 52), True), # 选中带落到 r1 + ("r0名", "启用", (24, 38), False), + ("r2名", "备服务器", (52, 66), False), ("r2值", "000.000.000.000", (52, 66), False), + ("r4名", "保存", (80, 95), False), + ], + "sntp_oct": [ + ("r1值改后", "000.001.000.000", (38, 52), True), # 退出编辑后选中带仍停在 r1 + ("r2值未动", "000.000.000.000", (52, 66), False), + ("r0值未动", "关闭", (24, 38), False), + ], + "sntp_en": [ + ("r0名", "启用", (24, 38), True), ("r0值切后", "开启", (24, 38), True), + ("r4名", "保存", (80, 95), False), + ], + "sntp_poll": [ + ("r3名", "周期(分)", (66, 80), True), # 选中带 = 编辑行 + ("r0名", "启用", (24, 38), False), + ], + "sntp_poll_big": [ + ("r3值粗调后", "120", (66, 80), True), # +60 钳带步进;退出编辑后选中带仍停在 r3 + ("r3名", "周期(分)", (66, 80), True), + ], + "sntp_save": [ + ("r0值", "开启", (24, 38), False), + ("r4名", "保存", (80, 95), True), ("r4值", "OK", (80, 95), True), + ], # ── 道F task-2:整定 4 页文本级断言 ───────────────────────────────────────── # 值全部来自 simulator/main.c 的固定帧(prot_set 出厂默认 + cal_block 恒 1), # 每条断言都是"某个具体事实":徽标在不在、保存行在第几页、越界提示、改值后落没落。 @@ -909,7 +969,7 @@ EXTRA = { # sim_oper.log 已经攒了 3 条(192 B = 3×64 B)→ "操作记录"页读到的是真记录, # 空态判据(第 0 行"无记录")根本拍不到。**判据本身没错,错在跑它的环境被前面的写路径 # 污染了**,所以给这 5 页各开一个空目录 = 现场"这些日志文件还不存在"的确定态; -# 其余 55 页环境一字不变(对当时存量 53 张黄金零影响,实测逐字节零漂移;当时集合 60 张,现集合 66 张)。 +# 其余 55 页环境一字不变(对当时存量 53 张黄金零影响,实测逐字节零漂移;当时集合 60 张,现集合 74 张)。 FRESH_CWD = ("accident", "oper", "alarm", "start", "yk") diff --git a/simulator/tests/ref/shot_sntp.bin b/simulator/tests/ref/shot_sntp.bin new file mode 100644 index 00000000..9498ff27 Binary files /dev/null and b/simulator/tests/ref/shot_sntp.bin differ diff --git a/simulator/tests/ref/shot_sntp_edit.bin b/simulator/tests/ref/shot_sntp_edit.bin new file mode 100644 index 00000000..d970415e Binary files /dev/null and b/simulator/tests/ref/shot_sntp_edit.bin differ diff --git a/simulator/tests/ref/shot_sntp_en.bin b/simulator/tests/ref/shot_sntp_en.bin new file mode 100644 index 00000000..1ef39a02 Binary files /dev/null and b/simulator/tests/ref/shot_sntp_en.bin differ diff --git a/simulator/tests/ref/shot_sntp_oct.bin b/simulator/tests/ref/shot_sntp_oct.bin new file mode 100644 index 00000000..91231eb0 Binary files /dev/null and b/simulator/tests/ref/shot_sntp_oct.bin differ diff --git a/simulator/tests/ref/shot_sntp_poll.bin b/simulator/tests/ref/shot_sntp_poll.bin new file mode 100644 index 00000000..ea4a05d5 Binary files /dev/null and b/simulator/tests/ref/shot_sntp_poll.bin differ diff --git a/simulator/tests/ref/shot_sntp_poll_big.bin b/simulator/tests/ref/shot_sntp_poll_big.bin new file mode 100644 index 00000000..ee44b0e8 Binary files /dev/null and b/simulator/tests/ref/shot_sntp_poll_big.bin differ diff --git a/simulator/tests/ref/shot_sntp_ro.bin b/simulator/tests/ref/shot_sntp_ro.bin new file mode 100644 index 00000000..9498ff27 Binary files /dev/null and b/simulator/tests/ref/shot_sntp_ro.bin differ diff --git a/simulator/tests/ref/shot_sntp_save.bin b/simulator/tests/ref/shot_sntp_save.bin new file mode 100644 index 00000000..2f8ecf80 Binary files /dev/null and b/simulator/tests/ref/shot_sntp_save.bin differ diff --git a/test/host/net_cfg/net_cfg_test.c b/test/host/net_cfg/net_cfg_test.c index 635199cb..45d8eb67 100644 --- a/test/host/net_cfg/net_cfg_test.c +++ b/test/host/net_cfg/net_cfg_test.c @@ -16,6 +16,11 @@ * 口令判定归 test/host/ftp_cfg/ 模块,两边不重复也不互相顶包。 * (不能把 ftp_cfg.c 也 include 进来:两个 .c 都有 static s_cfg, * 同一个 TU 里直接重定义。) + * - [sntp] 段:描述符由 net_cfg.c 的 s_sntp_register() 组装(分层 + * 门禁后 sntp_cfg.c 不再自注册),所以本测试直接**编译真实的 + * sntp_cfg.c 数据本体**(纯 stdint/string,无引擎依赖,改名后的 + * s_sntp_cfg 与 net_cfg.c 的 s_cfg 不再同名冲突),并用假引擎 + * 截获注册内容——段键表/偏移/默认值的机器钉在这里打。 * ⚠ 引擎的解析/写盘/沉降契约已由 test/host/cfg_file/(103 条)覆盖, * 本模块刻意不重测引擎,只测 net_cfg.c 这一层的编排与交接。 * @@ -37,8 +42,10 @@ * TC-NETCFG-003 未 load 时 gw = 192.168.31.1 * TC-NETCFG-004 未 load 时 mac = 全零(哨兵 = 不覆盖芯片内置 MAC) * —— 注册与 load(TC-NETCFG-005..011)—— - * TC-NETCFG-005 首次 load 注册 [network] 段恰好一次(且 load 返回 0) + * TC-NETCFG-005 首次 load 注册 [network]+[sntp] 各恰好一次(且 load 返回 0) * TC-NETCFG-006 首次 load 一并注册 [ftp] 段(FTP 鉴权:所有受管段先注册、后 load) + * TC-NETCFG-006b [sntp] 描述符内容:段名/键表(enable/server1/server2/poll_min + * 偏移 0/1/5/10 长 1/4/4/2)/默认值(关 + 60 分钟)/无变更回调 * TC-NETCFG-007 重复 load 不重复注册(幂等;否则引擎按"段名重复"拒登 → 静默失配) * TC-NETCFG-008 注册描述符内容:段名/尺寸/键数/默认值指针/变更回调/数据指针 = RAM 副本 * TC-NETCFG-009 load 成功:返回 0 且留一条 INFO、不误报 ERROR @@ -100,8 +107,10 @@ /* 真实引擎头(接口原汁)+ 被测生产源(原样纳入);dbg_log.h 命中本目录 stub */ #include "cfg_file.h" #include "net_cfg.h" +#include "sntp_cfg.h" #include "net_cfg.c" +#include "sntp_cfg.c" /* ===================== 断言框架 ===================== */ @@ -137,6 +146,7 @@ typedef struct { const void *p_defaults; size_t data_size; uint8_t key_count; + const cfg_key_def_t *p_keys; /* 键表是生产文件里的 static const,指针长期有效,不必深拷贝 */ void (*p_cb)(void); } fake_reg_t; @@ -179,6 +189,7 @@ int cfg_file_register(const cfg_section_reg_t *p_reg) rec->p_defaults = p_reg->p_defaults; rec->data_size = p_reg->data_size; rec->key_count = p_reg->key_count; + rec->p_keys = p_reg->p_keys; rec->p_cb = p_reg->p_cb; g_reg_n++; return 0; @@ -278,14 +289,41 @@ static void tc_register_load(void) printf("-- TC-NETCFG-005..011 register + load --\n"); fake_reset(); rc = net_cfg_load(); - CHECK((rc == 0) && (g_reg_n == 1) && (strcmp(g_regs[0].name, "network") == 0), - "TC-NETCFG-005 first load registers [network] exactly once"); + CHECK((rc == 0) && (g_reg_n == 2) && (strcmp(g_regs[0].name, "network") == 0), + "TC-NETCFG-005 first load registers [network] + [sntp] exactly once each"); CHECK((g_ftp_reg_calls == 1) && (g_load_calls == 1), "TC-NETCFG-006 also registers [ftp], registered before load"); + { + const fake_reg_t *rs = (g_reg_n == 2) ? &g_regs[1] : NULL; + const sntp_cfg_t *d = (const sntp_cfg_t *)(rs != NULL ? rs->p_defaults : NULL); + int keys_ok = (rs != NULL) && (rs->key_count == 4u) && (rs->p_keys != NULL); + CHECK((rs != NULL) && (strcmp(rs->name, "sntp") == 0) && keys_ok && + (rs->data_size == sizeof(sntp_cfg_t)) && + (rs->p_data == (const void *)sntp_cfg_data()) && + (rs->p_cb == NULL), + "TC-NETCFG-006b [sntp] descriptor: name/4 keys/size/RAM copy/no cb"); + CHECK(keys_ok && + (strcmp(rs->p_keys[0].name, "enable") == 0) && + (rs->p_keys[0].type == CFG_VAL_UINT) && + (rs->p_keys[0].offset == 0u) && (rs->p_keys[0].len == 1u) && + (strcmp(rs->p_keys[1].name, "server1") == 0) && + (rs->p_keys[1].type == CFG_VAL_IP4) && + (rs->p_keys[1].offset == 1u) && (rs->p_keys[1].len == 4u) && + (strcmp(rs->p_keys[2].name, "server2") == 0) && + (rs->p_keys[2].type == CFG_VAL_IP4) && + (rs->p_keys[2].offset == 5u) && (rs->p_keys[2].len == 4u) && + (strcmp(rs->p_keys[3].name, "poll_min") == 0) && + (rs->p_keys[3].type == CFG_VAL_UINT) && + (rs->p_keys[3].offset == 10u) && (rs->p_keys[3].len == 2u), + "TC-NETCFG-006c [sntp] key table: enable/server1/server2/poll_min at 0/1/5/10"); + CHECK((d != NULL) && (d->enable == 0u) && (d->poll_min == 60u) && + (d->server_pri[0] == 0u) && (d->server_bak[0] == 0u), + "TC-NETCFG-006d [sntp] factory defaults: off + empty servers + 60 min"); + } rc = net_cfg_load(); - CHECK((rc == 0) && (g_reg_n == 1) && (g_ftp_reg_calls == 2), - "TC-NETCFG-007 repeated load does not re-register [network]"); + CHECK((rc == 0) && (g_reg_n == 2) && (g_ftp_reg_calls == 2), + "TC-NETCFG-007 repeated load does not re-register [network]/[sntp]"); CHECK((strcmp(g_regs[0].name, "network") == 0) && (g_regs[0].data_size == sizeof(net_cfg_t)) && diff --git a/test/host/net_cfg/run_unit_test.py b/test/host/net_cfg/run_unit_test.py index f6806ac6..e6ad3f4f 100644 --- a/test/host/net_cfg/run_unit_test.py +++ b/test/host/net_cfg/run_unit_test.py @@ -21,6 +21,9 @@ 桩的边界(细节见测试单元文件头): 配置文件引擎用**真实头**(接口原汁)+ 测试单元内的可控实现;[ftp] 段的 ftp_cfg_register 只计数——它归 test/host/ftp_cfg/ 模块,两边不互相顶包。 + sntp_cfg.c 是分层门禁后的纯数据本体,随 net_cfg.c 一并编译入 TU, + [sntp] 段描述符(键表/偏移/工厂默认)由假引擎截获断言——段注册契约 + 的机器钉在本模块(TC-NETCFG-005/006b..d/007)。 用法(仓库根目录): python test/host/net_cfg/run_unit_test.py diff --git a/test/host/sntp/run_unit_test.py b/test/host/sntp/run_unit_test.py new file mode 100644 index 00000000..9d867d04 --- /dev/null +++ b/test/host/sntp/run_unit_test.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""SNTP 客户端(time_sntp)的主机侧纯逻辑单元测试运行器(纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/sntp/sntp_test.c(它以 #include 纳入**生产源 + 文件** App/sync/time_sntp.c + Drivers/BSP/NETCFG/sntp_cfg.c + vendor + Lib/coreSNTP/core_sntp_serializer.c),在 PC 上离线跑: + RFC 4330 §10 节奏(初始 1~5min 随机散布 / 失败超时翻倍封顶 60s / 成功按 + poll_min 重排)、§5.3 防欺骗(应答源 IP:port / originate 含随机低位精确 + 匹配 / mode / LI=3 / stratum>15 / KoD DENY·RSTR·RATE·他码)、§5.5 步进 + 策略(±2s 死区只记录、冷启动直采 T3、钟面 = 本地 UTC+8 恒加 28800、 + 值域守卫)、[sntp] 配置注册与 poll 钳位、网络代次/停用收口与 socket 生命周期。 + 失败即非 0 退出。零硬件、零烧录、毫秒级。 + + 工具链探测 / 编译运行 / 退出码判定全部委托通用 harness + `test/host/common/unit_test_harness.py`(本文件只做声明)。 + +为什么要主机侧单测(而不是只用真机): + SNTP 的失败路径(伪造应答、KoD、超时风暴、tick 回绕)在真机上不可复现或 + 要等小时级;且本模块直接拨 RTC(sys_clock→SD2506 写回),拨错一次就是 + 事故——判据必须逐包断言。真机只做正向链路回归(PC W32Time 对端)。 + +用法(仓库根目录): + & '.venv\\Scripts\\python.exe' test/host/sntp/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:#57 调研(docs 见 sntp-调研报告.md)+ App/sync/time_sntp.c 文件头; +创建日期:2026-11-04 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +# harness 位于 test/host/common/ +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 顺序敏感:stub 目录必须**先于**生产目录,才能覆盖 FreeRTOS.h / task.h / +# cmsis_os2.h / net_types.h / net_socket.h / sys_clock.h / sys_events.h / +# sys_log.h / dbg_log.h(真实头会拖入整套 FreeRTOS / HAL / CH395F)。 +# cfg_file.h 用**真实头**(App/cfg,纯 stdint/stddef——注册描述符契约原汁); +# sntp_cfg.h / time_sntp.h 用真实头(接口契约原汁)。 +BUILD = TestBuild( + name="SNTP Client Tests", + src=os.path.join(HERE, "sntp_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "App", "sync"), + os.path.join(ROOT, "App", "cfg"), + os.path.join(ROOT, "Drivers", "BSP", "NETCFG"), + os.path.join(ROOT, "Lib", "coreSNTP"), + os.path.join(ROOT, "Lib", "coreSNTP", "include"), + HERE, + ], + summary_marker="=== SNTP Client Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/sntp/sntp_test.c b/test/host/sntp/sntp_test.c new file mode 100644 index 00000000..0e0f4cfd --- /dev/null +++ b/test/host/sntp/sntp_test.c @@ -0,0 +1,967 @@ +/* + * 模块名称:Time SNTP 主机单测(test/host/sntp) + * 模块功能:把 App/sync/time_sntp.c + Drivers/BSP/NETCFG/sntp_cfg.c + + * vendor Lib/coreSNTP/core_sntp_serializer.c 一起编进本 TU, + * 用脚本化 net/sys_clock/dbg fake 驱动状态机,验证: + * RFC 4330 §10 节奏(初始散布/失败翻倍封顶/成功重排)、§5.3 防欺骗 + * (源 IP:port / originate 精确匹配 / mode / LI=3 / stratum / KoD)、 + * §5.5 步进策略(±2s 死区、冷启动直采 T3、时区面 +28800)、 + * poll 钳位与 set 入口、代次/停用收口。 + * ([sntp] 段描述符注册断言在 test/host/net_cfg——分层门禁后 + * 描述符由 net_cfg.c 组装,本套件只并 sntp_cfg.c 数据本体。) + * 约束:生产 .c 以 #include 方式并入——static 白盒可达;fake 计数前缀唯一, + * 避免同名 tentative static 静默合并(2026-10-10 教训)。 + */ +#include +#include +#include + +/* stub 头先行:fake 定义要用到这些类型/宏 */ +#include "FreeRTOS.h" +#include "task.h" +#include "cmsis_os2.h" +#include "net_types.h" +#include "net_socket.h" +#include "sys_clock.h" +#include "sys_events.h" +#include "sys_log.h" +#include "dbg_log.h" + +/* ---- fake 存储(stub/dbg_log.h 已声明 g_dbg_*_count extern,此处落定义) ---- */ +int g_dbg_err_count; +int g_dbg_warn_count; +int g_dbg_info_count; + +/* 系统 tick(ms) */ +static uint32_t g_tick; + +/* 时钟面:UINT32_MAX = 未初始化;其余 = 本地秒 */ +static uint32_t g_face; +static uint32_t g_face_set_last; +static int g_face_set_calls; + +/* 事件标志 / 操作日志 */ +osEventFlagsId_t sys_evtHandle; /* 测试里指到假对象(非 NULL) */ +static int g_evt_calls; +static uint32_t g_evt_last; +static int g_syslog_calls; +static syslog_id_t g_syslog_last_id; + +/* ---- net fake ---- */ +#define SNTP_T_NET_SEND_MAX 8 +#define SNTP_T_NET_RECV_MAX 8 +#define SNTP_T_NET_IP(o1,o2,o3,o4) { (o1), (o2), (o3), (o4) } + +static uint32_t g_gen; +static int g_sock_next_fd = 100; +static int g_sock_opens; +static int g_sock_close_calls; +static int g_last_close_fd = -1; +static int g_bind_calls; +static int g_bind_ret; /* 脚本:0 成功 / -1 失败 */ +static int g_errno_val; +static int g_send_calls; +static uint8_t g_sent_ip[4]; +static uint16_t g_sent_port_be; +static uint8_t g_sent_pkt[128]; +static int g_sent_len; +static int g_send_ret; /* 脚本:48 / -1 */ +static int g_send_history_n; /* 每轮 send_calls 快照(历史计数) */ + +struct sntp_t_recv { + int kind; /* NET_RECV_* */ + int len; /* kind==DATA 有效 */ + uint8_t data[128]; + uint8_t ip[4]; + uint16_t port_be; +}; +static struct sntp_t_recv g_recv_q[SNTP_T_NET_RECV_MAX]; +static int g_recv_head, g_recv_tail; +static uint32_t g_recv_last_timeout_ms; /* 最近一次 recv 的片超时(50ms 断言) */ + +/* ---- cfg fake:[sntp] 段描述符组装已移 net_cfg.c(分层门禁),注册内容 + * 断言归 test/host/net_cfg;本套件只需 sntp_cfg.c 数据本体可用,无引擎调用 ---- */ + +/* ---- dbg 宏(stub/dbg_log.h 已声明 g_dbg_*,此处无需再定义宏) ---- */ + +/* ---- sys_clock / sys_events / sys_log / cmsis fake 实现 ---- */ +uint32_t sys_clock_get(void) +{ + return g_face; +} + +int sys_clock_set_unix(uint32_t unix_secs) +{ + g_face_set_calls++; + g_face_set_last = unix_secs; + g_face = unix_secs; + return 0; +} + +uint32_t xTaskGetTickCount(void) +{ + return g_tick; +} + +uint32_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t events) +{ + (void)ef_id; + g_evt_calls++; + g_evt_last = events; + return events; +} + +void syslog_post_quiet(syslog_id_t id, const char *desc) +{ + (void)desc; + g_syslog_calls++; + g_syslog_last_id = id; +} + +/* ---- net fake 实现 ---- */ +static void sntp_t_net_reset(void) +{ + g_sock_next_fd = 100; + g_sock_opens = 0; + g_sock_close_calls = 0; + g_last_close_fd = -1; + g_bind_calls = 0; + g_bind_ret = 0; + g_errno_val = 0; + g_send_calls = 0; + g_send_ret = 48; + g_send_history_n = 0; + g_recv_head = g_recv_tail = 0; + g_recv_last_timeout_ms = 0; + memset(g_sent_ip, 0, sizeof(g_sent_ip)); + memset(g_sent_pkt, 0, sizeof(g_sent_pkt)); + g_sent_len = 0; + g_sent_port_be = 0; +} + +static void sntp_t_push_recv(int kind, const uint8_t *data, int len, + const uint8_t *ip, uint16_t port_be) +{ + struct sntp_t_recv *r; + + if (g_recv_tail - g_recv_head >= SNTP_T_NET_RECV_MAX) { + printf("recv queue overflow\n"); + return; + } + r = &g_recv_q[g_recv_tail++ % SNTP_T_NET_RECV_MAX]; + r->kind = kind; + r->len = len; + r->port_be = port_be; + if (data != NULL && len > 0) { + memcpy(r->data, data, (size_t)len); + } + if (ip != NULL) { + memcpy(r->ip, ip, 4); + } else { + memset(r->ip, 0, 4); + } +} + +int net_socket(int domain, int type) +{ + (void)domain; + (void)type; + g_sock_opens++; + return g_sock_next_fd++; +} + +int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) +{ + (void)sockfd; + (void)addr; + (void)addrlen; + g_bind_calls++; + return g_bind_ret; +} + +int net_sendto(int sockfd, const void *buf, int len, int flags, + const struct net_sockaddr *dest, int addrlen) +{ + const struct net_sockaddr_in *d = (const struct net_sockaddr_in *)dest; + + (void)sockfd; + (void)flags; + (void)addrlen; + g_send_calls++; + g_send_history_n = g_send_calls; + if (len > 0 && len <= (int)sizeof(g_sent_pkt)) { + memcpy(g_sent_pkt, buf, (size_t)len); + } + g_sent_len = len; + if (dest != NULL) { + memcpy(g_sent_ip, &d->sin_addr.s_addr, 4); + g_sent_port_be = d->sin_port; + } + return g_send_ret; +} + +int net_recvfrom_to(int sockfd, void *buf, int len, uint32_t timeout_ms, + struct net_sockaddr *src, int *addrlen) +{ + struct sntp_t_recv *r; + struct net_sockaddr_in *sin = (struct net_sockaddr_in *)src; + + (void)sockfd; + g_recv_last_timeout_ms = timeout_ms; + if (g_recv_head == g_recv_tail) { + return -2; /* 队列空 = 无应答(合同 -2) */ + } + r = &g_recv_q[g_recv_head++ % SNTP_T_NET_RECV_MAX]; + if (r->kind != NET_RECV_DATA) { + return r->kind; /* -2 超时 / -1 错误 / 0 防御 */ + } + if (r->len > len) { + r->len = len; + } + memcpy(buf, r->data, (size_t)r->len); + if (src != NULL && addrlen != NULL && *addrlen >= (int)sizeof(*sin)) { + memset(sin, 0, sizeof(*sin)); + sin->sin_family = NET_AF_INET; + sin->sin_port = r->port_be; + memcpy(&sin->sin_addr.s_addr, r->ip, 4); + } + return r->len; +} + +int net_close(int sockfd) +{ + g_sock_close_calls++; + g_last_close_fd = sockfd; + return 0; +} + +int net_get_errno(void) +{ + return g_errno_val; +} + +uint32_t net_get_generation(void) +{ + return g_gen; +} + +/* ---- 生产代码并入(顺序:vendor → 配置 → 客户端) ---- */ +#include "core_sntp_serializer.c" +#include "sntp_cfg.c" +#include "time_sntp.c" + +/* ---- 测试框架 ---- */ +static int g_total, g_pass, g_fail; + +#define CHECK(cond, name) \ + do { \ + g_total++; \ + if (cond) { g_pass++; } \ + else { g_fail++; printf(" FAIL %s (line %d)\n", (name), __LINE__); } \ + } while (0) + +static void s_case_reset(void) +{ + sntp_cfg_t *c; + + /* 配置先归零,再借代次跳变走"生产复位通道"(关 socket + 状态回 IDLE) */ + c = (sntp_cfg_t *)(void *)sntp_cfg_get(); + c->enable = 0u; + memset(c->server_pri, 0, 4); + memset(c->server_bak, 0, 4); + c->poll_min = 60u; + + g_gen++; + (void)time_sntp_run(g_gen); + + g_tick = 10000u; /* 避开 0 */ + g_face = UINT32_MAX; + g_face_set_calls = 0; + g_face_set_last = 0u; + g_evt_calls = 0; + g_evt_last = 0; + g_syslog_calls = 0; + g_dbg_err_count = g_dbg_warn_count = g_dbg_info_count = 0; + sntp_t_net_reset(); + sys_evtHandle = (osEventFlagsId_t)&g_evt_calls; /* 假句柄:非 NULL */ + + s_ctx.srv = 0u; +} + +static void s_cfg_enable(const uint8_t *pri, const uint8_t *bak, uint16_t poll) +{ + sntp_cfg_t *c = (sntp_cfg_t *)(void *)sntp_cfg_get(); + + c->enable = 1u; + if (pri != NULL) { memcpy(c->server_pri, pri, 4); } + if (bak != NULL) { memcpy(c->server_bak, bak, 4); } + c->poll_min = poll; +} + +/* 推进到"下一次"发出请求(基线=当前计数,有界轮询),返回 1=新发一包 */ +static int s_run_until_send(uint32_t max_ms) +{ + uint32_t t; + int base = g_send_calls; + + for (t = 0; t < max_ms; t += 200u) { + g_tick += 200u; + time_sntp_run(g_gen); + if (g_send_calls > base) { + return 1; + } + } + return 0; +} + +/* 从 g_sent_pkt 的 transmit 时间戳(40..47)拼一条合法应答(不含 KoD) */ +static void s_build_resp(uint8_t *out, uint8_t li, uint8_t stratum, + uint32_t t2_sec, uint32_t t2_frac, + uint32_t t3_sec, uint32_t t3_frac) +{ + memset(out, 0, 48); + out[0] = (uint8_t)(((li & 3u) << 6) | (4u << 3) | 4u); /* VN4 mode=4 */ + out[1] = stratum; + memcpy(&out[24], &g_sent_pkt[40], 8); /* originate = T1 原样 */ + /* recvTs 非零(serializer 要求三时间戳非零) */ + out[32] = (uint8_t)(t2_sec >> 24); out[33] = (uint8_t)(t2_sec >> 16); + out[34] = (uint8_t)(t2_sec >> 8); out[35] = (uint8_t)t2_sec; + out[36] = (uint8_t)(t2_frac >> 24); out[37] = (uint8_t)(t2_frac >> 16); + out[38] = (uint8_t)(t2_frac >> 8); out[39] = (uint8_t)t2_frac; + out[40] = (uint8_t)(t3_sec >> 24); out[41] = (uint8_t)(t3_sec >> 16); + out[42] = (uint8_t)(t3_sec >> 8); out[43] = (uint8_t)t3_sec; + out[44] = (uint8_t)(t3_frac >> 24); out[45] = (uint8_t)(t3_frac >> 16); + out[46] = (uint8_t)(t3_frac >> 8); out[47] = (uint8_t)t3_frac; +} + +static uint32_t s_be32(const uint8_t *p) +{ + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} + +/* 等应答态喂一包(服务器地址 = 本轮实际发送目的),返回后状态自取 */ +static void s_feed(const uint8_t *resp, int len, const uint8_t *src_ip, + uint16_t src_port) +{ + sntp_t_push_recv(NET_RECV_DATA, resp, len, src_ip, src_port); + g_tick += 200u; + time_sntp_run(g_gen); +} + +/* ============================ 测试用例 ============================ */ + +static void tc_poll_clamp(void) +{ + sntp_cfg_t *c; + + /* [sntp] 段描述符/键表/默认值的注册断言在 test/host/net_cfg(描述符 + 已改由 net_cfg.c 组装);这里只钉客户端实际消费的 poll 钳位口 */ + s_case_reset(); + c = (sntp_cfg_t *)(void *)sntp_cfg_get(); + c->poll_min = 0u; CHECK(sntp_cfg_poll_min() == 60u, "cfg: poll 0 -> 60"); + c->poll_min = 1441u; CHECK(sntp_cfg_poll_min() == 60u, "cfg: poll 1441 -> 60"); + c->poll_min = 2u; CHECK(sntp_cfg_poll_min() == 2u, "cfg: poll 2 kept"); + CHECK(c->poll_min == 0u || c->poll_min == 2u || c->poll_min == 1441u, + "cfg: clamp no writeback"); + c->poll_min = 60u; + + /* set(NULL) 不改写 + set 整包拷贝(UI 编辑入口的行为钉) */ + sntp_cfg_set(NULL); + CHECK(sntp_cfg_poll_min() == 60u, "cfg: set(NULL) no-op"); +} + +static void tc_disabled_silent(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + + s_case_reset(); + s_cfg_enable(pri, NULL, 60u); + ((sntp_cfg_t *)sntp_cfg_get())->enable = 0u; + (void)time_sntp_run(g_gen); + CHECK(g_send_calls == 0 && g_sock_opens == 0, "disable: no send/no socket"); +} + +static void tc_first_send_window(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + uint32_t t0; + + s_case_reset(); + t0 = g_tick; + s_cfg_enable(pri, NULL, 60u); + CHECK(s_run_until_send(320000u) == 1, "first: send happened"); + CHECK(g_tick - t0 >= SNTP_FIRST_DELAY_MIN_MS && g_tick - t0 <= + SNTP_FIRST_DELAY_MIN_MS + SNTP_FIRST_DELAY_SPAN_MS, "first: 1..5min window"); + CHECK(g_send_calls == 1, "first: single send"); + CHECK(g_sent_len == 48, "first: 48B request"); + CHECK(g_sent_pkt[0] == 0x23u, "first: LI0 VN4 mode3"); + CHECK(g_sent_port_be == net_htons(123u), "first: dst port 123 BE"); + CHECK(memcmp(g_sent_ip, pri, 4) == 0, "first: dst = server1"); + CHECK(s_be32(&g_sent_pkt[40]) != 0u, "first: T1 nonzero"); + CHECK(g_sock_opens == 1 && g_bind_calls == 1, "first: socket opened+bound"); +} + +static void tc_cold_start_step_t3(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + uint8_t resp[48]; + + s_case_reset(); + s_cfg_enable(pri, NULL, 60u); + (void)s_run_until_send(320000u); + g_tick = (g_tick / 1000u) * 1000u; /* 分数归零,期望精确 */ + /* 冷启动:偏移乱也无妨——直接采用服务器 T3(调研 §5.5)。 + * T3 用真实当前时刻(服务器不受本地冷钟 epoch 起点影响) */ + { + const uint32_t real_unix = 1750000000u; /* 2025-06-15,域内 */ + s_build_resp(resp, 0u, 5u, + SNTP_TIME_AT_UNIX_EPOCH_SECS + real_unix + 1u, 0u, + SNTP_TIME_AT_UNIX_EPOCH_SECS + real_unix, 0u); + s_feed(resp, 48, pri, net_htons(123u)); + CHECK(time_sntp_sync_count() >= 1u, "cold: sync counted"); + CHECK(g_face_set_calls == 1, "cold: step once"); + CHECK(g_face_set_last == real_unix + 28800u, + "cold: face = T3 unix + 8h"); + } + CHECK(g_syslog_calls == 1 && g_syslog_last_id == SYSLOG_OPER, "cold: oper log"); + CHECK(g_evt_calls == 1 && g_evt_last == SYS_EVT_TIMESYNC_READY, "cold: ready evt"); + /* 成功节奏:下一轮 = poll_min(60 分钟),退避复位 */ + CHECK(s_ctx.state == SNTP_ST_WAIT_DUE && s_ctx.timeout_ms == 5000u, + "cold: backoff reset"); +} + +static void tc_step_valid_clock(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + uint8_t resp[48]; + uint32_t t1s; + uint32_t face_before = 1750000123u + 28800u; /* 有效本地钟(2025+) */ + + s_case_reset(); + g_face = face_before; + s_cfg_enable(pri, NULL, 60u); + (void)s_run_until_send(320000u); + t1s = s_be32(&g_sent_pkt[40]); + g_tick = (g_tick / 1000u) * 1000u; + /* 服务器快 30s:T2=T3=T1+30s,T4=T1 → offset=+30000ms(>2s 死区 → 步进) */ + s_build_resp(resp, 0u, 5u, t1s + 30u, 0u, t1s + 30u, 0u); + s_feed(resp, 48, pri, net_htons(123u)); + CHECK(g_face_set_calls == 1, "step: stepped once"); + CHECK(g_face_set_last == face_before + 30u, "step: face += 30s"); + CHECK(time_sntp_last_offset_ms() >= 29499 && time_sntp_last_offset_ms() <= 30001, + "step: offset ~30000ms"); +} + +static void tc_deadband_no_step(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + uint8_t resp[48]; + uint32_t t1s; + uint32_t cnt0 = time_sntp_sync_count(); + + s_case_reset(); + g_face = 1750000000u + 28800u; + s_cfg_enable(pri, NULL, 60u); + (void)s_run_until_send(320000u); + t1s = s_be32(&g_sent_pkt[40]); + g_tick = (g_tick / 1000u) * 1000u; + /* ±1s < 2s 死区:只记录不拨针(防 SD2506 磨损,调研 §5.5) */ + s_build_resp(resp, 0u, 5u, t1s, 0u, t1s + 1u, 0u); + s_feed(resp, 48, pri, net_htons(123u)); + CHECK(g_face_set_calls == 0, "deadband: no step"); + CHECK(g_syslog_calls == 0, "deadband: no oper log"); + CHECK(time_sntp_sync_count() == cnt0 + 1u, "deadband: counted sync"); + CHECK(s_ctx.state == SNTP_ST_WAIT_DUE, "deadband: next poll scheduled"); +} + +static void tc_anti_spoof_src(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + uint8_t other[] = SNTP_T_NET_IP(192, 168, 31, 66); + uint8_t resp[48]; + uint32_t t1s; + + s_case_reset(); + s_cfg_enable(pri, NULL, 60u); + (void)s_run_until_send(320000u); + t1s = s_be32(&g_sent_pkt[40]); + s_build_resp(resp, 0u, 5u, t1s, 0u, t1s, 0u); + /* 源 IP 不对:丢弃继续等(§5.3 第一道) */ + s_feed(resp, 48, other, net_htons(123u)); + CHECK(g_face_set_calls == 0, "spoof: wrong ip not stepped"); + CHECK(s_ctx.state == SNTP_ST_WAIT_RESP, "spoof: still waiting"); + /* 源端口不对(123→999):同样丢弃 */ + s_feed(resp, 48, pri, net_htons(999u)); + CHECK(g_face_set_calls == 0 && s_ctx.state == SNTP_ST_WAIT_RESP, "spoof: wrong port"); + /* originate 篡改:丢弃 */ + resp[24] ^= 0xA5u; + s_feed(resp, 48, pri, net_htons(123u)); + CHECK(g_face_set_calls == 0 && s_ctx.state == SNTP_ST_WAIT_RESP, "spoof: bad originate"); + /* mode=3(服务器回显错 mode):serializer 判无效 → 丢弃 */ + s_build_resp(resp, 0u, 5u, t1s, 0u, t1s, 0u); + resp[0] = (uint8_t)((resp[0] & 0xF8u) | 3u); + s_feed(resp, 48, pri, net_htons(123u)); + CHECK(g_face_set_calls == 0 && s_ctx.state == SNTP_ST_WAIT_RESP, "spoof: bad mode"); + /* 短包(<48):丢弃 */ + s_feed(resp, 40, pri, net_htons(123u)); + CHECK(g_face_set_calls == 0 && s_ctx.state == SNTP_ST_WAIT_RESP, "spoof: short pkt"); + /* 最后合法包仍可达:链路未被误伤 */ + s_build_resp(resp, 0u, 5u, t1s, 0u, t1s, 0u); + g_face = 1750000000u + 28800u; + s_feed(resp, 48, pri, net_htons(123u)); + CHECK(g_face_set_calls == 0, "spoof: valid after noise (deadband)"); + CHECK(s_ctx.state == SNTP_ST_WAIT_DUE, "spoof: recovered to schedule"); +} + +static void tc_li3_stratum(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + uint8_t bak[] = SNTP_T_NET_IP(192, 168, 31, 13); + uint8_t resp[48]; + uint32_t t1s; + int sends_at_li; + + s_case_reset(); + s_cfg_enable(pri, bak, 60u); + (void)s_run_until_send(320000u); + /* LI=3:服务器没同步 → 立即失败 + 切备 + 翻倍退避(serializer 不拦,我们拦) */ + t1s = s_be32(&g_sent_pkt[40]); + s_build_resp(resp, 3u, 5u, t1s, 0u, t1s, 0u); + sends_at_li = g_send_calls; + s_feed(resp, 48, pri, net_htons(123u)); + CHECK(g_face_set_calls == 0, "li3: no step"); + CHECK(s_ctx.state == SNTP_ST_WAIT_DUE && s_ctx.timeout_ms == 10000u, + "li3: doubled backoff"); + CHECK(s_ctx.srv == 1u, "li3: switched to backup"); + /* 备机应答 stratum=16(非法):失败退避但不再切换(只拦包不怪服务器方向? + * 调研:stratum 非法 = 无效应答 → 退避;主备保持) */ + (void)s_run_until_send(20000u); + CHECK(g_send_calls == sends_at_li + 1, "li3: retry to backup"); + CHECK(memcmp(g_sent_ip, bak, 4) == 0, "li3: dst=backup now"); + t1s = s_be32(&g_sent_pkt[40]); + s_build_resp(resp, 0u, 16u, t1s, 0u, t1s, 0u); + s_feed(resp, 48, bak, net_htons(123u)); + CHECK(s_ctx.state == SNTP_ST_WAIT_DUE && s_ctx.timeout_ms == 20000u, + "stratum16: backoff"); + /* 本仓无 DBG_WARN 宏(dbg_log.h 只有 ERROR/INFO/DEBUG,net_udp.c:756 钉), + * 生产侧 WARN 走 DBG_INFO("WARN: …")——计数看 info */ + CHECK(g_dbg_info_count >= 2, "stratum/li3: warned"); +} + +static void tc_kod(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + uint8_t bak[] = SNTP_T_NET_IP(192, 168, 31, 13); + uint8_t resp[48]; + uint32_t t1s; + + s_case_reset(); + s_cfg_enable(pri, bak, 60u); + /* KoD stratum=0 + refId 码:DENY → 切备(§5.3/RFC) */ + (void)s_run_until_send(320000u); + t1s = s_be32(&g_sent_pkt[40]); + s_build_resp(resp, 0u, 0u, t1s, 0u, t1s, 0u); + resp[12] = 'D'; resp[13] = 'E'; resp[14] = 'N'; resp[15] = 'Y'; + s_feed(resp, 48, pri, net_htons(123u)); + CHECK(g_face_set_calls == 0 && s_ctx.srv == 1u, "kod DENY: switch"); + /* RATE:退避但保留当前服务器 */ + (void)s_run_until_send(20000u); + t1s = s_be32(&g_sent_pkt[40]); + s_build_resp(resp, 0u, 0u, t1s, 0u, t1s, 0u); + resp[12] = 'R'; resp[13] = 'A'; resp[14] = 'T'; resp[15] = 'E'; + s_feed(resp, 48, bak, net_htons(123u)); + CHECK(s_ctx.srv == 1u && s_ctx.timeout_ms == 20000u, "kod RATE: keep+backoff"); + /* 未知码:按 OtherCode 切走 */ + (void)s_run_until_send(40000u); + t1s = s_be32(&g_sent_pkt[40]); + s_build_resp(resp, 0u, 0u, t1s, 0u, t1s, 0u); + resp[12] = '?'; resp[13] = '?'; resp[14] = '?'; resp[15] = '?'; + s_feed(resp, 48, bak, net_htons(123u)); + CHECK(s_ctx.srv == 0u, "kod other: switch"); +} + +static void tc_timeout_backoff_cap(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + uint32_t t; + int i; + + s_case_reset(); + s_cfg_enable(pri, NULL, 60u); + (void)s_run_until_send(320000u); + /* 无应答:5s 片到点 → 退避翻倍;socket 保留 */ + for (t = 0; t < 6000u; t += 200u) { + g_tick += 200u; + time_sntp_run(g_gen); + } + CHECK(s_ctx.state == SNTP_ST_WAIT_DUE && s_ctx.timeout_ms == 10000u, + "timeout: doubled 5s->10s"); + CHECK(g_sock_close_calls == 0, "timeout: socket kept"); + /* 连打封顶 60s(RFC 间隔递增),单参数化推进:每轮 = 应窗 + 应答窗 */ + for (i = 0; i < 10 && s_ctx.timeout_ms < SNTP_RESP_TIMEOUT_MAX_MS; i++) { + uint32_t win = s_ctx.timeout_ms * 2u + 2000u; + (void)s_run_until_send(win); + for (t = 0; t < win && s_ctx.state == SNTP_ST_WAIT_RESP; t += 200u) { + g_tick += 200u; + time_sntp_run(g_gen); + } + } + CHECK(s_ctx.timeout_ms == SNTP_RESP_TIMEOUT_MAX_MS, "timeout: cap 60s"); +} + +static void tc_poll_pacing_after_success(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + uint8_t resp[48]; + uint32_t t1s, t; + int send_at_success; + + s_case_reset(); + s_cfg_enable(pri, NULL, 2u); /* 2 分钟一轮(钳位带内) */ + (void)s_run_until_send(320000u); + t1s = s_be32(&g_sent_pkt[40]); + s_build_resp(resp, 0u, 5u, t1s, 0u, t1s, 0u); + s_feed(resp, 48, pri, net_htons(123u)); + send_at_success = g_send_calls; + /* 下一轮 = poll_min*60s:1 分 50s 内不应有第 2 包 */ + for (t = 0; t < 110000u; t += 200u) { + g_tick += 200u; + time_sntp_run(g_gen); + } + CHECK(g_send_calls == send_at_success, "poll: not early (<2min)"); + for (t = 0; t < 20000u; t += 200u) { + g_tick += 200u; + time_sntp_run(g_gen); + if (g_send_calls > send_at_success) { break; } + } + CHECK(g_send_calls == send_at_success + 1, "poll: second round on time"); +} + +static void tc_no_server_warn_once(void) +{ + uint32_t t; + + s_case_reset(); + s_cfg_enable(NULL, NULL, 60u); /* enable 但两侧全零 */ + for (t = 0; t < 700000u; t += 1000u) { + g_tick += 1000u; + time_sntp_run(g_gen); + } + CHECK(g_send_calls == 0, "nosrv: never sends"); + CHECK(g_dbg_info_count >= 1, "nosrv: warned"); + { + int w0 = g_dbg_info_count; + g_tick += 1000u; + time_sntp_run(g_gen); + CHECK(g_dbg_info_count == w0, "nosrv: warn once only"); + } + /* 配置补齐即恢复(不发请求不算失败,无退避拖尾) */ + { + sntp_cfg_t *c = (sntp_cfg_t *)(void *)sntp_cfg_get(); + uint8_t pri[] = SNTP_T_NET_IP(10, 0, 0, 9); + memcpy(c->server_pri, pri, 4); + CHECK(s_run_until_send(320000u) == 1, "nosrv: recovers on config"); + CHECK(memcmp(g_sent_ip, pri, 4) == 0, "nosrv: new server used"); + } +} + +static void tc_disable_closes_socket(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + sntp_cfg_t *c; + + s_case_reset(); + s_cfg_enable(pri, NULL, 60u); + (void)s_run_until_send(320000u); + CHECK(g_sock_opens == 1, "disable: opened once"); + c = (sntp_cfg_t *)(void *)sntp_cfg_get(); + c->enable = 0u; + g_tick += 200u; + time_sntp_run(g_gen); + CHECK(g_sock_close_calls == 1 && s_ctx.state == SNTP_ST_IDLE, "disable: closed+idle"); + g_tick += 200u; + time_sntp_run(g_gen); + CHECK(g_sock_close_calls == 1, "disable: no double close"); +} + +static void tc_gen_change_resets(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + int before; + + s_case_reset(); + s_cfg_enable(pri, NULL, 60u); + (void)s_run_until_send(320000u); + before = g_send_calls; + /* 网络代次变化(UI 保存重建 / 链路复位):弃 socket 重新散布; + * 同一次 run 内 enable==1 时 IDLE 立即重排为 WAIT_DUE(重新 1..5min 散布) */ + g_gen++; + g_tick += 200u; + time_sntp_run(g_gen); + CHECK(g_sock_close_calls == 1 && s_ctx.state == SNTP_ST_WAIT_DUE && + s_ctx.gen == g_gen, "gen: close+re-spread"); + { + uint32_t t; + for (t = 0; t < 59000u; t += 200u) { /* 首 59s 不该再发 */ + g_tick += 200u; + time_sntp_run(g_gen); + } + CHECK(g_send_calls == before, "gen: fresh 1..5min re-spread"); + } +} + +static void tc_send_fail_switch(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + uint8_t bak[] = SNTP_T_NET_IP(192, 168, 31, 13); + + s_case_reset(); + s_cfg_enable(pri, bak, 60u); + g_send_ret = -1; + g_errno_val = NET_ERR_NOSPACE; + (void)s_run_until_send(320000u); + CHECK(s_ctx.state == SNTP_ST_WAIT_DUE && s_ctx.timeout_ms == 10000u, + "sendfail: backoff"); + CHECK(s_ctx.srv == 1u, "sendfail: switched"); + g_send_ret = 48; + (void)s_run_until_send(20000u); + CHECK(memcmp(g_sent_ip, bak, 4) == 0, "sendfail: retry to backup"); +} + +static void tc_recv_err_deadfd(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + uint32_t t; + + s_case_reset(); + s_cfg_enable(pri, NULL, 60u); + (void)s_run_until_send(320000u); + sntp_t_push_recv(NET_RECV_ERR, NULL, 0, NULL, 0); + g_errno_val = NET_ERR_CONNRESET; + g_tick += 200u; + time_sntp_run(g_gen); + CHECK(g_last_close_fd >= 100 && s_ctx.fd == -1, "recverr: dead fd closed"); + CHECK(s_ctx.state == SNTP_ST_WAIT_DUE, "recverr: backoff"); + /* 下一轮到点自动重开 socket */ + for (t = 0; t < 12000u; t += 200u) { + g_tick += 200u; + time_sntp_run(g_gen); + if (g_send_calls >= 2) { break; } + } + CHECK(g_send_calls == 2 && g_sock_opens == 2, "recverr: reopened"); +} + +static void tc_oversized_resp_ok(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + uint8_t resp[64]; + uint32_t t1s; + + s_case_reset(); + g_face = 1750000000u + 28800u; + s_cfg_enable(pri, NULL, 60u); + (void)s_run_until_send(320000u); + t1s = s_be32(&g_sent_pkt[40]); + memset(resp, 0xAA, sizeof(resp)); + s_build_resp(resp, 0u, 5u, t1s, 0u, t1s, 0u); /* 前 48 合法,尾部垃圾 */ + s_feed(resp, 56, pri, net_htons(123u)); /* 截断交付 = BSD 语义 */ + CHECK(time_sntp_sync_count() >= 1u && s_ctx.state == SNTP_ST_WAIT_DUE, + "oversize: parsed first 48"); +} + +static void tc_recv_slice_50ms(void) +{ + uint8_t pri[] = SNTP_T_NET_IP(192, 168, 31, 161); + + s_case_reset(); + s_cfg_enable(pri, NULL, 60u); + (void)s_run_until_send(320000u); + g_recv_last_timeout_ms = 0u; + g_tick += 200u; + time_sntp_run(g_gen); + CHECK(g_recv_last_timeout_ms == SNTP_RECV_SLICE_MS, "slice: 50ms watchdog-safe"); +} + +static void tc_helpers_direct(void) +{ + uint32_t x, i, prev; + + s_case_reset(); + /* 回绕安全到期比较 */ + CHECK(s_tick_due(1000u, 900u) == 1u, "due: past"); + CHECK(s_tick_due(800u, 900u) == 0u, "due: future"); + CHECK(s_tick_due(5u, 0xFFFFFFF0u) == 1u, "due: wrap past"); + CHECK(s_tick_due(0xFFFFFFF0u - 5u, 0xFFFFFFF0u) == 0u, "due: wrap future"); + /* int64 截断 */ + CHECK(s_off_clamp(3000LL) == 3000, "clamp: passthrough"); + CHECK(s_off_clamp(9999999999999LL) == 2147483647, "clamp: hi"); + CHECK(s_off_clamp(-9999999999999LL) == -2147483647, "clamp: lo"); + /* xorshift 永不为 0(0 会卡死) */ + s_rng_seed_once(); + prev = s_rng_next(); + CHECK(prev != 0u, "rng: nonzero"); + for (i = 0; i < 100000u; i++) { + x = s_rng_next(); + if (x == 0u) { break; } + prev = x; + } + CHECK(prev != 0u && i == 100000u, "rng: stream nonzero 100k"); + /* 冷启动兜底 T1 域:2036 前进位用 uint32 环绕语义(seconds 直接 ++ 可回绕) */ + { + SntpTimestamp_t ts; + g_face = 0xFFFFFFFFu - 1000u; /* 钟面 > 0x7FFFFFFF → 视为无效域外? */ + /* s_utc_now: face>=MIN+TZ 且 !=UINT32_MAX → 有效;epoch 加法环绕 */ + s_ts_now(&ts); + { + /* uint32 模运算逐项环绕,与 s_ts_now 内部语义一致 */ + uint32_t expu = (uint32_t)((0xFFFFFFFFu - 1000u - 28800u) + + SNTP_TIME_AT_UNIX_EPOCH_SECS); + CHECK(ts.seconds == expu, "ts: era wrap"); + } + g_face = UINT32_MAX; + } +} + +/* 把 host 序 32bit 以大端铺进 p[0..3](origin/receive/transmit 字段都要大端) */ +static void s_fill_be(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t)(v >> 24); p[1] = (uint8_t)(v >> 16); + p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v; +} + +static void tc_serializer_vendored(void) +{ + SntpTimestamp_t t1, t4; + SntpResponseData_t r; + SntpTimestamp_t zero = { 0u, 0u }; + uint8_t req[48]; + uint8_t resp[48]; + uint32_t us = 0u, us2 = 0u; + + s_case_reset(); + t1.seconds = SNTP_TIME_AT_UNIX_EPOCH_SECS + 1000000000UL; /* 2001+,era0 */ + t1.fractions = 0x12345678UL; + CHECK(Sntp_SerializeRequest(&t1, 0xAABBCCDDu, req, 48u) == SntpSuccess, "ser: ok"); + CHECK(req[0] == 0x23u, "ser: LI0 VN4 mode3"); + /* 防欺骗核心:randomNumber>>16(16bit)OR 进 T1 分数低位 + 回写 pRequestTime + * ——应答必须原样 echo 这个合成值,否则判无效 */ + CHECK(t1.fractions == (0x12345678u | 0xAABBu), "ser: random merged into fractions"); + CHECK(s_be32(&req[40]) == t1.seconds && s_be32(&req[44]) == t1.fractions, + "ser: BE transmit stamped"); + CHECK(Sntp_SerializeRequest(&zero, 0u, req, 48u) == SntpErrorBadParameter, + "ser: zero T1 rejected"); + CHECK(Sntp_SerializeRequest(&t1, 0u, req, 47u) == SntpErrorBufferTooSmall, + "ser: small buf"); + /* 合法应答 round-trip:origin = 序列化后的 t1 精确回显(大端) */ + memset(resp, 0, 48); + resp[0] = (uint8_t)((0u << 6) | (4u << 3) | 4u); + resp[1] = 5u; + s_fill_be(&resp[24], t1.seconds); s_fill_be(&resp[28], t1.fractions); + s_fill_be(&resp[32], t1.seconds + 1u); s_fill_be(&resp[36], 0u); + s_fill_be(&resp[40], t1.seconds + 2u); s_fill_be(&resp[44], 0x00000102u); + t4 = t1; + CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r) == SntpSuccess, + "ser: deserialize ok"); + CHECK(r.leapSecondType == NoLeapSecond, "ser: leap none"); + CHECK(r.serverTime.seconds == t1.seconds + 2u, "ser: T3 parsed"); + /* originate 不匹配 → InvalidResponse(客户机据此丢弃继续等待) */ + resp[24] ^= 0xFFu; + CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r) == SntpInvalidResponse, + "ser: origin mismatch rejected"); + resp[24] ^= 0xFFu; + /* mode≠4:回显包/伪造包直接判无效 */ + resp[0] = (uint8_t)((resp[0] & 0xF8u) | 3u); + CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r) == SntpInvalidResponse, + "ser: mode!=4 rejected"); + /* KoD:stratum=0 + refId 码分类(DENY/RSTR 换服、RATE 退避、其余 OtherCode) */ + resp[0] = (uint8_t)((4u << 3) | 4u); + resp[1] = 0u; + resp[12] = 'D'; resp[13] = 'E'; resp[14] = 'N'; resp[15] = 'Y'; + CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r) + == SntpRejectedResponseChangeServer, "ser: KoD DENY"); + resp[12] = 'R'; resp[13] = 'A'; resp[14] = 'T'; resp[15] = 'E'; + CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r) + == SntpRejectedResponseRetryWithBackoff, "ser: KoD RATE"); + resp[12] = 'R'; resp[13] = 'S'; resp[14] = 'T'; resp[15] = 'R'; + CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r) + == SntpRejectedResponseChangeServer, "ser: KoD RSTR"); + resp[12] = '?'; + CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r) + == SntpRejectedResponseOtherCode, "ser: KoD other"); + resp[1] = 5u; + /* 2036 纪元语义:era0 尾回减、era1 头回加 */ + { + SntpTimestamp_t e; + e.seconds = SNTP_TIME_AT_UNIX_EPOCH_SECS; /* 1970-01-01 恰在界上 */ + e.fractions = 0u; + CHECK(Sntp_ConvertToUnixTime(&e, &us, &us2) == SntpSuccess && us == 0u, + "ser: epoch boundary -> unix 0"); + e.seconds = SNTP_TIME_AT_UNIX_EPOCH_SECS - 1u; /* era-1 起点附近 */ + CHECK(Sntp_ConvertToUnixTime(&e, &us, &us2) == SntpSuccess && + us == UNIX_TIME_SECS_AT_SNTP_ERA_1_SMALLEST_TIME + + (SNTP_TIME_AT_UNIX_EPOCH_SECS - 1u), "ser: era1 wrap"); + } +} + +static void tc_offset_math(void) +{ + /* 四元组中点:T1 含随机低分数(fractionsToMs 向下取整抹掉)、T4 无 → 精确 5000ms */ + SntpTimestamp_t t1, t4; + SntpResponseData_t r; + uint8_t resp[48]; + int64_t exp; + + s_case_reset(); + t1.seconds = SNTP_TIME_AT_UNIX_EPOCH_SECS + 1500000000UL; + t1.fractions = (4295000u * 123u) | 0x1234u; /* 123ms + 亚 ms 随机噪声 */ + memset(resp, 0, 48); + resp[0] = (uint8_t)((4u << 3) | 4u); + resp[1] = 3u; + s_fill_be(&resp[24], t1.seconds); s_fill_be(&resp[28], t1.fractions); + { + uint32_t f = 4295000u * 123u; /* 服务器对齐到整 ms */ + uint32_t s = t1.seconds + 5u; /* T2 = T3 = T1 + 5s */ + s_fill_be(&resp[32], s); s_fill_be(&resp[36], f); + s_fill_be(&resp[40], s); s_fill_be(&resp[44], f); + } + t4.seconds = t1.seconds; + t4.fractions = 4295000u * 123u; /* 本地 ms 分量(无随机) */ + CHECK(Sntp_DeserializeResponse(&t1, &t4, resp, 48u, &r) == SntpSuccess, "off: ok"); + exp = r.clockOffsetMs; + CHECK(exp == 5000LL, "off: (T2-T1+T3-T4)/2 == 5000ms exact"); +} + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("=== SNTP Client Tests ===\n"); + +#define TC(fn) do { printf("[" #fn "]\n"); fn(); } while (0) + TC(tc_poll_clamp); + TC(tc_disabled_silent); + TC(tc_first_send_window); + TC(tc_cold_start_step_t3); + TC(tc_step_valid_clock); + TC(tc_deadband_no_step); + TC(tc_anti_spoof_src); + TC(tc_li3_stratum); + TC(tc_kod); + TC(tc_timeout_backoff_cap); + TC(tc_poll_pacing_after_success); + TC(tc_no_server_warn_once); + TC(tc_disable_closes_socket); + TC(tc_gen_change_resets); + TC(tc_send_fail_switch); + TC(tc_recv_err_deadfd); + TC(tc_oversized_resp_ok); + TC(tc_recv_slice_50ms); + TC(tc_helpers_direct); + TC(tc_serializer_vendored); + TC(tc_offset_math); +#undef TC + + printf("\n=== SNTP Client Tests: %d/%d PASSED (failed=%d) ===\n", + g_pass, g_total, g_fail); + return (g_fail == 0) ? 0 : 1; +} diff --git a/test/host/sntp/stub/FreeRTOS.h b/test/host/sntp/stub/FreeRTOS.h new file mode 100644 index 00000000..05953b54 --- /dev/null +++ b/test/host/sntp/stub/FreeRTOS.h @@ -0,0 +1,5 @@ +#ifndef SNTP_TEST_FREE_RTOS_H +#define SNTP_TEST_FREE_RTOS_H +#include +typedef uint32_t TickType_t; +#endif /* SNTP_TEST_FREE_RTOS_H */ diff --git a/test/host/sntp/stub/cmsis_os2.h b/test/host/sntp/stub/cmsis_os2.h new file mode 100644 index 00000000..1fe7bbde --- /dev/null +++ b/test/host/sntp/stub/cmsis_os2.h @@ -0,0 +1,14 @@ +/* 主机测试固件(test/host/sntp):cmsis_os2.h 替身——事件标志最小子集。 */ +#ifndef SNTP_TEST_CMSIS_OS2_H +#define SNTP_TEST_CMSIS_OS2_H +#include +typedef void *osEventFlagsId_t; +#define osFlagsWaitAll 0u +#define osFlagsNoClear 0u +#define osFlagsErrorUnknown ((uint32_t)0xFFFFFFFFu) +#define osFlagsErrorTimeout ((uint32_t)0xFFFFFFFEu) +#define osFlagsErrorParameter ((uint32_t)0xFFFFFFFAu) +#define osFlagsErrTimeout osFlagsErrorTimeout +uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout); +uint32_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t events); +#endif /* SNTP_TEST_CMSIS_OS2_H */ diff --git a/test/host/sntp/stub/dbg_log.h b/test/host/sntp/stub/dbg_log.h new file mode 100644 index 00000000..cb876aa2 --- /dev/null +++ b/test/host/sntp/stub/dbg_log.h @@ -0,0 +1,13 @@ +/* 主机测试固件(test/host/sntp):dbg_log.h 替身。 + * 计数宏替代真实日志;不定义 DBG_TAG/DBG_LINE_BUF_MAX(生产 .c 自带,重复定义触 C4005 /W4)。 */ +#ifndef SNTP_TEST_DBG_LOG_H +#define SNTP_TEST_DBG_LOG_H +#include +extern int g_dbg_err_count; +extern int g_dbg_warn_count; +extern int g_dbg_info_count; +#define DBG_ERROR(fmt, ...) (g_dbg_err_count++) +#define DBG_WARN(fmt, ...) (g_dbg_warn_count++) +#define DBG_INFO(fmt, ...) (g_dbg_info_count++) +#define DBG_DEBUG(fmt, ...) ((void)0) +#endif /* SNTP_TEST_DBG_LOG_H */ diff --git a/test/host/sntp/stub/net_socket.h b/test/host/sntp/stub/net_socket.h new file mode 100644 index 00000000..cf003048 --- /dev/null +++ b/test/host/sntp/stub/net_socket.h @@ -0,0 +1,27 @@ +/* 主机测试固件(test/host/sntp):net_socket.h 替身——UDP 客户端最小子集。 + * 合同与真机 Drivers/BSP/NET/net_socket.h(PR #74 后)一致: + * net_sendto 返回 >0 = 实发字节数(须恰等 48),-1 = 错误(net_get_errno 可查) + * net_recvfrom_to >0 交付 / -2 片内无数据(不置 errno)/ -1 错误 / 0 防御 + * 载荷/源地址/返回值由测试脚本注入(sntp_test.c 的 fake 实现)。 */ +#ifndef SNTP_TEST_NET_SOCKET_H +#define SNTP_TEST_NET_SOCKET_H +#include "task.h" +#include "net_types.h" + +/* 脚本注入 token(fake 队列 kind 用):数值即 fake net_recvfrom_to 的返回值 */ +#define NET_RECV_DATA 1 +#define NET_RECV_TIMEOUT (-2) +#define NET_RECV_CONNCLOSE 0 +#define NET_RECV_ERR (-1) + +int net_socket(int domain, int type); +int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen); +int net_sendto(int sockfd, const void *buf, int len, int flags, + const struct net_sockaddr *dest, int addrlen); +int net_recvfrom_to(int sockfd, void *buf, int len, uint32_t timeout_ms, + struct net_sockaddr *src, int *addrlen); +int net_close(int sockfd); +int net_get_errno(void); +uint32_t net_get_generation(void); + +#endif /* SNTP_TEST_NET_SOCKET_H */ diff --git a/test/host/sntp/stub/net_types.h b/test/host/sntp/stub/net_types.h new file mode 100644 index 00000000..7b7bbfc9 --- /dev/null +++ b/test/host/sntp/stub/net_types.h @@ -0,0 +1,50 @@ +/* 主机测试固件(test/host/sntp):net_types.h 替身——IPv4 地址与 sockaddr 结构、错误码、htons。 + * 字段布局与真机 Drivers/BSP/NET/net_types.h 一致(sin_port 大端;s_addr 内存首字节=首个八位组)。 */ +#ifndef SNTP_TEST_NET_TYPES_H +#define SNTP_TEST_NET_TYPES_H +#include + +#define NET_AF_INET 2 +#define NET_SOCK_STREAM 1 +#define NET_SOCK_DGRAM 2 +#define NET_INADDR_ANY 0u + +#define NET_OK 0 +#define NET_ERR_BADF -2 +#define NET_ERR_INVAL -3 +#define NET_ERR_NOMEM -4 +#define NET_ERR_NOTCONN -5 +#define NET_ERR_ISCONN -6 +#define NET_ERR_WOULDBLOCK -7 +#define NET_ERR_CONNRESET -8 +#define NET_ERR_TIMEDOUT -9 +#define NET_ERR_NOSPACE -10 +#define NET_ERR_BUSY -11 + +struct net_in_addr { + uint32_t s_addr; +}; + +struct net_sockaddr { + uint16_t sa_family; + char sa_data[14]; +}; + +struct net_sockaddr_in { + uint16_t sin_family; + uint16_t sin_port; /* 大端 */ + struct net_in_addr sin_addr; + char sin_zero[8]; +}; + +static inline uint16_t net_htons(uint16_t hostorder) +{ + return (uint16_t)(((hostorder & 0x00FFu) << 8) | ((hostorder & 0xFF00u) >> 8)); +} + +static inline uint16_t net_ntohs(uint16_t netorder) +{ + return net_htons(netorder); +} + +#endif /* SNTP_TEST_NET_TYPES_H */ diff --git a/test/host/sntp/stub/sys_clock.h b/test/host/sntp/stub/sys_clock.h new file mode 100644 index 00000000..b77e2930 --- /dev/null +++ b/test/host/sntp/stub/sys_clock.h @@ -0,0 +1,7 @@ +/* 主机测试固件(test/host/sntp):sys_clock.h 替身——time_sntp.c 只用这两个入口。 */ +#ifndef SNTP_TEST_SYS_CLOCK_H +#define SNTP_TEST_SYS_CLOCK_H +#include +uint32_t sys_clock_get(void); +int sys_clock_set_unix(uint32_t unix_secs); +#endif /* SNTP_TEST_SYS_CLOCK_H */ diff --git a/test/host/sntp/stub/sys_events.h b/test/host/sntp/stub/sys_events.h new file mode 100644 index 00000000..17e8964d --- /dev/null +++ b/test/host/sntp/stub/sys_events.h @@ -0,0 +1,15 @@ +/* 主机测试固件(test/host/sntp):sys_events.h 替身——只暴露 time_sntp.c 用到的成员。 */ +#ifndef SNTP_TEST_SYS_EVENTS_H +#define SNTP_TEST_SYS_EVENTS_H +#include "cmsis_os2.h" + +extern osEventFlagsId_t sys_evtHandle; + +#define SYS_EVT_NET_READY (1u << 0) +#define SYS_EVT_STORAGE_READY (1u << 1) +#define SYS_EVT_FTP_READY (1u << 2) +#define SYS_EVT_TIMESYNC_READY (1u << 3) +#define SYS_EVT_NET_RECONFIG (1u << 4) +#define SYS_EVT_CFG_SAVE (1u << 5) + +#endif /* SNTP_TEST_SYS_EVENTS_H */ diff --git a/test/host/sntp/stub/sys_log.h b/test/host/sntp/stub/sys_log.h new file mode 100644 index 00000000..08eb2b5e --- /dev/null +++ b/test/host/sntp/stub/sys_log.h @@ -0,0 +1,19 @@ +/* 主机测试固件(test/host/sntp):sys_log.h 替身——syslog_post_quiet 记数即可。 */ +#ifndef SNTP_TEST_SYS_LOG_H +#define SNTP_TEST_SYS_LOG_H +#include + +typedef enum { + SYSLOG_RUN = 0u, + SYSLOG_SELFTEST, + SYSLOG_ACCIDENT, + SYSLOG_ALARM, + SYSLOG_START, + SYSLOG_OPER, + SYSLOG_YK, + SYSLOG_COUNT +} syslog_id_t; + +void syslog_post_quiet(syslog_id_t id, const char *desc); + +#endif /* SNTP_TEST_SYS_LOG_H */ diff --git a/test/host/sntp/stub/task.h b/test/host/sntp/stub/task.h new file mode 100644 index 00000000..a8436ce9 --- /dev/null +++ b/test/host/sntp/stub/task.h @@ -0,0 +1,6 @@ +#ifndef SNTP_TEST_TASK_H +#define SNTP_TEST_TASK_H +#include +#include "FreeRTOS.h" +uint32_t xTaskGetTickCount(void); +#endif /* SNTP_TEST_TASK_H */ diff --git a/test/host/time_sync/time_sync_test.c b/test/host/time_sync/time_sync_test.c index 11bcf582..3a10d990 100644 --- a/test/host/time_sync/time_sync_test.c +++ b/test/host/time_sync/time_sync_test.c @@ -341,6 +341,24 @@ int g_tstest_info_count = 0; else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ } while (0) +/* ---- SNTP 客户机替身(time_sync.c v1.4 起循环调用 time_sntp_run; + * 真实头 App/sync/time_sntp.h 与 time_sync.c 同目录,引号包含直达声明, + * 这里只落三个空实现——行为断言在 test/host/sntp 独立套件里做) ---- */ +static int s_sntp_run_calls; + +void time_sntp_run(uint32_t net_gen) { + (void)net_gen; + s_sntp_run_calls++; +} + +uint32_t time_sntp_sync_count(void) { + return 0u; +} + +int32_t time_sntp_last_offset_ms(void) { + return 0; +} + /* ===================== 被测代码(原样纳入生产源) ===================== */ #include "time_sync.c" diff --git a/ui_cn_chars.txt b/ui_cn_chars.txt index 1d0c7331..c1b9fdb5 100644 --- a/ui_cn_chars.txt +++ b/ui_cn_chars.txt @@ -1 +1 @@ -ⅠⅡⅢ▶一三上不主事二交件位低作保信值入公共关写出分制削前动化区压原参双发取口可号合听启告周回因围地址型域复外失子字存定实密已帧常年序度延开异式引强归当录态总恢息户手投抖护报据掉接控掩操收放故效数整无日时显最月期未本机板查标校样检模欠次正死段求池波流测消清温点片版状狗独用电界监直看知码硬确示秒程窗立站端第类系线终统续维网置脚自节芯范荷行装警计认讯记设询该误请读负败越跳软载输过运近返退送通道遥部配采重量钟钥锁错长门闭间闸防除障零面页频馈首验默,: +ⅠⅡⅢ▶一三上不主事二交件位低作保信值入公共关写出分制削前务动化区压原参双发取口可号合听启告周器回因围地址型域备复外失子字存定实密已帧常年序度延开异式引强归当录态总恢息户手投抖护报据掉接控掩操收放故效数整无日时显最月服期未本机板查标校样检模欠次正死段求池波流测消清温点片版状狗独用电界监直看知码硬确示秒程窗立站端第类系线终统续维网置脚自节芯范荷行装警计认讯记设询该误请读负败越跳软载输过运近返退送通道遥部配采重量钟钥锁错长门闭间闸防除障零面页频馈首验默,: diff --git a/ui_menu_tree.txt b/ui_menu_tree.txt index 4d63c08f..4d6cdd80 100644 --- a/ui_menu_tree.txt +++ b/ui_menu_tree.txt @@ -47,7 +47,7 @@ 通讯参数 通讯设置 #P #page=comm 网口设置 #P #page=net_cfg - SNTP设置 #P + SNTP设置 #P #page=sntp 记录查询 SOE记录 #page=soe 事故记录 #page=accident