Files
STM32F4-Base/Drivers/BSP/NET/net_socket.h
T
super_agent_bot a4b8bee209
CI / 静态门禁 (pre-commit) (push) Has been cancelled
CI / 主机侧单测 (MSVC) (push) Has been cancelled
CI / 静态门禁 (pre-commit) (pull_request) Has been cancelled
CI / 主机侧单测 (MSVC) (pull_request) Has been cancelled
feat(net/udp): UDP 数据面并入 netMsgQueue 串行执行(#73)
前置来自 #57 SNTP 调研:旧 net_sendto / net_recvfrom 由调用方直调
SPI 且无限自旋收包,既违约「netTask 是芯片唯一访问者」,又把 IWDG
预算吃在自旋里。

改造面
- 新增 Drivers/BSP/NET/net_udp.c/.h(真机专属,模拟器不编译):
  bind/set_peer/sendto/recvfrom 四条执行体 + 挂起收包表 + tick;
  net_msg_type_t 加四型,公开 API 一律「入队 → 睡在通知上」。
- 源地址出参不搭 net_msg_t(按值拷贝,AGENTS 禁加回传字段):改住
  net_udp.c 的 per-fd 槽,netTask 就地写回调用方内存,通知值只带
  结果码;每条出口都清槽,泄漏 = 该 fd 永久 BUSY。
- 发送复用 net_send_locked,不写第二份分块逻辑;「会话是否已死」
  收成一条判据 net_send_state_down()(TCP 看 ESTABLISHED、UDP 看
  UDP_OPEN),原先 4 处各写一遍。
- 顺带修掉真缺陷(新条 TRAP-NET-22):旧 sendto 把 DesIP 写成具体
  IP,芯片静默切 UDP Client,而旧 recvfrom 无条件剥 8 字节 → SNTP
  这类 48 B 应答丢载荷前 8 字节并交出垃圾源地址。加 udp_mode(塞进
  原有 padding,sizeof 不变)、bind 恒 SERVER、显式对端 sendto 切
  CLIENT、set_peer(fd, NULL) 轻量回 SERVER,仅 SERVER 且 avail >= 8
  才剥头。
- 截断定案:一次调用交付一个数据报,buf 装不下的尾巴用 64 B 栈上
  scratch 排空(不用 clear_recv_buf——它连后面已到达的包一起丢)。
- 取消点接齐:net_init 重建 cancel_all、net_socket 分配 fd clear、
  net_close settle_close + clear、重 bind 前先结算等包者,
  net_process_messages 第 5 个 tick 位挂 net_udp_tick。
- 收紧两处契约(原实现无生产调用者):UDP 未 bind 一律 -5;
  net_recv / _nb / _to 拒 DGRAM(绕过剥头就是静默吃 8 字节);
  NET_ERR_NOT_IN_NETTASK(-12) 退役为历史常量并写明原因。

验证
- 主机单测 test/host/net_socket 全套 616/616 绿(改造前 470):新
  分片 ns_s7_udp.c 20 组,改写 440 / 555 / 560 组;覆盖率
  net_udp.c 234/236(99.2%)、net_socket.c 956/969(98.7%),
  scripts/test_coverage_baseline.json 随本提交登记新文件。
- 测试抓到 4 个被测码真 bug:自等守卫误用调用侧谓词、芯片长度被读
  两次、槽指针取在 fd 合法性检查之前、重 bind 静默作废等待者。
- Keil ARMCC 0 Error(s) 0 Warning(s);net_udp.o Code 1,358 / ZI 224
  (8 fd × 28 B,其余模块零增长),全仓 RW_IRAM1 72.2%;内存台账
  受管区按门禁提示用 --write / --update 机器回填,未手改数字。
- pre-commit 全量钩子(含 doc-links、coding-standard、mem-budget、
  generated-fresh、bsp-err、registration、nested-comment)全绿。

限度
- 本轮无板子:真机回归 5 条(两端互通、set_peer(NULL) 能否恢复收
  广播、截断排空后下一包完整性、挂起 recv 期间 IWDG 余量、bind 写
  DesIP 与 open 先后的真芯片效果)已写进覆盖率口径「台架必验」。
- scripts/stack_report.py 退出码 1 是 10-26 起挂在账上的 iec104Task
  栈债(margin 1,016 < 1,024,见 docs/代码规范/pre-commit使用说明.md:
  122);本轮只让 net_bind 的 UDP 分支多一层静态帧(1,544 → 1,600),
  netTask margin 1,888 与 timeSyncTask 1,152 不受影响。抬栈要过
  mem-budget 台账,不在本分支自行处理。
- 登记簿销项按仓库约定留给主工作树,本分支不动 docs/任务规划/**。
2026-11-03 18:08:27 +08:00

775 lines
42 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.
/*
* 模块名称:Network Socket API
* 模块功能:BSD Socket API 兼容接口,提供 TCP/UDP 网络通信功能,
* 底层基于 CH395F 以太网协议栈芯片
* 适用平台:STM32F4 系列(CH395F 以太网芯片)
* 作者:王建锋
* 创建日期:2026-07-18
* 修改记录:
*/
#ifndef NET_SOCKET_H
#define NET_SOCKET_H
#ifdef __cplusplus
extern "C" {
#endif
#include "net_types.h"
/*
* OS 相关接口(FreeRTOS 消息队列 / 任务句柄 / netTask 内部 API)仅在启用
* RTOS 的构建(MCU)编译;纯 PC 模拟器定义 NET_NO_OS 后只保留 BSD Socket 核心 API。
*/
#ifndef NET_NO_OS
#include "FreeRTOS.h"
#include "task.h"
#include "cmsis_os2.h" /* osMessageQueueId_t:net_msg_queue_adopt() 的参数类型 */
#endif /* NET_NO_OS */
#ifndef NET_NO_OS
/*
* 消息队列类型定义
*/
typedef enum {
NET_MSG_SEND,
NET_MSG_RECV,
NET_MSG_CLOSE,
NET_MSG_CONNECT,
NET_MSG_LISTEN,
NET_MSG_ACCEPT,
NET_MSG_DROP_CONN, /* 主动拆会话、保监听(见 net_drop_conn) */
/* UDP 队列化改造(2026-11-03,工单前置 = SNTP 客户端 #57):UDP 三个碰芯片的
* 动作从"调用方上下文直调 SPI"改为交 netTask 串行执行,字段用法:
* NET_MSG_UDP_BIND - sockfd + len=本地端口(主机序,0 = 执行体内取动态号)
* 恒 DesIP=0xFFFFFFFF 服务器模式(bind 不选对端,
* 客户端模式见 NET_MSG_UDP_PEER / SENDTO 的 addr)
* NET_MSG_UDP_SENDTO - buf/len=载荷 + addr=本包目的地址(按值拷入,**入参**方向)
* NET_MSG_UDP_RECVFROM - buf/len=载荷 + timeout_ms=等待预算 + flags
* ⚠ 源地址出参(src_addr / addrlen 两个指针)**不走本结构**:结构按值拷入
* 队列,指针若在 int 字段里过桥,64 位主机单测构建会被截断,且违反
* AGENTS「勿给 net_msg_t 加回传用字段」。改走 net_udp.c 的 per-fd 挂起槽
* (调用方在入队前登记自己的指针,netTask 完成后写进调用方内存 + 通知值回码),
* 与 TCP 挂起 recv 把数据直接写进调用方 buf 是同一个模型。
* 详见 docs/网络/BSD_Socket_API_使用指南.md §6 */
NET_MSG_UDP_BIND,
NET_MSG_UDP_SENDTO,
NET_MSG_UDP_RECVFROM,
/* 静默回收认领 + 手动自愈 API 退役(docs/网络/网络自愈架构设计.md §4.1、§4.2):
* NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN 已删——
* 静默回收由对账⑤自动认领(net_reconcile_tick),重建由 reconciler
* 形态③④/D10 单一
* 所有者负责;手动自愈 API 面退役(生产调用者本就为零) */
/* UDP 模式切换(追加在枚举末尾:既有成员编号是主机单测/板测的钉值对象,
* 不在中间插入)——addr 按值携带对端则切客户端模式,addrlen == 0 则回
* 服务器模式;不重开 Socket,只写 DesIP/DesPort,语义见 net_udp_set_peer */
NET_MSG_UDP_PEER
} net_msg_type_t;
typedef struct {
net_msg_type_t type; /* 操作类型 */
int sockfd; /* Socket 描述符 */
void *buf; /* 数据缓冲区(send/recv) */
int len; /* 数据长度/缓冲区大小 */
int flags; /* 接收标志 */
uint32_t timeout_ms; /* 接收超时(入队方向;0 = 无限等待,无默认预算——
* NET_RECV_TIMEOUT_MS 30s 默认已删除) */
TaskHandle_t caller; /* 调用方任务句柄 */
int result; /* 操作结果 */
/* 连接参数 */
struct net_sockaddr_in addr; /* 连接地址 */
int addrlen; /* 地址长度 */
} net_msg_t;
#endif /* NET_NO_OS */
#ifndef NET_NO_OS
/*
* 函数功能:把消息队列句柄交给本模块持有(**句柄的唯一使用者是 net_socket.c**)
* 入口参数:queue_id - 已创建的消息队列句柄
* 返回值:0 = 绑定成功;-1 = 句柄为空、或已绑定过(重复绑定保留首次句柄,不覆盖)
* 限定条件:必须在 osKernelInitialize() 之后、netTask 启动之前调用一次
* 函数说明:队列对象本身仍由 CubeMX 在 MX_FREERTOS_Init() 里创建——创建点不能外移,
* 因为 osKernelInitialize() 之前任何创建类 API 都会杀死启动
* (docs/架构设计/项目启动顺序管理.md)。外移创建点还会让重新生成把它抹掉。
* 所以这里用"创建后立刻绑定":调用点见 Src/freertos.c 的
* USER CODE BEGIN RTOS_QUEUES 区。此前本模块靠对本文件外全局变量的
* 裸 extern 取用,谁改名或删掉创建语句都不会有提示。
*/
int net_msg_queue_adopt(osMessageQueueId_t queue_id);
#endif /* NET_NO_OS */
/*
* 网络初始化接口
*/
/*
* 函数功能:网络子系统初始化(配置 CH395F + 启用多连接模式)
* 入口参数:ip - 本地 IP 地址字符串(点分十进制),NULL 使用 DHCP
* mask - 子网掩码字符串,NULL 使用默认
* gateway - 网关地址字符串,NULL 使用默认
* mac - MAC 地址(6 字节,低字节在前),NULL 或全零使用芯片内置
* 返回值:0 成功,-1 失败
* 限定条件:SPI2 已正确初始化
* 函数说明:必须在所有网络操作之前调用;mac 非空且非全零时在协议栈初始化
* (CMD_INIT_CH395)之前下发(手册 §9.2.1 步骤①:SET_MAC 写芯片 EEPROM,
* INIT_CH395 加载 MAC——顺序反了则本次上电不生效,回落芯片内置/上次值)
*/
int net_init(const char *ip, const char *mask, const char *gateway, const uint8_t *mac);
/*
* 函数功能:取网络代次号
* 入口参数:无
* 返回值:代次号(每次网络重建完成递增) uint32_t
* 限定条件:无
* 函数说明:TCP 服务(FTP / 对时 / 远程显示)据此感知"网络已重建"——代次变化
* 表示旧 Socket 已随芯片复位失效,服务应放弃旧监听并重建。
* 递增点在 net_stack_bring_up() 末尾(PHY/链路就绪后),避免服务在网络
* 尚未可用时抢先重建、也避免 uiTask 阻塞在消息队列上。
*/
uint32_t net_get_generation(void);
/*
* 函数功能:递增网络代次号(网络重建完成、对外可用时调用)
* 入口参数:无
* 返回值:无
* 限定条件:net_init() 成功且 PHY/链路就绪后调用(当前仅 net_stack_bring_up)
* 函数说明:见 net_get_generation()
*/
void net_generation_inc(void);
/*
* 函数功能:轮询所有 Socket 状态(必须在主循环中调用)
* 返回值:有事件发生的 socket 数量
* 限定条件:net_init() 已调用
* 函数说明:此函数处理所有 Socket 的中断事件,更新状态,
* 并触发事件回调。在非阻塞模式下必须定期调用。
*/
int net_poll(void);
/*
* 核心 Socket API
*/
/*
* 函数功能:创建 socket
* 入口参数:domain - 地址族,仅支持 AF_INET
* type - SOCK_STREAM(TCP) 或 SOCK_DGRAM(UDP)
* 返回值:socket 描述符 (0~7),失败返回 -1
* 限定条件:net_init() 已调用
* 函数说明:自动分配空闲 Socket 控制块
*/
int net_socket(int domain, int type);
/*
* 函数功能:绑定本地地址和端口
* 入口参数:sockfd - socket 描述符
* addr - 本地地址结构体指针
* addrlen - 地址结构体长度
* 返回值:0 成功,-1 失败
* 限定条件:socket 已创建且未绑定
* 函数说明:设置 CH395F 源端口
*/
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen);
/*
* 函数功能:TCP Server 监听
* 入口参数:sockfd - socket 描述符
* 返回值:0 成功,-1 失败
* 限定条件:socket 已绑定
* 函数说明:内部调用 ch395f_tcp_listen,监听 Socket 自身即为数据通道
*/
int net_listen(int sockfd);
/*
* 函数功能:TCP Server 接受连接
* 入口参数:sockfd - 监听 socket 描述符
* addr - 输出:客户端地址
* addrlen - 输入输出:地址长度
* 返回值:新 socket 描述符,失败返回 -1
* 限定条件:socket 正在监听
* 函数说明:在非阻塞模式下,如果没有新连接,返回 -1 并设置
* net_errno 为 NET_ERR_WOULDBLOCK
*/
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen);
/* accept 门控迁移(docs/网络/网络自愈架构设计.md §4.2):net_can_accept 已退役——只认 ESTABLISHED 的旧门控会把
* "监听器在服役、本轮待 CONNECT"窗口全挡掉(CONNECT 事件补 accept 探针因此从不运行);
* 监听 accept 门控统一用 net_listener_armed(),recv 门控 net_can_read() 保留。 */
/*
* 函数功能:查询监听 Socket 是否处于"应该尝试 accept"的就绪态(**纯 RAM 读,不发起 RPC**)
* 入口参数:sockfd - 监听 Socket 描述符 int
* 出口参数:无
* 返回值:1 - 就绪(ESTABLISHED 待 accept / LISTENING / CLOSED+auto_relisten
* +残留数据);0 - 不就绪 int
* 限定条件:net_init() 已调用;任意任务上下文
* 函数说明:1. 不变量 II(单一重建所有者;补齐 ESTABLISHED 放行):取代已退役的
* `net_can_accept()`(只认 ESTABLISHED)作为监听 accept 门控——原门控使
* :7003 永不进 `net_accept()`,
* `net_lost_connect_probe`(CONNECT 事件未收到补 accept,TRAP-NET-20 实证的
* "数据被吞/425"主根因修复)对该端口等于从未运行
* 2. 放行 LISTENING:探针与 "closed-but-readable 接受" 都发生在
* `net_accept_locked` **内部**,门控挡在外面就永远到不了
* 2b. 放行 ESTABLISHED(i104 台架实证):单连接模型下 CONNECT 处理
* (net_poll 分派 / 探针补 accept / 对账② standalone accept)把监听 fd
* 转 ESTABLISHED,这正是"待 accept"电平——漏放行则任务周期慢的消费者
* (i104 200ms 抢不过对账② ~400ms 去抖)永不 accept、会话整场饿死
* (实测全 18 项 FAIL)。此态 accept 必返 fd,无风暴风险
* 3. 放行 CLOSED+auto_relisten **且 readable≠0**(有残留数据):
* 保住 TRAP-NET-20 的 "closed-but-readable" 接受;**无残留时必须返回 0**,
* 否则调用方会在整个 CLOSED 窗口内每轮发起 `net_accept`(RPC 无上界)
* → 队列积压 → 喂不上狗(2026-10-12 P2a 台架实测 48 条 task stall)。
* 重建由 `net_reconcile_tick` 形态③④ 负责,调用方无需催促
* 4. 与已退役 `net_can_accept` 的关系:本函数 = 其 ESTABLISHED 放行态
* ∪ LISTENING ∪ CLOSED+残留;回答"这个监听器这一轮**值不值得**发起
* 一次 accept",而非"一定有连接"
* 5. 跨任务读单字,同 `net_can_read` 的最坏情形论证
*/
int net_listener_armed(int sockfd);
/*
* 函数功能:查询 Socket 是否"值得发一次 recv"(**纯 RAM 读,不发起 RPC**)
* 入口参数:sockfd - Socket 描述符 int
* 出口参数:无
* 返回值:1 - 应发 recv(有数据可读,或连接已非 ESTABLISHED 需发 recv 取回 EOF/错误);0 - 不必发 int
* 限定条件:net_init() 已调用;任意任务上下文
* 函数说明:1. ESTABLISHED → 看 readable(net_poll 在 RECV_OK 置位、读空清零)
* 2. **非 ESTABLISHED 一律返回 1**:连接已消失时必须让上层真的 recv 一次
* 拿到 EOF/错误并自行复位;若把它门掉,"连接已不存在"会被永久藏住
* (推流侧 g_client 永不复位 → 再也不接受新客户端)
* 3. 描述符失效(net_get_sock 返回 NULL)同样返回 1 → 让 recv 报 BADF,
* 走调用方既有的错误分支重建,而不是静默卡死
* 4. ⚠ **不可作为唯一判据**:readable 只由 RECV_OK 中断置位,而
* net_reconcile_tick 的三类对账**不含**"芯片 RX 非空但 readable=0"
* → 事件未收到时本函数会恒返 0。调用方须保留**低频兜底 recv**
* (ui_net_sink 每 32 轮无条件收一次,最坏延迟 ≈320 ms)
*/
int net_can_read(int sockfd);
/*
* 函数功能:TCP Client 发起连接
* 入口参数:sockfd - socket 描述符
* addr - 服务器地址
* addrlen - 地址长度
* 返回值:0 成功,-1 失败
* 限定条件:socket 已创建
* 函数说明:连接过程是异步的,需要调用 net_poll() 等待连接完成,
* 或使用 select() 等待可写事件
*/
int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen);
/*
* 函数功能:发送数据(TCP)
* 入口参数:sockfd - socket 描述符
* buf - 数据缓冲区
* len - 数据长度
* flags - 通常为 0
* 返回值:实际写入字节数——**= len 为全部写入;< len 为短写,调用方必须检查**;
* 失败返回 -1(errno 已置位)
* 限定条件:TCP 连接已建立,指针非空
* 函数说明:1. 快路径:短暂自旋等待发送缓冲区空闲(预算 NET_SEND_SPIN_MS,默认 2ms),
* 拷贝数据后触发发送——健康链路下(实测 219us 量级)行为与旧版逐字节一致
* 2. 慢路径:自旋超预算后把请求**挂起登记**给 netTask,由
* net_send_pending_tick 在发送缓冲区空闲时续传(分片 NET_SEND_CHUNK_MAX),
* 调用方在任务通知上睡眠——**不再占用 netTask 执行者**(消除队头阻塞)
* 3. 短写语义:无论快路径超时还是挂起完成,只要存在未写完字节即按已发数返回,
* 不假装全部成功;调用方按短写重试剩余部分(见 BSD_Socket_API 使用指南 §3.5)
* 4. ⚠ 禁止在 IWDG required_mask 内任务做**无预算**的整块发送;挂起路径本身
* 受 NET_SEND_TIMEOUT_MS 无进展超时兜底,但仍建议调用方自带超时预算
*/
int net_send(int sockfd, const void *buf, int len, int flags);
/*
* 函数功能:接收数据(TCP,**默认无限阻塞**)
* 入口参数:sockfd - socket 描述符
* buf - 接收缓冲区
* len - 缓冲区大小
* flags - 通常为 0;NET_MSG_DONTWAIT 单次强制非阻塞
* 返回值:实际接收字节数,0=对端关闭,-1=错误
* 限定条件:TCP 连接已建立
* 函数说明:1. 无数据时**无限等待**(无默认超时预算,对齐 POSIX/Python:模式归
* 使用者、超时归调用方);唤醒源 = 数据 / EOF / close / 网络重建
* (NET_ERR_CONNRESET)——会话死亡由对账①在 ~400ms 内判死兜底,
* 不存在"对端消失后永久挂死"的窗口
* 2. 模式属性:net_setblocking(fd, 0) 后本接口无数据立即返回
* NET_ERR_WOULDBLOCK;NET_MSG_DONTWAIT 仅单次覆盖该属性
* 3. ⚠ 文档契约(嵌入式特有):IWDG required_mask 内任务**禁止无限
* 阻塞 recv**——必须 net_select 超时或 net_recv_to 显式预算,
* 否则"对端连上不发数据"= IWDG 复位(超时归使用者对受监控任务是义务)
* 4. 超时首选 net_select(r,w,e,timeout_ms);net_recv_to 为显式预算便捷入口
*/
int net_recv(int sockfd, void *buf, int len, int flags);
/*
* 函数功能:接收数据(非阻塞具名入口,原 NET_MSG_DONTWAIT 路径转正)
* 入口参数:sockfd - socket 描述符
* buf - 接收缓冲区
* len - 缓冲区大小
* 返回值:实际接收字节数;0 = 对端关闭(net 层已自动重监听);
* -1 = 错误(无数据 = NET_ERR_WOULDBLOCK,其余 errno 可查)
* 限定条件:非 netTask 上下文(netTask 内用 net_recv_sock + DONTWAIT)
* 函数说明:等价 net_recv(sockfd, buf, len, NET_MSG_DONTWAIT);
* 不改变 socket 的 blocking 属性(模式用 net_setblocking 设置)
*/
int net_recv_nb(int sockfd, void *buf, int len);
/*
* 函数功能:设置 Socket 阻塞/非阻塞**模式属性**(对齐 BSD O_NONBLOCK / Python
* setblocking,模式挂 socket 控制块属性)
* 入口参数:sockfd - socket 描述符
* on - 1 = 阻塞(net_socket 默认);0 = 非阻塞
* 返回值:0 成功;-1 失败(描述符无效/未占用)
* 限定条件:socket 已创建
* 函数说明:1. 属性对 net_recv / net_recv_sock 生效:非阻塞模式下无数据立即
* 返回 NET_ERR_WOULDBLOCK(EOF 判定语义不变:CLOSED/session_dead
* 无数据仍返回 0 并触发自动重监听)
* 2. 直写控制块单字节(先例 net_set_event_cb),任意上下文可调;
* 最坏一次在途 recv 用旧模式,无撕裂风险
* 3. 与超时预算正交:模式是粘滞属性,单次预算用 net_recv_to/net_select
*/
int net_setblocking(int sockfd, int on);
/*
* 函数功能:接收数据(**带调用方指定超时**的阻塞版,用于分片有界读)
* 入口参数:sockfd - Socket 描述符
* buf/len - 接收缓冲与容量
* timeout_ms - 单次调用在 netTask 内的最长等待(毫秒,>0)
* 返回值:>0 实际字节数;0 = 对端关闭(EOF);-1 = 真错误(errno 可查);**-2 = 超时无数据**
* 限定条件:非 netTask 上下文;Socket 已连接
* 函数说明:1. **显式预算**便捷入口(等价 Python settimeout,
* 不再有隐藏默认值):net_recv 的无限阻塞 + 本接口的显式预算构成
* 超时语义的全部来源;超时首选仍是 net_select
* 2. 修复配套:lftpd 等"等对端下一行/下一块数据"的调用方
* 若无限阻塞 recv,会把 netTask(全部 socket 操作的唯一执行者)长期占住
* 3. 用本接口把等待切成 ≤timeout_ms 的片,调用方自行累计总预算:总超时语义不变、单次占用有界
* 4. 超时用独立返回码 -2(而非靠 errno 区分)→ 无跨任务 errno 竞态
* 5. ⚠ IWDG required_mask 内任务必须走本接口或 net_select(见 net_recv 契约)
* 6. 本接口**走消息队列**、任意任务可调用(与 net_recv 同一条路径),
* 与 net_recvfrom(仅限执行任务、直调芯片)不是一回事,勿混
*/
int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms);
/*
* 函数功能:发送 UDP 数据报(**走消息队列**,任意任务可调用)
* 入口参数:sockfd - socket 描述符
* buf - 数据缓冲区
* len - 数据长度(>0)
* flags - 目前不使用,传 0(与 net_send 同)
* dest_addr - 本包目的地址;NULL = 沿用本 Socket 当前 DesIP
* addrlen - 地址长度(NULL 目的时传 0 即可)
* 返回值:实际发送字节数 int > 0
* -1 失败,精确码经本任务错误码槽给出(net_get_errno()):
* NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_TIMEDOUT / NET_ERR_BUSY
* 限定条件:socket 已创建(DGRAM 用法须先 net_bind 或 net_udp_set_peer 打开芯片侧
* Socket);net_init() 已执行;**任意任务上下文**(含 netTask 自身,
* netTask 内直接执行不入队——自投递给自身 = 死锁)
* 函数说明:1. 目的地址是**入参**,按值拷进 net_msg_t.addr 随消息进 netTask,
* 与 net_connect 传地址同一条通道;结果(已发字节数/错误码)
* 搭任务通知值回来(AGENTS「勿给 net_msg_t 加回传字段」)。
* 2. 带显式目的地址 = 芯片 DesIP 切到该 IP → 本 Socket 进入
* **UDP 客户端模式**(p_sock->udp_mode = NET_UDP_MODE_CLIENT):
* 此后芯片只收该 IP 的包且**不再添加 8 字节信息头**,
* net_recvfrom 相应地不剥头、源地址回填该固定对端。
* 要回服务器模式(任意源 + 带信息头)调 net_udp_set_peer(sockfd, NULL)。
* 3. dest_addr=NULL 时不改 DesIP:服务器模式下 DesIP=0xFFFFFFFF 即
* 向 255.255.255.255 广播(板级 UDP 广播测试的用法),模式不变。
* 4. 分块上限 NET_SEND_CHUNK_MAX(1KB 发送 FIFO,TRAP-NET-16),
* 门控 SENBUF_FREE(TRAP-NET-04);单块等待超 NET_SEND_SPIN_MS
* 登记挂起 send、由 net_send_pending_tick 续传 → **netTask 不被
* 一次大报发送占住**(与 TCP 发送同一套机制)。
* 5. 短写:返回值 < len 表示未全部交付(停滞预算 NET_SEND_TIMEOUT_MS
* 用尽),调用方须检查返回值——同 net_send 契约。
* 6. 手册 §9.2.4:UDP 一包最大 1472 字节;本层不做长度上限校验
* (超出只会分多块写、对端收不到完整包),调用方自负。
*/
int net_sendto(int sockfd, const void *buf, int len, int flags,
const struct net_sockaddr *dest_addr, int addrlen);
/*
* 函数功能:接收 UDP 数据报(含源地址;**走消息队列**,任意任务可调用)
* 入口参数:sockfd - socket 描述符
* buf - 接收缓冲区
* len - 缓冲区大小(>0)
* flags - 0 = 按 socket 的 blocking 属性等待;NET_MSG_DONTWAIT = 单次非阻塞
* src_addr - 输出:发送方地址(可为 NULL)
* addrlen - 输入输出:src_addr 结构容量/实际长度(可为 NULL)
* 返回值:实际接收字节数 int >= 0
* -1 失败,精确码经本任务错误码槽给出(net_get_errno()):
* NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_WOULDBLOCK /
* NET_ERR_TIMEDOUT / NET_ERR_CONNRESET(close/重建取消)/ NET_ERR_BUSY
* 限定条件:socket 已创建且为 DGRAM;net_init() 已执行;**任意任务上下文**
* (netTask 内直接执行不入队;netTask 内的阻塞等待被登记守卫拒绝 →
* 须用 NET_MSG_DONTWAIT)
* 函数说明:1. 与 net_recv 同一条路径(入队 → netTask 串行执行芯片事务 →
* 结果搭通知值回来),**不再是**"仅限执行任务、自旋 net_poll"的
* 历史形态;非 netTask 调用不再回 NET_ERR_NOT_IN_NETTASK。
* 2. 一次调用 = 一个数据报:芯片缓冲内该包**全部读空**(服务器模式
* 手册要求一次性读出),载荷超出 len 的部分按 BSD 语义截断丢弃,
* 不会留下半包污染下一次解析。
* 3. 源地址按 udp_mode 取:服务器模式剥 8 字节信息头
* ([2-3] 源端口芯片小端、[4-7] 源 IP 线序字节 → 本层 s_addr 网络序
* 约定,见 net_inet_addr 注释,2026-10-24 定案同族);
* 客户端模式不剥头、源地址=本 Socket 固定对端。
* ⚠ 客户端模式若仍按服务器模式剥头,等于吃掉载荷前 8 字节
* (SNTP 应答 48 字节会前 8 字节丢失)——这是本字段存在的理由。
* 4. 阻塞语义与 net_recv 同构:无数据 + 阻塞 → **登记挂起、netTask
* 零占用返回**,数据到点断连由 net_udp_tick 按 1ms 节拍结束;
* 不再是自旋(旧形态会吃满 IWDG 预算并违反 netTask 契约)。
* 5. ⚠ IWDG required_mask 内任务禁止无限等待形态:用
* net_recvfrom_to() 显式预算或 net_select()(同 net_recv 契约)。
*/
int net_recvfrom(int sockfd, void *buf, int len, int flags,
struct net_sockaddr *src_addr, int *addrlen);
/*
* 函数功能:接收 UDP 数据报(**带调用方指定超时**,对应 net_recv_to 的 UDP 版)
* 入口参数:sockfd - socket 描述符
* buf/len - 接收缓冲与容量
* flags - 目前不使用,传 0(超时由 timeout_ms 表达)
* timeout_ms - 本次等待预算(毫秒,>0)
* src_addr - 输出:发送方地址(可为 NULL)
* addrlen - 输入输出:src_addr 容量/实际长度(可为 NULL)
* 返回值:>0 实际字节数;-1 = 真错误(errno 可查);**-2 = 超时无数据**
* 限定条件:非 netTask 上下文(netTask 内须用 net_recvfrom + NET_MSG_DONTWAIT)
* 函数说明:1. 与 net_recv_to 同一条路径与同一套超时收口:挂起登记带 deadline,
* 到点由 net_udp_tick 以超时结束,对外统一 -2(不靠 errno 区分,
* 无跨任务 errno 竞态)。
* 2. 受 IWDG 监控的任务(含未来的 SNTP/对时任务)**必须**用本接口或
* net_select,不得用 net_recvfrom 的无限等待形态。
* 3. timeout_ms = 0 视为无效参数(无限等待请用 net_recvfrom)。
*/
int net_recvfrom_to(int sockfd, void *buf, int len, uint32_t timeout_ms,
struct net_sockaddr *src_addr, int *addrlen);
/*
* 函数功能:设置/查询 UDP Socket 的芯片模式(客户端 ↔ 服务器,见 net_types.h
* NET_UDP_MODE_* 与手册 §9.2.4)
* 入口参数:sockfd - socket 描述符(须为 DGRAM)
* peer - 非 NULL = 客户端模式:DesIP/DesPort 设为该对端,芯片只收
* 它的包且**不加** 8 字节信息头;
* NULL = 服务器模式:DesIP=0xFFFFFFFF、DesPort=0,收任意来源
* 且每个包带信息头(net_bind 的默认形态)
* 返回值:0 - 成功 int
* -1 - 失败(NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_BUSY,errno 可查)
* 限定条件:socket 已创建且为 DGRAM 且已 net_bind(芯片侧 Socket 已打开);
* 任意任务上下文(走消息队列,netTask 内直接执行)
* 函数说明:1. 存在的理由:模式由"最近一次写 DesIP"决定,net_sendto 带目的地址
* 发一次就把芯片静默切到客户端模式。要给多个对端收发(TFTP 式
* "收到谁的包回给谁、然后继续听所有人")必须在每次带地址发送后
* 显式回服务器模式,否则回程包会被当成带头的包错解析。
* 2. 只改 DesIP/DesPort 两个寄存器,不重开 Socket(不丢接收缓冲内
* 已到的数据);本层 udp_mode 同步更新,recvfrom 剥头判定据此走。
* 3. 客户端 → 服务器切换后**先前**收到的包(无头)无法补上源地址,
* 模式切换时刻之后的包才按新模式解析——协议栈请"先切模式再等包"。
* 4. 切模式不改动 local_port,也不影响已登记的挂起 recv;挂起期间
* 切模式由 tick 在结算时读当前模式解析(同一包不会两解)。
*/
int net_udp_set_peer(int sockfd, const struct net_sockaddr_in *peer);
/*
* 函数功能:查询 UDP Socket 当前芯片模式
* 入口参数:sockfd - socket 描述符
* 返回值:NET_UDP_MODE_SERVER / NET_UDP_MODE_CLIENT uint8_t
* -1 - 描述符无效或非 DGRAM int
* 限定条件:任意上下文(纯 RAM 读,不发起芯片事务)
* 函数说明:诊断/单测/上层"要不要切回服务器模式"的判读入口
*/
int net_udp_get_mode(int sockfd);
/*
* 函数功能:关闭 socket
* 入口参数:sockfd - socket 描述符
* 返回值:0 成功,-1 失败
* 限定条件:socket 已打开
* 函数说明:释放 Socket 控制块,TCP 先断开连接
*/
int net_close(int sockfd);
/*
* 函数功能:主动拆掉当前 TCP 会话,**保留监听**
* 入口参数:sockfd - socket 描述符
* 返回值:0 成功(**监听型 fd 的“成功”= 拆除与重建已登记**,芯片未必已拆完,
* 见函数说明 2;含幂等:无活动会话),-1 失败
* 限定条件:非 netTask 上下文(经消息队列委托,同其它 net_* API)
* 函数说明:1. 适用场景=应用层放弃会话(对时 30s 预算耗尽、协议超时等):
* 单连接模型禁止对监听 fd net_close(连监听一起毁,AGENTS 约定),
* 而挂起但 TCP 仍活的客户端会永久占住单连接监听(PC 内核自动
* ACK keepalive,芯片僵尸判不死——《网络自愈架构设计》§5.2 E13 定案缺口)
* 2. 实现(2026-10-25 探针窗口心跳缺口修复起):auto_relisten fd = 登记 QUARANTINE 隔离
* 沉降 + REQ_DROP 重建请求,**登记即返**——“FIN 关闭 + 排渣 + 排空
* 事件 + 等 byte0=00H”(TRAP-NET-18 异常释放语义)与重建由
* net_listener_tick 按 30ms 节奏异步执行,端口恢复窗 ~0.4-1s 与被动
* 断连同型;**调用方不得按旧语义假设“返回即芯片已 CLOSED”**——返回
* 后 accept/recv 得 WOULDBLOCK 直至重建完成(time_sync 既有轮询天然
* 容忍)。旧同步形态曾使 netTask 占用 400-600ms、五任务心跳同窗
* 缺席声明 stall(《网络自愈架构设计》§4.6 记录,勿复活)。
* 非 auto_relisten(client fd)无监听可保,仍同步排渣(600ms 封顶)
* 3. 幂等:非 ESTABLISHED/CONNECTING 直接返 0,不动监听
*/
int net_drop_conn(int sockfd);
/*
* 事件回调 API
*/
/*
* 函数功能:注册事件回调
* 入口参数:sockfd - socket 描述符
* cb - 回调函数
* arg - 用户参数
* 返回值:0 成功,-1 失败
* 限定条件:socket 已创建
* 函数说明:回调在 net_poll 上下文中触发
*/
int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg);
/*
* 辅助函数
*/
/*
* 函数功能:获取 **本任务** 最后一次 net_*() 调用失败时的精确错误码
* 入口参数:无
* 出口参数:无
* 返回值:错误码(NET_ERR_xxx);本任务尚无失败记录时为 NET_OK(0)
* 限定条件:无(任何上下文可调用,含中断——查询为纯本任务索引读)
* 函数说明:1. **按任务隔离**(2026-09-25):错误码存在每个任务自己的
* 槽位里,任务间互不干扰,不存在跨任务覆写竞态——旧实现的单一全局
* 变量会被 netTask/其他任务的 net 调用交错覆盖,调用方可能读到别人的码
* 2. 语义:本任务最后一次返回 -1 的 net_*() 调用所对应的精确码;
* 调用成功时**不清零**(与本 API 历史行为一致,保持向后兼容)
* 3. 任务句柄槽位用尽时回退到共享槽位(仅影响排障,不影响功能)
* 4. 超时类场景(net_recv_to)请直接用返回码 -2 判定,不要依赖本接口
*/
int net_get_errno(void);
/*
* 函数功能:查询 socket 当前状态(只读快照,可能滞后于 netTask)
* 返回值:状态值(net_sock_state_t);描述符无效/未占用 → -1
*/
int net_sock_state(int sockfd);
/* 手动自愈 API 退役(docs/网络/网络自愈架构设计.md §4.2):net_resync_listen / net_relisten 声明已删——
* 静默回收形态由对账⑤自动认领(2 轮去抖 + 1s 冷却,纪律"不发芯片命令"
* 原样保留在认领动作内);监听重建由 reconciler 形态③④(100ms 周期)/
* D10(30ms 重试)单一所有者。勿再引入"应用层手动自愈"入口。 */
/*
* 函数功能:将 IP 地址字符串转为网络字节序
* 入口参数:cp - 点分十进制字符串
* 返回值:网络字节序 IP,失败返回 0
* 限定条件:cp 指针非空
* 函数说明:不支持域名解析
*
* s_addr 的**唯一约定**(2026-10-24 定案:两套 IP 字节序约定统一为 POSIX 网络序;全仓只此一族,勿再引入第二套):
* 网络字节序整数 —— &s_addr 的第 0 个内存字节 == 点分第一段。
* 小端主机(STM32F407 与 PC 模拟器都是)上 "192.168.31.100" 的数值 = 0x641FA8C0。
* 与 net_inet_ntoa 互为逆运算;net_getsockname / net_getpeername / net_recvfrom
* 填出来的 s_addr、以及 net_bind / net_connect / net_sendto 收进去的 s_addr
* 全在同一族,故可直接做相等比较(旧实现里 inet_addr 与其余四个不互逆,
* 曾造成「解析→存储→打印」链路输出反序 IP,见 TRAP-NET-21 与本函数实现注释)。
* 另:本层 inet_addr 语义与 BSD 一致——失败回 0,故 "0.0.0.0" 与非法串同值;
* 需要区分时用 net_inet_addr 的返回值再判,勿拿 NET_INADDR_NONE 当失败哨兵。
*/
uint32_t net_inet_addr(const char *cp);
/*
* 函数功能:将网络字节序 IP 转为字符串
* 入口参数:addr - 网络字节序 IP
* buf - 输出缓冲区(至少 16 字节)
* 返回值:buf 指针
* 限定条件:buf 指针非空
*/
char *net_inet_ntoa(uint32_t addr, char *buf);
/*
* 函数功能:端口字节序转换(主机序 -> 网络序)
* 入口参数:hostshort - 主机序端口
* 返回值:网络序端口
* 限定条件:无
* 函数说明:小端 → 大端
*/
uint16_t net_htons(uint16_t hostshort);
/*
* 函数功能:端口字节序转换(网络序 -> 主机序)
* 入口参数:netshort - 网络序端口
* 返回值:主机序端口
* 限定条件:无
* 函数说明:大端 → 小端
*/
uint16_t net_ntohs(uint16_t netshort);
/*
* 函数功能:IP 地址字节序转换(主机序 -> 网络序)
* 入口参数:hostlong - 主机序 IP
* 返回值:网络序 IP
* 限定条件:无
* 函数说明:小端 → 大端
*/
uint32_t net_htonl(uint32_t hostlong);
/*
* 函数功能:IP 地址字节序转换(网络序 -> 主机序)
* 入口参数:netlong - 网络序 IP
* 返回值:主机序 IP
* 限定条件:无
* 函数说明:大端 → 小端
*/
uint32_t net_ntohl(uint32_t netlong);
#ifndef NET_NO_OS
/*
* 函数功能:处理消息队列中的请求(必须在 netTask 主循环中调用)
* 限定条件:net_init() 已调用
* 函数说明:处理其他任务通过 net_send/net_recv 等接口发送的请求,
* 在 netTask 上下文中串行化执行所有 CH395F 操作
*/
void net_process_messages(void);
#endif /* NET_NO_OS */
/*
* 函数功能:获取 Socket 控制块指针(内部使用)
* 入口参数:sockfd - socket 描述符
* 返回值:Socket 控制块指针,失败返回 NULL
*/
net_sock_t *net_get_sock(int sockfd);
/*
* 函数功能:直取 Socket 控制块数组元素,**不判 in_use**(net 层内部周期对账专用)
* 入口参数:sockfd - Socket 描述符 (0~7),调用方保证在范围内
* 返回值:控制块指针(永不为 NULL)
* 限定条件:netTask 上下文
* 函数说明:`net_get_sock()` 对未在用槽位返回 NULL,而周期对账必须看到 in_use==0 /
* state==CLOSED 的槽位(对账形态④ 认领孤儿监听正依赖 CLOSED 态)→ 单独提供。
* ⚠ 应用层请一律用 `net_get_sock()`;本函数仅供 net_events.c 的对账循环使用。
* 模拟器不实现(无调用者,链接期不需要)。
*/
net_sock_t *net_sock_raw(int sockfd);
/*
* 函数功能:记录**本任务**最后一次失败的精确错误码(net 层内部通用工具)
* 入口参数:err - 错误码(NET_ERR_xxx,负值)
* 出口参数:无
* 返回值:err 原值(便于写成 `return net_set_errno(NET_ERR_XXX);`)
* 限定条件:无
* 函数说明:1. 只写本任务的槽位,其他任务读不到、也改不了 → 消除跨任务覆写竞态
* 2. 监听生命周期模块化:由 net_socket.c 的 static 提升为**模块间可见**,
* 供 net_listener.c 等同目录内部模块复用(定义仍在 net_socket.c)
*/
int net_set_errno(int err);
/* 「记本任务错误码 + 统一失败返回 -1」的紧凑写法(取代 `s_net_errno = X; return -1;`)
* 监听生命周期模块化:原定义在 net_socket.c,随 net_set_errno 一并提升到本头文件 */
#define NET_ERR_RETURN_1(err) (net_set_errno(err), -1)
/*
* 函数功能:获取 Socket 本地地址(IP + Port)
* 入口参数:sockfd - socket 描述符
* addr - 输出:本地地址
* addrlen - 输入输出:地址结构体长度
* 返回值:0 成功,-1 失败
* 限定条件:socket 已创建或已绑定
*/
int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen);
/*
* 函数功能:获取 Socket 对端地址(IP + Port)
* 入口参数:sockfd - socket 描述符
* addr - 输出:对端地址
* addrlen - 输入输出:地址结构体长度
* 返回值:0 成功,-1 失败(未连接返回 NOTCONN)
* 限定条件:socket 已连接(MCU 端读 CONNECT 事件缓存的对端,零 SPI;
* 单连接模型下监听 fd 转数据通道后同样有效)
*/
int net_getpeername(int sockfd, struct net_sockaddr *addr, int *addrlen);
#ifndef NET_NO_OS
/*
* 内部 API(需在 netTask 上下文中调用,绕过消息队列)
*/
/*
* 函数功能:取下一个动态端口并推进游标(49152~65535 回绕,内部使用)
* 入口参数:无
* 返回值:分配到的端口(主机序) uint16_t
* 限定条件:单线程语义——TCP 绑定路径(调用方上下文)与 UDP 绑定执行体
* (netTask)都经此取号,簿记只住 net_socket.c 一处
* 函数说明:原逻辑内联在 net_bind 里;UDP 队列化后绑定动作挪进 netTask 执行,
* 两处都要用同一游标,故抽成函数(同一事实只在一处)。
* 不做"是否已被其他 Socket 占用"检查——与抽取前行为一致。
*/
uint16_t net_dynamic_port_next(void);
/*
* 函数功能:发送数据(内部版本,直接操作,不经过消息队列)
* 入口参数:p_sock - Socket 控制块指针
* buf - 数据缓冲区
* len - 数据长度
* 返回值:实际发送字节数,失败返回 -1
* 限定条件:必须在 netTask 上下文中调用
*/
int net_send_sock(net_sock_t *p_sock, const void *buf, int len);
/*
* 函数功能:接收数据(内部版本,直接操作,不经过消息队列)
* 入口参数:p_sock - Socket 控制块指针
* buf - 接收缓冲区
* len - 缓冲区大小
* flags - 通常为 0,可设置 NET_MSG_DONTWAIT
* 返回值:实际接收字节数,0=对端关闭,-1=错误
* 限定条件:必须在 netTask 上下文中调用
*/
int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags);
/*
* 函数功能:关闭 socket(内部版本,直接操作,不经过消息队列)
* 入口参数:p_sock - Socket 控制块指针
* 返回值:0 成功,-1 失败
* 限定条件:必须在 netTask 上下文中调用
*/
int net_close_sock(net_sock_t *p_sock);
/*
* 函数功能:启动 TCP Server 监听(内部版本,直接操作,不经过消息队列)
* 入口参数:sockfd - Socket 描述符
* 返回值:0 成功,-1 失败
* 限定条件:必须在 netTask 上下文中调用
*/
int net_listen_locked(int sockfd);
/*
* 函数功能:判当前上下文是否本层执行任务(netTask / 硬件层的 testTask)
* 入口参数:无
* 返回值:1 - 是本层执行任务(或 net_init 前未捕获);0 - 明确不是 uint8_t
* 限定条件:net_init 之后判据才有效;任意上下文可读
* 函数说明:队列化接口用本判据决定"直接执行"还是"投递给 netTask"——执行任务
* 自己不能往自己的队列里投(要等自己回来泵,队列满即自等死锁);
* 挂起登记前也用它确认"完成动作有人做"。判据与留痕见定义处注释。
*/
uint8_t net_ctx_is_owner_task(void);
/*
* 函数功能:发送数据(内部锁定版本,TCP 与 UDP 共用同一份分块实现)
* 入口参数:p_sock - Socket 控制块指针
* buf - 数据缓冲区
* len - 数据长度
* caller - 挂起 send 的完成通知目标(执行任务自身传 NULL 即拒登记)
* 返回值:实际发送字节数(>= 0);NET_PENDING_REGISTERED = 已挂起;-1 = 错误
* 限定条件:必须在 netTask 上下文中调用
* 函数说明:UDP 队列化后 net_udp_sendto_exec 也走本函数——分块粒度
* (NET_SEND_CHUNK_MAX)、SENBUF_FREE 门控、末块不等、停滞超时、
* 挂起 send 表全部只有一份(TRAP-NET-16 的复发形态就是"两条路径
* 各写一套分块自旋",故不复制)。状态门按 type 区分:TCP 须
* ESTABLISHED,UDP 须 UDP_OPEN。
*/
int net_send_locked(net_sock_t *p_sock, const void *buf, int len, TaskHandle_t caller);
#endif /* NET_NO_OS */
#ifdef __cplusplus
}
#endif
#endif /* NET_SOCKET_H */