refactor(time_sync)!: 退役 TCP :8888 对时任务,timeSyncTask 转纯 SNTP 校时节拍宿主(#78)
CI / 静态门禁 (pre-commit) (push) Failing after 43s
CI / 主机侧单测 (MSVC) (push) Failing after 45s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 45s
CI / 主机侧单测 (MSVC) (pull_request) Failing after 45s

SNTP 客户端已随 #73/PR #77 上线合入(merge 2b848c4a),用户裁定
8888 对时任务退出历史舞台(gitea:77 线程原话,跟踪 Issue #78)。

生产侧(App/sync/time_sync.c v2.0,任务入口名不变,freertos.c 零改动):
- 删除 :8888 监听、TIME 包重组、sys_clock 写入、OPER 落痕、
  net_drop_conn 队头自救整条链(TIME_SYNC_PORT 宏随退役删除)
- 任务体收敛为:就绪屏障(100ms 分片、osFlagsNoClear)→
  200ms 校时节拍 task_wd_beat → time_sntp_run(net_get_generation())
  → 分片延时;喂狗节奏(TASK_WD_TIMESYNC=5,required_mask)不变
- 时钟写入口自此 = SNTP / UI 手工 / 104 C_CS,双写竞态(后写者赢)
  随之消失(墙钟说明 §15 退役公告、§16 现势口径)

工具与测试:
- 删除 test/host/time_sync_server.py(对时源)与 e02_probe.py
  (队头阻塞探针,载体随监听退役;判据脚本 v5_log_gap_report.py
  保留供旧日志解读)
- time_sync 主机单测重写 v2.0:33 断言覆盖分片延时 / 屏障 /
  SNTP 节拍与网络代次当轮交接 / 未出网守门;stub 的
  net_socket.h 裁剪至 net_get_generation 作编译期绊线
- 覆盖率基线 --update-baseline 收口(time_sync.c 101→20 行为刻意
  缩减,全仓 48/48 套绿)

文档与图:墙钟说明 §15 改退役公告(教训保留:CH395F FIN 丢未读
缓冲)、§16 单一网络来源;产品需求 FR-TIME-2 退役注记、§6.3 重写;
架构总览 / 软件架构规格 / 网络配置 / 网络自愈 §9.3 / BSD 指南 /
看门狗说明心跳与白名单行 / 实机测试指南 / 部署环境 / 继保规划 /
UI补齐时基口径 / 页面补齐规划 SNTP 页行 / 任务规划 README 叶数与
暂缓项行(SNTP 叶已绑、UI_PAGE_MAP_COUNT=31 口径)等约 30 处现势
口径同步;arch / net_stack 两图生成器与 SVG 同笔重跑。

台账:ram_baseline --update + check_mem_numbers --write + 体积凭证
(Code 254,648→253,428,−1,220 B)。静态链复测发现 timeSyncTask
1,904 B / iec104Task 1,600 B 低于 ISR 硬线:master 2b848c4a 同判据
timeSyncTask 即 2,056 B(本 PR 只减不增,−152 B 旧任务自身帧),
双 FAIL 为 #74/#77 后既有债;抬栈属 Ask-first 未擅自执行,登记簿
新增「静态链硬线双 FAIL」待台架复采,复测快照入 docs/内存管理/data。

验证:keil-build 0 错 0 警;time_sync/sntp 主机单测绿;
ci_local 全绿;doc-links --all 0。
This commit is contained in:
2026-11-04 18:07:03 +08:00
parent 2b848c4a0b
commit 3a2f9b21fb
50 changed files with 621 additions and 1567 deletions
+8 -6
View File
@@ -10,7 +10,6 @@
|---|---|
| `run_boot_test.py` | **`/serial-boot-test` 一键闭环**:开串口+清日志 → 烧录 → wait-idle → `verify_log` 校验(退出码 0=PASS) |
| `verify_log.py` | **日志标记校验**:Required / Forbidden 标记 → PASS / FAIL;可选 `--require-suite-summary` 校验套件汇总行(`=== <名称>: N/M PASSED (failed=0) ===` 且 `N>0`),抓"没跑到汇总行/断言数为 0"的盲区 |
| `e02_probe.py` | 头阻塞专项探针 |
### 网络 / FTP
@@ -38,7 +37,9 @@
| 脚本 | 作用 |
|---|---|
| `latency_peer.py` | 链路时延测试的 PC 对端(peer) |
| `time_sync_server.py` | 时间同步推送服务(对时源) |
> 原 `time_sync_server.py`(:8888 推送对时源)已随 :8888 退役删除(2026-11-04,#78);
> 对时改为板端 SNTP 客户端出向(墙钟说明 §16),台架用真实/私有 NTP 源即可。
### 主机侧单元测试(纯逻辑,无硬件)
@@ -62,9 +63,9 @@
| `vol_lock/run_unit_test.py` | **卷锁单测**:离线编译真实 `App/fs/vol_lock.c`,33 断言点覆盖创建 / 获取 / 释放契约(**fail-open 不变量**:创建失败仍返回 1 且零阻塞)/ 幂等 / 超时透传 |
| `vol_lock/vol_lock_test.c` | 测试单元本体(`#include` 生产源码 + CMSIS 互斥桩) |
| `vol_lock/stub/` | 桩头文件(`cmsis_os.h`) |
| `time_sync/run_unit_test.py` | **时间同步单测**:离线编译真实 `App/sync/time_sync.c`,80 断言点覆盖分片喂狗延时 / 监听退避 / 时间包重组(半包·粘连·EOF·真错·预算耗尽·tick 回绕)/ 任务主循环(**探针窗口心跳缺口的全部逻辑贡献者**) |
| `time_sync/run_unit_test.py` | **时间同步任务载体单测(v2.0)**:离线编译真实 `App/sync/time_sync.c`,33 断言点覆盖分片喂狗延时 / 就绪屏障(100 ms 分片、`osFlagsNoClear`、永不就绪不挂死)/ SNTP 节拍驱动与**网络代次当轮交接** / 未出网守门(:8888 退役后任务即纯校时节拍宿主) |
| `time_sync/time_sync_test.c` | 测试单元本体(`#include` 生产源码 + 网络 / 事件 / RTOS 可脚本化桩 + `longjmp` 跳出 `for(;;)` 任务循环) |
| `time_sync/stub/` | 桩头文件(`FreeRTOS.h` / `task.h` / `cmsis_os2.h` / `net_socket.h` / `sys_clock.h` / `sys_events.h` / `dbg_log.h`) |
| `time_sync/stub/` | 桩头文件(`FreeRTOS.h` / `task.h` / `cmsis_os2.h` / `net_socket.h`(裁剪至 `net_get_generation`,**编译期绊线**:生产码若再引监听类 API 立刻炸)/ `sys_events.h`) |
| `iec104/run_unit_test.py` | **IEC 60870-5-104 协议栈单测(IEC 104 上行)**:离线编译真实 `App/protocol/iec104/` 三模块 + 数据源(prot_set/prot_elm/phasor/sample_ring),**82** 用例覆盖 CP56 往返 / APCI 半帧·粘帧·坏帧 / STARTDT 握手 / 总召分片+RBT / COV 死区 / k=12 窗满 / w=8 触发 S / t1·t2·t3 / N(R) 倒退判故障 / 遥控·定值两段式与 PN / C_CS / 未知类型·地址错误应答 |
| `nand_port/run_unit_test.py` | **dhara NAND 移植层单测**:离线编译真实 `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` + 真实 `Lib/dhara/{journal,map}.c`,以 **RAM 假芯片**驱动,**149** 断言覆盖 is_free 整页扫描 / 六个 HAL 入口的越界守卫 / 故障注入 / dhara map"写入 → 重新 resume"场景 |
| `nand_port/nand_port_test.c` | 测试单元本体(`#include` 生产源码 + RAM 假芯片;内联一份"只看页首 64 B"的**旧算法对照**,证明断言能咬人) |
@@ -139,7 +140,8 @@
- 本目录**被 pre-commit 门禁的 cppcheck / 代码规范排除**(`test/` 豁免);
但各模块的 `run_unit_test.py` 自身**作为独立钩子**参与门禁(见上,共 22 个)
- 台架探针族分工(2026-10-13 起):**自愈/Trap 常备回归腿在本目录**(`relisten_stress` /
`ftp_abort_probe` / `n30_repro` / `e02_probe` / `e6_soak` 等);`MDK-ARM/*.py` 只余被文档**当作
`ftp_abort_probe` / `n30_repro` / `e6_soak` 等;`e02_probe` 已随 :8888 退役删除,2026-11-04);
`MDK-ARM/*.py` 只余被文档**当作
可信判据引用过**的三件(`ftp_freeze_probe` / `tcp_reconnect_probe` / `bench_verdict`)——
2026-10-22 清掉了一批既无人引用、又因设备侧启用鉴权而再也登录不上的历史一次性探针
(`MDK-ARM/ftp_retr_abort_probe.py` 与整个 `MDK-ARM/bench_diag/`(4 个诊断脚本)),需要时从
@@ -157,5 +159,5 @@
- `docs/测试说明/单元测试/ADC输入层单元测试说明.md` — ADC 输入层单测(逐 Rank 钉死映射)
- `docs/测试说明/单元测试/相量层单元测试说明.md` — 相量层单测(傅氏系数 / 双区交换)
- `docs/测试说明/单元测试/卷锁单元测试说明.md` — 卷锁单测(fail-open 不变量)
- `docs/测试说明/单元测试/时间同步单元测试说明.md` — 时间同步单测(探针窗口心跳缺口逻辑贡献者)
- `docs/测试说明/单元测试/时间同步单元测试说明.md` — 时间同步任务载体单测 v2.0(SNTP 节拍 / 代次交接 / 未出网守门)
- `docs/测试说明/板级测试/CH395F_Test_Guide.md`、`FTP_Test_Guide.md`
-112
View File
@@ -1,112 +0,0 @@
#!/usr/bin/env python3
"""头阻塞专项台架探针(timeSync 侧,A/B 对照用)。
场景(历史审计的头阻塞触发形态,属主文档已于 2026-10-15 删除):
1. 一个"挂起"的对时客户端连上 :8888 后不发任何数据(空闲/故障客户端,
常见工况)——旧固件在此触发 30s 阻塞 net_recv 独占 netTask,
全体消费者的队列消息停摆最长 30s;
2. 2s 后从 PC 发起 FTP(:21) 连接,测 banner 到达时延 = 队列响应性:
修复前 ≈ 25~30s(队头冻结),修复后 ≈ 数百 ms 内;
3. 34s 时 30s 总预算应已耗尽:设备拆掉该会话(net_drop_conn:
FIN 关闭 + 本层收口 + 重监听),随后**本探针模拟守规矩对端**:
close 自己的 socket;
4. 芯片回 CLOSED(TC-NET-204 模式 A:TIME_WAIT 过渡 ~150ms)→ relisten,
:8888 应能再接受新连接(3s 重试窗内);新连接发完整 8 字节 TIME 包 →
设备应落 "wall clock updated"。
⚠ 定案口径(TRAP-NET-07 定案更正 2026-10-11 + 《网络自愈架构设计》§5.2 E13):CH395F close
**无 RST 语义**。若对端内核存活(app 挂起但 socket 不关,内核自动 ACK FIN),
芯片停在 FIN_WAIT_2、监听恢复受限于对端 close——探针第 3 步的 close 正是
模拟"对端配合"这一可恢复边界;不配合场景的兜底是芯片 TIMEOUT/keepalive
拆僵尸会话(E13 定案,30~90s)。
用法: python e02_probe.py <run-tag>
退出码: 0 = 队列响应性 PASS;1 = FAIL;2 = 探针自身异常
"""
import socket
import sys
import time
import bench_defaults # 台架连接默认值单一真源
DEV = bench_defaults.DEVICE_IP
tag = sys.argv[1] if len(sys.argv) > 1 else "run"
t0 = time.time()
def log(msg):
print("[%s] %9.3f %s" % (tag, time.time() - t0, msg), flush=True)
def main():
# 1. 挂起对时客户端:连接后不发任何数据
ts = socket.create_connection((DEV, 8888), timeout=5)
log("time-sync conn open (hung client: sends nothing)")
# 2. 2s 后测 FTP banner 时延(队列响应性)
time.sleep(2.0)
t1 = time.time()
banner = None
try:
ftp = socket.create_connection((DEV, 21), timeout=35)
banner = ftp.recv(256)
dt = time.time() - t1
log("FTP banner after %8.1f ms: %r" % (dt * 1000, banner[:40]))
ftp.close()
except Exception as e:
dt = time.time() - t1
log("FTP connect FAIL after %8.1f ms: %r" % (dt * 1000, e))
dt = None
queue_ok = (banner is not None) and (dt is not None) and (dt < 3.0)
log("queue responsiveness: %s (banner %s ms)"
% ("PASS" if queue_ok else "FAIL",
("%.0f" % (dt * 1000)) if dt is not None else "n/a"))
# 3. 等 30s 总预算耗尽(设备应拆会话:FIN + 本层收口 + 重监听)
remain = 34.0 - (time.time() - t0)
if remain > 0:
time.sleep(remain)
# 3b. 模拟守规矩对端:设备已发 FIN(drop_conn),本端 close 配合拆链。
# CH395F close 无 RST 语义(TRAP-NET-07 定案更正):不配合的内核存活对端
# 会把芯片停在 FIN_WAIT_2,监听恢复受限于对端 close——不在本探针判据内。
try:
ts.close()
log("hung client socket closed (cooperate with device FIN)")
except OSError:
pass # 设备已先拆连,本地 close 报 ECONNRESET 属预期
# 4. 监听应已重建(芯片 TIME_WAIT 过渡 ~150ms + relisten):3s 重试窗
t2 = time.time()
listener_ok = False
ts2 = None
while time.time() - t2 < 3.0:
try:
ts2 = socket.create_connection((DEV, 8888), timeout=2)
break
except Exception as e:
log("second time-sync conn retry: %r" % (e,))
time.sleep(0.3)
if ts2 is not None:
dt2 = time.time() - t2
epoch = int(time.time())
log("new time-sync conn in %6.0f ms, sent full TIME packet (epoch=%d)"
% (dt2 * 1000, epoch))
ts2.sendall(b"TIME" + epoch.to_bytes(4, "big"))
time.sleep(1.5) # 留时间给设备处理并打日志
ts2.close()
listener_ok = True
else:
log("second time-sync conn FAIL (3s window)")
log("OVERALL: queue=%s listener=%s"
% ("PASS" if queue_ok else "FAIL", "PASS" if listener_ok else "FAIL"))
return 0 if (queue_ok and listener_ok) else 1
if __name__ == "__main__":
try:
sys.exit(main())
except Exception as e:
print("[%s] PROBE ERROR: %r" % (tag, e), flush=True)
sys.exit(2)
+2 -1
View File
@@ -14,7 +14,8 @@
T7 会话结束重连、错口令一次 → 期望 530 且计数已清零(T5 成功清零)
注意:CH395F 单连接——每步用完必须 quit() 释放 socket,否则下一步连不上;
跑之前确保 UI 远程显示(7003)/对时(8888)客户端未占用 socket。
跑之前确保 UI 远程显示(7003)客户端未占用 socket(对时 :8888 已退役,
SNTP 只占 UDP 通道,与 TCP 单连接语义无关)。
用法(设备默认见 bench_defaults.DEVICE_IP,出厂口令 admin/admin):
& '.venv\\Scripts\\python.exe' test/host/ftp_auth_bench.py
+11 -13
View File
@@ -5,19 +5,18 @@
做什么:
用本机 MSVC 编译 test/host/time_sync/time_sync_test.c(它以 #include 纳入
**生产源文件** App/sync/time_sync.c),在 PC 上离线跑:
分片延时(心跳节奏)/ 监听建立与退避 / 时间包重组状态机(分段 + 30s 预算 +
tick 回绕)/ 任务主循环(就绪屏障、accept 轮询、代次变化重监听)。
分片延时(心跳节奏)/ 任务主循环(就绪屏障、SNTP 节拍、代次下传)。
失败即非 0 退出。零硬件、零烧录、毫秒级。
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
`test/host/common/unit_test_harness.py`(本文件只做声明)。
为什么要主机侧单测(而不是只用板端套件):
本模块是探针窗口心跳缺口那件事的当事方——"监听重建期间心跳缺口 800ms~1s,距 IWDG 最坏
超时 1701ms 余量仅 ~0.7s"。缺口成因全是本模块的**纯逻辑**(分片延时的喂狗
节奏、500/2000ms 监听退避、200ms accept 退避、50ms 收尾、屏障等待态喂心跳)。
真机上只能"跑起来看复位不复位",主机侧可**逐片断言**。时间包重组状态机
(分段送达 / 预算耗尽 / EOF / 真错误 / tick 回绕)同理。
本模块是探针窗口心跳缺口那件事的当事方——心跳节奏(delay_fed 的 ≤50ms
分片 + 每片前置心跳、屏障等待态 100ms 分片喂心跳)全是**纯逻辑**,真机上
只能"跑起来看复位不复位",主机侧可**逐片断言**(2026-10-02 台架根因、
第 8b 轮消抖两条教训都钉在这里)。:8888 监听/协议用例已随 2026-11-04 的
:8888 退役删除(墙钟维护说明 §15),历史覆盖见 git 历史。
用法(仓库根目录):
python test/host/time_sync/run_unit_test.py
@@ -25,8 +24,8 @@
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
设计依据:App/sync/time_sync.c 文件头注释 + docs/网络/网络自愈架构设计.md §4.6;
创建日期:2026-10-10
设计依据:App/sync/time_sync.c 文件头注释 + docs/子系统设计/系统墙钟时间维护说明.md §15/§16;
创建日期:2026-10-10(2026-11-04 随 :8888 退役收缩)
"""
import os
import sys
@@ -39,9 +38,9 @@ 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 /
# net_socket.h / sys_events.h / sys_clock.h / dbg_log.h(真实头会拖入整套
# FreeRTOS / CMSIS-RTOS / HAL)。task_watchdog.h 用**真实头**(App/util,只依赖
# stdint.h),time_sync.h 用真实头(接口契约原汁)。
# net_socket.h / sys_events.h(真实头会拖入整套 FreeRTOS / CMSIS-RTOS / HAL)。
# task_watchdog.h 与 time_sntp.h 用**真实头**(接口契约原汁:前者只依赖 stdint.h,
# 后者自包含——替身签名由编译器对表)。
BUILD = TestBuild(
name="Time Sync Tests",
src=os.path.join(HERE, "time_sync_test.c"),
@@ -49,7 +48,6 @@ BUILD = TestBuild(
os.path.join(HERE, "stub"),
os.path.join(ROOT, "App", "util"),
os.path.join(ROOT, "App", "sync"),
os.path.join(ROOT, "App", "log"), # sys_log.h 用真实头(纯 stdint,同 task_watchdog.h 先例)
HERE,
],
summary_marker="=== Time Sync Tests:",
-31
View File
@@ -1,31 +0,0 @@
/*
* 主机侧单测桩:dbg_log.h
* 模块功能:为 App/sync/time_sync.c 提供主机可编译的日志宏(映射到计数)。
*
* ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。
*
* 注意:time_sync.c 在 `#include "dbg_log.h"` 之前自行 `#define` 了
* DBG_LINE_BUF_MAX 与 DBG_TAG(真实头会用到它们),故本桩**不得**再定义这两个宏,
* 否则触发 C4005 宏重定义告警(/W4)。
*
* DBG_ERROR / DBG_INFO 计入计数器,供单测断言:
* - "net_socket/bind/listen FAIL"、"open listener FAIL, retry later" → ERROR
* - "no TIME packet in 30s, drop conn"、"wall clock updated"、"network re-init" → INFO
*
* 创建日期:2026-10-10
*/
#ifndef DBG_LOG_H
#define DBG_LOG_H
#include <stdio.h>
/* 计数(在 time_sync_test.c 实现) */
extern int g_tstest_err_count;
extern int g_tstest_info_count;
#define DBG_INFO(fmt, ...) do { g_tstest_info_count++; } while (0)
#define DBG_DEBUG(fmt, ...) do { } while (0)
#define DBG_ERROR(fmt, ...) do { g_tstest_err_count++; } while (0)
#endif /* DBG_LOG_H */
+10 -45
View File
@@ -1,20 +1,18 @@
/*
* 主机侧单测桩:net_socket.h(含内联 net_types.h 子集)
* 模块功能:为 App/sync/time_sync.c 提供主机可编译的网络 Socket API 声明与类型。
* 主机侧单测桩:net_socket.h(收缩版,:8888 退役后)
* 模块功能:为 App/sync/time_sync.c 提供主机可编译的 net_get_generation 声明。
*
* ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。
* 真实头(Drivers/BSP/NET/net_socket.h)包含 net_types.h → net_config.h,并经
* FreeRTOS.h / task.h 拖入整套 FreeRTOS;本桩改为包含同名桩头。
* FreeRTOS.h / task.h 拖入整套 FreeRTOS;本桩改为包含同名桩头(含 FreeRTOS/task
* 两条**保真**包含——生产源正是靠真实头的这条链拿到 vTaskDelay / pdMS_TO_TICKS)。
*
* 保真点:
* - 地址族/类型/特殊地址常量与 Drivers/BSP/NET/net_types.h 逐字一致;
* - struct net_sockaddr / net_in_addr / net_sockaddr_in 字段与真实头一致
* (被测代码写 sin_family / sin_port / sin_addr.s_addr,桩按同布局读取);
* - 7 个被测代码调用的接口签名与真实头一致(net_socket / net_bind / net_listen /
* net_accept / net_close / net_recv_to / net_get_generation / net_htons)。
* 全部由测试单元给强符号桩(脚本化返回值 + 调用记录)。
* 收缩说明(2026-11-04)::8888 退役后本任务只剩 net_get_generation 一处网络
* 调用。socket / bind / listen / accept / close / recv 族声明**刻意不列**——
* 若 time_sync 循环重新伸手碰监听族 API,测试 TU 会因"未声明 + 无强符号桩"在
* 编译/链接期直接爆,这是退役语义的编译期绊线(替代退役前的运行时断言)。
*
* 创建日期:2026-10-10
* 创建日期:2026-10-10(2026-11-04 随 :8888 退役收缩)
*/
#ifndef NET_SOCKET_H
@@ -29,41 +27,8 @@
extern "C" {
#endif
/* ===== 以下取自 Drivers/BSP/NET/net_types.h(逐字) ===== */
#define NET_AF_INET 2 /* IPv4 */
#define NET_SOCK_STREAM 1 /* TCP */
#define NET_SOCK_DGRAM 2 /* UDP */
#define NET_INADDR_ANY 0 /* 任意地址 */
#define NET_INADDR_NONE 0xFFFFFFFF /* 无效地址 */
struct net_sockaddr {
uint16_t sa_family; /* 地址族 */
char sa_data[14]; /* 地址数据 */
};
struct net_in_addr {
uint32_t s_addr; /* IP 地址(网络字节序) */
};
struct net_sockaddr_in {
uint16_t sin_family; /* 地址族,必须为 AF_INET */
uint16_t sin_port; /* 端口号(网络字节序) */
struct net_in_addr sin_addr; /* IP 地址(网络字节序) */
char sin_zero[8]; /* 填充字节 */
};
/* ===== 被测代码用到的 Socket API(签名与真实头一致) ===== */
int net_socket(int domain, int type);
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen);
int net_listen(int sockfd);
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen);
int net_close(int sockfd);
int net_drop_conn(int sockfd);
int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms);
/* 与 Drivers/BSP/NET/net_socket.h 签名一致 */
uint32_t net_get_generation(void);
uint16_t net_htons(uint16_t hostshort);
#ifdef __cplusplus
}
-29
View File
@@ -1,29 +0,0 @@
/*
* 主机侧单测桩:sys_clock.h
* 模块功能:为 App/sync/time_sync.c 提供主机可编译的墙钟设置接口声明。
*
* ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。
* 真实头(App/util/sys_clock.h)含日历/闰年等无关声明;本桩只保留被测代码
* 真正调用的那一个(签名与真实头逐字一致:int sys_clock_set_unix(uint32_t))。
*
* 由测试单元给强符号桩,记录"是否被调用"与"最后一次的 epoch"。
*
* 创建日期:2026-10-10
*/
#ifndef SYS_CLOCK_H
#define SYS_CLOCK_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
int sys_clock_set_unix(uint32_t unix_secs);
#ifdef __cplusplus
}
#endif
#endif /* SYS_CLOCK_H */
+3 -2
View File
@@ -4,8 +4,9 @@
*
* ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。
*
* 两个接口由测试单元给强符号桩:vTaskDelay 记录每片睡眠时长(断言"≤50ms 分片"),
* xTaskGetTickCount 由测试单元提供可控时钟(驱动 30s 预算耗尽与 32 位回绕用例)。
* 接口由测试单元给强符号桩:vTaskDelay 记录每片睡眠时长(断言"≤50ms 分片");
* xTaskGetTickCount 保留可控时钟声明(:8888 退役后生产源已不读 tick,桩仍提供,
* 供未来需要预算/回绕类用例时直接取用)。
*
* 创建日期:2026-10-10
*/
+118 -605
View File
@@ -1,69 +1,31 @@
/*
* 模块名称:Time Sync(PC 本地时间推送接收)主机侧单元测试
* 模块功能:在 PC 上对 App/sync/time_sync.c 做离线验证(4 个函数全覆盖:
* time_sync_delay_fed / time_sync_open_listener / time_sync_handle_conn /
* time_sync_task)。
* 模块名称:Time Sync(timeSyncTask 节拍宿主)主机侧单元测试
* 模块功能:在 PC 上对 App/sync/time_sync.c 做离线验证(2 个函数全覆盖:
* time_sync_delay_fed / time_sync_task)。
*
* 被测对象:真实源文件 ../../../App/sync/time_sync.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本)。平台依赖由本文件提供的**可注入桩**实现:
* - FreeRTOS/task.h -> vTaskDelay / xTaskGetTickCount(可控时钟)
* - net_socket.h -> socket/bind/listen/accept/close/recv_to/
* get_generation/htons(全部脚本化)
* - net_socket.h -> net_get_generation(脚本化 + 可跳出主循环)
* - sys_events.h -> osEventFlagsWait(可注入"未就绪")
* - sys_clock.h -> sys_clock_set_unix(记录 epoch)
* - task_watchdog.h -> task_wd_beat / task_wd_bind(记录心跳节奏)
* - dbg_log.h -> DBG_ERROR/DBG_INFO 计数
* - time_sntp.h -> time_sntp_run(替身:记录轮次与收到的代次)
*
* 为什么要主机侧单测(而不是只用板端套件):
* 1. **本模块是探针窗口心跳缺口的当事方**:当时的记录写着"time_sync 监听重建期间心跳缺口
* 800ms~1s,距 IWDG 最坏超时 1701ms 余量仅 ~0.7s"。缺口的成因全部是本模块的
* **纯逻辑**——分片延时(delay_fed)的心跳节奏、监听重试退避(500/2000ms)、
* accept 轮询退避(200ms)、收尾延时(50ms)、以及"屏障等待态也要喂心跳"。
* 这些在真机上只能靠"跑起来看复位不复位"间接观察,主机侧可**逐片断言**。
* 2. **时间包重组状态机**(handle_conn)是纯逻辑:累积到 8 字节才解析、30s 总预算
* 随 tick 推进耗尽、TCP 分段送达、EOF/真错误/超时三种返回码分支——板端套件
* 无法精确构造"分段 + 预算耗尽 + tick 回绕"的组合。
* 3. 不占 MCU 的 RAM,毫秒级完成。
* 版本史:v1 时代本套件还测 :8888 监听重试与 "TIME" 包重组状态机(80 断言点)。
* 2026-11-04 :8888 对时退役(PR #77 后用户裁定,理由见墙钟维护说明 §15),
* 监听/协议/OPER 留痕三组用例随生产代码一并退场——历史覆盖见 git 历史
* (test/host/time_sync/ 2026-10-10 版)。留下并扩强的部分是**这个任务
* 现在的全部生产逻辑**:心跳分片节奏(探针窗口心跳缺口的当事方至今
* 仍是本模块——第 8b 轮消抖、2026-10-02 屏障喂心跳两条台架教训都钉在
* 这里)与"循环按代次驱动 SNTP"。
*
* 覆盖点(TC-TS-xxx):
* —— time_sync_delay_fed(可喂心跳的分片延时)——
* TC-TIMESYNC-001 0ms 不睡眠不喂狗
* TC-TIMESYNC-002 恰好一片(50ms)
* TC-TIMESYNC-003 非整片(120ms)切成 50/50/20,每片喂心跳
* TC-TIMESYNC-004 长延时(2000ms)= 40 片 × 50ms,每片喂心跳
* TC-TIMESYNC-005 不足一片(49ms)单片处理
* TC-TIMESYNC-006 总预算 30s = 600 片(片长上限恒 50ms)
* —— time_sync_open_listener(监听建立与重试)——
* TC-TIMESYNC-010 首次成功:socket/bind/listen 各一次,端口经 net_htons,无退避
* TC-TIMESYNC-011 bind 失败 -> 关 socket + 退避 2000ms + 留痕,随后重试成功
* TC-TIMESYNC-012 listen 失败 -> 关 socket + 退避 2000ms,随后重试成功
* TC-TIMESYNC-013 socket 连续失败 -> 5 次后返回 -1(5 条 ERROR,5×500ms 退避)
* TC-TIMESYNC-014 第 3 次重试成功并返回新描述符
* TC-TIMESYNC-015 每次重试前 + 每片延时各喂一次心跳,且心跳位恒为 TIMESYNC
* —— time_sync_handle_conn(时间包重组状态机)——
* TC-TIMESYNC-020 8 字节一次收齐 -> 大端解析 epoch + 更新墙钟 + 关连接
* TC-TIMESYNC-021 分段送达(4+4)-> 累积后解析;第二次只请求剩余 12 字节
* TC-TIMESYNC-022 一次到达 16 字节 -> 只取前 8 字节
* TC-TIMESYNC-023 前缀非 "TIME" -> 不更新墙钟,但仍关连接
* TC-TIMESYNC-024 对端关闭(n==0)-> 立即退出、不解析
* TC-TIMESYNC-025 真错误(n==-1)-> 立即退出、不解析
* TC-TIMESYNC-026 只收到 4 字节后 EOF -> 不足 8 字节不解析
* TC-TIMESYNC-027 30s 预算耗尽 -> 留痕 "no TIME packet in 30s, drop conn"
* TC-TIMESYNC-028 剩余预算不足一片 -> 末片按剩余(20ms)切分
* TC-TIMESYNC-029 32 位 tick 回绕 -> 预算计算仍正确
* —— time_sync_task(任务主循环)——
* TC-TIMESYNC-040 sys_evtHandle==NULL:跳过就绪屏障;accept 轮询->成功->处理->收尾
* TC-TIMESYNC-041 就绪屏障:100ms 分片等待 NET_READY(osFlagsNoClear 不消费就绪位)
* TC-TIMESYNC-042 屏障未就绪期间持续喂心跳(2026-10-02 台架根因的回归防线)
* TC-TIMESYNC-043 网络代次变化 -> 关闭旧监听并重监听
* TC-TIMESYNC-044 监听建立失败 -> 5 次重试后放弃并退避 2000ms
* 对时操作留痕(道G task-50,语义行名不配号——编号规范已停发测试用例号):
* "sync trace on success" / "… rejected" / "… two in a row" / "… bad magic" 四组
* 断言口径(用户裁定 2026-10-27,编号规范 §9):新用例一律**语义行名**,
* 不再配 TC 号;旧 TC-TIMESYNC-* 号随用例退役永久作废、不得复用。
*
* 通过:全部 TC PASS(main 返回 0)。
* 通过:全部断言 PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-10
* 创建日期:2026-10-10(2026-11-04 随 :8888 退役收缩重写)
*/
#include <setjmp.h>
@@ -75,14 +37,13 @@
#include "FreeRTOS.h"
#include "task.h"
#include "net_socket.h"
#include "sys_clock.h"
#include "sys_events.h"
#include "task_watchdog.h" /* 真实头(App/util):task_wd_id_t / TASK_WD_TIMESYNC */
#include "sys_log.h" /* 真实头(App/log,纯 stdint 无 OS 依赖):syslog_id_t / SYSLOG_OPER 契约,假件签名与之逐字对齐 */
#include "time_sntp.h" /* 真实头(App/sync):time_sntp_run 签名原汁,替身必须对表 */
/* ===================== FreeRTOS 桩 ===================== */
static TickType_t s_tick;
#define DELAY_LOG_MAX 800
#define DELAY_LOG_MAX 2048
static uint32_t s_delays[DELAY_LOG_MAX];
static int s_delay_calls;
@@ -102,7 +63,7 @@ osEventFlagsId_t sys_evtHandle = NULL;
static uint32_t s_evt_seq[8];
static int s_evt_seq_n;
static uint32_t s_evt_default = 0xFFFFFFFFu; /* 未脚本化时的返回值(默认:就绪) */
static uint32_t s_evt_default = 0xFFFFFFFFu; /* 未脚本化时的返回值 */
static int s_evt_calls;
static int s_evt_longjmp_at = -1; /* >0 时第 N 次调用触发 longjmp */
static uint32_t s_evt_last_flags;
@@ -129,12 +90,12 @@ uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, uint32_t optio
return s_evt_default;
}
/* ===================== 看门狗桩 ===================== */
/* ===================== 看门狗桩 =====================
* ⚠ 计数变量命名带 s_wd_ 前缀:C 的"暂定定义"规则下,同一编译单元里
* 两个同名 `static int x;` 会被静默合并(不报错),历史版本里 task_wd_bind
* 与 net_bind 的计数器撞过一次名导致断言全部失真(2026-10-10 踩过)。 */
static int s_beat_calls;
static int s_beat_id_bad;
/* ⚠ 命名必须与 net_socket 桩的 net_bind 计数区分开:C 的"暂定定义"规则下,
* 同一编译单元里两个 `static int x;` 会被**静默合并为同一个对象**(不报错),
* 于是 task_wd_bind 与 net_bind 会共用计数器,断言全部失真(2026-10-10 踩过)。 */
static int s_wd_bind_calls;
static int s_wd_bind_id_bad;
@@ -152,126 +113,16 @@ void task_wd_bind(task_wd_id_t id) {
}
}
/* ===================== sys_clock 桩 ===================== */
static int s_set_unix_calls;
static uint32_t s_set_unix_last;
static int s_set_unix_rc; /* 可脚本化返回值:非 0 = 对时被拒(task-50) */
int sys_clock_set_unix(uint32_t unix_secs) {
s_set_unix_calls++;
s_set_unix_last = unix_secs;
return s_set_unix_rc;
}
/* ===================== sys_log 投递桩(task-50) =====================
* 假件按投递顺序记录:条数 / 日志 ID / 末条描述 / 对时文案出现次数。
* "出现次数"专为钉"连续两次不合并"——若生产侧哪天改成边沿锁存,
* 第三条用例会红,而那正是要拦的语义漂移。 */
static int s_post_calls;
static int s_post_id;
static char s_post_last_desc[32];
static int s_post_desc_count;
void syslog_post_quiet(syslog_id_t id, const char *desc) {
s_post_calls++;
s_post_id = (int)id;
if (desc != NULL) {
(void)strncpy(s_post_last_desc, desc, sizeof(s_post_last_desc) - 1u);
s_post_last_desc[sizeof(s_post_last_desc) - 1u] = '\0';
if (strcmp(desc, "clock set by 8888") == 0) {
s_post_desc_count++;
}
}
}
/* ===================== net_socket 桩 ===================== */
static uint16_t s_htons_last;
static int s_socket_calls;
static int s_socket_seq[8];
static int s_socket_seq_n;
static int s_socket_default = 5;
static int s_bind_calls;
static int s_bind_seq[8];
static int s_bind_seq_n;
static uint16_t s_bind_port;
static uint16_t s_bind_family;
static int s_listen_calls;
static int s_listen_seq[8];
static int s_listen_seq_n;
static int s_accept_calls;
static int s_accept_seq[16];
static int s_accept_seq_n;
static int s_close_calls;
static int s_close_socks[32];
/* ===================== net_socket 桩 =====================
* :8888 退役后本任务只剩 net_get_generation 一处网络调用。桩头刻意**不声明**
* socket/bind/listen/accept/close/recv——若循环哪天重新伸手碰监听族 API,
* 链接期直接爆(编译期绊线,替代退役前的运行时断言)。 */
static uint32_t s_gen_seq[8];
static int s_gen_seq_n;
static uint32_t s_gen_default = 1u;
static int s_gen_calls;
static int s_gen_longjmp_at = -1; /* >0 时第 N 次调用触发 longjmp */
uint16_t net_htons(uint16_t hostshort) {
s_htons_last = hostshort;
return (uint16_t)(((hostshort & 0xFFu) << 8) | (hostshort >> 8));
}
int net_socket(int domain, int type) {
int i = s_socket_calls++;
(void)domain;
(void)type;
return (i < s_socket_seq_n) ? s_socket_seq[i] : s_socket_default;
}
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) {
const struct net_sockaddr_in *a = (const struct net_sockaddr_in *)addr;
int i = s_bind_calls++;
(void)sockfd;
(void)addrlen;
s_bind_family = a->sin_family;
s_bind_port = a->sin_port;
return (i < s_bind_seq_n) ? s_bind_seq[i] : 0;
}
int net_listen(int sockfd) {
int i = s_listen_calls++;
(void)sockfd;
return (i < s_listen_seq_n) ? s_listen_seq[i] : 0;
}
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) {
int i = s_accept_calls++;
(void)sockfd;
(void)addr;
(void)addrlen;
return (i < s_accept_seq_n) ? s_accept_seq[i] : -1;
}
int net_close(int sockfd) {
if (s_close_calls < 32) {
s_close_socks[s_close_calls] = sockfd;
}
s_close_calls++;
return 0;
}
/* ---- net_drop_conn(头阻塞收口):计数 + 记录 fd ---- */
static int s_drop_calls;
static int s_drop_socks[32];
int net_drop_conn(int sockfd) {
if (s_drop_calls < 32) {
s_drop_socks[s_drop_calls] = sockfd;
}
s_drop_calls++;
return 0;
}
uint32_t net_get_generation(void) {
int i = s_gen_calls++;
@@ -281,59 +132,29 @@ uint32_t net_get_generation(void) {
return (i < s_gen_seq_n) ? s_gen_seq[i] : s_gen_default;
}
/* ---- net_recv_to:脚本化返回码 + 可推进 tick ---- */
typedef struct {
int n; /* 返回码:>0 字节数 / 0 EOF / -1 错误 / -2 超时无数据 */
uint8_t data[16]; /* 要"送达"的字节 */
int len; /* data 有效长度 */
uint32_t tick_adv; /* 返回前把假时钟推进多少 ms */
} recv_step_t;
/* ===================== SNTP 客户机替身 =====================
* 行为断言在 test/host/sntp 独立套件里做;本套件只钉两件事:
* ① 每轮循环恰好调一次 time_sntp_run;
* ② 收到的代次 = 本轮 net_get_generation() 的返回值(断网重入语义的输入端)。 */
static int s_sntp_run_calls;
static uint32_t s_sntp_gen_log[8];
static int s_sntp_gen_log_n;
static int s_sntp_run_longjmp_at = -1; /* >0 时第 N 次调用触发 longjmp */
static recv_step_t s_recv_seq[16];
static int s_recv_seq_n;
static int s_recv_idx;
static int s_recv_calls;
static int s_recv_default = 0; /* 脚本用尽后的返回码(0=EOF,保证循环终止) */
static uint32_t s_recv_to_log[64];
static int s_recv_len_log[64];
void time_sntp_run(uint32_t net_gen) {
int i = s_sntp_run_calls++;
int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) {
int i = s_recv_calls;
(void)sockfd;
if (i < 64) {
s_recv_to_log[i] = timeout_ms;
s_recv_len_log[i] = len;
if (i < (int)(sizeof(s_sntp_gen_log) / sizeof(s_sntp_gen_log[0]))) {
s_sntp_gen_log[i] = net_gen;
s_sntp_gen_log_n = i + 1;
}
s_recv_calls++;
if (s_recv_idx >= s_recv_seq_n) {
return s_recv_default;
}
{
const recv_step_t *st = &s_recv_seq[s_recv_idx];
int copy;
s_recv_idx++;
s_tick += st->tick_adv;
if (st->n <= 0) {
return st->n;
}
copy = (st->n <= len) ? st->n : len;
if (copy > st->len) {
copy = st->len;
}
if ((copy > 0) && (buf != NULL)) {
memcpy(buf, st->data, (size_t)copy);
}
return st->n;
if (s_sntp_run_longjmp_at > 0 && (i + 1) >= s_sntp_run_longjmp_at) {
longjmp(s_jmp, 3);
}
}
/* ===================== 断言 ===================== */
static int s_total = 0, s_passed = 0, s_failed = 0;
int g_tstest_err_count = 0;
int g_tstest_info_count = 0;
#define CHECK(cond, name) do { \
s_total++; \
@@ -341,24 +162,6 @@ 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"
@@ -369,8 +172,8 @@ int32_t time_sntp_last_offset_ms(void) {
* 入口参数:无
* 返 回 值:无
* 限定条件:无
* 函数说明:默认 = 网络全成功、gen 恒为 1、recv 脚本为空(脚本用尽返回 EOF)、
* 事件标志恒就绪。各用例按需覆盖。
* 函数说明:默认 = 事件标志健康就绪(屏障一次通过)、gen 恒为 1、sntp 替身直通。
* 各用例按需覆盖。
*/
static void fakes_reset(void) {
s_tick = 0;
@@ -390,64 +193,14 @@ static void fakes_reset(void) {
s_wd_bind_calls = 0;
s_wd_bind_id_bad = 0;
s_set_unix_calls = 0;
s_set_unix_last = 0;
s_set_unix_rc = 0;
s_post_calls = 0;
s_post_id = -1;
s_post_last_desc[0] = '\0';
s_post_desc_count = 0;
s_htons_last = 0;
s_socket_calls = 0;
s_socket_seq_n = 0;
s_socket_default = 5;
s_bind_calls = 0;
s_bind_seq_n = 0;
s_bind_port = 0;
s_bind_family = 0;
s_listen_calls = 0;
s_listen_seq_n = 0;
s_accept_calls = 0;
s_accept_seq_n = 0;
s_close_calls = 0;
s_drop_calls = 0;
s_gen_seq_n = 0;
s_gen_default = 1u;
s_gen_calls = 0;
s_gen_longjmp_at = -1;
s_recv_seq_n = 0;
s_recv_idx = 0;
s_recv_calls = 0;
s_recv_default = 0;
g_tstest_err_count = 0;
g_tstest_info_count = 0;
}
/* 装配一个 recv 脚本步 */
static void recv_set(int idx, int n, const uint8_t *data, int len, uint32_t tick_adv) {
memset(&s_recv_seq[idx], 0, sizeof(s_recv_seq[idx]));
s_recv_seq[idx].n = n;
s_recv_seq[idx].len = len;
s_recv_seq[idx].tick_adv = tick_adv;
if ((data != NULL) && (len > 0)) {
int copy = (len > 16) ? 16 : len;
memcpy(s_recv_seq[idx].data, data, (size_t)copy);
}
}
/* 装配一个 "TIME" + uint32 大端 时间包 */
static void make_time_packet(uint8_t *out, uint32_t epoch) {
out[0] = (uint8_t)'T';
out[1] = (uint8_t)'I';
out[2] = (uint8_t)'M';
out[3] = (uint8_t)'E';
out[4] = (uint8_t)(epoch >> 24);
out[5] = (uint8_t)(epoch >> 16);
out[6] = (uint8_t)(epoch >> 8);
out[7] = (uint8_t)(epoch);
s_sntp_run_calls = 0;
s_sntp_gen_log_n = 0;
s_sntp_run_longjmp_at = -1;
}
/* 检查 s_delays[0..n) 是否全部等于 v */
@@ -461,268 +214,100 @@ static int delays_all(uint32_t v, int n) {
return 1;
}
/* ===================== TC-TIMESYNC-001..006:delay_fed ===================== */
/* ===================== delay_fed:心跳分片延时 =====================
* 本模块的全部看门狗安全论证都压在这一个函数上:片长恒 ≤50ms、每片前置心跳,
* 抽样窗(100ms)内必有心跳——第 8b 轮消抖钉下的节奏,逐片断言。 */
static void tc_delay_fed(void) {
fakes_reset();
time_sync_delay_fed(0U);
CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "TC-TIMESYNC-001 delay_fed(0) neither sleeps nor beats");
CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "delay_fed(0) neither sleeps nor beats");
fakes_reset();
time_sync_delay_fed(50U);
CHECK((s_beat_calls == 1) && (s_delay_calls == 1) && (s_delays[0] == 50U),
"TC-TIMESYNC-002 delay_fed(50) = one 50 ms slice + 1 beat");
"delay_fed(50) = one 50 ms slice + 1 beat");
fakes_reset();
time_sync_delay_fed(120U);
CHECK((s_beat_calls == 3) && (s_delay_calls == 3), "TC-TIMESYNC-003 delay_fed(120) splits into 3 slices");
CHECK((s_beat_calls == 3) && (s_delay_calls == 3), "delay_fed(120) splits into 3 slices");
CHECK((s_delays[0] == 50U) && (s_delays[1] == 50U) && (s_delays[2] == 20U),
"TC-TIMESYNC-003b slice lengths 50/50/20 (last slice is the remainder)");
"delay_fed(120) slice lengths 50/50/20 (last slice is the remainder)");
fakes_reset();
time_sync_delay_fed(2000U);
CHECK((s_delay_calls == 40) && (s_beat_calls == 40) && delays_all(50U, 40),
"TC-TIMESYNC-004 delay_fed(2000) = 40 slices x 50 ms, one beat per slice");
"delay_fed(2000) = 40 slices x 50 ms, one beat per slice");
fakes_reset();
time_sync_delay_fed(49U);
CHECK((s_beat_calls == 1) && (s_delays[0] == 49U), "TC-TIMESYNC-005 less than one slice (49 ms) handled as a single slice");
CHECK((s_beat_calls == 1) && (s_delays[0] == 49U),
"delay_fed(49) less than one slice handled as a single slice");
fakes_reset();
time_sync_delay_fed(30000U);
CHECK(s_delay_calls == 600, "TC-TIMESYNC-006 total budget 30 s = 600 slices (slice cap is always 50 ms)");
CHECK(s_delay_calls == 600, "delay_fed(30000) = 600 slices (slice cap is always 50 ms)");
}
/* ===================== TC-TIMESYNC-010..015:open_listener ===================== */
static void tc_open_listener(void) {
fakes_reset();
{
int s = time_sync_open_listener();
CHECK(s == 5, "TC-TIMESYNC-010 open_listener success returns the socket descriptor");
CHECK((s_socket_calls == 1) && (s_bind_calls == 1) && (s_listen_calls == 1),
"TC-TIMESYNC-010b socket / bind / listen called once each");
CHECK(s_htons_last == (uint16_t)TIME_SYNC_PORT, "TC-TIMESYNC-010c port goes through net_htons(8888)");
CHECK(s_bind_port == net_htons((uint16_t)TIME_SYNC_PORT), "TC-TIMESYNC-010d sin_port is in network byte order");
CHECK(s_bind_family == (uint16_t)NET_AF_INET, "TC-TIMESYNC-010e sin_family = NET_AF_INET");
CHECK(s_delay_calls == 0, "TC-TIMESYNC-010f no backoff delay on the success path");
}
fakes_reset();
s_bind_seq[0] = -1;
s_bind_seq_n = 1;
{
int s = time_sync_open_listener();
CHECK(s == 5, "TC-TIMESYNC-011 bind succeeds on retry after one failure");
CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-011b bind failure closes that socket");
CHECK(s_delay_calls == 40, "TC-TIMESYNC-011c bind failure backs off 2000 ms (40 slices)");
CHECK(g_tstest_err_count == 1, "TC-TIMESYNC-011d bind failure leaves a DBG_ERROR trace");
}
fakes_reset();
s_listen_seq[0] = -1;
s_listen_seq_n = 1;
{
int s = time_sync_open_listener();
CHECK(s == 5, "TC-TIMESYNC-012 listen succeeds on retry after one failure");
CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-012b listen failure closes that socket");
CHECK(s_delay_calls == 40, "TC-TIMESYNC-012c listen failure backs off 2000 ms");
}
fakes_reset();
s_socket_default = -1;
{
int s = time_sync_open_listener();
CHECK(s == -1, "TC-TIMESYNC-013 socket keeps failing -> returns -1");
CHECK(s_socket_calls == 5, "TC-TIMESYNC-013b retry cap is 5 attempts");
CHECK(g_tstest_err_count == 5, "TC-TIMESYNC-013c every failure leaves its own trace (5 DBG_ERROR lines)");
CHECK(s_delay_calls == 50, "TC-TIMESYNC-013d every failure backs off 500 ms (5 x 10 slices)");
}
fakes_reset();
s_socket_seq[0] = -1;
s_socket_seq[1] = -1;
s_socket_seq[2] = 9;
s_socket_seq_n = 3;
{
int s = time_sync_open_listener();
CHECK(s == 9, "TC-TIMESYNC-014 3rd retry succeeds and returns the new descriptor");
CHECK(s_socket_calls == 3, "TC-TIMESYNC-014b exactly 3 attempts");
CHECK(s_delay_calls == 20, "TC-TIMESYNC-014c two failures back off 500 ms x 2");
}
fakes_reset();
s_socket_default = -1;
(void)time_sync_open_listener();
CHECK(s_beat_calls == 55, "TC-TIMESYNC-015 every retry round (5) + every delay slice (50) beats once");
CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-015b the beat bit is always TASK_WD_TIMESYNC");
}
/* ===================== TC-TIMESYNC-020..029:handle_conn =====================
* ⚠ 契约变更:handle_conn **不再** net_close
* (单连接模式下 conn==listen,close 会连监听一起毁);会话收尾只返回外层,
* 由 time_sync_task 在 `conn != ls`(模拟器多连接后端)时才关数据 fd。
* 故本组断言一律为 s_close_calls == 0;任务级关闭见 TC-TIMESYNC-040e。 */
static void tc_handle_conn(void) {
uint8_t pkt[16];
/* 1) 8 字节一次收齐 */
fakes_reset();
make_time_packet(pkt, 0x12345678u);
recv_set(0, 8, pkt, 8, 0U);
s_recv_seq_n = 1;
s_tick = 100U;
time_sync_handle_conn(7);
CHECK(s_set_unix_calls == 1, "TC-TIMESYNC-020 TIME packet in one shot -> wall clock updated once");
CHECK(s_set_unix_last == 0x12345678u, "TC-TIMESYNC-020b epoch parsed big-endian");
CHECK((s_recv_calls == 1) && (s_recv_len_log[0] == 16), "TC-TIMESYNC-020c the first request asks for 16 bytes");
CHECK(s_recv_to_log[0] == 50U, "TC-TIMESYNC-020d single wait slice length = 50 ms");
CHECK(s_close_calls == 0, "TC-TIMESYNC-020e after handling, the connection is **not** closed (close is the outer conn!=ls branch job)");
CHECK(s_drop_calls == 0, "TC-TIMESYNC-020g a successful session is not torn down (drop is only for give-up paths)");
CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-020f leaves the wall clock updated trace");
/* 2) 分段送达:4 + 4 */
fakes_reset();
make_time_packet(pkt, 10u);
recv_set(0, 4, pkt, 4, 10U);
recv_set(1, 4, pkt + 4, 4, 10U);
s_recv_seq_n = 2;
time_sync_handle_conn(3);
CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 10u), "TC-TIMESYNC-021 pieces accumulate and then parse correctly");
CHECK(s_recv_calls == 2, "TC-TIMESYNC-021b read twice");
CHECK((s_recv_len_log[0] == 16) && (s_recv_len_log[1] == 12),
"TC-TIMESYNC-021c the second request asks only for the remaining 12 bytes");
CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U), "TC-TIMESYNC-021d 50 ms per slice (budget is ample)");
/* 3) 16 字节一次到达(只取前 8) */
fakes_reset();
make_time_packet(pkt, 5u);
recv_set(0, 16, pkt, 16, 0U);
s_recv_seq_n = 1;
time_sync_handle_conn(4);
CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 5u), "TC-TIMESYNC-022 an over-long packet only takes the first 8 bytes");
CHECK(s_recv_calls == 1, "TC-TIMESYNC-022b stops as soon as one read is enough");
/* 4) 前缀不是 TIME */
fakes_reset();
pkt[0] = (uint8_t)'X';
pkt[1] = (uint8_t)'X';
pkt[2] = (uint8_t)'X';
pkt[3] = (uint8_t)'X';
pkt[4] = 0U;
pkt[5] = 0U;
pkt[6] = 0U;
pkt[7] = 1U;
recv_set(0, 8, pkt, 8, 0U);
s_recv_seq_n = 1;
time_sync_handle_conn(4);
CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-023 a non-TIME prefix does not update the wall clock");
CHECK(s_close_calls == 0, "TC-TIMESYNC-023b a non-TIME prefix also does not close the connection");
/* 5) 对端关闭(EOF) */
fakes_reset();
recv_set(0, 0, NULL, 0, 0U);
s_recv_seq_n = 1;
time_sync_handle_conn(6);
CHECK((s_set_unix_calls == 0) && (s_recv_calls == 1), "TC-TIMESYNC-024 peer close (EOF) exits immediately without parsing");
CHECK(s_close_calls == 0, "TC-TIMESYNC-024b EOF also does not close the connection (net-layer auto_relisten re-arms the listener)");
/* 6) 真错误 */
fakes_reset();
recv_set(0, -1, NULL, 0, 0U);
s_recv_seq_n = 1;
time_sync_handle_conn(6);
CHECK((s_recv_calls == 1) && (s_set_unix_calls == 0), "TC-TIMESYNC-025 a real error exits immediately without parsing");
/* 7) 4 字节后 EOF */
fakes_reset();
make_time_packet(pkt, 7u);
recv_set(0, 4, pkt, 4, 0U);
recv_set(1, 0, NULL, 0, 0U);
s_recv_seq_n = 2;
time_sync_handle_conn(6);
CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-026 fewer than 8 bytes are not parsed");
CHECK(s_recv_calls == 2, "TC-TIMESYNC-026b keeps waiting for the remaining bytes");
/* 8) 30s 预算耗尽 */
fakes_reset();
recv_set(0, -2, NULL, 0, 30000U);
s_recv_seq_n = 1;
time_sync_handle_conn(6);
CHECK(s_recv_calls == 1, "TC-TIMESYNC-027 no more reads once the budget is exhausted (only 1 read)");
CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-027b leaves the no TIME packet in 30s, drop conn trace");
CHECK(s_close_calls == 0, "TC-TIMESYNC-027c budget exhausted still does not close the connection");
CHECK(s_drop_calls == 1, "TC-TIMESYNC-027d budget exhausted drops the session on purpose (head-of-line wrap-up: net_drop_conn kills the connection, keeps the listener)");
/* 9) 剩余预算不足一片 */
fakes_reset();
recv_set(0, -2, NULL, 0, 29980U);
s_recv_seq_n = 1;
time_sync_handle_conn(6);
CHECK(s_recv_calls == 2, "TC-TIMESYNC-028 ends after two waits");
CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 20U),
"TC-TIMESYNC-028b second slice length = the remaining 20 ms");
/* 10) 32 位 tick 回绕 */
fakes_reset();
s_tick = 0xFFFFFF00U;
recv_set(0, -2, NULL, 0, 512U); /* 推进后回绕到 0x00000100 */
s_recv_seq_n = 1;
time_sync_handle_conn(6);
CHECK((s_recv_calls == 2) && (s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U),
"TC-TIMESYNC-029 after a 32-bit tick wraparound the budget math is still correct");
}
/* ===================== TC-TIMESYNC-040..044:time_sync_task ===================== */
/* ===================== time_sync_task:屏障 + SNTP 节拍 ===================== */
static void tc_task(void) {
uint8_t pkt[16];
/* A) 无就绪屏障(sys_evtHandle == NULL):accept 轮询 2 次失败后成功 */
/* A) 无就绪屏障(sys_evtHandle == NULL):一轮完整节拍后于第 2 轮顶部脱出 */
fakes_reset();
make_time_packet(pkt, 9u);
recv_set(0, 8, pkt, 8, 0U);
s_recv_seq_n = 1;
s_accept_seq[0] = -1;
s_accept_seq[1] = -1;
s_accept_seq[2] = 7;
s_accept_seq_n = 3;
/* gen 调用序:iter1 顶部(1) + accept 两次失败各一次(2,3) + iter2 顶部(4) → 第 4 次跳出 */
s_gen_longjmp_at = 4;
s_gen_longjmp_at = 2; /* iter1 顶部(1) + iter2 顶部(2) -> 跳出 */
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
CHECK((s_wd_bind_calls == 1) && (s_wd_bind_id_bad == 0),
"TC-TIMESYNC-040 task entry registers the watchdog beat bit (task_wd_bind once)");
CHECK(s_evt_calls == 0, "TC-TIMESYNC-040b skips the readiness barrier when sys_evtHandle==NULL");
CHECK(s_accept_calls == 3, "TC-TIMESYNC-040c accept polled 3 times (2 with no connection + 1 success)");
CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 9u), "TC-TIMESYNC-040d the time packet arrived and the wall clock was updated");
CHECK(s_close_calls == 1, "TC-TIMESYNC-040e the connection is closed once");
CHECK(s_delay_calls == 9, "TC-TIMESYNC-040f delay slice count = 4+4+1 (200+200+50 ms)");
CHECK(delays_all(50U, 9), "TC-TIMESYNC-040g every delay is sliced at 50 ms");
CHECK(s_beat_calls == 13, "TC-TIMESYNC-040h beat count = main loop 1 + backoff 8 + accepted connection 1 + wrap-up 1 + next cycle 1");
CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-040i every beat sets the TIMESYNC bit");
"task entry registers the watchdog beat bit (task_wd_bind once)");
CHECK(s_evt_calls == 0, "skips the readiness barrier when sys_evtHandle==NULL");
CHECK((s_gen_calls == 2) && (s_sntp_run_calls == 1),
"one full tick: generation read once per round, SNTP driven once per round");
CHECK((s_sntp_gen_log_n == 1) && (s_sntp_gen_log[0] == 1u),
"time_sntp_run receives this round's generation value");
CHECK(s_delay_calls == 4, "tick wait 200 ms = 4 slices (slice cap 50 ms unchanged)");
CHECK(delays_all(50U, 4), "every tick slice sleeps exactly 50 ms");
CHECK(s_beat_calls == 6, "beat count = loop heads 2 (2nd beat precedes the bail-out gen) + slices 4");
CHECK(s_beat_id_bad == 0, "every beat sets the TIMESYNC bit");
/* B) 就绪屏障:2 次未就绪 + 1 次就绪 */
/* B) 代次变化原样下传:1 -> 2(断网重建后 SNTP 内部自弃旧 socket,循环本身
* 不再有任何监听簿记——桩头不声明 socket 族 API,伸手即链接不过) */
fakes_reset();
s_gen_seq[0] = 1u;
s_gen_seq[1] = 2u;
s_gen_seq_n = 2;
s_gen_default = 2u;
s_gen_longjmp_at = 3; /* 第 3 轮顶部跳出:已跑满 2 个完整节拍 */
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
CHECK(s_sntp_run_calls == 2, "two full ticks before bail-out");
CHECK((s_sntp_gen_log_n == 2) && (s_sntp_gen_log[0] == 1u) && (s_sntp_gen_log[1] == 2u),
"generation change is handed to the SNTP client verbatim, round by round");
CHECK(s_delay_calls == 8, "each round waits its own 200 ms (4+4 slices)");
CHECK(s_beat_calls == 11, "beat count = (loop head + 4 slices) x 2 rounds + loop head of round 3");
/* C) 就绪屏障:2 次未就绪 + 1 次就绪 */
fakes_reset();
sys_evtHandle = (osEventFlagsId_t)0x55;
s_evt_seq[0] = osFlagsErrorTimeout;
s_evt_seq[1] = osFlagsErrorTimeout;
s_evt_seq[2] = SYS_EVT_NET_READY;
s_evt_seq_n = 3;
s_gen_longjmp_at = 1;
s_gen_longjmp_at = 1; /* 屏障通过后第 1 轮顶部跳出 */
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
CHECK(s_evt_calls == 3, "TC-TIMESYNC-041 readiness barrier polled 3 times (2 not ready + 1 ready)");
CHECK(s_evt_last_timeout == 100U, "TC-TIMESYNC-041b barrier waits are sliced at 100 ms");
CHECK(s_evt_last_flags == SYS_EVT_NET_READY, "TC-TIMESYNC-041c waits for the NET_READY bit");
CHECK(s_evt_last_options == (osFlagsWaitAny | osFlagsNoClear), "TC-TIMESYNC-041d the ready bit is not consumed");
CHECK(s_beat_calls == 3, "TC-TIMESYNC-041e the two not-ready rounds each beat once (+ 1 in the main loop)");
CHECK(s_wd_bind_calls == 1, "TC-TIMESYNC-041f still registers the beat routing first");
CHECK(s_evt_calls == 3, "readiness barrier polled 3 times (2 not ready + 1 ready)");
CHECK(s_evt_last_timeout == 100U, "barrier waits are sliced at 100 ms");
CHECK(s_evt_last_flags == SYS_EVT_NET_READY, "waits for the NET_READY bit");
CHECK(s_evt_last_options == (osFlagsWaitAny | osFlagsNoClear),
"the ready bit is not consumed (ftpTask and friends may still see it)");
CHECK(s_beat_calls == 3, "two not-ready rounds each beat once + 1 at the loop head");
CHECK(s_sntp_run_calls == 0, "no SNTP round before the barrier passes");
CHECK(s_wd_bind_calls == 1, "still registers the beat routing first");
/* C) 屏障期间持续喂心跳(就绪位长期不置位) */
/* D) 屏障期间持续喂心跳(就绪位长期不置位,2026-10-02 台架根因的回归防线) */
fakes_reset();
sys_evtHandle = (osEventFlagsId_t)0x55;
s_evt_default = osFlagsErrorTimeout;
@@ -730,93 +315,25 @@ static void tc_task(void) {
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
CHECK(s_evt_calls == 4, "TC-TIMESYNC-042 barrier waited 4 times, none ready (loop did not hang)");
CHECK(s_beat_calls == 3, "TC-TIMESYNC-042b beats once after each not-ready round (before the 4th breaks out)");
/* D) 网络代次变化 -> 关闭旧监听并重监听 */
fakes_reset();
s_gen_seq[0] = 1u;
s_gen_seq[1] = 2u;
s_gen_seq_n = 2;
s_gen_default = 2u;
s_gen_longjmp_at = 4;
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-043 closes the old listener when the network generation changes");
/* INFO 三条:两次 open_listener 成功各一条 "TCP listen on" + 一条 "network re-init, relisten" */
CHECK(g_tstest_info_count == 3, "TC-TIMESYNC-043b leaves the network re-init, relisten trace (plus 2 listen-success lines)");
CHECK(s_accept_calls == 2, "TC-TIMESYNC-043c polls accept only once per loop then gives up");
CHECK((s_socket_calls == 2) && (s_bind_calls == 2) && (s_listen_calls == 2),
"TC-TIMESYNC-043d the second loop rebuilt the listener");
/* E) 监听建立失败 -> 5 次重试后放弃并退避 2000ms */
fakes_reset();
s_socket_default = -1;
s_gen_longjmp_at = 2;
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
CHECK(s_socket_calls == 5, "TC-TIMESYNC-044 open_listener gives up after 5 internal retries");
CHECK(g_tstest_err_count == 6, "TC-TIMESYNC-044b 5 net_socket FAIL lines + 1 retry later line");
CHECK(s_delay_calls == 90, "TC-TIMESYNC-044c backoff 5x500 ms + 2000 ms = 90 slices");
CHECK(s_beat_calls == 97, "TC-TIMESYNC-044d beat count = 2 + 5 + 50 + 40");
CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-044e every beat sets the TIMESYNC bit");
CHECK(s_evt_calls == 4, "barrier waited 4 times, none ready (loop did not hang)");
CHECK(s_beat_calls == 3, "beats once after each not-ready round (before the 4th breaks out)");
CHECK((s_gen_calls == 0) && (s_sntp_run_calls == 0),
"network untouched until NET_READY: no generation read, no SNTP round");
}
/* ===================== 对时操作留痕四组(道G task-50) =========================
* 成功应用投一条 quiet OPER;被拒不投(失败不得记成"改过时钟");
* 连续两次各一条不合并;魔术字不符不投。
* ⚠ 断言标签用语义行名:编号规范 §9 批次三已对测试用例号停发(用户裁定 2026-10-27),
* 本组是新口径下写的——追溯靠函数名 tc_oper_trace + 下面的语义标签。 */
static void tc_oper_trace(void) {
uint8_t pkt[16];
/* 1) 对时成功 → 恰一条 OPER */
/* ===================== SNTP 驻留后的循环延续 =====================
* time_sntp_run 的契约是"单次驻留 ≤50ms,调用方在片间喂心跳"——替身模拟一次
* 驻留后返回,循环必须按原节拍继续(心跳不欠账)。 */
static void tc_sntp_dwell_then_continue(void) {
fakes_reset();
make_time_packet(pkt, 0x20251027u);
recv_set(0, 8, pkt, 8, 0U);
s_recv_seq_n = 1;
s_set_unix_rc = 0;
time_sync_handle_conn(7);
CHECK(s_set_unix_calls == 1, "sync trace: clock layer applied once");
CHECK(s_post_calls == 1, "sync trace on success: exactly one operation record");
CHECK(s_post_id == (int)SYSLOG_OPER, "sync trace: record goes to the operation log");
CHECK(strcmp(s_post_last_desc, "clock set by 8888") == 0,
"sync trace: wording of the same family as the 104 time-sync record");
/* 2) 对时被拒(时钟层返非 0)→ 零留痕 */
fakes_reset();
make_time_packet(pkt, 7u);
recv_set(0, 8, pkt, 8, 0U);
s_recv_seq_n = 1;
s_set_unix_rc = -1;
time_sync_handle_conn(7);
CHECK(s_set_unix_calls == 1, "sync trace rejected: call still reaches the clock layer once");
CHECK(s_post_calls == 0, "sync trace rejected: NO operation record written");
/* 3) 连续两次成功会话 → 两条独立记录,不合并 */
fakes_reset();
s_set_unix_rc = 0;
make_time_packet(pkt, 100u);
recv_set(0, 8, pkt, 8, 0U);
recv_set(1, 8, pkt, 8, 0U);
s_recv_seq_n = 2;
time_sync_handle_conn(7);
time_sync_handle_conn(7);
CHECK(s_set_unix_calls == 2, "sync trace twice: two applied syncs");
CHECK(s_post_calls == 2, "sync trace twice: two independent records, not merged");
CHECK(s_post_desc_count == 2, "sync trace twice: both records are the time-sync wording (no coalescing latch)");
/* 4) 前缀不是 TIME → 时钟层都没到,更不该留痕 */
fakes_reset();
(void)memset(pkt, 'X', 8);
recv_set(0, 8, pkt, 8, 0U);
s_recv_seq_n = 1;
s_set_unix_rc = 0;
time_sync_handle_conn(7);
CHECK((s_set_unix_calls == 0) && (s_post_calls == 0),
"sync trace bad magic: neither moves the clock nor writes a record");
s_sntp_run_longjmp_at = 2; /* 第 2 轮驻留中脱出(模拟慢收片场景收尾) */
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
CHECK(s_sntp_run_calls == 2, "loop re-enters the SNTP client every tick");
CHECK(s_beat_calls == 6, "beats = (head + 4 slices) for round 1 + head of round 2 (dwell bail-out)");
CHECK(s_delay_calls == 4, "round 1 waited its full 200 ms; round 2 bailed mid-tick");
CHECK(s_beat_id_bad == 0, "the beat bit stays TIMESYNC across rounds");
}
/* ===================== main ===================== */
@@ -826,14 +343,10 @@ int main(void) {
printf("-- delay_fed (heartbeat-fed slicing) --\n");
tc_delay_fed();
printf("-- open_listener (retry / backoff) --\n");
tc_open_listener();
printf("-- handle_conn (packet reassembly / budget) --\n");
tc_handle_conn();
printf("-- time_sync_task (barrier / accept / relisten) --\n");
printf("-- time_sync_task (barrier / SNTP cadence) --\n");
tc_task();
printf("-- operation trace on sync (task-50) --\n");
tc_oper_trace();
printf("-- sntp dwell then continue --\n");
tc_sntp_dwell_then_continue();
printf("\n=== Time Sync Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
-83
View File
@@ -1,83 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
本地时间同步服务(PC -> MCU 板子)
功能:
周期性把本机(PC)的本地时间,通过 TCP 推送到 STM32 板子。
板子端在 timeSyncTask 中以 TCP Server 方式监听端口,收到后调用
sys_clock_set_unix() 更新系统墙钟并写回 SD2506 RTC,使板子时间 == 你电脑的时间。
协议(板子端实现见 App/sync/time_sync.c 的 time_sync_handle_conn):
字节 0..3 : 魔术字 "TIME" (0x54 0x49 0x4D 0x45)
字节 4..7 : 本地 Unix 时间戳,uint32,大端(主机字节序转换后由板子分解)
用法:
python time_sync_server.py # 默认推送到设备 IP(bench_defaults.DEVICE_IP):8888
python time_sync_server.py <设备IP> # 指定板子 IP
python time_sync_server.py <设备IP> 9000 3 # 指定端口与间隔(秒)
按 Ctrl+C 停止。
"""
import socket
import struct
import sys
import time
import calendar
import bench_defaults # 台架连接默认值单一真源
DEFAULT_MCU_IP = bench_defaults.DEVICE_IP
DEFAULT_PORT = 8888
DEFAULT_INTERVAL = 5.0
def main():
mcu_ip = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_MCU_IP
port = int(sys.argv[2]) if len(sys.argv) > 2 else DEFAULT_PORT
interval = float(sys.argv[3]) if len(sys.argv) > 3 else DEFAULT_INTERVAL
print("[time_sync] 本地时间推送服务已启动(TCP 客户端)")
print(f" 目标板子 : {mcu_ip}:{port}")
print(f" 推送间隔 : {interval}s")
print(f" 协议帧 : 'TIME' + uint32(BE) 本地 Unix 时间戳")
print("[time_sync] 按 Ctrl+C 停止")
print("-" * 48)
while True:
try:
# 本地 wall-clock 当作 UTC 算出的 epoch(与电脑显示一致)。
# 注意:板子 RTC 存本地时间且 unix_to_sd2506 无时区处理,
# 故必须发"本地墙钟当 UTC"的伪 epoch(calendar.timegm),
# 不能用 time.mktime(返回真正 UTC epoch,会导致板子慢 8 小时)。
local_epoch = int(calendar.timegm(time.localtime()))
pkt = b"TIME" + struct.pack(">I", local_epoch)
with socket.create_connection((mcu_ip, port), timeout=5) as sock:
sock.sendall(pkt)
# 关键:发送后保持连接打开,给板子留出读完 8 字节的窗口。
# 若立即 close,CH395F 收到 FIN 会丢弃接收缓冲里尚未被板子读出的
# 8 字节时间包,导致板子 net_recv 看到 CLOSED 而无数据、时间同步失败。
# 板子会话收尾**不再 close**(单连接监听跨会话常驻,
# EOF 时 net_close 会连监听一起毁掉)——这里 recv 等 2s 宽限后由
# 本端关闭;板子 net 层 auto_relisten 随即恢复监听,下一轮照常连接。
sock.settimeout(2.0)
try:
sock.recv(64)
except (socket.timeout, OSError):
pass
now_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print(f"[{time.strftime('%H:%M:%S')}] -> {now_str} (epoch={local_epoch})")
except OSError as e:
print(f"[{time.strftime('%H:%M:%S')}] 连接/发送失败: {e}")
time.sleep(interval)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n[time_sync] 已停止")