94b0d79026
本笔含两组收尾工作(同一工作区内交错在 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 字),随该笔入库。
657 lines
26 KiB
C
657 lines
26 KiB
C
/*
|
||
* 模块名称: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;
|
||
}
|