feat(sntp): 板端 SNTP 校时接入——coreSNTP 序列化器 + [sntp] 段 + 设置页(#73)
CI / 静态门禁 (pre-commit) (push) Failing after 5m33s
CI / 主机侧单测 (MSVC) (push) Successful in 45s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 5m20s
CI / 主机侧单测 (MSVC) (pull_request) Successful in 31s

- vendored coreSNTP v2.0.0 序列化器(MIT,仅 serializer+LICENSE,零堆),
  48 字节报文编解码在库,节奏与准入判据在本方(RFC 4330 §10 调研口径)
- App/sync/time_sntp.c 挂 timeSyncTask 的 8888 事件循环,等待切片 ≤50 ms,
  不饿任务狗与 IWDG;懒建 UDP socket,代际变化/禁用/死错误码即关闭归还
- 防伪 = OriginTimestamp 逐位等于发出时 T1(含低 16 位随机小数位);
  mode/stratum/闰秒指示/KoD 按 RFC 拒收;DENY·RSTR 换服务器、RATE 只退避
- 主备轮换 + 失败指数退避(5 s 起、60 s 封顶)+ 首轮 1~5 分钟随机错峰;
  成功恢复 poll_min 周期(1~1440 分,出厂默认 60,默认不启用不发包)
- 钟面 = 本地时间:UTC+8 固定偏移落 sys_clock_set_unix(§1.1 时基裁定,
  第三个写入口,装置仍不存时区);|offset|<2 s 只记日志不步进,冷启动或
  更大偏差步进并回写 SD2506,留痕 [SNTP] step 与 syslog OPER
- 首个校验通过的同步置 SYS_EVT_TIMESYNC_READY(就绪位,消费方按需 NoClear)
- [sntp] 配置段(enable/server1/server2/poll_min)属主 sntp_cfg.c,
  注册在 net_cfg_load();UI 页 31「SNTP 设置」(密码叶):启用开关 +
  主/备服务器 octet 编辑 + 周期(分) + 保存,复用 net_cfg_save_request 链
- 测试与门禁:test/host/sntp 新套件(报文/防伪/KoD/退避周期/阶跃域/配置
  注册表)、net_cfg 与 time_sync 套件补 fake、黄金参考 66→74 张零漂移、
  字符集 228 字、Keil 0 错 0 警(RO +5,848 B 含字体)、覆盖率基线回填
- 文档:墙钟维护说明 §16、网络配置说明 [sntp] 节、配置文件设计说明变更记录、
  产品需求 FR-TIME-3 与 8.3/9.1.2 台账、UI 补齐规划/UI补齐 31 叶口径、
  登记簿销项并登记「SNTP 校时真机判据」等台架(PC 时间源 192.168.31.161/.13)
This commit is contained in:
2026-11-04 15:27:41 +08:00
parent c1beec50e7
commit c2dd6f34c0
69 changed files with 4641 additions and 272 deletions
+17 -1
View File
@@ -143,6 +143,8 @@ typedef struct {
static fake_reg_t g_regs[CFG_FILE_MAX_SECTIONS];
static int g_reg_n;
static int g_ftp_reg_calls; /* ftp_cfg_register 被 net_cfg_load 调了几次 */
static int g_sntp_reg_calls; /* sntp_cfg_register 同上 */
static int g_sntp_reg_before_load; /* 其中发生在 cfg_file_load 之前的次数(先注册后 load 契约) */
static int g_load_calls;
static int g_save_calls;
static int g_apply_calls;
@@ -205,10 +207,21 @@ void ftp_cfg_register(void)
g_ftp_reg_calls++;
}
/* [sntp] 段归 test/host/sntp/ 模块测;同 ftp 口径:只计数 + 先注册后 load 序 */
void sntp_cfg_register(void)
{
g_sntp_reg_calls++;
if (g_load_calls == 0) {
g_sntp_reg_before_load++;
}
}
static void fake_reset(void)
{
g_reg_n = 0;
g_ftp_reg_calls = 0;
g_sntp_reg_calls = 0;
g_sntp_reg_before_load = 0;
g_load_calls = 0;
g_save_calls = 0;
g_apply_calls = 0;
@@ -282,9 +295,12 @@ static void tc_register_load(void)
"TC-NETCFG-005 first load registers [network] exactly once");
CHECK((g_ftp_reg_calls == 1) && (g_load_calls == 1),
"TC-NETCFG-006 also registers [ftp], registered before load");
CHECK((g_sntp_reg_calls == 1) && (g_sntp_reg_before_load == 1),
"TC-NETCFG-006b also registers [sntp], registered before load");
rc = net_cfg_load();
CHECK((rc == 0) && (g_reg_n == 1) && (g_ftp_reg_calls == 2),
CHECK((rc == 0) && (g_reg_n == 1) && (g_ftp_reg_calls == 2) &&
(g_sntp_reg_calls == 2),
"TC-NETCFG-007 repeated load does not re-register [network]");
CHECK((strcmp(g_regs[0].name, "network") == 0) &&
+63
View File
@@ -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))
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
#ifndef SNTP_TEST_FREE_RTOS_H
#define SNTP_TEST_FREE_RTOS_H
#include <stdint.h>
typedef uint32_t TickType_t;
#endif /* SNTP_TEST_FREE_RTOS_H */
+14
View File
@@ -0,0 +1,14 @@
/* 主机测试固件(test/host/sntp):cmsis_os2.h 替身——事件标志最小子集。 */
#ifndef SNTP_TEST_CMSIS_OS2_H
#define SNTP_TEST_CMSIS_OS2_H
#include <stdint.h>
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 */
+13
View File
@@ -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 <stdint.h>
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 */
+27
View File
@@ -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 */
+50
View File
@@ -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 <stdint.h>
#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 */
+7
View File
@@ -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 <stdint.h>
uint32_t sys_clock_get(void);
int sys_clock_set_unix(uint32_t unix_secs);
#endif /* SNTP_TEST_SYS_CLOCK_H */
+15
View File
@@ -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 */
+19
View File
@@ -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 <stdint.h>
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 */
+6
View File
@@ -0,0 +1,6 @@
#ifndef SNTP_TEST_TASK_H
#define SNTP_TEST_TASK_H
#include <stdint.h>
#include "FreeRTOS.h"
uint32_t xTaskGetTickCount(void);
#endif /* SNTP_TEST_TASK_H */
+18
View File
@@ -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"