ad7338365c
- 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)
625 lines
22 KiB
C
625 lines
22 KiB
C
/*
|
||
* 模块名称: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 <string.h>
|
||
|
||
#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;
|
||
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) {
|
||
const uint8_t *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;
|
||
}
|