b8c6a3dab0
BREAKING CHANGE: 删除公开 API net_relisten / net_resync_listen / net_can_accept 与消息类型 NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN。 生产调用者为零(仅看门狗文档与 iec104 门控),应用侧无迁移成本; 如有外部消费者须改用对账/pump 自动机制(见下)。 1. 对账①扩展为①②⑤(F-NET-2 缺口,先补缺口再删 API) net_reconcile_tick 新增 bad=5:本层 ESTABLISHED 且芯片 0x05/0x01 (Socket OPEN + TCP LISTEN,静默回收回监听、不发 DISCONNECT) → RESYNC 语义只修本层(state→LISTENING + last_peer_port 转登 + lost_conn_seen 清零 + 取消挂起 recv),**不发任何芯片命令** (纪律同 pump③ U11,run8 实证重下发会 170s 永拒)。 原判据①要求 TCP==CLOSED、②要求本层 LISTENING,均不命中此形态 ——此前生产无人认领(仅手动 API 能修)。 2. 删 net_relisten(net_socket.c 函数 + 消息分派 + 头文件声明) footgun:对健康监听器无条件 open→USE→close 自毁 ~1s(原注释自认); 与 pump(100ms)/D10(30ms) 完全重叠。 3. 删 net_resync_listen(同上;语义并入第 1 条) 4. 删 net_can_accept(net_socket.c + net_socket.h + sim 三处) 只认 ESTABLISHED 的旧门控把"监听器在服役、待 CONNECT"窗口全挡掉, :2404 丢 CONNECT 探针因此从不运行;iec104 门控迁 net_listener_armed。 5. timeSync :8888 会话收尾不再 net_close(同 fd 续 accept,监听跨会话 常驻;原 close→重建每会话一个 Trap 09 沉降窗)。server 脚本注释同步 新契约(PC 侧 2s 宽限后自行关闭)。 6. lftpd 应用层阻塞重试退役:lftpd_inet_listen 5 轮(500ms/2000ms,最坏 10s 阻塞 ftpTask) → 单次 + 失败即 425;cmd_pasv 3×200ms 同删。 就绪屏障已消除 Trap 09 启动形态,客户端自带传输重试语义。 台架实证修正(本提交内二次修复,务必留痕): - net_listener_armed 补齐 **ESTABLISHED 放行**。首版迁移只放行 LISTENING/CLOSED+残留,漏了 ESTABLISHED=单连接模型的"待 accept"电平 → iec104 200ms 任务周期抢不过对账② 的 ~400ms 去抖,对账②先 standalone accept 转 ESTABLISHED 后 armed=0 → 应用整场不 accept,18/18 检查全 FAIL (设备日志证据:sock1 standalone accept 后无 i104 accepted,直至对端 FIN 才由 closed-with-data 救回)。补放行后 18/18 PASS,且耗时 127s→8.3s。 该态 accept 必返 fd,无 RPC 风暴风险。 - 期间顺带验证 M3 无辜:sock1 suspect DISCONNECT on live conn 命中后 defer→对账①→closed-with-data accept 把未 accept 的 RX 数据救回, 正是 M3 设计意图(资产门保护)。 验证(全绿):@build 0/0(Code=209580;含 CH395F 套件+Phase13 全量重编 0/0 的编译验证);@test PASS(回读无差异);iec104 18/18(8.3s); n30 0/50;:21 @0ms 50/50 p50=512;:7003 50/50 p50=517;ftp_regression 11/11;Trap23 RST 5/5 + FIN 5/5;time_sync :8888 连续推送正常(RTC 更新、 无 [ERR])。文档同步:BSD 指南(§3.3/3.6/10.1/10.2/12 + 变更记录)、 函数调用关系、监听生命周期设计说明、IEC104 上行设计说明、模拟器差距 清单、链路延迟测试说明。 门禁说明:doc-links 工作树 junction 伪影按既定口径 SKIP;mem-budget 分支 .map 漂移同前;cppcheck/coding-standard 手跑 Passed。
327 lines
12 KiB
C
327 lines
12 KiB
C
/*
|
||
* 模块名称:IEC 60870-5-104 被控站服务(板端胶水任务)
|
||
* 模块功能:2404 监听 + 单连接 socket 收发 + 把字节流喂给 iec104_link 纯逻辑
|
||
* 层;对时经 sys_clock_set_unix 落墙钟;数据源走 iec104_pts 默认源
|
||
* (phasor / prot_elm / prot_set)。协议语义全部在 App/protocol/iec104,
|
||
* 本文件只做 I/O 编排(A-4 约束 1)。
|
||
* 线程约定:iec104Task 独占(NET_READY 屏障后循环,100ms 节拍 + 看门狗心跳)。
|
||
* 创建日期:2026-10-12(B4)
|
||
*/
|
||
#include "iec104_serv.h"
|
||
|
||
#define DBG_LINE_BUF_MAX 128U /* 深链模块日志缓冲收紧(time_sync 先例) */
|
||
#define DBG_TAG "[I104]"
|
||
|
||
#include "iec104_link.h"
|
||
#include "iec104_pts.h"
|
||
#include "net_socket.h"
|
||
#include "prot_set.h" /* 定值脏标志 peek(B7 落盘通知) */
|
||
#include "sys_clock.h"
|
||
#include "sys_events.h"
|
||
#include "task_watchdog.h"
|
||
#include "dbg_log.h"
|
||
|
||
#include "cmsis_os2.h"
|
||
|
||
#include <string.h>
|
||
|
||
/* 服务周期:COV 延迟 ≤100ms(远小于 SOE 5000ms 验收线);心跳间隔同值 */
|
||
#define IEC104_POLL_MS 100u
|
||
/* 接收搬运缓冲(静态,不入栈;link 层内部另有 512B 重组缓冲) */
|
||
#define IEC104_RX_SZ 128u
|
||
/* 兜底节拍:门控判"无数据"时每 32 轮无条件收一次(ui_net_sink RM 先例,
|
||
* 防 RECV_OK 中断丢失把数据永久藏住);须为 2 的幂 */
|
||
#define IEC104_LIVENESS_DIV 32u
|
||
/* 连续发送失败阈值:同 ui_net_sink E-04(短写=对端收坏帧) */
|
||
#define IEC104_SEND_FAIL_MAX 3u
|
||
|
||
/* ---------------- 服务状态(iec104Task 独占) ---------------- */
|
||
static iec104_link_t s_link;
|
||
static int s_listen = -1;
|
||
static int s_client = -1; /* 单连接模型:MCU 上即 s_listen */
|
||
static uint32_t s_gen; /* 网络代次(重建自愈,ui_net_sink 先例) */
|
||
static uint8_t s_rx[IEC104_RX_SZ];
|
||
static uint8_t s_liveness_ctr;
|
||
static uint8_t s_send_fail_run;
|
||
static uint8_t s_conn_dead; /* 发送连败 → 本轮 link 调用返回后拆链 */
|
||
|
||
/* 前向声明(cfg 初始化引用) */
|
||
static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len);
|
||
static int s_clock_sync(void *ctx, uint64_t epoch_ms);
|
||
static uint64_t s_epoch_ms(void *ctx);
|
||
|
||
/* V0.1 公共地址固定 1(点表口径;可配置项挂二期) */
|
||
static const iec104_link_cfg_t s_cfg = { 1u, s_send_apdu, s_clock_sync, s_epoch_ms };
|
||
|
||
/*
|
||
* 函数功能:link 层发送回调——经 net_send 串行 RPC 发出一个 APDU
|
||
* 入口参数:ctx 未用;apdu/len = 完整 APCI 帧
|
||
* 返回值:无
|
||
* 限定条件:s_client >= 0(在 link 调用栈内执行,即本任务上下文)
|
||
* 函数说明:短写/失败累计(E-04 先例);连续 3 次 → 置 s_conn_dead,
|
||
* 由 s_service_link 在 link 调用返回后统一拆链(严禁在回调里拆)
|
||
*/
|
||
static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len) {
|
||
int ret;
|
||
|
||
(void)ctx;
|
||
if (s_client < 0) {
|
||
return;
|
||
}
|
||
ret = net_send(s_client, apdu, (int)len, 0);
|
||
if (ret == (int)len) {
|
||
s_send_fail_run = 0u;
|
||
return;
|
||
}
|
||
if (s_send_fail_run < 255u) {
|
||
s_send_fail_run++;
|
||
}
|
||
DBG_ERROR("i104 send short: ret=%d/%u run=%u", ret, (unsigned)len,
|
||
(unsigned)s_send_fail_run);
|
||
if (s_send_fail_run >= IEC104_SEND_FAIL_MAX) {
|
||
s_conn_dead = 1u;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:link 层对时回调——epoch 毫秒落系统墙钟(含 SD2506 RTC 写回)
|
||
* 入口参数:ctx 未用;epoch_ms = C_CS 解析出的 UTC 时刻
|
||
* 返回值:0 成功 / -1 失败(link 层回 actcon+PN)
|
||
* 函数说明:uint32 秒域上限 2106-02-07,越界显式拒绝(CP56 域到 2127)
|
||
*/
|
||
static int s_clock_sync(void *ctx, uint64_t epoch_ms) {
|
||
uint64_t secs = epoch_ms / 1000ULL;
|
||
|
||
(void)ctx;
|
||
if (secs > 0xFFFFFFFFULL) {
|
||
return -1;
|
||
}
|
||
return sys_clock_set_unix((uint32_t)secs);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:link 层时标源——当前墙钟 epoch 毫秒(B9:M_SP_TB_1 自发用)
|
||
* 入口参数:ctx 未用
|
||
* 返回值:epoch 毫秒;墙钟未建(UINT32_MAX)→ UINT64_MAX(link 置 CP56 IV)
|
||
* 函数说明:sys_clock 秒域时基 → ms 字段恒 0(不造假亚秒,B9 口径);
|
||
* 纯 RAM 推算,无 I/O、无副作用。
|
||
*/
|
||
static uint64_t s_epoch_ms(void *ctx) {
|
||
uint32_t s = sys_clock_get();
|
||
|
||
(void)ctx;
|
||
if (s == UINT32_MAX) {
|
||
return UINT64_MAX;
|
||
}
|
||
return (uint64_t)s * 1000ULL;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:创建并监听 2404(socket+bind+listen,抄 ui_net_sink bind_listen)
|
||
* 返回值:0 成功(s_listen 写回)/ -1 失败
|
||
* 限定条件:net 子系统可用(NET_READY 屏障后)
|
||
*/
|
||
static int s_bind_listen(void) {
|
||
int ls;
|
||
struct net_sockaddr_in addr;
|
||
|
||
ls = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||
if (ls < 0) {
|
||
return -1;
|
||
}
|
||
(void)memset(&addr, 0, sizeof(addr));
|
||
addr.sin_family = NET_AF_INET;
|
||
addr.sin_port = net_htons(IEC104_SERV_PORT);
|
||
addr.sin_addr.s_addr = NET_INADDR_ANY;
|
||
if (net_bind(ls, (struct net_sockaddr *)&addr, (int)sizeof(addr)) < 0) {
|
||
net_close(ls);
|
||
return -1;
|
||
}
|
||
if (net_listen(ls) < 0) {
|
||
net_close(ls);
|
||
return -1;
|
||
}
|
||
s_listen = ls;
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:主动断开当前 104 连接并重建监听(协议故障/t1 超时/发送连败)
|
||
* 入口参数:why = 日志原因串(ASCII)
|
||
* 返回值:无
|
||
* 函数说明:与 EOF 路径不同——协议违例时主站不会自行断开,必须本端 close
|
||
* 促其重连;单连接模型下 client 即 listen,close 后统一重建
|
||
* (time_sync 会话收尾先例,非芯片侧 auto_relisten 风暴路径)
|
||
*/
|
||
static void s_drop_conn(const char *why) {
|
||
DBG_ERROR("i104 drop: %s", why);
|
||
if (s_client >= 0) {
|
||
net_close(s_client);
|
||
}
|
||
s_client = -1;
|
||
s_listen = -1;
|
||
iec104_link_close(&s_link);
|
||
s_send_fail_run = 0u;
|
||
s_conn_dead = 0u;
|
||
s_liveness_ctr = 0u;
|
||
if (s_bind_listen() < 0) {
|
||
s_listen = -1; /* 主循环下轮重试 */
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:link 层节拍服务 + 故障检测(poll 后查 IDLE 跳变/发送连败)
|
||
* 入口参数:now = 注入时基(tick ms)
|
||
* 返回值:无
|
||
*/
|
||
static void s_service_link(uint32_t now) {
|
||
iec104_link_poll(&s_link, now);
|
||
if (s_conn_dead != 0u) {
|
||
s_drop_conn("send fail");
|
||
return;
|
||
}
|
||
if (iec104_link_state(&s_link) == IEC104_ST_IDLE) {
|
||
s_drop_conn("protocol fault");
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:单次服务循环(代次自愈 → accept → 门控收 → 喂 link → 节拍)
|
||
* 返回值:无
|
||
* 限定条件:NET_READY 已置位;s_cfg/s_link 已 init
|
||
* 函数说明:门控(net_listener_armed/net_can_read 纯 RAM 读)+ 兜底节拍照
|
||
* ui_net_sink I-1/I-2;EOF 时 client==listen 严禁 close(net 层
|
||
* 已 auto_relisten,close 会连监听一起毁)
|
||
*/
|
||
static void s_serv_poll(void) {
|
||
uint32_t now = osKernelGetTickCount();
|
||
int n;
|
||
|
||
/* 网络重建(代次变化):旧监听/客户端随芯片复位失效 → 重建 */
|
||
if (net_get_generation() != s_gen) {
|
||
if ((s_client >= 0) && (s_client != s_listen)) {
|
||
net_close(s_client);
|
||
}
|
||
if (s_listen >= 0) {
|
||
net_close(s_listen);
|
||
s_listen = -1;
|
||
}
|
||
s_client = -1;
|
||
iec104_link_close(&s_link);
|
||
s_send_fail_run = 0u;
|
||
s_conn_dead = 0u;
|
||
s_liveness_ctr = 0u;
|
||
s_gen = net_get_generation();
|
||
(void)s_bind_listen();
|
||
return;
|
||
}
|
||
|
||
if (s_listen < 0) {
|
||
(void)s_bind_listen(); /* 启动失败/被毁:重试 */
|
||
return;
|
||
}
|
||
|
||
if (s_client < 0) {
|
||
int c;
|
||
|
||
/* N34 阶段1(F-NET-4):门控从 net_can_accept(只认 ESTABLISHED,已退役)
|
||
* 迁 net_listener_armed——旧门控把 LISTENING 窗口全挡掉,:2404 的丢
|
||
* CONNECT 探针(net_accept_locked 内)因此从不运行(与已修的 :7003
|
||
* 同型);armed=ESTABLISHED 待 accept / LISTENING /
|
||
* CLOSED+auto_relisten+残留(ESTABLISHED 放行为 2026-10-13 台架补齐:
|
||
* 漏它则本任务 200ms 周期抢不过对账② ~400ms 去抖、整场饿死) */
|
||
if (net_listener_armed(s_listen) == 0) {
|
||
if (net_get_sock(s_listen) == NULL) {
|
||
s_listen = -1; /* 控制块消失:交回上方重建 */
|
||
}
|
||
return;
|
||
}
|
||
c = net_accept(s_listen, NULL, NULL);
|
||
if (c >= 0) {
|
||
s_client = c;
|
||
s_send_fail_run = 0u;
|
||
s_conn_dead = 0u;
|
||
s_liveness_ctr = 0u;
|
||
iec104_link_open(&s_link, now);
|
||
DBG_INFO("i104: accepted fd=%d", c);
|
||
}
|
||
return;
|
||
}
|
||
|
||
/* 门控 + 兜底:无数据多数轮直接走 link 节拍;每 32 轮无条件收一次 */
|
||
if (net_can_read(s_client) == 0) {
|
||
if (((++s_liveness_ctr) & (IEC104_LIVENESS_DIV - 1u)) != 0u) {
|
||
s_service_link(now);
|
||
return;
|
||
}
|
||
} else {
|
||
s_liveness_ctr = 0u;
|
||
}
|
||
|
||
n = net_recv(s_client, s_rx, (int)sizeof(s_rx), NET_MSG_DONTWAIT);
|
||
if (n > 0) {
|
||
iec104_link_on_rx(&s_link, s_rx, (uint16_t)n, now);
|
||
} else if (n == 0) {
|
||
/* 对端关闭:net 层已 auto_relisten;client==listen 时严禁 close */
|
||
if (s_client != s_listen) {
|
||
net_close(s_client); /* 模拟器多连接后端:数据通道独立 */
|
||
}
|
||
s_client = -1;
|
||
s_liveness_ctr = 0u;
|
||
iec104_link_close(&s_link);
|
||
if ((net_get_sock(s_listen) == NULL) && (s_bind_listen() < 0)) {
|
||
s_listen = -1;
|
||
}
|
||
return;
|
||
} else if (net_get_errno() != NET_ERR_WOULDBLOCK) {
|
||
s_drop_conn("recv err");
|
||
return;
|
||
}
|
||
|
||
s_service_link(now);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:iec104Task 任务体(freertos.c USER CODE 区创建)
|
||
* 入口参数:argument 未用
|
||
* 返回值:无(不返回)
|
||
* 函数说明:NET_READY 屏障(100ms 分片 + 每片喂心跳,timeSyncTask 先例)→
|
||
* 点表/链路 init + 建监听 → 100ms 服务循环(每轮喂看门狗)
|
||
*/
|
||
void iec104_serv_task(void *argument) {
|
||
(void)argument;
|
||
|
||
if (sys_evtHandle != NULL) {
|
||
for (;;) {
|
||
uint32_t r = osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
|
||
osFlagsWaitAny | osFlagsNoClear, 100u);
|
||
if ((r & osFlagsError) == 0u) {
|
||
break; /* 就绪位是状态位(NoClear),不消费 */
|
||
}
|
||
task_wd_beat(TASK_WD_IEC104);
|
||
}
|
||
}
|
||
|
||
iec104_pts_init();
|
||
iec104_link_init(&s_link, &s_cfg, NULL);
|
||
s_gen = net_get_generation();
|
||
if (s_bind_listen() < 0) {
|
||
DBG_ERROR("i104: bind port %u FAIL, retry in loop",
|
||
(unsigned)IEC104_SERV_PORT);
|
||
} else {
|
||
DBG_INFO("i104: listening on %u", (unsigned)IEC104_SERV_PORT);
|
||
}
|
||
|
||
for (;;) {
|
||
task_wd_beat(TASK_WD_IEC104);
|
||
s_serv_poll();
|
||
/* 定值改动(C_SE 执行段 / YK12)→ 通知属主任务落盘 /prot_set.bin(B7)。
|
||
* peek 非消费:写盘失败时脏标志仍在,下一轮再投递 = 天然重试;
|
||
* 本任务不碰文件系统(配置引擎属主 = defaultTask,模块约定) */
|
||
if ((prot_set_dirty_peek() != 0u) && (sys_evtHandle != NULL)) {
|
||
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_PROT_SET_SAVE);
|
||
}
|
||
osDelay(IEC104_POLL_MS);
|
||
}
|
||
}
|