Files
STM32F4-Base/test/host/iec104_serv/iec104_serv_test.c
T
wandering1 94b0d79026 feat(ui)+chore(docs): UI 补齐清单清空;数据可信销项与「卡」号清退
本笔含两组收尾工作(同一工作区内交错在 6 个共享文档/门禁配置里,
拆开会产生"文档声称的页数与树里黄金数不一致"的中间提交,故合为一笔):

一、UI 补齐(清单清空)
- 新建 6 个内容页模块:ui_page_accident / oper / alarm / start / yk
  复用 ui_w_records 共享组件(分页、空态"无记录"、按实例读盘缓存),
  记录层与五个写入点已由道F task-10 / 道G task-12 落地,本批只补页面;
  ui_page_runreport 为 13 行 2 页只读统计,四个数逐个挂范围标签
  (自电池周期 / 本次开机),boot_ready=0 显 "--" 不显 0。
- 数据面:ui_data_info_t 尾部追加 9 字段,ui_data_feed 用
  boot_stats_get() + iec104_serv_get_stats() 纯搬运;模拟器填确定值假数据
  (差异登记 docs/测试说明/模拟器差距清单.md §2.3 两行)。
- 接线:菜单树 6 叶绑 #page=,绑定叶 22→28、UI_PAGE_MAP_COUNT=28;
  字库 185→218 字 / 280→313 字形;双端注册 uvprojx +30、CMakeLists +6。
- 回归:内容页黄金 53→60(含 runreport_p2 真拍第 2 页),文本断言
  425 正向 / 36 反向;存量 53 张逐字节零漂移;记录 5 页各用独立空目录
  (模拟器把操作留痕写进 cwd 的 sim_oper.log,共用 tmp 会让空态拍不到)。

二、数据可信销项 + 「数据可信」任务卡号整族清退
- 删除已闭环的"已知电压注入验证"行与闭环结论块(证据在
  docs/硬件平台/ADC通道映射说明.md §8.1 与
  docs/测试说明/ADC前端注入验证流程.md §5~§7),§5 标识列改语义短语。
- 卡 0/0b/1~12 共 14 个号全仓 288 站点 / 56 文件清退成语义短语,
  代码注释、测试、门禁配置与文档一并改;换算表入 docs/编号规范.md §9.3。
  这一族是中文前缀,不进 check_ids 语料,刻意不入 id_aliases.json。

验证(实测)
- @build 0 错 0 警:Code=236456 RO=28124 RW=1876 ZI=137084;
  RAM +464 B(RW +304 / ZI +160),RW_IRAM1 62.2% / RW_CCM 87.9%,
  体积同码凭证与 RAM 台账已回填。
- check_menu_rows 8 状态 rc=0(池 5368/5448 常量未动);
  check_content_pages 60 页 rc=0;check_font_charset / registration /
  coding-standard / bom-drift / mojibake / doc-links --all / layer-deps /
  nested-comment 全 rc=0;check_ids --strict 无错误;主机单测
  ui_data 295 / boot_stats 21 / sys_log_mcu 141 / soe 32 / soe_store 27。

未做(不是遗漏)
- 六页真机判据(真记录翻页、掉电保持、清除后归零、boot_count 自增、
  拔 RTC 电池落 RESTARTED、IWDG 复位落 rst_cause、主站连一次 conn_up+1)
  已登记 docs/任务规划/登记簿.md §7;模拟器结构上照不到。
- docs/产品需求/产品需求设计文档.md 属另一条在飞改动,本笔不含;
  其中我已同步 6 处现状数字(28 叶 / 60 页 / 218 字),随该笔入库。
2026-10-28 07:23:16 +08:00

657 lines
26 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:IEC 60870-5-104 上行服务生命周期(iec104_serv)主机侧单元测试
* 模块功能:在 PC 上离线验证 App/task/iec104_serv.c 的服务侧骨架:监听建立与失败
* 回收、**网络代次一变就重建监听**(改了 IP 端口必须在新地址上继续服务)、
* accept 门控与表项失效自愈、单连接 socket 的断线语义(对端 FIN 只关数据
* 口、绝不关监听)、连续短写判死、活性分频探活、协议状态掉回 IDLE 时丢
* 连接并立即重建,以及时钟桥接的两个溢出边界(>4G 秒拒绝、未对时返回哨兵
* 而不是把 1970 当真实时间)。
*
* 被测对象:真实源文件 ../../../App/task/iec104_serv.c(#include 纳入,故它的 static
* s_bind_listen / s_drop_conn / s_service_link / s_serv_poll / s_send_apdu /
* s_clock_sync / s_epoch_ms 都能直接驱动——**不跑 iec104_serv_task 的无限
* 主循环**,那是 never-returning 的任务体,不适合单测)。
* iec104_link.h / iec104_pts.h / prot_set.h / sys_clock.h / sys_events.h /
* task_watchdog.h / net_types.h 全用**真实头**(iec104_state_t 枚举、
* iec104_link_cfg_t 函数指针签名、TASK_WD_IEC104=7、SYS_EVT_* 位值一改即
* 编译期报错);链路层(iec104_link_*)与 pts 层给假件——链路协议本身有
* test/host/iec104 的 97 条专测,本模块只管"服务怎么活、怎么死、怎么重生"。
*
* 为什么要主机侧单测(而不是只靠台架):
* 1. 104 主站掉线重连是**现场最常见的故障形态**,但台架上没法按需制造"改 IP 时
* 正好有连接""对端 FIN 与 RST 的区别""连续三次短写"这些时序;而这里每一条
* 处理错的后果都是端口永久失联或会话卡死,只能靠主站侧超时才发现。
* 2. 判死用**连续**短写计数(一次成功即复位),写成"累计"就会把偶发拥塞误判成
* 断线,主站看到的就是莫名掉线——这条只有能精确注入短写/成功序列才可测。
* 3. 时钟桥接的两个边界:epoch 秒超过 32 位必须拒绝(否则截断成 2106 年前的某个
* 乱值写进 RTC),未对时(sys_clock_get 返回 UINT32_MAX)必须回哨兵而不是 0,
* 否则对端会以为设备时间是 1970-01-01。
* 4. 活性分频(每 32 轮才探一次)是 CPU 预算的取舍:写错成每轮都探,就把 100ms
* 轮询变成了每轮两次系统调用;写错成分频相位,TESTFR 会迟迟不发。
* 5. 操作记录是**审计面**(道F task-19):只要"投了"而不管"改成功了没有",屏上的
* 操作记录页就会出现从没发生过的对时;越界拒绝与 RTC 写失败这两条腿必须
* 一条都不投,而这只有能注入 sys_clock_set_unix 的返回值才测得到。
* 同时钉住"用 quiet 口不用 post 口"——post 的两个满态留痕分支各带约 216 B
* 内联行缓冲,iec104Task 的静态最坏链放不下它(台架与单测都看不出这个差别,
* 只有 stack_report 会响,所以先在源码侧钉住)。
*
* 覆盖点:TC-I104SRV-001..004 建监听(001b/c = 端口与公共地址取自 [comm] 段
* getter,道G task-8 取值来源改造);010..013 发送与连续短写判死;020..023 时钟
* 桥接;030..033 代次重建与 accept 门控;034..036 活性分频与对端 FIN;037..041
* 错误丢弃与协议掉线重生;050..053 对时的操作记录(道F task-19:成功才记、
* 越界拒绝与写钟失败两条腿都不记、每次对时各记一条、且只走 quiet 投递口)。
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-23
*/
#include <stdio.h>
#include <string.h>
#include "iec104_serv.h"
#include "net_socket.h"
#include "iec104_serv.c"
static int s_total;
static int s_passed;
static int s_failed;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" PASS %s\n", name); } \
else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } \
} while (0)
int g_i4_info_count;
int g_i4_error_count;
int g_i4_debug_count;
/* 真实 sys_events.h 把它声明成 extern,这里给定义(本模块只在主循环里用,
测试设成 NULL 走"事件组不可用"支路,不阻塞静态辅助函数的驱动)。 */
osEventFlagsId_t sys_evtHandle;
/* ===================== 可控 socket 面 ===================== */
typedef struct {
int socket_rc;
int bind_rc;
int listen_rc;
int accept_rc;
int send_rc;
int recv_rc;
int can_read;
int armed;
int errno_val;
uint32_t gen;
int sock_exists;
int socket_calls;
int bind_calls;
int listen_calls;
int accept_calls;
int close_calls;
int last_closed_fd;
int last_accept_sock;
int recv_calls;
uint16_t last_bind_port_net;
uint16_t last_bind_family;
uint32_t last_bind_addr;
int delay_calls;
} i4_net_t;
static i4_net_t g_n;
/* net_get_sock 要返回一个真 net_sock_t(结构来自真实 net_types.h),先声明再用 */
static net_sock_t g_sock_probe;
int net_socket(int domain, int type) { (void)domain; (void)type; g_n.socket_calls++; return g_n.socket_rc; }
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen)
{
const struct net_sockaddr_in *a = (const struct net_sockaddr_in *)addr;
(void)sockfd;
(void)addrlen;
g_n.bind_calls++;
g_n.last_bind_port_net = a->sin_port;
g_n.last_bind_family = a->sin_family;
g_n.last_bind_addr = a->sin_addr.s_addr;
return g_n.bind_rc;
}
int net_listen(int sockfd) { (void)sockfd; g_n.listen_calls++; return g_n.listen_rc; }
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen)
{
(void)addr;
(void)addrlen;
g_n.accept_calls++;
g_n.last_accept_sock = sockfd;
return g_n.accept_rc;
}
int net_close(int sockfd) { g_n.close_calls++; g_n.last_closed_fd = sockfd; return 0; }
int net_send(int sockfd, const void *buf, int len, int flags)
{
(void)sockfd;
(void)buf;
(void)flags;
return g_n.send_rc;
}
int net_recv_nb(int sockfd, void *buf, int len)
{
(void)sockfd;
(void)buf;
(void)len;
g_n.recv_calls++;
return g_n.recv_rc;
}
int net_can_read(int sockfd) { (void)sockfd; return g_n.can_read; }
int net_get_errno(void) { return g_n.errno_val; }
int net_listener_armed(int sockfd) { (void)sockfd; return g_n.armed; }
uint32_t net_get_generation(void) { return g_n.gen; }
net_sock_t *net_get_sock(int sockfd) { (void)sockfd; return (g_n.sock_exists != 0) ? &g_sock_probe : NULL; }
uint16_t net_htons(uint16_t v) { return (uint16_t)((v >> 8) | (v << 8)); }
/* ===================== 假链路层与假 pts ===================== */
static int g_lk_init;
static uint16_t g_lk_init_ca; /* [comm] ca 透传钉(道G task-8):init 收到的 cfg->ca */
static int g_lk_open;
static int g_lk_close;
static int g_lk_poll;
static int g_lk_onrx;
static uint32_t g_lk_last_now;
static iec104_state_t g_lk_state;
void iec104_link_init(iec104_link_t *lk, const iec104_link_cfg_t *cfg, void *ctx)
{
(void)lk; (void)ctx;
g_lk_init_ca = cfg->ca; /* 道G task-8:钉 ca 真的进了链路 */
g_lk_init++;
}
void iec104_link_open(iec104_link_t *lk, uint32_t now_ms)
{
(void)lk;
g_lk_open++;
g_lk_last_now = now_ms;
}
void iec104_link_close(iec104_link_t *lk) { (void)lk; g_lk_close++; }
void iec104_link_poll(iec104_link_t *lk, uint32_t now_ms)
{
(void)lk;
g_lk_poll++;
g_lk_last_now = now_ms;
}
void iec104_link_on_rx(iec104_link_t *lk, const uint8_t *data, uint16_t len, uint32_t now_ms)
{
(void)lk; (void)data; (void)len; (void)now_ms;
g_lk_onrx++;
}
iec104_state_t iec104_link_state(const iec104_link_t *lk) { (void)lk; return g_lk_state; }
static int g_pts_init;
void iec104_pts_init(void) { g_pts_init++; }
/* ===================== [comm] getter 桩(道G task-8) =====================
* 真 getter(合法带自愈)在 iec104_comm.c,由 test/host/iec104 真链套
* TC-IEC104-031~033 覆盖;本套只钉 serv 的**取值来源**:端口每次 bind 现取、
* ca 任务启动取一次。 */
static uint16_t g_comm_ca = IEC104_COMM_DEF_CA;
static uint16_t g_comm_port = IEC104_COMM_DEF_PORT;
uint16_t iec104_comm_ca(void) { return g_comm_ca; }
uint16_t iec104_comm_port(void) { return g_comm_port; }
/* ===================== 假时钟 / 定值 / 看门狗 / RTOS ===================== */
static uint32_t g_clock_val;
static int g_clock_set_rc;
static uint32_t g_clock_last_secs;
static int g_clock_set_calls;
static uint32_t g_dirty;
static int g_evt_set_calls;
static uint32_t g_evt_last;
static int g_wd_beats;
static int g_wd_last_id;
static uint32_t g_now;
/* ===================== sys_log 桩(task-19 操作记录) =====================
* 两个投递口都抓下来:quiet 是 iec104Task 该用的那个(本任务栈深,不许在调用链上
* 内联日志行缓冲),post 一旦在这里被调到就是"绕回旧口",053 会红。
* 投递本身(收/丢/计数)归 test/host/sys_log_mcu,本套只钉"哪条腿投了哪一句"。 */
#include "sys_log.h"
#define RP_I4_REC_MAX 8u
static uint8_t rp_i4_id[RP_I4_REC_MAX];
static char rp_i4_desc[RP_I4_REC_MAX][SYSLOG_DESC_MAX + 1u];
static uint32_t rp_i4_n; /* quiet 通道累计(不回绕,供"恰好几条"断言) */
static uint32_t rp_i4_post_n; /* 误用 syslog_post 的次数,期望恒 0 */
static void rp_i4_rec_clear(void)
{
rp_i4_n = 0u;
rp_i4_post_n = 0u;
(void)memset(rp_i4_id, 0, sizeof(rp_i4_id));
(void)memset(rp_i4_desc, 0, sizeof(rp_i4_desc));
}
void syslog_post_quiet(syslog_id_t id, const char *desc)
{
uint32_t slot = rp_i4_n % RP_I4_REC_MAX;
size_t dl = 0u;
if (desc == NULL) {
return;
}
while ((dl < SYSLOG_DESC_MAX) && (desc[dl] != '\0')) {
dl++;
}
(void)memcpy(rp_i4_desc[slot], desc, dl);
rp_i4_desc[slot][dl] = '\0';
rp_i4_id[slot] = (uint8_t)id;
rp_i4_n++;
}
void syslog_post(syslog_id_t id, const char *desc)
{
(void)id;
(void)desc;
rp_i4_post_n++;
}
uint32_t syslog_post_dropped(void) { return 0u; }
static int rp_i4_at(uint32_t k, syslog_id_t id, const char *desc)
{
if (k >= rp_i4_n) {
return 0;
}
return (rp_i4_id[k % RP_I4_REC_MAX] == (uint8_t)id)
&& (strcmp(rp_i4_desc[k % RP_I4_REC_MAX], desc) == 0) ? 1 : 0;
}
uint32_t sys_clock_get(void) { return g_clock_val; }
int sys_clock_set_unix(uint32_t unix_secs)
{
g_clock_set_calls++;
g_clock_last_secs = unix_secs;
return g_clock_set_rc;
}
uint8_t prot_set_dirty_peek(void) { return (uint8_t)g_dirty; }
/* run_par 脏 peek 假件(运行参数段):任务主循环在单测中不跑,脏恒 0 =
* 不投递 SYS_EVT_RUN_PAR_SAVE;落盘链真身覆盖在 test/host/run_par。 */
uint8_t run_par_dirty_peek(void) { return 0u; }
uint32_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags)
{
(void)ef_id;
g_evt_set_calls++;
g_evt_last = flags;
return flags;
}
uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout)
{
(void)ef_id; (void)flags; (void)options; (void)timeout;
return 0u;
}
void task_wd_beat(task_wd_id_t id) { g_wd_beats++; g_wd_last_id = (int)id; }
uint32_t osKernelGetTickCount(void) { return g_now; }
void osDelay(uint32_t ticks)
{
(void)ticks;
g_n.delay_calls++;
if (g_n.delay_calls > 500) {
printf(" ABORT runaway (>500 osDelay rounds)\n");
fflush(stdout);
exit(2);
}
}
/* ===================== 复位 ===================== */
static void reset_all(void)
{
(void)memset(&g_n, 0, sizeof(g_n));
g_n.socket_rc = 5;
g_n.bind_rc = 0;
g_n.listen_rc = 0;
g_n.accept_rc = -1;
g_n.send_rc = 0;
g_n.armed = 1;
g_n.sock_exists = 1;
g_n.errno_val = NET_ERR_WOULDBLOCK;
g_n.gen = 100u;
(void)memset(&g_sock_probe, 0, sizeof(g_sock_probe));
g_lk_init = 0; g_lk_open = 0; g_lk_close = 0; g_lk_poll = 0; g_lk_onrx = 0;
g_lk_last_now = 0u;
g_lk_state = IEC104_ST_RUN;
g_lk_init_ca = 0u;
g_pts_init = 0;
g_comm_ca = IEC104_COMM_DEF_CA;
g_comm_port = IEC104_COMM_DEF_PORT;
s_cfg.ca = IEC104_COMM_DEF_CA;
g_clock_val = 1700000000u;
rp_i4_rec_clear();
g_clock_set_rc = 0;
g_clock_last_secs = 0u;
g_clock_set_calls = 0;
g_dirty = 0u;
g_evt_set_calls = 0;
g_evt_last = 0u;
g_wd_beats = 0;
g_wd_last_id = -1;
g_now = 5000u;
g_i4_info_count = 0;
g_i4_error_count = 0;
g_i4_debug_count = 0;
sys_evtHandle = NULL;
s_listen = -1;
s_client = -1;
s_gen = 100u;
s_liveness_ctr = 0u;
s_send_fail_run = 0u;
s_conn_dead = 0u;
}
/* ===================== 建监听 ===================== */
static void tc_bind_listen(void)
{
printf("-- TC-I104SRV-001..004 bind/listen --\n");
reset_all();
CHECK((s_bind_listen() == 0) && (s_listen == 5) && (g_n.listen_calls == 1) &&
(g_n.last_bind_family == NET_AF_INET) &&
(g_n.last_bind_port_net == net_htons(IEC104_COMM_DEF_PORT)) &&
(g_n.last_bind_addr == NET_INADDR_ANY),
"TC-I104SRV-001 build the listener: [comm] default port through htons, bind to any address, remember the listen handle");
reset_all();
g_comm_port = 9002u; /* [comm] 可配值:非标准端口(道G task-8) */
CHECK((s_bind_listen() == 0) && (g_n.last_bind_port_net == net_htons(9002u)),
"TC-I104SRV-001b bind takes the [comm] getter value, not the compile-time 2404");
reset_all();
g_comm_ca = 7u;
s_apply_comm_ca(); /* 任务启动链路同款动作(ca = 重启生效) */
iec104_link_init(&s_link, &s_cfg, NULL);
CHECK((s_cfg.ca == 7u) && (g_lk_init_ca == 7u) && (g_lk_init == 1),
"TC-I104SRV-001c task-start ca fill reaches the link cfg ([comm] value source)");
g_comm_ca = 9u;
s_apply_comm_ca();
CHECK(s_cfg.ca == 9u, "TC-I104SRV-001c every task start re-reads the section (restart semantics)");
reset_all();
g_n.socket_rc = -1;
CHECK((s_bind_listen() == -1) && (g_n.bind_calls == 0) && (g_n.close_calls == 0),
"TC-I104SRV-002 if no socket can be created return at once: no bind and no close of a handle that does not exist");
reset_all();
g_n.bind_rc = -1;
CHECK((s_bind_listen() == -1) && (g_n.close_calls == 1) && (g_n.listen_calls == 0),
"TC-I104SRV-003 a bind failure must close the socket just created (otherwise every retry leaks a handle)");
reset_all();
g_n.listen_rc = -1;
CHECK((s_bind_listen() == -1) && (g_n.close_calls == 1) && (s_listen == -1),
"TC-I104SRV-004 a listen failure also closes it and leaves no half-built listener");
}
/* ===================== 发送与连续短写判死 ===================== */
static void tc_send(void)
{
const uint8_t apdu[4] = { 0x68u, 0x02u, 0x00u, 0x00u };
printf("-- TC-I104SRV-010..013 send & fail streak --\n");
reset_all();
s_client = -1;
s_send_apdu(NULL, apdu, 4u);
CHECK((g_n.send_rc == 0) && (g_n.close_calls == 0) && (g_i4_error_count == 0),
"TC-I104SRV-010 with no connection it returns directly (no net_send, no error)");
reset_all();
s_client = 7;
g_n.send_rc = 4;
s_send_apdu(NULL, apdu, 4u);
CHECK((s_send_fail_run == 0u) && (s_conn_dead == 0u),
"TC-I104SRV-011 a fully sent frame clears the consecutive-failure counter");
reset_all();
s_client = 7;
g_n.send_rc = 2; /* 短写:只发了 2/4 */
s_send_apdu(NULL, apdu, 4u);
CHECK((s_send_fail_run == 1u) && (s_conn_dead == 0u) && (g_i4_error_count == 1),
"TC-I104SRV-012 the first short write is logged but does not declare death (congestion is normal)");
s_send_apdu(NULL, apdu, 4u);
s_send_apdu(NULL, apdu, 4u);
CHECK((s_send_fail_run == 3u) && (s_conn_dead == 1u),
"TC-I104SRV-012b only the 3rd failure in a row declares death (IEC104_SEND_FAIL_MAX)");
reset_all();
s_client = 7;
g_n.send_rc = 1;
s_send_apdu(NULL, apdu, 4u);
s_send_apdu(NULL, apdu, 4u);
g_n.send_rc = 4;
s_send_apdu(NULL, apdu, 4u); /* 中间成功一次 */
g_n.send_rc = 1;
s_send_apdu(NULL, apdu, 4u);
s_send_apdu(NULL, apdu, 4u);
CHECK((s_conn_dead == 0u) && (s_send_fail_run == 2u),
"TC-I104SRV-013 success resets the counter: occasional congestion never adds up into a false kill");
}
/* ===================== 时钟桥接边界 ===================== */
static void tc_clock(void)
{
printf("-- TC-I104SRV-020..023 clock bridge --\n");
reset_all();
CHECK(s_clock_sync(NULL, 0x100000000ULL * 1000ULL) == -1,
"TC-I104SRV-020 any second count beyond 32 bits is rejected (truncating it into the RTC means garbage past 2106)");
CHECK(g_clock_set_calls == 0, "TC-I104SRV-020b when rejected the clock must not be touched at all");
reset_all();
(void)s_clock_sync(NULL, 1700000123000ULL);
CHECK((g_clock_set_calls == 1) && (g_clock_last_secs == 1700000123u),
"TC-I104SRV-021 milliseconds become seconds by integer division before the clock is written");
reset_all();
g_clock_val = UINT32_MAX; /* 未对时的哨兵值 */
CHECK(s_epoch_ms(NULL) == UINT64_MAX,
"TC-I104SRV-022 without time sync return the UINT64_MAX sentinel (1970 must never pass for device time)");
reset_all();
g_clock_val = 1700000123u;
CHECK(s_epoch_ms(NULL) == 1700000123000ULL, "TC-I104SRV-023 with a synced clock the answer is in milliseconds");
/* ---- task-19:对时 = 操作记录(审计面只记真发生过的动作) ---- */
reset_all();
(void)s_clock_sync(NULL, 1700000123000ULL);
CHECK((rp_i4_n == 1u) && (rp_i4_at(0u, SYSLOG_OPER, "clock set by 104") == 1),
"TC-I104SRV-050 a successful sync books exactly one operation record naming the source");
reset_all();
(void)s_clock_sync(NULL, 0x100000000ULL * 1000ULL);
CHECK((rp_i4_n == 0u) && (g_clock_set_calls == 0),
"TC-I104SRV-050b the out-of-range rejection books nothing (an audit record for something that never happened is worse than none)");
reset_all();
g_clock_set_rc = -1; /* 写钟失败(RTC 回写失败那一类) */
CHECK(s_clock_sync(NULL, 1700000123000ULL) == -1,
"TC-I104SRV-051 a failed clock write still answers the link layer with -1");
CHECK(rp_i4_n == 0u,
"TC-I104SRV-051b a failed clock write books no operation record");
reset_all();
(void)s_clock_sync(NULL, 1700000123000ULL);
(void)s_clock_sync(NULL, 1700000456000ULL);
CHECK((rp_i4_n == 2u) && (rp_i4_at(1u, SYSLOG_OPER, "clock set by 104") == 1),
"TC-I104SRV-052 every re-sync is its own operation record (no dedup, no latching)");
/* 053:用错投递口的形状——iec104Task 是深链任务,只许用 quiet。
* 顺带量文案长度:超过 SYSLOG_DESC_MAX 会在投递侧被静默截断。 */
reset_all();
(void)s_clock_sync(NULL, 1700000123000ULL);
CHECK((rp_i4_post_n == 0u) && (rp_i4_n == 1u)
&& (strlen("clock set by 104") <= SYSLOG_DESC_MAX),
"TC-I104SRV-053 the record goes out over the stack-safe channel only, and its text fits the record field");
}
/* ===================== 代次重建与 accept 门控 ===================== */
static void tc_generation(void)
{
printf("-- TC-I104SRV-030..033 generation & accept gate --\n");
reset_all();
s_listen = 5;
s_client = 9;
g_n.gen = 101u; /* 网络重建(现场改了 IP) */
s_serv_poll();
CHECK((g_n.close_calls == 2) && (g_n.last_closed_fd == 5) &&
(s_client == -1) && (s_liveness_ctr == 0u) && (s_conn_dead == 0u) &&
(g_lk_close == 1) && (s_gen == 101u) && (g_n.bind_calls == 1),
"TC-I104SRV-030 a generation change tears down the old connection and listener and rebuilds them (the port must keep serving on the new IP)");
reset_all();
s_listen = -1;
s_serv_poll();
CHECK((g_n.bind_calls == 1) && (s_listen == 5) && (g_n.accept_calls == 0),
"TC-I104SRV-031 a lost listener is rebuilt and this round does not accept");
reset_all();
s_listen = 5;
g_n.armed = 0; /* 监听还没就绪 */
g_n.sock_exists = 1;
s_serv_poll();
CHECK((g_n.accept_calls == 0) && (s_listen == 5),
"TC-I104SRV-032 no accept while the listener gate is down (otherwise it spins on a handle that is not ready)");
reset_all();
s_listen = 5;
g_n.armed = 0;
g_n.sock_exists = 0; /* 表项已不存在 */
s_serv_poll();
CHECK((s_listen == -1) && (g_n.accept_calls == 0),
"TC-I104SRV-032b once the slot is invalid the listen handle is dropped and rebuilt next round");
reset_all();
s_listen = 5;
g_n.accept_rc = 9;
s_send_fail_run = 3u;
s_conn_dead = 1u;
s_liveness_ctr = 7u;
s_serv_poll();
CHECK((s_client == 9) && (g_lk_open == 1) && (g_lk_last_now == 5000u) &&
(s_send_fail_run == 0u) && (s_conn_dead == 0u) && (s_liveness_ctr == 0u) &&
(g_i4_info_count == 1),
"TC-I104SRV-033 a new connection opens the link and resets all three failure counters");
}
/* ===================== 活性分频、对端 FIN、错误丢弃 ===================== */
static void tc_liveness(void)
{
printf("-- TC-I104SRV-034..041 liveness / EOF / drop --\n");
reset_all();
s_listen = 5;
s_client = 9;
g_n.can_read = 0;
s_liveness_ctr = 0u;
s_serv_poll();
CHECK((g_n.recv_calls == 0) && (g_lk_poll == 1) && (s_liveness_ctr == 1u),
"TC-I104SRV-034 with nothing readable the liveness probe runs on a divide-by-32 tick and this round receives nothing (saves CPU budget)");
reset_all();
s_listen = 5;
s_client = 9;
g_n.can_read = 0;
/* 这一轮收到的是"暂无数据"(非阻塞的空读),而不是 0——0 在 recv_nb 的语义里是
* 对端 FIN,会直接走 036 那条断开支路,测不到分频本身。 */
g_n.recv_rc = -1;
g_n.errno_val = NET_ERR_WOULDBLOCK;
s_liveness_ctr = 31u; /* 下一轮正好到分频点 */
s_serv_poll();
CHECK((g_n.recv_calls == 1) && (g_lk_poll == 1) && (s_client == 9),
"TC-I104SRV-035 only when the divider fires does it receive once (liveness frames such as TESTFR depend on it going out)");
reset_all();
s_listen = 5;
s_client = 9;
g_n.can_read = 1;
g_n.recv_rc = 0; /* 对端 FIN */
s_serv_poll();
CHECK((g_n.last_closed_fd == 9) && (s_client == -1) && (g_lk_close == 1) &&
(g_n.close_calls == 1) && (s_listen == 5) && (g_n.bind_calls == 0),
"TC-I104SRV-036 a normal peer disconnect closes only the data socket: the listener must stay (on a single-socket chip closing the wrong one kills the port)");
reset_all();
s_listen = 5;
s_client = 5; /* 单连接复用:数据号 == 监听号 */
g_n.can_read = 1;
g_n.recv_rc = 0;
s_serv_poll();
CHECK((g_n.close_calls == 0) && (s_client == -1) && (g_lk_close == 1),
"TC-I104SRV-036b when the data handle equals the listen handle it must NOT be closed (that would destroy the listener too)");
reset_all();
s_listen = 5;
s_client = 9;
g_n.can_read = 1;
g_n.recv_rc = -1;
g_n.errno_val = NET_ERR_WOULDBLOCK; /* 空转:没数据可读 */
s_serv_poll();
CHECK((s_client == 9) && (g_lk_close == 0) && (g_i4_error_count == 0),
"TC-I104SRV-037 WOULDBLOCK is normal idling and must not drop the connection as an error");
reset_all();
s_listen = 5;
s_client = 9;
g_n.can_read = 1;
g_n.recv_rc = -1;
g_n.errno_val = NET_ERR_CONNRESET; /* 真错误 */
s_serv_poll();
CHECK((s_client == -1) && (s_listen == 5) && (g_i4_error_count == 1) &&
(g_lk_close == 1) && (s_liveness_ctr == 0u),
"TC-I104SRV-038 a real error drops the connection and rebuilds the listener on the spot (no gap in service)");
reset_all();
s_listen = 5;
s_client = 9;
s_conn_dead = 1u;
s_service_link(1000u);
CHECK((s_client == -1) && (g_n.close_calls == 1) && (g_i4_error_count == 1),
"TC-I104SRV-039 after death is declared service_link does the teardown (the send-failure path is a separate case)");
reset_all();
s_listen = 5;
s_client = 9;
g_lk_state = IEC104_ST_IDLE; /* 链路层自己回到了未建立 */
s_service_link(1000u);
CHECK((s_client == -1) && (g_lk_close >= 1) && (s_listen == 5),
"TC-I104SRV-040 falling back to IDLE also drops and respawns the session (otherwise the service stays up but never serves)");
reset_all();
s_listen = 9;
s_client = 9;
g_n.socket_rc = -1; /* 重建失败 */
g_n.sock_exists = 0; /* 且表项已经没了 */
g_n.can_read = 1;
g_n.recv_rc = 0;
s_serv_poll();
CHECK((s_client == -1) && (s_listen == -1) && (g_n.close_calls == 0),
"TC-I104SRV-041 slot gone and the rebuild failed: drop the stale listen handle and retry next round (never close it while it equals the data handle)");
}
int main(void)
{
printf("=== IEC104 SERV Tests ===\n");
tc_bind_listen();
tc_send();
tc_clock();
tc_generation();
tc_liveness();
printf("\n=== IEC104 SERV Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}