/* * 模块名称: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 会迟迟不发。 * * 覆盖点:TC-I104SRV-001..004 建监听;010..013 发送与连续短写判死;020..023 时钟 * 桥接;030..033 代次重建与 accept 门控;034..036 活性分频与对端 FIN;037..041 * 错误丢弃与协议掉线重生。 * * 通过:全部 TC PASS(main 返回 0)。 * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 * * 创建日期:2026-10-23 */ #include #include #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 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)cfg; (void)ctx; 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++; } /* ===================== 假时钟 / 定值 / 看门狗 / 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; 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; } 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_pts_init = 0; g_clock_val = 1700000000u; 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_SERV_PORT)) && (g_n.last_bind_addr == NET_INADDR_ANY), "TC-I104SRV-001 build the listener: port 2404 through htons, bind to any address, remember the listen handle"); 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"); } /* ===================== 代次重建与 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; }