diff --git a/bspMCU/l_stm32.c b/bspMCU/l_stm32.c index 2da8339..137fa92 100644 --- a/bspMCU/l_stm32.c +++ b/bspMCU/l_stm32.c @@ -767,8 +767,16 @@ int luaopen_stm32(lua_State *L) lua_call(L, 0, 1); lua_getfield(L, -1, "new"); lua_setfield(L, -3, "COM"); + lua_getfield(L, -1, "new"); + lua_setglobal(L, "COM"); /* 全局 COM: 上位机/新脚本用法 (st.COM 保留兼容) */ lua_getfield(L, -1, "console"); lua_setfield(L, -3, "CON"); + lua_getfield(L, -1, "console"); + lua_setglobal(L, "CON"); /* 全局 CON */ +#ifdef USE_LWIP + lua_getfield(L, -1, "udp"); + lua_setglobal(L, "UDP"); /* 全局 UDP: 上位机 provision 行 c=UDP(...) 依赖 */ +#endif lua_pop(L, 1); #endif diff --git a/bspMCU/l_uart.c b/bspMCU/l_uart.c index 4bb0773..a2fa9ff 100644 --- a/bspMCU/l_uart.c +++ b/bspMCU/l_uart.c @@ -3,16 +3,25 @@ #include "lua_base.h" #include "bsp_uart.h" #include "ymodem.h" +#ifdef USE_LWIP +#include "rd_udp.h" /* UDP 链路句柄 (上位机 provision 经 telnet 下发 c=UDP(...)) */ +#endif #ifdef PRINT_ID #ifdef USE_USB #include "bsp_usb.h" #endif #endif +/* 句柄类型 (主仓库 lacmd.c 同款语义) */ +#define COM_TYPE_SERIAL 0 +#define COM_TYPE_UDP 1 + // 串口对象结构体 typedef struct { TComCtrl *m_ptComCtrl; + int m_iType; /* COM_TYPE_* */ + int m_iOwned; /* 1=自建句柄 GC 关闭 (仅 UDP); 0=借用, 维持 robot 现状不关 */ } stm32_com_t; TComCtrl *g_ptCom1; @@ -42,7 +51,9 @@ static int l_com_new(lua_State *L) // 创建 userdata stm32_com_t *u = (stm32_com_t*)lua_newuserdata(L, sizeof(stm32_com_t)); - + u->m_iType = COM_TYPE_SERIAL; + u->m_iOwned = 0; /* 串口句柄维持 robot 现状: 不随 GC 关闭 */ + if (id == 1 || id == 2) { TUartUserData *ptUartUserData = UART_userdata_init(LUACOM_ID, baudrate, 4096); @@ -184,6 +195,8 @@ static int l_con_new(lua_State *L) return 1; } stm32_com_t *u = (stm32_com_t *)lua_newuserdata(L, sizeof(stm32_com_t)); + u->m_iType = COM_TYPE_SERIAL; + u->m_iOwned = 0; u->m_ptComCtrl = ptCtrl; luaL_getmetatable(L, "stm32.COMMT"); lua_setmetatable(L, -2); @@ -194,6 +207,67 @@ static int l_con_new(lua_State *L) #endif } +#ifdef USE_LWIP +// __gc: 仅自建 UDP 句柄关闭 (上位机 provision 行 c=nil collectgarbage() +// 重绑旧句柄、telnet 断线重连 lua_init 重建 Lua 态时, 靠这里释放旧 UDP +// 链路/8007 端口, 否则二次 Rd_UdpCreate bind 失败); +// 串口/CON 借用句柄维持 robot 现状 (不关, 生命周期归宿主) +static int l_com_gc(lua_State *L) +{ + stm32_com_t *u = (stm32_com_t*)luaL_testudata(L, 1, "stm32.COMMT"); + if (u != NULL && u->m_iOwned && u->m_iType == COM_TYPE_UDP && u->m_ptComCtrl != NULL) + { + Rd_UdpClose(u->m_ptComCtrl); + u->m_ptComCtrl = NULL; + } + return 0; +} + +// 构造函数: UDP(localport, "remoteip" [,remoteport [,txsrcport]]) — UDP 链路句柄 +// 上位机连接后自动下发 c=UDP(8007,"<本端IP>",9007) 建 ymodem 文件链路, +// 之后 c:ry('.')/c:sy('f') 与串口句柄用法完全一致 (rym_* 按 TComCtrl 分派) +static int l_udp_new(lua_State *L) +{ + if (lua_gettop(L) == 0) + { + lua_print("Usage: UDP(localport, \"remoteip\" [,remoteport [,txsrcport]])\n"); + lua_print(" UDP(8007, \"192.168.1.100\", 9007) -- 固定直连: bind 8007 rx, 发往 9007\n"); + return 0; + } + + int localport = (int)luaL_checkinteger(L, 1); + const char *ip = luaL_checkstring(L, 2); + int remoteport = (int)luaL_optinteger(L, 3, 0); + int txsrcport = (int)luaL_optinteger(L, 4, 0); + + TRdUdpCfg stUdpCfg = { + .m_psRemoteIp = ip, + .m_iRemotePort = remoteport, + .m_iLocalPort = localport, + .m_iTxSrcPort = txsrcport, + .m_uiBufSize = 4096, + .m_psName = "lua", + .m_pfCheck = NULL, /* 数据报全透传: ymodem 原始字节语义 */ + .m_pfDecode = NULL, + }; + TComCtrl *ptComCtrl = Rd_UdpCreate(&stUdpCfg); + if (ptComCtrl == NULL) + { + return luaL_error(L, "UDP(%d, %s, %d) create failed", localport, ip, remoteport); + } + + stm32_com_t *u = (stm32_com_t*)lua_newuserdata(L, sizeof(stm32_com_t)); + u->m_ptComCtrl = ptComCtrl; + u->m_iType = COM_TYPE_UDP; + u->m_iOwned = 1; // 自建句柄: GC 时关闭 (重连 provision 幂等的关键) + luaL_getmetatable(L, "stm32.COMMT"); + lua_setmetatable(L, -2); + + lua_print("UDP bind %d txsrc %d -> %s:%d\n", localport, txsrcport, ip, remoteport); + return 1; +} +#endif /* USE_LWIP */ + // 模块注册 int luaopen_stm32_com(lua_State *L) { @@ -209,6 +283,9 @@ int luaopen_stm32_com(lua_State *L) {"read", l_com_read}, {"ry", l_com_ry}, {"sy", l_com_sy}, +#ifdef USE_LWIP + {"__gc", l_com_gc}, +#endif {NULL, NULL} }; const luaL_Reg *l; @@ -231,6 +308,11 @@ int luaopen_stm32_com(lua_State *L) lua_pushcfunction(L, l_con_new); lua_setfield(L, -2, "console"); +#ifdef USE_LWIP + lua_pushcfunction(L, l_udp_new); + lua_setfield(L, -2, "udp"); +#endif + return 1; } diff --git a/bspMCU/rd_udp.c b/bspMCU/rd_udp.c new file mode 100644 index 0000000..b336ba7 --- /dev/null +++ b/bspMCU/rd_udp.c @@ -0,0 +1,874 @@ +/****************************************************************************** + + 版权所有 (C), 2018-2099, Radkil + + ****************************************************************************** + 文 件 名 : rd_udp.c + 版 本 号 : 初稿 + 作 者 : Radkil + 生成日期 : 2026年11月3日 + 最近修改 : + 功能描述 : 跨平台 UDP 传输实现 (TComCtrl 集成, 与 rd_serial 同构) + USE_LWIP → lwip/sockets.h (需 tcpip 栈已启动) + BUILD_PLATFORM_LINUX → POSIX socket + pthread + BUILD_PLATFORM_WIN32 → winsock2 + 与 Linux 标准 BSD socket 的差异收敛到本文件内: + - 头文件按平台宏分支 + - IP 解析不依赖 inet_addr/inet_pton (sscanf + htonl) + - 接收超时不用 SO_RCVTIMEO (lwip 为 int 毫秒, Linux 为 + struct timeval), 统一 fcntl(O_NONBLOCK) + 让步轮询 + UDP 广播设备发现 (beacon, 见 rd_udp.h 宏): + - 独立发现 socket 绑 RD_UDP_DISCOVERY_PORT, 周期广播 beacon + - peer 表记录 (源IP, data_port), 数据帧扇出给全部活 peer + - m_psRemoteIp 非空时固定直连与发现扇出并存 + + 修改历史 : + 1.日 期 : 2026年11月3日 + 作 者 : Radkil + 修改内容 : 创建文件 + + 2.日 期 : 2026年11月4日 + 作 者 : Radkil + 修改内容 : 新增 beacon 设备发现: 发现 socket+线程/peer 表/ + 发送扇出, m_psRemoteIp 可空 (空 = 纯发现模式) + +******************************************************************************/ + +/* ─── 平台头文件 ─────────────────────────────────────────────────────── */ +/* __ANDROID__ (NDK 自动定义) 并入 POSIX 分支: Bionic 的 socket/fcntl/errno + * 与 Linux 同名同义, 头文件一致 (与 rd_thread.h 的平台分支同模式); + * 不要用 BUILD_PLATFORM_LINUX 识别安卓 —— 会误激活 rd_serial 的 termios 路径 */ +#include "common.h" +#ifdef USE_LWIP +#include "lwip/sockets.h" +#include "lwip/netdb.h" +/* robot 适配: 线程创建走固件既有 CMSIS-RTOS2 (与 My_freeRTOS.c talnet 任务同款), + * 不引入 Rd_ThreadCreate 跨平台抽象; 蹦床终止用 FreeRTOS vTaskDelete + * (头文件顺序与 My_freeRTOS.c 完全一致) */ +#include "cmsis_os.h" +#include "cmsis_os2.h" +#include "FreeRTOS.h" +#include "task.h" +#endif /* USE_LWIP */ + +#if defined(BUILD_PLATFORM_LINUX) || defined(__ANDROID__) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif /* BUILD_PLATFORM_LINUX || __ANDROID__ */ + +#ifdef BUILD_PLATFORM_WIN32 +#include +#include +#include +#endif /* BUILD_PLATFORM_WIN32 */ + +/* ─── 项目头文件 ─────────────────────────────────────────────────────── */ +#include "rd_thread.h" +#include "rd_time.h" +#include "com.h" +#include "rd_udp.h" + +/*==============================================* + * 常量/宏 * + *==============================================*/ +#define RD_UDP_RECV_BUF_SIZE 2048 /* 接收线程单次读取缓冲(单报文上限) */ + +/* ── beacon 报文布局 (22B 二进制, 周期重发自愈 → 无需 CRC) ── + * [0..1] magic "RD" (0x52 0x44) 与 55 55 数据帧天然区分 + * [2] ver = 1 + * [3] type = 1 (beacon) + * [4..7] instance_id (u32, 网络序) 防自环: 同机广播自己也会收到一份 + * [8..9] data_port (htons) 本端数据帧端口 (对端据此回发数据) + * [10..21] name[12] 设备名截断 (可选, 空补 0) */ +#define RD_UDP_BEACON_SIZE 22 +#define RD_UDP_BEACON_MAGIC_0 0x52 /* 'R' */ +#define RD_UDP_BEACON_MAGIC_1 0x44 /* 'D' */ +#define RD_UDP_BEACON_VER 1 +#define RD_UDP_BEACON_TYPE 1 /* 1 = beacon */ +#define RD_UDP_NAME_MAX 12 /* beacon name 字段宽度 */ + +/* 未识别平台: 不提供 socket 环境, 创建直接失败 */ +#if !defined(USE_LWIP) && !defined(BUILD_PLATFORM_LINUX) && \ + !defined(BUILD_PLATFORM_WIN32) && !defined(__ANDROID__) +#define RD_UDP_NO_PLATFORM 1 +#endif + +#ifndef RD_UDP_NO_PLATFORM + +#if defined(BUILD_PLATFORM_WIN32) +#define RD_UDP_SOCK_ERR() WSAGetLastError() +/* Win32 错误码走 WSA* 命名空间, 与 errno 体系不同 */ +#define RD_UDP_ERR_YIELD(e) ((e) == WSAEWOULDBLOCK || (e) == WSAEINTR || (e) == WSAECONNREFUSED) +#define RD_UDP_CLOSE(s) closesocket(s) +#else +#define RD_UDP_SOCK_ERR() errno +/* ECONNREFUSED: Linux 对端未监听时 ICMP 异步错误, 容忍不退出 */ +#define RD_UDP_ERR_YIELD(e) ((e) == EAGAIN || (e) == EWOULDBLOCK || (e) == EINTR || (e) == ECONNREFUSED) +#define RD_UDP_CLOSE(s) close(s) +#endif + +/* ═══════════════════════════════════════════════════════════════════════ * + * 平台特定 UserData 结构 * + * ═══════════════════════════════════════════════════════════════════════ */ +/* beacon 发现的活 peer (design: 同 ip+port 刷新 last_seen, 超时剔除) */ +typedef struct +{ + uint32_t m_uiIp; /* 对端 IP (网络序, beacon 源地址) */ + uint16_t m_uiDataPort; /* beacon 携带的对端数据帧端口 */ + uint32_t m_uiLastSeen; /* 最近活跃时刻 Rd_GetTime() (ms) */ + char m_sName[RD_UDP_NAME_MAX + 1]; +} TRdUdpPeer; + +typedef struct +{ + int m_iRxSocket; /* 接收 socket (bind 本地端口) */ + int m_iTxSocket; /* 发送 socket (可复用 m_iRxSocket) */ + struct sockaddr_in m_stRemoteAddr; /* 固定对端地址 (发送目的, 可无效) */ + volatile int m_iRunning; /* 接收/发现线程运行标志 */ + /* ── beacon 设备发现 ── */ + uint32_t m_uiInstanceId; /* 本实例标识 (create 时生成, 防自环) */ + uint16_t m_uiDataPort; /* 本端数据帧端口 (beacon 携带, 0=只发不收) */ + char m_sName[RD_UDP_NAME_MAX + 1]; /* 设备名 (beacon 携带, 可空) */ + struct sockaddr_in m_stDiscAddr; /* beacon 广播目的 255.255.255.255:发现端口 */ + int m_iDiscSocket; /* 发现接收 socket (-1 = 发现降级关闭) */ + int m_iFixedValid; /* 固定对端有效 (m_psRemoteIp 非空) */ + int m_iBcnWarned; /* beacon 广播失败已警告 (仅首次, 防刷屏) */ + TRdUdpPeer m_astPeers[RD_UDP_PEER_MAX]; /* 无锁: 发现线程单写 + + 发送线程读, u32/u16 对齐写在双平台均原子, demo 级容忍 */ +} TUdpUserData; + +/* ═══════════════════════════════════════════════════════════════════════ * + * 文件级全局 (cbk_ComSend 签名限制, 单链路约束) * + * ═══════════════════════════════════════════════════════════════════════ */ +static TComCtrl *g_ptUdpComCtrl = NULL; + +/* ═══════════════════════════════════════════════════════════════════════ * + * 内部辅助 * + * ═══════════════════════════════════════════════════════════════════════ */ + +/* 接收线程让步 (非阻塞轮询间隔) */ +static void rd_udp_yield(void) +{ +#if defined(BUILD_PLATFORM_LINUX) || defined(__ANDROID__) + usleep(1000); +#elif defined(BUILD_PLATFORM_WIN32) + Sleep(1); +#else + Rd_Delay(1); +#endif +} + +/* 点分十进制 → sockaddr_in (不依赖 inet_addr/inet_pton, 全平台一致) */ +static int rd_udp_fill_remote(struct sockaddr_in *_pstAddr, + const char *_psIp, int _iPort) +{ + unsigned int a = 0, b = 0, c = 0, d = 0; + + if (_pstAddr == NULL || _psIp == NULL || + RD_STRLEN(_psIp) >= RD_UDP_IP_MAX - 1) + { + return RD_FAILURE; + } + if (sscanf(_psIp, "%u.%u.%u.%u", &a, &b, &c, &d) != 4 || + a > 255 || b > 255 || c > 255 || d > 255) + { + return RD_FAILURE; + } + + RD_MEMSET(_pstAddr, 0, sizeof(*_pstAddr)); + _pstAddr->sin_family = AF_INET; + _pstAddr->sin_port = htons((uint16_t)_iPort); + _pstAddr->sin_addr.s_addr = htonl((a << 24) | (b << 16) | (c << 8) | d); + return RD_SUCCESS; +} + +/* 接收 socket 非阻塞 (lwip fcntl 支持 F_GETFL/F_SETFL/O_NONBLOCK) */ +static int rd_udp_set_nonblock(int _iSocket) +{ +#if defined(BUILD_PLATFORM_WIN32) + u_long ulNonBlock = 1; + return (ioctlsocket(_iSocket, FIONBIO, &ulNonBlock) == 0) ? RD_SUCCESS : RD_FAILURE; +#else + int iFlags = fcntl(_iSocket, F_GETFL, 0); + if (iFlags < 0) return RD_FAILURE; + return (fcntl(_iSocket, F_SETFL, iFlags | O_NONBLOCK) >= 0) ? RD_SUCCESS : RD_FAILURE; +#endif +} + +/* ─────────────────── beacon 设备发现内部辅助 ─────────────────── */ + +/* 网络序 IP → "a.b.c.d" (静态缓冲, 仅日志用, 用完即弃) */ +static const char *rd_udp_ip_str(uint32_t _uiIpNet) +{ + static char s_szIp[16]; + uint32_t ui = ntohl(_uiIpNet); + snprintf(s_szIp, sizeof(s_szIp), "%u.%u.%u.%u", + (unsigned)((ui >> 24) & 0xFF), (unsigned)((ui >> 16) & 0xFF), + (unsigned)((ui >> 8) & 0xFF), (unsigned)(ui & 0xFF)); + return s_szIp; +} + +/* 实例标识: 地址/时刻/本地数据端口混合散列 (不依赖平台 rand, lwip 无 rand); + * 并存实例必然本地端口不同 (否则数据 bind 冲突), 身份不依赖远端配置; + * 两活实例撞号只会互相当自环过滤掉 (发现失效), 无其他副作用 */ +static uint32_t rd_udp_make_instance_id(const TUdpUserData *_ptUd, uint16_t _hwDataPort) +{ + uint32_t uiId = (uint32_t)(uintptr_t)_ptUd; + uiId ^= Rd_GetTime(); + uiId ^= (uint32_t)_hwDataPort << 16; + uiId *= 2654435761U; /* Knuth 乘法散列, 拉开低位 */ + return (uiId != 0) ? uiId : 0x52440001U; /* 避开全 0 */ +} + +/* beacon 入账: 同 ip+port 静默刷新 last_seen, 新发现打 log_i; + * 表满时覆盖 last_seen 最旧的项 */ +static void rd_udp_peer_update(TUdpUserData *_ptUd, uint32_t _uiIp, + uint16_t _hwDataPort, const char *_psName, + uint32_t _uiNow) +{ + TRdUdpPeer *ptEmpty = NULL, *ptOldest = NULL, *ptSlot = NULL; + int i; + + for (i = 0; i < RD_UDP_PEER_MAX; i++) + { + TRdUdpPeer *pt = &_ptUd->m_astPeers[i]; + if (pt->m_uiIp == 0) /* 空位 (0.0.0.0 不会是合法对端) */ + { + if (ptEmpty == NULL) ptEmpty = pt; + continue; + } + if (pt->m_uiIp == _uiIp && pt->m_uiDataPort == _hwDataPort) + { + pt->m_uiLastSeen = _uiNow; /* 已知 peer */ + return; + } + if (ptOldest == NULL || pt->m_uiLastSeen < ptOldest->m_uiLastSeen) + { + ptOldest = pt; + } + } + + ptSlot = (ptEmpty != NULL) ? ptEmpty : ptOldest; + if (ptSlot == NULL) return; + RD_MEMSET(ptSlot, 0, sizeof(*ptSlot)); + ptSlot->m_uiIp = _uiIp; + ptSlot->m_uiDataPort = _hwDataPort; + ptSlot->m_uiLastSeen = _uiNow; + if (_psName != NULL) + { + uint32_t uiLen = (uint32_t)RD_STRLEN(_psName); + if (uiLen > RD_UDP_NAME_MAX) uiLen = RD_UDP_NAME_MAX; + RD_MEMCPY(ptSlot->m_sName, _psName, uiLen); /* 目标已清零, 天然 NUL 结尾 */ + } + log_i("udp discovery: peer %s:%u (%s)", rd_udp_ip_str(_uiIp), + (unsigned)_hwDataPort, ptSlot->m_sName[0] ? ptSlot->m_sName : "?"); +} + +/* 超时剔除 (发现线程每轮顺手执行, 表仅 RD_UDP_PEER_MAX 项) */ +static void rd_udp_peer_sweep(TUdpUserData *_ptUd, uint32_t _uiNow) +{ + int i; + for (i = 0; i < RD_UDP_PEER_MAX; i++) + { + TRdUdpPeer *pt = &_ptUd->m_astPeers[i]; + if (pt->m_uiIp == 0) continue; + /* uint32 回绕安全减法 */ + if ((uint32_t)(_uiNow - pt->m_uiLastSeen) > RD_UDP_PEER_TIMEOUT_MS) + { + log_i("udp discovery: peer %s:%u (%s) timeout", + rd_udp_ip_str(pt->m_uiIp), (unsigned)pt->m_uiDataPort, + pt->m_sName[0] ? pt->m_sName : "?"); + RD_MEMSET(pt, 0, sizeof(*pt)); + } + } +} + +/* 组装并广播 beacon (复用 tx socket, 已设 SO_BROADCAST; + * 周期重发自愈 → 失败仅首次警告防刷屏) */ +static void rd_udp_beacon_send(TUdpUserData *_ptUd) +{ + uint8_t ucBcn[RD_UDP_BEACON_SIZE]; + uint32_t uiIdNet = htonl(_ptUd->m_uiInstanceId); + uint16_t hwPortNet = htons(_ptUd->m_uiDataPort); + int iSent; + + if (_ptUd->m_iTxSocket < 0) return; + + RD_MEMSET(ucBcn, 0, sizeof(ucBcn)); + ucBcn[0] = RD_UDP_BEACON_MAGIC_0; + ucBcn[1] = RD_UDP_BEACON_MAGIC_1; + ucBcn[2] = RD_UDP_BEACON_VER; + ucBcn[3] = RD_UDP_BEACON_TYPE; + RD_MEMCPY(&ucBcn[4], &uiIdNet, sizeof(uiIdNet)); + RD_MEMCPY(&ucBcn[8], &hwPortNet, sizeof(hwPortNet)); + if (_ptUd->m_sName[0] != '\0') + { + /* name 字段定长 12B, 源串已保证 NUL 结尾, 不足补 0 (RD_MEMSET 已清) */ + RD_MEMCPY(&ucBcn[10], _ptUd->m_sName, + RD_STRLEN(_ptUd->m_sName) < RD_UDP_NAME_MAX + ? RD_STRLEN(_ptUd->m_sName) : RD_UDP_NAME_MAX); + } + + iSent = (int)sendto(_ptUd->m_iTxSocket, (char *)ucBcn, sizeof(ucBcn), 0, + (struct sockaddr *)&_ptUd->m_stDiscAddr, + sizeof(_ptUd->m_stDiscAddr)); + if (iSent < 0 && !_ptUd->m_iBcnWarned) + { + _ptUd->m_iBcnWarned = 1; + log_w("udp discovery: beacon broadcast failed, err=%d (discovery degraded)", + RD_UDP_SOCK_ERR()); + } +} + +/* 收到疑似 beacon: 校验 magic/ver/type → 自环过滤 → 入账; + * 数据地址取 beacon 源 IP, 数据端口取报文 data_port (非源端口) */ +static void rd_udp_beacon_on_rx(TUdpUserData *_ptUd, const uint8_t *_pBuf, + uint32_t _iLen, const struct sockaddr_in *_pstFrom) +{ + uint32_t uiId = 0; + uint16_t hwPortNet = 0; + char sName[RD_UDP_NAME_MAX + 1]; + + if (_iLen < RD_UDP_BEACON_SIZE || + _pBuf[0] != RD_UDP_BEACON_MAGIC_0 || _pBuf[1] != RD_UDP_BEACON_MAGIC_1 || + _pBuf[2] != RD_UDP_BEACON_VER || _pBuf[3] != RD_UDP_BEACON_TYPE) + { + return; /* 非 beacon, 发现通道上直接忽略 */ + } + + RD_MEMCPY(&uiId, &_pBuf[4], sizeof(uiId)); + if (ntohl(uiId) == _ptUd->m_uiInstanceId) return; /* 防自环: 同机广播回环 */ + + RD_MEMCPY(&hwPortNet, &_pBuf[8], sizeof(hwPortNet)); + RD_MEMSET(sName, 0, sizeof(sName)); + RD_MEMCPY(sName, &_pBuf[10], RD_UDP_NAME_MAX); /* 定宽字段, 已清零保 NUL */ + + rd_udp_peer_update(_ptUd, _pstFrom->sin_addr.s_addr, + ntohs(hwPortNet), sName, Rd_GetTime()); +} + +/* ═══════════════════════════════════════════════════════════════════════ * + * 发送回调 (cbk_ComSend → 全局 TComCtrl → UserData → 扇出 sendto) * + * ═══════════════════════════════════════════════════════════════════════ */ +/* 扇出策略: 固定对端 (remote ip 非空) + beacon 发现的全部活 peer; + * 任一路成功即返回字节数, 全失败返回 RD_FAILURE (cbk_ComSend 语义不变) */ +static int udp_send_impl(char *_pBuffer, uint32_t _iSize) +{ + if (g_ptUdpComCtrl == NULL) return RD_FAILURE; + TUdpUserData *ptUd = (TUdpUserData *)(g_ptUdpComCtrl->m_pUserData); + if (ptUd == NULL || ptUd->m_iTxSocket < 0) return RD_FAILURE; + + int iBest = -1; + uint32_t uiNow = Rd_GetTime(); + int i; + + /* 1. 固定对端 (m_psRemoteIp 非空时保留现状) */ + if (ptUd->m_iFixedValid) + { + int iSent = (int)sendto(ptUd->m_iTxSocket, _pBuffer, _iSize, 0, + (struct sockaddr *)&ptUd->m_stRemoteAddr, + sizeof(ptUd->m_stRemoteAddr)); + if (iSent > 0) + { + iBest = iSent; + } + } + + /* 2. 发现的活 peer 扇出 */ + for (i = 0; i < RD_UDP_PEER_MAX; i++) + { + TRdUdpPeer *pt = &ptUd->m_astPeers[i]; + struct sockaddr_in stTo; + int iSent; + + if (pt->m_uiIp == 0 || pt->m_uiDataPort == 0) continue; /* 空位/无数据口 */ + if ((uint32_t)(uiNow - pt->m_uiLastSeen) > RD_UDP_PEER_TIMEOUT_MS) continue; + + /* 【去重】固定对端同时被 beacon 发现时 (ip+data_port 相同), 若不再跳过 + * 会对同一地址双发每个报文 —— ymodem 协议字节双发直接错位/损坏文件. + * 字节序: m_uiIp 网络序直存, data_port 为入表 ntohs 后的主机序 */ + if (ptUd->m_iFixedValid && + pt->m_uiIp == ptUd->m_stRemoteAddr.sin_addr.s_addr && + pt->m_uiDataPort == ntohs(ptUd->m_stRemoteAddr.sin_port)) + { + continue; /* peer 即固定对端, 第 1 步固定发送已覆盖 */ + } + + RD_MEMSET(&stTo, 0, sizeof(stTo)); + stTo.sin_family = AF_INET; + stTo.sin_port = htons(pt->m_uiDataPort); + stTo.sin_addr.s_addr = pt->m_uiIp; + iSent = (int)sendto(ptUd->m_iTxSocket, _pBuffer, _iSize, 0, + (struct sockaddr *)&stTo, sizeof(stTo)); + if (iSent > 0 && iBest < 0) + { + iBest = iSent; + } + } + + if (iBest < 0) + { + log_e("udp_send_impl: sendto all failed (fixed=%d), err=%d", + ptUd->m_iFixedValid, RD_UDP_SOCK_ERR()); + return RD_FAILURE; + } + return iBest; +} + +/* ═══════════════════════════════════════════════════════════════════════ * + * 接收线程 (非阻塞 recvfrom + 让步轮询) * + * ═══════════════════════════════════════════════════════════════════════ */ +#if defined(BUILD_PLATFORM_WIN32) +static DWORD WINAPI udp_recv_thread(LPVOID _lpParam) +#else +static void *udp_recv_thread(void *_pArg) +#endif +{ +#if defined(BUILD_PLATFORM_WIN32) + TComCtrl *ptComCtrl = (TComCtrl *)_lpParam; +#else + TComCtrl *ptComCtrl = (TComCtrl *)_pArg; +#endif + TUdpUserData *ptUd = (TUdpUserData *)(ptComCtrl->m_pUserData); + uint8_t ucBuf[RD_UDP_RECV_BUF_SIZE]; + + while (ptUd->m_iRunning) + { + struct sockaddr_in stFrom; + socklen_t iFromLen = sizeof(stFrom); + int iRd = (int)recvfrom(ptUd->m_iRxSocket, (char *)ucBuf, + sizeof(ucBuf), 0, + (struct sockaddr *)&stFrom, &iFromLen); + if (iRd > 0) + { + rd_ComRecvProc(ptComCtrl, (const char *)ucBuf, (uint32_t)iRd); + } + else if (iRd < 0) + { + int iErr = RD_UDP_SOCK_ERR(); + if (RD_UDP_ERR_YIELD(iErr)) + { + rd_udp_yield(); + } + else + { + log_e("udp_recv_thread: recvfrom failed, err=%d", iErr); + break; + } + } + else + { + rd_udp_yield(); + } + } + return 0; +} + +/* ═══════════════════════════════════════════════════════════════════════ * + * 发现线程 (非阻塞收 beacon + 周期广播 + 超时剔除) * + * ═══════════════════════════════════════════════════════════════════════ */ +#if defined(BUILD_PLATFORM_WIN32) +static DWORD WINAPI udp_disc_thread(LPVOID _lpParam) +#else +static void *udp_disc_thread(void *_pArg) +#endif +{ +#if defined(BUILD_PLATFORM_WIN32) + TComCtrl *ptComCtrl = (TComCtrl *)_lpParam; +#else + TComCtrl *ptComCtrl = (TComCtrl *)_pArg; +#endif + TUdpUserData *ptUd = (TUdpUserData *)(ptComCtrl->m_pUserData); + uint8_t ucBuf[RD_UDP_BEACON_SIZE + 16]; /* 容忍 beacon 带少量尾部冗余 */ + uint32_t uiLastBeacon = 0; /* 0 → 首轮立即广播, 尽快入对端表 */ + + /* 发现是增强通道: 收发失败只降级, 不退出 (m_iRunning 为唯一退出条件) */ + while (ptUd->m_iRunning) + { + struct sockaddr_in stFrom; + socklen_t iFromLen = sizeof(stFrom); + uint32_t uiNow; + int iRd = (int)recvfrom(ptUd->m_iDiscSocket, (char *)ucBuf, + sizeof(ucBuf), 0, + (struct sockaddr *)&stFrom, &iFromLen); + if (iRd > 0) + { + rd_udp_beacon_on_rx(ptUd, ucBuf, (uint32_t)iRd, &stFrom); + } + + uiNow = Rd_GetTime(); + /* 周期广播 (uint32 回绕安全减法) */ + if ((uint32_t)(uiNow - uiLastBeacon) >= RD_UDP_BEACON_INTERVAL_MS) + { + uiLastBeacon = uiNow; + rd_udp_beacon_send(ptUd); + } + rd_udp_peer_sweep(ptUd, uiNow); + + rd_udp_yield(); + } + return 0; +} + +#if defined(USE_LWIP) +/* robot 适配: FreeRTOS 任务不允许从入口函数返回 (返回=未定义行为/断言), + * 蹦床包一层 "函数返回即 vTaskDelete(NULL) 自杀"; 真正逻辑仍在原线程函数 */ +static void udp_recv_task_entry(void *_pArg) +{ + udp_recv_thread(_pArg); + vTaskDelete(NULL); +} + +static void udp_disc_task_entry(void *_pArg) +{ + udp_disc_thread(_pArg); + vTaskDelete(NULL); +} +#endif /* USE_LWIP */ + +/* ═══════════════════════════════════════════════════════════════════════ * + * Rd_UdpCreate — 统一入口 * + * ═══════════════════════════════════════════════════════════════════════ */ + +/* 平台 socket 初始化 (Win32: WSAStartup; 其余平台空) */ +static int rd_udp_sock_init(void) +{ +#if defined(BUILD_PLATFORM_WIN32) + static int s_iInitDone = 0; + WSADATA stWsa; + if (!s_iInitDone) + { + if (WSAStartup(MAKEWORD(2, 2), &stWsa) != 0) return RD_FAILURE; + s_iInitDone = 1; + } +#endif + return RD_SUCCESS; +} + +WEAK TComCtrl *Rd_UdpCreate(const TRdUdpCfg *_ptCfg) +{ + if (_ptCfg == NULL) + { + log_e("Rd_UdpCreate: invalid config"); + return NULL; + } + /* m_psRemoteIp 允许 NULL/"": 纯发现模式 (对端经 beacon 自动获知, + * m_iRemotePort 忽略可 0); 固定直连时远端端口才必填 */ + if (_ptCfg->m_psRemoteIp != NULL && _ptCfg->m_psRemoteIp[0] != '\0' && + (_ptCfg->m_iRemotePort <= 0 || _ptCfg->m_iRemotePort > 65535)) + { + log_e("Rd_UdpCreate: invalid remote port %d (fixed-ip mode)", + _ptCfg->m_iRemotePort); + return NULL; + } + if (_ptCfg->m_iLocalPort < 0 || _ptCfg->m_iLocalPort > 65535 || + _ptCfg->m_iTxSrcPort < 0 || _ptCfg->m_iTxSrcPort > 65535) + { + log_e("Rd_UdpCreate: invalid port config"); + return NULL; + } + if (rd_udp_sock_init() != RD_SUCCESS) + { + log_e("Rd_UdpCreate: socket subsystem init failed"); + return NULL; + } + + /* 1. 分配 UserData */ + TUdpUserData *ptUd = (TUdpUserData *)RD_CALLOC(1, sizeof(TUdpUserData)); + if (ptUd == NULL) return NULL; + ptUd->m_iRxSocket = -1; + ptUd->m_iTxSocket = -1; + ptUd->m_iDiscSocket = -1; + + /* 1.5 发现参数: 实例标识 / beacon 名 / 本端数据端口 / 广播目的 */ + ptUd->m_uiDataPort = (uint16_t)(_ptCfg->m_iLocalPort > 0 ? _ptCfg->m_iLocalPort : 0); + ptUd->m_uiInstanceId = rd_udp_make_instance_id(ptUd, ptUd->m_uiDataPort); + if (_ptCfg->m_psName != NULL && _ptCfg->m_psName[0] != '\0') + { + uint32_t uiLen = (uint32_t)RD_STRLEN(_ptCfg->m_psName); + if (uiLen > RD_UDP_NAME_MAX) uiLen = RD_UDP_NAME_MAX; + RD_MEMCPY(ptUd->m_sName, _ptCfg->m_psName, uiLen); /* 目标已清零保 NUL */ + } + RD_MEMSET(&ptUd->m_stDiscAddr, 0, sizeof(ptUd->m_stDiscAddr)); + ptUd->m_stDiscAddr.sin_family = AF_INET; + ptUd->m_stDiscAddr.sin_port = htons(RD_UDP_DISCOVERY_PORT); + ptUd->m_stDiscAddr.sin_addr.s_addr = htonl(INADDR_BROADCAST); + + /* 2. 固定对端地址 (m_psRemoteIp NULL/"" = 纯发现模式, 不设固定直连) */ + if (_ptCfg->m_psRemoteIp != NULL && _ptCfg->m_psRemoteIp[0] != '\0') + { + if (rd_udp_fill_remote(&ptUd->m_stRemoteAddr, + _ptCfg->m_psRemoteIp, _ptCfg->m_iRemotePort) != RD_SUCCESS) + { + log_e("Rd_UdpCreate: bad remote ip '%s'", _ptCfg->m_psRemoteIp); + RD_FREE(ptUd); + return NULL; + } + ptUd->m_iFixedValid = 1; + } + + /* 3. 接收 socket: bind 本地端口 (0 = 只发不收, 不创建) */ + if (_ptCfg->m_iLocalPort > 0) + { + struct sockaddr_in stLocal; + int iSock = (int)socket(AF_INET, SOCK_DGRAM, 0); + if (iSock < 0) + { + log_e("Rd_UdpCreate: rx socket failed, err=%d", RD_UDP_SOCK_ERR()); + RD_FREE(ptUd); + return NULL; + } + + RD_MEMSET(&stLocal, 0, sizeof(stLocal)); + stLocal.sin_family = AF_INET; + stLocal.sin_port = htons((uint16_t)_ptCfg->m_iLocalPort); + stLocal.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(iSock, (struct sockaddr *)&stLocal, sizeof(stLocal)) < 0) + { + log_e("Rd_UdpCreate: bind(%d) failed, err=%d", + _ptCfg->m_iLocalPort, RD_UDP_SOCK_ERR()); + RD_UDP_CLOSE(iSock); + RD_FREE(ptUd); + return NULL; + } + + if (rd_udp_set_nonblock(iSock) != RD_SUCCESS) + { + log_e("Rd_UdpCreate: set nonblock failed"); + RD_UDP_CLOSE(iSock); + RD_FREE(ptUd); + return NULL; + } + ptUd->m_iRxSocket = iSock; + } + + /* 4. 发送 socket: 指定源端口且与接收端口不同时单独创建 (对齐固件 + * 7006/8006 双 PCB 语义), 否则复用接收 socket (本地端口 0 时新建 + * 匿名绑定 socket 专用于发送) */ + if (_ptCfg->m_iTxSrcPort > 0 && _ptCfg->m_iTxSrcPort != _ptCfg->m_iLocalPort) + { + struct sockaddr_in stSrc; + int iSock = (int)socket(AF_INET, SOCK_DGRAM, 0); + if (iSock < 0) + { + log_e("Rd_UdpCreate: tx socket failed, err=%d", RD_UDP_SOCK_ERR()); + if (ptUd->m_iRxSocket >= 0) RD_UDP_CLOSE(ptUd->m_iRxSocket); + RD_FREE(ptUd); + return NULL; + } + + RD_MEMSET(&stSrc, 0, sizeof(stSrc)); + stSrc.sin_family = AF_INET; + stSrc.sin_port = htons((uint16_t)_ptCfg->m_iTxSrcPort); + stSrc.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(iSock, (struct sockaddr *)&stSrc, sizeof(stSrc)) < 0) + { + log_e("Rd_UdpCreate: bind tx src(%d) failed, err=%d", + _ptCfg->m_iTxSrcPort, RD_UDP_SOCK_ERR()); + RD_UDP_CLOSE(iSock); + if (ptUd->m_iRxSocket >= 0) RD_UDP_CLOSE(ptUd->m_iRxSocket); + RD_FREE(ptUd); + return NULL; + } + ptUd->m_iTxSocket = iSock; + } + else + { + ptUd->m_iTxSocket = ptUd->m_iRxSocket; + if (ptUd->m_iTxSocket < 0) + { + /* 只发不收且未指定源端口: 新建匿名 socket */ + ptUd->m_iTxSocket = (int)socket(AF_INET, SOCK_DGRAM, 0); + if (ptUd->m_iTxSocket < 0) + { + log_e("Rd_UdpCreate: tx-only socket failed, err=%d", RD_UDP_SOCK_ERR()); + RD_FREE(ptUd); + return NULL; + } + } + } + + /* 5. tx socket 开广播 (beacon 发 255.255.255.255; 三平台同名宏同义, + * 失败不致命: 固定直连仍可用, beacon 首次发送失败再警告) */ + { + int iOn = 1; + if (setsockopt(ptUd->m_iTxSocket, SOL_SOCKET, SO_BROADCAST, + (const char *)&iOn, sizeof(iOn)) < 0) + { + log_w("Rd_UdpCreate: SO_BROADCAST failed, err=%d", RD_UDP_SOCK_ERR()); + } + } + + /* 6. 发现 socket: SO_REUSEADDR + bind 发现端口 + 非阻塞 + * (SO_REUSEADDR: 同机多实例同端口共存, 广播各收一份; + * 绑定失败仅降级关闭发现, 链路创建继续) */ + { + int iOn = 1; + int iSock = (int)socket(AF_INET, SOCK_DGRAM, 0); + if (iSock < 0) + { + log_w("Rd_UdpCreate: disc socket failed, err=%d (discovery off)", + RD_UDP_SOCK_ERR()); + } + else + { + struct sockaddr_in stLocal; + setsockopt(iSock, SOL_SOCKET, SO_REUSEADDR, + (const char *)&iOn, sizeof(iOn)); + RD_MEMSET(&stLocal, 0, sizeof(stLocal)); + stLocal.sin_family = AF_INET; + stLocal.sin_port = htons(RD_UDP_DISCOVERY_PORT); + stLocal.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(iSock, (struct sockaddr *)&stLocal, sizeof(stLocal)) < 0) + { + log_w("Rd_UdpCreate: bind disc(%d) failed, err=%d (discovery off)", + RD_UDP_DISCOVERY_PORT, RD_UDP_SOCK_ERR()); + RD_UDP_CLOSE(iSock); + } + else if (rd_udp_set_nonblock(iSock) != RD_SUCCESS) + { + log_w("Rd_UdpCreate: disc nonblock failed (discovery off)"); + RD_UDP_CLOSE(iSock); + } + else + { + ptUd->m_iDiscSocket = iSock; + } + } + } + + /* 7. 创建 TComCtrl (m_pfSend = udp_send_impl) */ + uint32_t uiBufSize = _ptCfg->m_uiBufSize > 0 ? _ptCfg->m_uiBufSize : 4096; + TComCtrl *ptComCtrl = rd_ComCreate(_ptCfg->m_pfCheck, _ptCfg->m_pfDecode, + udp_send_impl, (int)uiBufSize, ptUd); + if (ptComCtrl == NULL) + { + log_e("Rd_UdpCreate: rd_ComCreate failed"); + if (ptUd->m_iDiscSocket >= 0) RD_UDP_CLOSE(ptUd->m_iDiscSocket); + if (ptUd->m_iRxSocket >= 0 && ptUd->m_iTxSocket != ptUd->m_iRxSocket) + RD_UDP_CLOSE(ptUd->m_iTxSocket); + if (ptUd->m_iRxSocket >= 0) RD_UDP_CLOSE(ptUd->m_iRxSocket); + RD_FREE(ptUd); + return NULL; + } + + /* 8. 记录全局引用 (供发送回调使用) */ + g_ptUdpComCtrl = ptComCtrl; + + /* 9. 启动接收线程 (local_port = 0 只发不收, 无接收线程) */ + ptUd->m_iRunning = 1; /* 接收/发现线程共用运行标志 */ + if (ptUd->m_iRxSocket >= 0) + { + /* robot 适配: 固件既有 CMSIS-RTOS2 直接创建 (栈字节/优先级语义与 + * 主仓库 FreeRTOS 封装默认值一致: 4KB / osPriorityNormal) */ + const osThreadAttr_t attr = { .name = "udp_recv", .stack_size = 4 * 1024, + .priority = (osPriority_t)osPriorityNormal }; + if (osThreadNew(udp_recv_task_entry, ptComCtrl, &attr) == NULL) + { + log_e("Rd_UdpCreate: osThreadNew failed"); + ptUd->m_iRunning = 0; + g_ptUdpComCtrl = NULL; + rd_ComDelete(ptComCtrl); + if (ptUd->m_iDiscSocket >= 0) RD_UDP_CLOSE(ptUd->m_iDiscSocket); + if (ptUd->m_iRxSocket >= 0 && ptUd->m_iTxSocket != ptUd->m_iRxSocket) + RD_UDP_CLOSE(ptUd->m_iTxSocket); + RD_UDP_CLOSE(ptUd->m_iRxSocket); + RD_FREE(ptUd); + return NULL; + } + } + + /* 10. 启动发现线程 (发现 socket 就绪才有; 增强功能, 失败只关发现) */ + if (ptUd->m_iDiscSocket >= 0) + { + const osThreadAttr_t attr = { .name = "udp_disc", .stack_size = 4 * 1024, + .priority = (osPriority_t)osPriorityNormal }; + if (osThreadNew(udp_disc_task_entry, ptComCtrl, &attr) == NULL) + { + log_w("Rd_UdpCreate: disc thread failed (discovery off)"); + RD_UDP_CLOSE(ptUd->m_iDiscSocket); + ptUd->m_iDiscSocket = -1; + } + } + + if (ptUd->m_iFixedValid) + { + log_i("Rd_UdpCreate: local=%d txsrc=%d -> %s:%d (discovery %s)", + _ptCfg->m_iLocalPort, _ptCfg->m_iTxSrcPort, + _ptCfg->m_psRemoteIp, _ptCfg->m_iRemotePort, + ptUd->m_iDiscSocket >= 0 ? "on" : "off"); + } + else + { + log_i("Rd_UdpCreate: local=%d txsrc=%d, pure discovery mode (disc %s)", + _ptCfg->m_iLocalPort, _ptCfg->m_iTxSrcPort, + ptUd->m_iDiscSocket >= 0 ? "on" : "off"); + } + return ptComCtrl; +} + +/* ═══════════════════════════════════════════════════════════════════════ * + * Rd_UdpClose — 统一销毁 * + * ═══════════════════════════════════════════════════════════════════════ */ +WEAK void Rd_UdpClose(TComCtrl *_ptComCtrl) +{ + if (_ptComCtrl == NULL) return; + + TUdpUserData *ptUd = (TUdpUserData *)(_ptComCtrl->m_pUserData); + if (ptUd != NULL) + { + /* 1. 通知接收/发现线程停止 (detached 线程无法 join, 发信号后 sleep; + * 两个线程均为 1ms 级轮询, 多让步几拍确保脱离共享数据) */ + ptUd->m_iRunning = 0; + rd_udp_yield(); + rd_udp_yield(); + rd_udp_yield(); + rd_udp_yield(); + + /* 2. 关闭 socket (唤醒轮询中的 recvfrom 并使其退出) */ + if (ptUd->m_iDiscSocket >= 0) + { + RD_UDP_CLOSE(ptUd->m_iDiscSocket); + ptUd->m_iDiscSocket = -1; + } + if (ptUd->m_iTxSocket >= 0 && ptUd->m_iTxSocket != ptUd->m_iRxSocket) + { + RD_UDP_CLOSE(ptUd->m_iTxSocket); + ptUd->m_iTxSocket = -1; + } + if (ptUd->m_iRxSocket >= 0) + { + RD_UDP_CLOSE(ptUd->m_iRxSocket); + ptUd->m_iRxSocket = -1; + } + + RD_FREE(ptUd); + } + + /* 3. 销毁 TComCtrl */ + rd_ComDelete(_ptComCtrl); + + if (g_ptUdpComCtrl == _ptComCtrl) + g_ptUdpComCtrl = NULL; + + log_i("Rd_UdpClose: done"); +} + +#else +TComCtrl *Rd_UdpCreate(const TRdUdpCfg *_ptCfg) +{ + return NULL; +} + +void Rd_UdpClose(TComCtrl *_ptComCtrl) +{ + return; +} +#endif /* RD_UDP_NO_PLATFORM */ \ No newline at end of file diff --git a/peripheral/include/rd_udp.h b/peripheral/include/rd_udp.h new file mode 100644 index 0000000..d639d9a --- /dev/null +++ b/peripheral/include/rd_udp.h @@ -0,0 +1,102 @@ +/****************************************************************************** + + 版权所有 (C), 2018-2099, Radkil + + ****************************************************************************** + 文 件 名 : rd_udp.h + 版 本 号 : 初稿 + 作 者 : Radkil + 生成日期 : 2026年11月3日 + 最近修改 : + 功能描述 : 跨平台 UDP 传输抽象层公共头文件 (TComCtrl 集成) + 与 rd_serial 同构: 创建 TComCtrl + 内部接收线程, + 平台差异在 peripheral/rd_udp.c 内通过宏处理: + USE_LWIP → lwip/sockets.h (STM32 固件, 需 tcpip 栈已启动) + BUILD_PLATFORM_LINUX → POSIX socket + BUILD_PLATFORM_WIN32 → winsock2 + + 修改历史 : + 1.日 期 : 2026年11月3日 + 作 者 : Radkil + 修改内容 : 创建文件 + +******************************************************************************/ + +#ifndef __RD_UDP_H__ +#define __RD_UDP_H__ + +#ifdef __cplusplus +#if __cplusplus +extern "C"{ +#endif +#endif /* __cplusplus */ + +/*==============================================* + * include header files * + *----------------------------------------------*/ +#include +#include "common.h" +#include "com.h" + +/*==============================================* + * constants or macros define * + *----------------------------------------------*/ +#define RD_UDP_IP_MAX 16 /* 点分十进制 IP 最大长度("255.255.255.255"+0) */ + +/* ── UDP 广播设备发现 (beacon) 参数 ── + * 机制: 各实例按 RD_UDP_BEACON_INTERVAL_MS 周期向 + * 255.255.255.255:RD_UDP_DISCOVERY_PORT 广播 22B beacon, + * 收到方校验 magic 后记录 (源IP, data_port) 进 peer 表, + * 数据帧扇出给所有 RD_UDP_PEER_TIMEOUT_MS 内活跃的 peer */ +#define RD_UDP_DISCOVERY_PORT 9005 /* 发现通道端口 (beacon 收发, 两端一致) */ +#define RD_UDP_PEER_MAX 8 /* peer 表容量 (满时覆盖最旧) */ +#define RD_UDP_PEER_TIMEOUT_MS 5000 /* peer 存活超时 (beacon 周期整数倍, 防误剔) */ +#define RD_UDP_BEACON_INTERVAL_MS 1000 /* beacon 广播周期 */ + +/* UDP 链路配置 */ +typedef struct +{ + const char *m_psRemoteIp; /* 对端 IP (发送目的), 点分十进制; + * NULL/"" = 纯发现模式 (数据帧扇出给 beacon 发现的活 peer) */ + int m_iRemotePort; /* 对端端口 (仅固定 IP 模式使用; 纯发现模式忽略, 可 0) */ + int m_iLocalPort; /* 本地绑定端口 (接收), 0 = 只发不收 (beacon data_port 同源) */ + int m_iTxSrcPort; /* 发送源端口 (对齐固件 7006 双 PCB 语义), + * 0 = 复用接收 socket (源端口 = 本地端口) */ + uint32_t m_uiBufSize; /* TComCtrl 环形缓冲区大小 */ + const char *m_psName; /* 设备名 (beacon 携带, 对端日志可见), NULL = 不填 */ + cbk_ComCheck m_pfCheck; + cbk_ComDecode m_pfDecode; +} TRdUdpCfg; + +/*==============================================* + * routines' or functions' implementations * + *----------------------------------------------*/ + +/** + * @brief 创建 UDP 链路 TComCtrl (内部启动接收线程 + beacon 发现线程) + * @param _ptCfg UDP 配置 (不可为空; m_psRemoteIp 可为 NULL/"") + * @return TComCtrl* 成功, NULL 失败 + * @note m_psRemoteIp 为 NULL/"" 时进入纯发现模式: 不再要求配置对端, + * 数据帧自动扇出给 beacon 发现的活 peer (rd_udp.h 宏可调); + * 非空时固定直连与发现并存 (扇出 = 固定对端 + 全部活 peer)。 + * 发现 socket 绑定失败仅降级关闭发现 (log_w), 链路创建不受影响。 + * USE_LWIP 平台需先完成 tcpip 栈/netif 初始化再调用 + * (SO_REUSEADDR 需 LWIP_SO_REUSE); + * 发送回调经模块级静态上下文解析, 每进程仅支持一条 UDP 链路 + * (与 rd_serial 的单实例约束同模式, 不改 com.c) + */ +TComCtrl *Rd_UdpCreate(const TRdUdpCfg *_ptCfg); + +/** + * @brief 关闭并销毁 UDP 链路 TComCtrl + * @param _ptComCtrl TComCtrl 控制块 + */ +void Rd_UdpClose(TComCtrl *_ptComCtrl); + +#ifdef __cplusplus +#if __cplusplus +} +#endif +#endif /* __cplusplus */ + +#endif /* __RD_UDP_H__ */