Browse Source

add: Air8000的tcp/udp server示例代码

wangshihao 4 months ago
parent
commit
f0439d50de

+ 98 - 0
module/Air8000/demo/socket/server/main.lua

@@ -0,0 +1,98 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑 
+@version 1.0
+@date    2025.09.15
+@author  王世豪
+@usage
+本demo演示的核心功能为:
+1、分别创建tcp server和udp server;
+2、等待client的连接;
+3、tcp/udp server按照以下几种逻辑发送数据给client
+- 串口应用功能模块uart_app.lua,通过uart1接收到串口数据,将串口数据增加send from uart: 前缀后发送给client;
+- 定时器应用功能模块timer_app.lua,定时产生数据,将数据增加send from timer: 前缀后发送给client;
+
+4、netdrv_device:配置连接外网使用的网卡,目前支持以下三种选择(三选一)
+        (1) netdrv_wifi_ap:WIFI AP网卡
+        (2) netdrv_wifi_sta:WIFI STA网卡
+        (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
+
+注意:
+一个tcp server仅支持一路client连接;
+UDP 协议本身是无连接的,这意味着任何在同一局域网下的客户端都可以向服务器的 IP 和端口发送数据包;
+目前只能支持局域网内的client连接,不支持公网ip连接。
+
+更多说明参考本目录下的readme.md文件
+]]
+
+
+--[[
+必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
+PROJECT:项目名,ascii string类型
+        可以随便定义,只要不使用,就行
+VERSION:项目版本号,ascii string类型
+        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
+                X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
+                因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
+        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+PROJECT = "tcp_udp_server_demo"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- -- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- -- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+-- if wdt then
+--     --配置喂狗超时时间为9秒钟
+--     wdt.init(9000)
+--     --启动一个循环定时器,每隔3秒钟喂一次狗
+--     sys.timerLoopStart(wdt.feed, 3000)
+-- end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- -- 加载网络驱动设备功能模块
+require "netdrv_device"
+
+-- -- -- 加载串口应用功能模块
+require "uart_app"
+
+-- -- -- 加载定时器应用功能模块
+require "timer_app"
+
+-- -- 加载tcp server socket主应用功能模块
+require "tcp_server_main"
+
+-- -- 加载udp server socket主应用功能模块
+-- require "udp_server_main"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

+ 68 - 0
module/Air8000/demo/socket/server/netdrv/netdrv_eth_spi.lua

@@ -0,0 +1,68 @@
+--[[
+@module  netdrv_eth_spi
+@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块 
+@version 1.0
+@date    2025.09.15
+@author  王世豪
+@usage
+本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块 ,核心业务逻辑为:
+1、打开CH390H芯片供电开关;
+2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
+]]
+
+local exnetif = require "exnetif"
+
+local function ip_ready_func(adapter)
+    if adapter == socket.LWIP_ETH then
+        log.info("netdrv_eth_spi.ip_ready_func", "IP_READY: ", socket.localIP(socket.LWIP_ETH))
+    end
+end
+
+local function ip_lose_func(adapter)
+    if adapter == socket.LWIP_ETH then
+        log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
+        sys.publish(SERVER_TOPIC, "SOCKET_CLOSED")
+    end
+end
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI外挂CH390H芯片的以太网卡”的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
+-- 各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+
+-- 以太网断网后,内核固件会产生一个"IP_LOSE"消息
+-- 各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI外挂CH390H芯片的以太网卡”的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 配置SPI外接以太网芯片CH390H的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_ETH
+-- 本demo使用Air8000开发板测试,开发板上的硬件配置为:
+-- GPIO140为CH390H以太网芯片的供电使能控制引脚
+-- 使用spi1,片选引脚使用GPIO12
+-- 如果使用的硬件不是Air8000开发板,根据自己的硬件配置修改以下参数
+exnetif.set_priority_order({
+    {
+        ETHERNET = {
+            pwrpin = 140, 
+            tp = netdrv.CH390,
+            opts = {spi = 1, cs = 12}
+        }
+    }
+})

+ 81 - 0
module/Air8000/demo/socket/server/netdrv/netdrv_wifi_ap.lua

@@ -0,0 +1,81 @@
+--[[
+@module  netdrv_wifi_ap
+@summary "WIFI AP网卡"驱动模块
+@version 1.0
+@date    2025.10.16
+@author  王世豪
+@usage
+本文件为WIFI AP网卡驱动模块,核心业务逻辑为:
+1、初始化WiFi AP功能;
+2、配置热点名称、密码等参数;
+3、启动接入点供其他设备连接;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_wifi_ap"就可以加载运行;
+]]
+
+dnsproxy = require("dnsproxy")
+dhcpsrv = require("dhcpsrv")
+
+
+local function ip_ready_func(ip,adapter)
+    if adapter == socket.LWIP_AP then
+        log.info("netdrv_wifi.ip_ready_func", "IP_READY: ", ip)
+    end
+end
+
+local function ip_lose_func(ip,adapter)
+    if adapter == socket.LWIP_AP then
+        log.warn("netdrv_wifi.ip_lose_func", "IP_LOSE")
+        sys.publish(SERVER_TOPIC, "SOCKET_CLOSED")
+    end
+end
+
+-- 监听WLAN_AP_INC消息,处理WiFi接入点相关事件
+local function ap_ready_func(evt, data)
+    -- evt 可能的值有: "CONNECTED", "DISCONNECTED"
+    -- 当evt=CONNECTED, data是连接的AP的新STA的MAC地址
+    -- 当evt=DISCONNECTED, data是断开与AP连接的STA的MAC地址
+    log.info("收到AP事件", evt, data and data:toHex())
+end
+
+-- 订阅系统网络相关消息,实现事件驱动的网络状态管理
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+sys.subscribe("WLAN_AP_INC", ap_ready_func)
+
+
+-- 设置默认网卡为socket.LWIP_AP
+socket.dft(socket.LWIP_AP)
+
+--这个task的核心业务逻辑是:执行WiFi AP初始化和配置流程
+local function netdrv_wifi_ap_task_func()
+    -- wlan初始化
+    wlan.init()
+    -- 创建热点,SSID=LuatOS+IMEI,密码=12345678
+    wlan.createAP("LuatOS" .. mobile.imei(), "12345678")
+    -- 为AP网卡分配静态IPv4地址、子网掩码、网关
+    netdrv.ipv4(socket.LWIP_AP, "192.168.4.1", "255.255.255.0", "0.0.0.0")
+
+    -- 等待AP接口就绪
+    while netdrv.ready(socket.LWIP_AP) ~= true do
+        sys.wait(100)
+    end
+    -- 配置DNS代理
+    dnsproxy.setup(socket.LWIP_AP, socket.LWIP_GP)
+    -- 在AP接口上创建DHCP服务器,为连接到热点的设备自动分配IP地址
+    dhcpsrv.create({adapter=socket.LWIP_AP})
+    -- 配置网络共享(NAPT),使用4G网络作为主网关出口
+    while 1 do
+        if netdrv.ready(socket.LWIP_GP) then
+            netdrv.napt(socket.LWIP_GP)
+            log.info("AP 创建成功,如果无法连接,需要将按照https://docs.openluat.com/air8000/luatos/app/updatwifi/update/ 升级固件")
+            log.info("AP 创建成功,如果无法连接,请升级本仓库的最新core")
+            break
+        end
+        sys.wait(1000)
+    end
+end
+
+--创建并且启动一个task
+--task的处理函数为netdrv_wifi_ap_task_func
+sys.taskInit(netdrv_wifi_ap_task_func)

+ 59 - 0
module/Air8000/demo/socket/server/netdrv/netdrv_wifi_sta.lua

@@ -0,0 +1,59 @@
+--[[
+@module  netdrv_wifi
+@summary “WIFI STA网卡”驱动模块 
+@version 1.0
+@date    2025.09.15
+@author  王世豪
+@usage
+本文件为WIFI STA网卡驱动模块,核心业务逻辑为:
+1、初始化WIFI网络;
+2、连接WIFI路由器;
+3、和WIFI路由器之间的连接状态发生变化时,在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_wifi"就可以加载运行;
+]]
+
+local exnetif = require "exnetif"
+
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_STA then
+        log.info("netdrv_wifi.ip_ready_func", "IP_READY: ", ip, json.encode(wlan.getInfo()))
+    end
+end
+
+local function ip_lose_func(adapter)
+    if adapter == socket.LWIP_STA then
+        log.warn("netdrv_wifi.ip_lose_func", "IP_LOSE")
+        sys.publish(SERVER_TOPIC, "SOCKET_CLOSED")
+    end
+end
+
+--WIFI联网成功(做为STATION成功连接AP,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
+--各个功能模块可以订阅"IP_READY"消息实时处理WIFI联网成功的事件
+--也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功
+
+--WIFI断网后,内核固件会产生一个"IP_LOSE"消息
+--各个功能模块可以订阅"IP_LOSE"消息实时处理WIFI断网的事件
+--也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察WIFI的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+-- 配置WiFi设备模式的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_STA
+-- ssid为要连接的WiFi路由器名称;
+-- password为要连接的WiFi路由器密码;
+-- 注意:仅支持2.4G的WiFi,不支持5G的WiFi;
+-- 实际测试时,根据自己要连接的WiFi热点信息修改以下参数
+exnetif.set_priority_order({
+    {
+        WIFI = {
+            -- ssid = "茶室-降功耗,找合宙!", 
+            -- password = "Air123456"
+            ssid = "xiaomi15",
+            password = "wsh123456"
+        }
+    }
+})

+ 29 - 0
module/Air8000/demo/socket/server/netdrv_device.lua

@@ -0,0 +1,29 @@
+--[[
+@module  netdrv_device
+@summary 网络驱动设备功能模块 
+@version 1.0
+@date    2025.09.15
+@author  王世豪
+@usage
+本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
+1、netdrv_wifi_sta:socket.LWIP_STA,WIFI STA网卡;
+2、netdrv_wifi_ap:socket.LWIP_AP,WIFI AP网卡;
+3、netdrv_ethernet_spi:socket.LWIP_USER1,通过SPI外挂CH390H芯片的以太网卡;
+
+根据自己的项目需求,只需要require以上三种中的一种即可;
+
+
+本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
+]]
+
+
+-- 根据自己的项目需求,只需要require以下三种中的一种即可;
+
+-- 加载“WIFI STA网卡”驱动模块
+require "netdrv_wifi_sta"
+
+-- 加载“WIFI AP网卡”驱动模块
+-- require "netdrv_wifi_ap"
+
+-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
+-- require "netdrv_eth_spi"

+ 187 - 0
module/Air8000/demo/socket/server/readme.md

@@ -0,0 +1,187 @@
+## 功能模块介绍
+
+1、main.lua:主程序入口;
+
+2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的三种网卡(单wifi ap网卡,单wifi sta网卡,单spi以太网卡)中的任何一种网卡;
+
+3、tcp文件夹:tcp server以及数据收发处理逻辑;
+
+4、udp文件夹:udp server以及数据收发处理逻辑;
+
+5、timer_app.lua:通知server定时发送数据给client;
+
+6、uart_app.lua:server和uart外设之间透传数据;
+
+> 注意:
+> 
+> 一个tcp server仅支持一路client连接;
+> 
+> UDP 协议本身是无连接的,这意味着任何在同一局域网下的客户端都可以向服务器的 IP 和端口发送数据包;
+
+## 系统消息介绍
+
+1、"IP_READY":某种网卡已经获取到ip信息,仅仅获取到了ip信息,能否和外网连通还不确认;
+
+2、"IP_LOSE":某种网卡已经掉网;
+
+## 用户消息介绍
+
+1、"RECV_DATA_FROM_CLIENT":tcp/udp server收到客户端上发的数据后,通过此消息发布出去,给其他应用模块处理;
+
+2、"SEND_DATA_REQ":其他应用模块发布此消息,通知tcp/udp server发送数据给客户端;
+
+## 演示功能概述
+
+1、创建tcp/udp server,在目录中对应两个文件夹详情如下
+
+- TCP文件夹功能为创建一个tcp server,等待tcp client连接;
+
+- UDP文件夹功能为创建一个udp server,等待udp client连接;
+
+2、tcp/udp server 与client连接成功后,server按照以下几种逻辑发送数据给client
+
+- 串口应用功能模块uart_app.lua,通过uart1接收到串口数据,将串口数据增加send from uart: 前缀后发送给client;
+
+- 定时器应用功能模块timer_app.lua,定时产生数据,将数据增加send from timer:前缀后发送给client;
+
+3、netdrv_device:配置连接外网使用的网卡,目前支持以下三种选择(三选一)
+
+   (1) netdrv_wifi_ap:WIFI AP网卡
+
+   (2) netdrv_wifi_sta:WIFI STA网卡
+
+   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
+
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/air8000/luatos/app/image/netdrv_multi.jpg)
+
+1、Air8000开发板一块+wifi天线一根+网线一根:
+
+- 天线装到开发板上
+
+- 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口
+
+2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air8000开发板和数据线的硬件接线方式为:
+
+- Air8000开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
+
+- TYPE-C USB数据线直接插到开发板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+- USB转串口数据线,一般来说,白线连接开发板的UART1_TX,绿线连接开发板的UART1_RX,黑线连接开发板的GND,另外一端连接电脑USB口;
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air8000 V2016版本固件](https://docs.openluat.com/air8000/luatos/firmware/)(理论上,2025年7月26日之后发布的固件都可以)
+
+3、PC端的串口工具,建议使用SSCOM(SSCOM可以创建TCP客户端或UDP客户端,测试TCP/UDP 通信功能)
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、demo脚本代码netdrv_device.lua中,按照自己的网卡需求启用对应的Lua文件
+
+- 如果需要单WIFI AP网卡,打开require "netdrv_wifi_ap",其余注释掉;同时netdrv_wifi_ap.lua中的wlan.createAP("LuatOS" .. mobile.imei(), "12345678"),表示创建wifi的名称和密码,根据自己需求改动即可;
+
+- 如果需要单WIFI STA网卡,打开require "netdrv_wifi_sta",其余注释掉;同时netdrv_wifi_sta.lua中的wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1),前两个参数,修改为自己测试时wifi热点的名称和密码;注意:仅支持2.4G的wifi,不支持5G的wifi
+
+- 如果需要以太网卡,打开require "netdrv_eth_spi",其余注释掉
+
+
+3、demo脚本代码中,测试TCP server和UDP server时,需要修改的地方如下:
+
+- 测试TCP server时,main.lua打开 require "tcp_server_main",注释掉 require "udp_server_main";同时timer_app.lua和uart_app.lua中的enable_tcp设为true,enable_udp设为false。
+
+- 测试UDP server时,main.lua打开 require "udp_server_main",注释掉 require "tcp_server_main";同时timer_app.lua和uart_app.lua中的enable_udp设为true,enable_tcp设为false。
+
+4、Luatools烧录内核固件和修改后的demo脚本代码
+
+5、烧录成功后,自动开机运行
+
+6、TCP演示:
+
+(1)根据烧录日志,找到TCP server的ip,此外 port 在示例代码中默认是50003
+
+ip获取方式,是在每个netdrv网卡文件中的 ip_ready_func接口中,此处演示WIFI_STA网卡的情况下,如何找到创建的TCP server的ip
+
+```lua
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_STA then
+        log.info("netdrv_wifi.ip_ready_func", "IP_READY: ", ip, json.encode(wlan.getInfo()))
+    end
+end
+```
+luatools日志打印如下:
+
+![image](https://docs.openLuat.com/cdn/image/socket/tcp_ip_ready.png)
+
+(2)PC 端打开一个TCP客户端,连接到Air8000开发板创建的TCP server (本例使用SSCOM打开一个TCP客户端):
+
+端口号:选择TCPCLient
+
+远程:填写TCP server的ip地址 和TCP监听的port ,默认是50003
+
+本地:填写本地PC端的IP地址
+
+![image](https://docs.openLuat.com/cdn/image/socket/tcp_client.png)
+
+成功连接之后,即可收到TCP server主动发送的第一条消息:
+
+![image](https://docs.openLuat.com/cdn/image/socket/tcp_client1.png)
+
+(3)另外再打开一个PC端的串口工具连接到Air8000开发板的uart1, 做串口收发,选择对应的端口,配置波特率115200,数据位8,停止位1,无奇偶校验位
+
+(4)PC端的串口工具输入一段数据 "hello client!",点击发送,在作为TCP客户端的SSCOM上可以收到此数据;在作为TCP 客户端的SSCOM输入一段数据 "i am tcp client",点击发送,在PC端的串口工具上可以收到此数据,如下所示:
+
+![image](https://docs.openLuat.com/cdn/image/socket/tcp_client2.png)
+
+
+7、UDP演示:
+
+(1)根据烧录日志,找到UDP server的ip,此外 port 在示例代码中默认是50003
+
+ip获取方式,是在每个netdrv网卡文件中的 ip_ready_func接口中,此处演示WIFI_STA网卡的情况下,如何找到创建的UDP server的ip
+
+```lua
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_STA then
+        log.info("netdrv_wifi.ip_ready_func", "IP_READY: ", ip, json.encode(wlan.getInfo()))
+    end
+end
+```
+luatools日志打印如下:
+
+![image](https://docs.openLuat.com/cdn/image/socket/udp_ip_ready.png)
+
+(2)PC 端打开一个UDP客户端,连接到Air8000开发板创建的UDP server (本例使用SSCOM打开一个UDP客户端):
+
+端口号:选择UDP
+
+远程:填写UDP server的ip地址 和UDP监听的port ,默认是50003
+
+本地:填写本地PC端的IP地址, 本例填写的port是50000
+
+![image](https://docs.openLuat.com/cdn/image/socket/udp_client.png)
+
+成功连接之后,即可收到UDP server主动发送的第一条消息:
+
+![image](https://docs.openLuat.com/cdn/image/socket/udp_client1.png)
+
+(3)另外再打开一个PC端的串口工具连接到Air8000开发板的uart1, 做串口收发,选择对应的端口,配置波特率115200,数据位8,停止位1,无奇偶校验位
+
+(4)PC端的串口工具输入一段数据 "hello udp server!",点击发送,在作为UDP客户端的SSCOM上可以收到此数据;在作为UDP 客户端的SSCOM输入一段数据 "i am udp client",点击发送,在PC端的串口工具上可以收到此数据,如下所示: 
+
+![image](https://docs.openLuat.com/cdn/image/socket/udp_client2.png)
+
+8、注意事项
+
+UDP server 在未收到 client发的数据时,会每隔15秒向255.255.255.255 发送一条心跳广播消息,同时timer_app定时发送功能 由于无法确定客户端的ip和port, 会打印 "尚未收到客户端数据, 无法确定目标IP和端口" 的错误提示;
+
+UDP server 在收到client 发的数据后,会记录下来发送消息的client的ip和port,然后通过timer_app 每隔5秒向client发送数据。
+
+目前只能支持局域网内的client连接,不支持公网ip连接。

+ 332 - 0
module/Air8000/demo/socket/server/tcp/tcp_server_main.lua

@@ -0,0 +1,332 @@
+--[[
+@module  tcp_server_main
+@summary tcp server主应用功能模块 
+@version 1.0
+@date    2025.09.15
+@author  王世豪
+@usage  
+本文件为tcp server主应用功能模块,核心业务逻辑为:
+1、创建一个tcp server ,等待client连接;
+2、处理连接异常,出现异常后,关闭当前连接,等待下一个client连接;
+3、调用tcp_server_receiver和tcp_server_sender中的外部接口,进行数据收发处理;
+
+本文件没有对外接口,直接在main.lua中require "tcp_server_main"就可以加载运行;
+]]
+
+local libnet = require "libnet"
+
+-- 加载TCP服务器数据接收功能模块
+local tcp_server_receiver = require "tcp_server_receiver"
+-- 加载TCP服务器数据发送功能模块
+local tcp_server_sender = require "tcp_server_sender"
+
+-- tcp_server_main的任务名
+local TASK_NAME = tcp_server_sender.TASK_NAME
+
+-- 处理未识别的消息
+local function tcp_server_main_cbfunc(msg)
+	log.info("tcp_server_main_cbfunc", msg[1], msg[2], msg[3], msg[4])
+end
+
+-- tcp server socket的任务处理函数
+local function tcp_server_main_task_func()
+    local netc = nil
+    local result, param
+    local listen_port = 50003 -- tcp server监听的端口号
+
+    while true do
+        -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+        while not socket.adapter(socket.dft()) do
+            log.warn("tcp_server_main_task_func", "wait IP_READY", socket.dft())
+            -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+            -- 或者等待1秒超时退出阻塞等待状态;
+            -- 注意:此处的1000毫秒超时不要修改的更长;
+            -- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+            -- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+            -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+            sys.waitUntil("IP_READY", 1000)
+        end
+
+        -- 检测到了IP_READY消息
+        log.info("tcp_server_main_task_func", "recv IP_READY", socket.dft())
+
+        netc = socket.create(socket.dft(), TASK_NAME)
+        if not netc then
+            log.error("tcp_server_task_func", "socket.create失败")
+            goto EXCEPTION_PROC
+        end
+
+        socket.debug(netc, true)
+        -- 配置socker server 对象为tcp server
+        result = socket.config(netc, listen_port)
+        -- 如果配置失败
+        if not result then
+            log.error("tcp_server_task_func", "socket.config失败")
+            goto EXCEPTION_PROC
+        end
+
+        -- 监听tcp server端口
+        result = libnet.listen(TASK_NAME, 0, netc)
+        -- 如果监听失败
+        if not result then
+            log.error("tcp_server_task_func", "监听失败")
+            goto EXCEPTION_PROC
+        end
+
+        -- 客户端连上了, 发一条数据给客户端
+        libnet.tx(TASK_NAME, 0, netc, "TCP server is UP!")
+
+        -- 数据收发以及网络连接异常事件总处理逻辑
+        while true do
+            -- 数据接收处理
+            if not tcp_server_receiver.proc(netc) then
+                log.info("tcp_server_task_func", "tcp_server_receiver.proc error")
+                break
+            end
+            
+            -- 数据发送处理
+            if not tcp_server_sender.proc(TASK_NAME, netc) then
+                log.info("tcp_server_task_func", "tcp_server_sender.proc error")
+                break
+            end
+
+            -- 阻塞等待socket.EVENT事件或者15秒钟超时
+            result, param = libnet.wait(TASK_NAME, 15000, netc)
+            log.info("tcp_server_task_func", "wait result", result, param)
+
+            -- 如果连接异常,则退出循环
+            if not result then
+                log.info("tcp_server_task_func", "客户端断开")
+                break
+            end
+        end
+
+        -- 出现异常    
+        ::EXCEPTION_PROC::
+
+        -- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
+        tcp_server_sender.exception_proc()
+
+        -- 如果存在socket server对象
+        if netc then
+            -- 关闭socket server连接
+            libnet.close(TASK_NAME, 5000, netc)
+
+            -- 释放socket server对象
+            socket.release(netc)
+            netc = nil  
+        end
+
+        -- 等待5秒后,再次尝试创建新的连接
+        sys.wait(5000)
+    end
+end
+
+--创建并且启动一个task
+--运行这个task的主函数tcp_server_main_task_func
+sys.taskInitEx(tcp_server_main_task_func, TASK_NAME, tcp_server_main_cbfunc)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+-- --[[
+-- @module  tcp_server_main
+-- @summary TCP服务器主应用功能模块
+-- @version 1.0
+-- @date    2025.07.01
+-- @author  AI Assistant
+-- @usage
+-- 本文件为TCP服务器主应用功能模块,核心业务逻辑为:
+-- 1、创建一个TCP服务器,监听指定端口;
+-- 2、持续监听并接受客户端连接;
+-- 3、为每个客户端连接创建独立的任务处理数据收发;
+
+-- 本文件没有对外接口,直接在main.lua中require "tcp_server_main"就可以加载运行;
+-- ]]
+
+-- local libnet = require "libnet"
+
+-- -- 加载TCP服务器数据接收功能模块
+-- local tcp_server_receiver = require "tcp_server_receiver"
+-- -- 加载TCP服务器数据发送功能模块
+-- local tcp_server_sender = require "tcp_server_sender"
+
+-- -- 任务名称
+-- local TASK_NAME = "TCP_SERVER_TASK"
+
+-- -- 客户端连接处理任务
+-- local function client_handler_task(client_socket, client_ip, client_port)
+--     log.info("client_handler_task", "开始处理客户端连接", client_ip, client_port)
+    
+--     local tx_buff = zbuff.create(1024)
+--     local rx_buff = zbuff.create(1024)
+    
+--     -- 发送欢迎消息
+--     sys.publish("TCP_SERVER_SEND", "欢迎连接到服务器", client_socket)
+    
+--     -- 数据收发循环
+--     while true do
+--         -- 数据接收处理
+--         local recv_result, data = tcp_server_receiver.proc(client_socket, rx_buff)
+--         if not recv_result then
+--             log.info("client_handler_task", "客户端断开", client_ip, client_port)
+--             break
+--         end
+        
+--         -- 数据发送处理
+--         if not tcp_server_sender.proc(client_socket, tx_buff) then
+--             log.info("client_handler_task", "发送失败", client_ip, client_port)
+--             break
+--         end
+        
+--         -- 等待网络事件
+--         local result, param = libnet.wait("CLIENT_"..client_port, 5000, client_socket)
+--         if not result then
+--             log.info("client_handler_task", "客户端超时或断开", client_ip, client_port, result, param)
+--             break
+--         end
+--     end
+    
+--     -- 关闭客户端连接
+--     libnet.close("CLIENT_"..client_port, 5000, client_socket)
+--     log.info("client_handler_task", "客户端连接处理结束", client_ip, client_port)
+-- end
+
+-- -- 处理接收到的数据
+-- local function tcp_data_handler(_, data, remote_ip, remote_port, client_socket)
+--     log.info("tcp_data_handler", "收到数据", data, remote_ip, remote_port)
+    
+--     -- 示例:回声功能,将接收到的数据原样发送回去
+--     sys.publish("TCP_SERVER_SEND", data, client_socket)
+-- end
+
+-- -- 处理未识别的消息
+-- local function netCB(msg)
+--     log.info("未处理消息", msg[1], msg[2], msg[3], msg[4])
+-- end
+
+-- -- TCP服务器监听任务函数
+-- local function tcp_server_listener_task(port, adapter)
+--     log.info("tcp_server_listener_task", "准备监听端口", socket.localIP(adapter), port)
+    
+--     -- 创建监听socket
+--     local listen_socket = socket.create(adapter, TASK_NAME)
+    
+--     if not listen_socket then
+--         log.error("tcp_server_listener_task", "创建监听socket失败")
+--         return
+--     end
+    
+--     socket.debug(listen_socket, true)
+--     socket.config(listen_socket, port)
+    
+--     -- 等待网络连接
+--     local result = libnet.waitLink(TASK_NAME, 0, listen_socket)
+--     if not result then
+--         log.error("tcp_server_listener_task", "等待网络连接失败")
+--         socket.release(listen_socket)
+--         return
+--     end
+    
+--     -- 开始监听
+--     result = libnet.listen(TASK_NAME, 0, listen_socket)
+--     if not result then
+--         log.error("tcp_server_listener_task", "监听失败")
+--         socket.release(listen_socket)
+--         return
+--     end
+    
+--     log.info("tcp_server_listener_task", "开始监听端口", port)
+    
+--     -- 主监听循环
+--     while true do
+--         -- 接受客户端连接
+--         local client_socket, client_info = socket.accept(listen_socket, nil)
+        
+--         if client_socket then
+--             local client_ip, client_port = socket.getpeerip(client_socket)
+--             log.info("tcp_server_listener_task", "接受客户端连接", client_ip, client_port)
+            
+--             -- 为每个客户端创建独立的任务
+--             sys.taskInit(client_handler_task, client_socket, client_ip, client_port)
+--         else
+--             -- 接受连接失败,等待一段时间后重试
+--             log.warn("tcp_server_listener_task", "接受连接失败")
+--             sys.wait(1000)
+--         end
+        
+--         -- 检查监听socket是否仍然有效
+--         if not socket.isValid(listen_socket) then
+--             log.error("tcp_server_listener_task", "监听socket无效,重新创建")
+--             socket.release(listen_socket)
+            
+--             -- 重新创建监听socket
+--             listen_socket = socket.create(adapter, TASK_NAME)
+--             if not listen_socket then
+--                 log.error("tcp_server_listener_task", "重新创建监听socket失败")
+--                 break
+--             end
+            
+--             socket.config(listen_socket, port)
+--             libnet.listen(TASK_NAME, 0, listen_socket)
+--         end
+        
+--         sys.wait(100)  -- 短暂等待,避免忙循环
+--     end
+    
+--     -- 关闭监听socket
+--     socket.release(listen_socket)
+-- end
+
+-- -- TCP服务器演示函数
+-- function tcp_server_demo(port, adapter)
+--     sysplus.taskInitEx(tcp_server_listener_task, TASK_NAME, netCB, port, adapter)
+-- end
+
+-- -- 订阅接收到的数据
+-- sys.subscribe("TCP_SERVER_RECV", tcp_data_handler)
+
+-- -- 启动TCP服务器(默认端口和适配器)
+-- sys.taskInit(function()
+--     sys.wait(3000)  -- 等待系统初始化
+--     tcp_server_demo(8080, socket.LWIP_ETH)  -- 监听8080端口,使用以太网适配器
+-- end)

+ 87 - 0
module/Air8000/demo/socket/server/tcp/tcp_server_receiver.lua

@@ -0,0 +1,87 @@
+--[[
+@module  tcp_server_receiver
+@summary tcp server socket数据接收应用功能模块 
+@version 1.0
+@date    2025.09.15
+@author  王世豪
+@usage
+本文件为tcp server 数据接收应用功能模块,核心业务逻辑为:
+从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;
+
+本文件的对外接口有2个:
+1、tcp_server_receiver.proc(netc):数据接收应用逻辑处理入口,在tcp_server_main.lua中调用;
+2、sys.publish("RECV_DATA_FROM_CLIENT", "recv from tcp server: ", data):
+    将接收到的数据通过消息"RECV_DATA_FROM_CLIENT"发布出去;
+    需要处理数据的应用功能模块订阅处理此消息即可,本demo项目中uart_app.lua中订阅处理了本消息;
+]]
+
+local tcp_server_receiver = {}
+
+-- socket数据接收缓冲区
+local recv_buff = nil
+
+--[[
+检查socket server是否收到数据,如果收到数据,读取并且处理完所有数据
+@api tcp_server_receiver.proc(netc)
+
+@param1 netc userdata
+表示由socket.create接口创建的socket server对象;
+必须传入,不允许为空或者nil;
+
+@return1 result bool
+表示处理结果,成功为true,失败为false
+
+@usage
+-- 示例:处理tcp server接收数据
+tcp_server_receiver.proc(netc)
+]]
+function tcp_server_receiver.proc(netc)
+    -- 如果socket数据接收缓冲区还没有申请过空间,则先申请内存空间
+    if recv_buff==nil then
+        recv_buff = zbuff.create(1024)
+        -- 当recv_buff不再使用时,不需要主动调用recv_buff:free()去释放
+        -- 因为Lua的垃圾处理器会自动释放recv_buff所申请的内存空间
+        -- 如果等不及垃圾处理器自动处理,在确定以后不会再使用recv_buff时,则可以主动调用recv_buff:free()释放内存空间
+    end
+
+    -- 循环从内核的缓冲区读取接收到的数据
+    -- 如果读取失败,返回false,退出循环
+    -- 如果读取成功,处理数据,并且继续循环读取
+    -- 如果读取成功,并且读出来的数据为空,表示已经没有数据可读,返回true,退出循环
+    while true do
+        -- 从内核的缓冲区中读取数据到recv_buff中
+        local succ, param = socket.rx(netc, recv_buff)
+
+        -- 读取数据失败
+        -- 有两种情况:
+        -- 1、recv_buff扩容失败
+        -- 2、socket server和client之间的连接断开
+        if not succ then
+            log.info("tcp_server_receiver.proc", "socket.rx error", param)
+            return false
+        end
+
+        -- 如果读取到了数据, used()就必然大于0, 进行处理
+        if recv_buff:used() > 0 then
+            log.info("tcp_server_receiver.proc", "recv data len", recv_buff:used())
+            
+            -- 读取socket数据接收缓冲区中的数据,赋值给data
+            local data = recv_buff:query()
+
+            log.info("tcp_server_receiver.proc", "recv data", data)
+
+            -- 将数据通过"RECV_DATA_FROM_CLIENT"消息publish出去,给其他应用模块处理
+            sys.publish("RECV_DATA_FROM_CLIENT", data)
+
+            -- 清空socket数据接收缓冲区中的数据
+            recv_buff:del()
+        else
+            -- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
+            break
+        end
+    end
+
+    return true
+end
+
+return tcp_server_receiver

+ 136 - 0
module/Air8000/demo/socket/server/tcp/tcp_server_sender.lua

@@ -0,0 +1,136 @@
+--[[
+@module  tcp_server_sender
+@summary tcp server socket数据发送应用功能模块 
+@version 1.0
+@date    2025.09.15
+@author  王世豪
+@usage
+本文件为tcp server socket数据发送应用功能模块,核心业务逻辑为:
+1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列send_queue中;
+2、tcp_server_main主任务调用tcp_server_sender.proc接口,遍历队列send_queue,逐条发送数据到server;
+3、tcp server socket和server之间的连接如果出现异常,tcp_server_main主任务调用tcp_server_sender.exception_proc接口,丢弃掉队列send_queue中未发送的数据;
+4、任何一条数据无论发送成功还是失败,只要这条数据有回调函数,都会通过回调函数通知数据发送方;
+
+本文件的对外接口有3个:
+1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func):订阅"SEND_DATA_REQ"消息;
+   其他应用模块如果需要发送数据,直接sys.publish这个消息即可,将需要发送的数据以及回调函数和回调参数一起publish出去;
+   本demo项目中uart_app.lua和timer_app.lua中publish了这个消息;
+2、tcp_server_sender.proc:数据发送应用逻辑处理入口,在tcp_server_main.lua中调用;
+3、tcp_server_sender.exception_proc:数据发送应用逻辑异常处理入口,在tcp_server_main.lua中调用;
+]]
+
+local tcp_server_sender = {}
+
+local libnet = require "libnet"
+
+--[[
+数据发送队列,数据结构为:
+{
+    [1] = {data="send from tag: data1", cb=callback_struct1},
+    [2] = {data="send from tag: data2", cb=callback_struct2},
+}
+data的内容为带发送方标识前缀的实际数据,必须存在;
+ip为目标IP地址,可以不存在;
+port为目标端口号,可以不存在;
+cb为用户回调函数结构,可以不存在;
+]]
+local send_queue = {}
+
+-- tcp_server_main的任务名
+tcp_server_sender.TASK_NAME = "tcp_server_main"
+
+-- "SEND_DATA_REQ"消息的处理函数
+local function send_data_req_proc_func(tag, data, cb)
+    -- 将原始数据增加前缀,然后插入到发送队列send_queue中
+    table.insert(send_queue, {data="send from "..tag..": "..data, cb=cb})
+    -- 通知tcp_server_main主任务有数据需要发送
+    -- tcp_server_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
+    sys.sendMsg(tcp_server_sender.TASK_NAME, socket.EVENT, 0)
+end
+
+--[[
+检查socket server是否需要发送数据,如果需要发送数据,读取并且发送完发送队列中的所有数据
+
+@api tcp_server_sender.proc(task_name, socket_server)
+
+@param1 task_name string
+表示socket.create接口创建socket server对象时所处的task的name;
+必须传入,不允许为空或者nil;
+
+@param2 socket_server userdata
+表示由socket.create接口创建的socket server对象;
+必须传入,不允许为空或者nil;
+
+@return1 result bool
+表示处理结果,成功为true,失败为false
+
+@usage
+tcp_server_sender.proc("tcp_server_main", socket_server)
+]]
+function tcp_server_sender.proc(task_name, netc)
+    local send_item
+    local result, buff_full
+
+    -- 遍历数据发送队列send_queue
+    while #send_queue>0 do
+        -- 取出来第一条数据赋值给send_item
+        -- 同时从队列send_queue中删除这一条数据
+        send_item = table.remove(send_queue,1)
+
+        -- 发送这条数据,超时时间15秒钟
+        result, buff_full = libnet.tx(task_name, 15000, netc, send_item.data)
+
+        -- 检查发送结果
+        if not result then
+            log.error("tcp_server_sender.proc", "libnet.tx error")
+            
+            -- 如果当前发送的数据有用户回调函数,则执行用户回调函数
+            if send_item.cb and send_item.cb.func then
+                send_item.cb.func(false, send_item.cb.para)
+            end
+            
+            return false
+        end
+        
+        -- 如果内核固件中缓冲区满了,则将send_item再次插入到send_queue的队首位置,等待下次尝试发送
+        if buff_full then
+            log.error("tcp_client_sender.proc", "buffer is full, wait for the next time")
+            table.insert(send_queue, 1, send_item)
+            return true
+        end
+
+        log.info("tcp_server_sender.proc", "send success")
+        -- 发送成功,如果当前发送的数据有用户回调函数,则执行用户回调函数
+        if send_item.cb and send_item.cb.func then
+            send_item.cb.func(true, send_item.cb.para)
+        end
+    end
+
+    return true
+end
+
+--[[
+socket server连接出现异常时,清空等待发送的数据,并且执行发送方的回调函数
+
+@api tcp_server_sender.exception_proc()
+
+@usage
+tcp_server_sender.exception_proc()
+]]
+function tcp_server_sender.exception_proc()
+    -- 遍历数据发送队列send_queue
+    while #send_queue>0 do
+        local send_item = table.remove(send_queue,1)
+        -- 发送失败,如果当前发送的数据有用户回调函数,则执行用户回调函数
+        if send_item.cb and send_item.cb.func then
+            send_item.cb.func(false, send_item.cb.para)
+        end
+    end
+end
+
+-- 订阅"SEND_DATA_REQ"消息;
+-- 其他应用模块如果需要发送数据,直接sys.publish这个消息即可,将需要发送的数据以及回调函数和回调参数一起publish出去;
+-- 本demo项目中uart_app.lua和timer_app.lua中publish了这个消息;
+sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)
+
+return tcp_server_sender

+ 71 - 0
module/Air8000/demo/socket/server/timer_app.lua

@@ -0,0 +1,71 @@
+--[[
+@module  timer_app
+@summary 定时器应用功能模块 
+@version 1.0
+@date    2025.09.15
+@author  王世豪
+@usage
+本文件为定时器应用功能模块,核心业务逻辑为:
+创建一个5秒的循环定时器,每次产生一段数据,通知TCP或UDP server进行处理;
+
+本文件的对外接口有一个:
+1、sys.publish("SEND_DATA_REQ", "timer", data, ip, port, {func=send_data_cbfunc, para="timer"..data}),通过publish通知TCP或UDP server数据发送功能模块发送data数据;
+    数据发送结果通过执行回调函数send_data_cbfunc通知本功能模块;
+]]
+
+local config = {
+    enable_udp = true,            -- 是否启用UDP发送
+    enable_tcp = false             -- 是否启用TCP发送
+}
+
+local data = 1
+
+local udp_server_receiver = require "udp_server_receiver"
+
+-- 数据发送结果回调函数
+-- result:发送结果,true为发送成功,false为发送失败
+-- para:回调参数,sys.publish("SEND_DATA_REQ", "timer", data, ip, port, {func=send_data_cbfunc, para="timer"..data})中携带的para
+local function send_data_cbfunc(result, para)
+    log.info("send_data_cbfunc", result, para)
+    -- 无论上一次发送成功还是失败,启动一个5秒的定时器,5秒后发送下次数据
+    sys.timerStart(send_data_req_timer_cbfunc, 5000)
+end
+
+-- 定时器回调函数
+function send_data_req_timer_cbfunc()
+    -- 发布消息"SEND_DATA_REQ"
+    -- 携带的第一个参数"timer"表示是定时器应用模块发布的消息
+    -- 携带的第二个参数data为要发送的原始数据
+    -- 携带的第三个参数client_ip为目标IP地址
+    -- 携带的第四个参数port为目标端口号
+    -- 携带的第五个参数cb为发送结果回调(可以为空,如果为空,表示不关心TCP或UDP server发送数据成功还是失败),其中:
+    --       cb.func为回调函数(可以为空,如果为空,表示不关心TCP或UDP server发送数据成功还是失败)
+    --       cb.para为回调函数的第二个参数(可以为空),回调函数的第一个参数为发送结果(true表示成功,false表示失败)
+
+    -- UDP发送处理
+    if config.enable_udp then
+        -- 获取客户端信息
+        local client_info = udp_server_receiver.get_client_info()
+        
+        -- 检查是否有客户端IP和端口
+        if client_info.ip and client_info.port then
+            -- 使用记录的客户端信息发送
+            sys.publish("SEND_DATA_REQ", "timer", data, client_info.ip, client_info.port, {func=send_data_cbfunc, para="udp_timer"..data})
+        else
+            -- 未收到过客户端数据,提示错误
+            log.error("timer_app", "尚未收到客户端数据, 无法确定目标IP和端口")
+            sys.timerStart(send_data_req_timer_cbfunc, 5000)
+        end
+        -- TCP发送处理
+    elseif config.enable_tcp then
+        -- 当前TCP server与client是一对一连接,publish的消息可忽略ip和port参数
+        sys.publish("SEND_DATA_REQ", "timer", data, {func=send_data_cbfunc, para="tcp_timer"..data})  
+    end
+
+    data = data + 1
+    log.info("send_data_req_timer_cbfunc", data)
+end
+
+-- 启动一个5秒的单次定时器
+-- 时间到达后,执行一次send_data_req_timer_cbfunc函数
+sys.timerStart(send_data_req_timer_cbfunc, 5000)

+ 101 - 0
module/Air8000/demo/socket/server/uart_app.lua

@@ -0,0 +1,101 @@
+--[[
+@module  uart_app
+@summary 串口应用功能模块 
+@version 1.0
+@date    2025.09.15
+@author  王世豪
+@usage
+本文件为串口应用功能模块,核心业务逻辑为:
+1、打开uart1,波特率115200,数据位8,停止位1,无奇偶校验位;
+2、uart1和pc端的串口工具相连;
+3、从uart1接收到pc端串口工具发送的数据后,通知TCP或UDP server进行处理;
+4、收到TCP或UDP server从client接收到的数据后,将数据通过uart1发送到pc端串口工具;
+
+本文件的对外接口有两个:
+1、sys.publish("SEND_DATA_REQ", "uart", read_buf, client_ip, port),通过publish通知TCP或UDP server数据发送功能模块发送read_buf数据,不关心数据发送成功还是失败;
+2、sys.subscribe("RECV_DATA_FROM_CLIENT", recv_data_from_client_proc),订阅RECV_DATA_FROM_CLIENT消息,处理消息携带的数据;
+]]
+
+
+-- 使用UART1
+local UART_ID = 1
+-- 串口接收数据缓冲区
+local read_buf = ""
+
+local config = {
+    enable_udp = true,              -- 是否启用UDP发送
+    enable_tcp = false               -- 是否启用TCP发送
+}
+
+-- 加载UDP服务器数据接收功能模块
+local udp_server_receiver = require "udp_server_receiver"
+
+-- 将前缀prefix和数据data拼接
+-- 然后末尾增加回车换行两个字符,通过uart发送出去,方便在PC端换行显示查看
+local function recv_data_from_client_proc(data)
+    log.info("uart_app.recv_data_from_client_proc", data)
+    uart.write(UART_ID, data.."\r\n")
+end
+
+local function concat_timeout_func()
+    -- 如果存在尚未处理的串口缓冲区数据;
+    -- 将数据通过publish通知其他应用功能模块处理;
+    -- 然后清空本文件的串口缓冲区数据
+    if read_buf:len() > 0 then
+        if config.enable_udp then
+            -- 获取客户端信息
+            local client_info = udp_server_receiver.get_client_info()
+            -- 检查是否有客户端IP和端口
+            if client_info.ip and client_info.port then
+                -- 使用记录的客户端信息
+                sys.publish("SEND_DATA_REQ", "uart", read_buf, client_info.ip, client_info.port)
+            else
+                -- 未收到过客户端数据,提示错误
+                log.error("uart_app", "尚未收到客户端数据,无法确定目标IP和端口")
+            end
+        elseif config.enable_tcp then
+            -- 当前TCP server与client是一对一连接,publish的消息可忽略ip和port参数
+            sys.publish("SEND_DATA_REQ", "uart", read_buf)
+        end
+
+        read_buf = ""
+    end
+end
+
+
+-- UART1的数据接收中断处理函数,UART1接收到数据时,会执行此函数
+local function read()
+    local s
+    while true do
+        -- 非阻塞读取UART1接收到的数据,最长读取1024字节
+        s = uart.read(UART_ID, 1024)
+        
+        -- 如果从串口没有读到数据
+        if not s or s:len() == 0 then
+            -- 启动50毫秒的定时器,如果50毫秒内没收到新的数据,则处理当前收到的所有数据
+            -- 这样处理是为了防止将一大包数据拆分成多个小包来处理
+            -- 例如pc端串口工具下发1100字节的数据,可能会产生将近20次的中断进入到read函数,才能读取完整
+            -- 此处的50毫秒可以根据自己项目的需求做适当修改,在满足整包拼接完整的前提下,时间越短,处理越及时
+            sys.timerStart(concat_timeout_func, 50)
+            -- 跳出循环,退出本函数
+            break
+        end
+
+        log.info("uart_app.read len", s:len())
+        -- log.info("uart_app.read", s)
+
+        -- 将本次从串口读到的数据拼接到串口缓冲区read_buf中
+        read_buf = read_buf..s
+    end
+end
+
+-- 初始化UART1,波特率115200,数据位8,停止位1
+uart.setup(UART_ID, 115200, 8, 1)
+
+-- 注册UART1的数据接收中断处理函数,UART1接收到数据时,会执行read函数
+uart.on(UART_ID, "receive", read)
+
+-- 订阅"RECV_DATA_FROM_CLIENT"消息的处理函数recv_data_from_client_proc
+-- 收到"RECV_DATA_FROM_CLIENT"消息后,会执行函数recv_data_from_client_proc   
+sys.subscribe("RECV_DATA_FROM_CLIENT", recv_data_from_client_proc)
+

+ 112 - 0
module/Air8000/demo/socket/server/udp/udp_server_main.lua

@@ -0,0 +1,112 @@
+--[[
+@module  udp_server_main
+@summary udp server 主应用功能模块 
+@version 1.0
+@date    2025.09.16
+@author  王世豪
+@usage
+本文件为udp server 主应用功能模块,核心业务逻辑为:
+1、创建一个udp server,监听指定端口;
+2、处理通信异常,出现异常后,重新初始化UDP服务以恢复正常数据接收;
+3、调用udp_server_receiver和udp_server_sender中的外部接口,进行数据收发处理;
+
+本文件没有对外接口,直接在main.lua中require "udp_server_main"就可以加载运行;
+]]
+
+local udpsrv = require "udpsrv"
+
+-- 加载UDP服务器数据接收功能模块
+local udp_server_receiver = require "udp_server_receiver"
+-- 加载UDP服务器数据发送功能模块
+local udp_server_sender = require "udp_server_sender"
+
+-- 服务器监听端口
+local SERVER_PORT = 50003
+-- 服务器主题(用于接收消息)
+SERVER_TOPIC = "udp_server"
+
+-- udp server socket的任务处理函数
+local function udp_server_main_task_func() 
+    local udp_server
+    local ret, data, remote_ip, remote_port
+
+    while true do
+        -- 如果当前时间点设置的网卡还没有连接成功,一直在这里循环等待
+        while not socket.adapter(socket.dft()) do
+            log.warn("udp_client_main_task_func", "wait IP_READY", socket.dft())
+            -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+            -- 或者等待1秒超时退出阻塞等待状态;
+            -- 注意:此处的1000毫秒超时不要修改的更长;
+            -- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+            -- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+            -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+            sys.waitUntil("IP_READY", 1000)
+        end
+
+        -- 检测到了IP_READY消息
+        log.info("udp_server_main_task_func", "recv IP_READY", socket.dft())
+
+        -- 创建UDP服务器对象
+        -- 注意:udpsrv.create有3个参数,最后一个参数是网络适配器编号
+        udp_server = udpsrv.create(SERVER_PORT, SERVER_TOPIC, socket.dft())
+
+        if not udp_server then
+            log.error("udp_server_main_task_func", "udpsrv.create error")
+            goto EXCEPTION_PROC
+        end
+
+        log.info("udp_server_main_task_func", "UDP server started on port", SERVER_PORT)
+
+        -- 发送一条广播消息,通知端口号为50000的客户端,UDP服务器已启动
+        udp_server:send("UDP Server is UP", "255.255.255.255", 50000)
+
+        -- 数据收发以及网络连接异常事件总处理逻辑
+        while true do
+            -- 数据发送处理
+            if not udp_server_sender.proc(udp_server) then
+                log.error("udp_server_main_task_func", "udp_server_sender.proc error")
+            end
+
+            -- 等待接收数据事件
+            ret, data, remote_ip, remote_port = sys.waitUntil(SERVER_TOPIC, 15000)
+
+            if ret then
+                -- 判断是否是发送就绪事件(通过 data 内容或 remote_ip 是否为 nil)
+                if data == "SEND_READY" and remote_ip == nil then
+                    -- 这是发送就绪事件,无需处理接收数据,直接继续循环以发送数据
+                    log.info("udp_server_main_task_func", "send ready event received")
+                -- 网络异常事件
+                elseif data == "SOCKET_CLOSED" then
+                    goto EXCEPTION_PROC
+                else
+                    -- 真实接收到的数据
+                    if not udp_server_receiver.proc(data, remote_ip, remote_port) then
+                        log.error("udp_server_main_task_func", "udp_server_receiver.proc error")
+                    end
+                end
+            else
+                -- 超时,发送一条心跳广播
+                log.info("udp_server_main_task_func", "No data received, sending broadcast heartbeat")
+                udp_server:send("UDP Server Heartbeat", "255.255.255.255", 50000)
+            end
+        end
+
+        ::EXCEPTION_PROC::
+
+        -- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
+        udp_server_sender.exception_proc()
+
+        -- 关闭UDP服务器
+        if udp_server then
+            udp_server:close()
+            udp_server = nil
+        end
+        
+        -- 5秒后跳转到循环体开始位置,重建udp server
+        sys.wait(5000)
+    end
+end
+
+--创建并且启动一个task
+--运行这个task的主函数udp_server_main_task_func
+sys.taskInit(udp_server_main_task_func)

+ 83 - 0
module/Air8000/demo/socket/server/udp/udp_server_receiver.lua

@@ -0,0 +1,83 @@
+--[[
+@module  udp_server_receiver
+@summary udp server socket数据接收应用功能模块 
+@version 1.0
+@date    2025.09.16
+@author  王世豪
+@usage
+本文件为udp server socket数据接收应用功能模块,核心业务逻辑为:
+从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;
+
+本文件的对外接口有2个:
+1、udp_server_receiver.proc(socket_server):数据接收应用逻辑处理入口,在udp_server_main.lua中调用;
+2、sys.publish("RECV_DATA_FROM_CLIENT", data, remote_ip, remote_port):
+    将接收到的数据通过消息"RECV_DATA_FROM_CLIENT"发布出去;
+    需要处理数据的应用功能模块订阅处理此消息即可;
+]]
+
+local udp_server_receiver = {}
+
+-- 客户端信息
+local client_info = {}
+-- 标记是否已经通知过客户端信息更新
+local client_info_notified = false
+
+-- 获取客户端信息
+function udp_server_receiver.get_client_info()
+    return client_info
+end
+
+-- 重置客户端信息
+function udp_server_receiver.reset_client_info()
+    client_info.ip = nil
+    client_info.port = nil
+    -- 重置通知标记,以便下次收到客户端信息时可以重新通知
+    client_info_notified = false
+end
+
+-- 初始化客户端信息
+udp_server_receiver.reset_client_info()
+
+--[[
+检查udp server是否收到数据,如果收到数据,读取并且处理完所有数据
+
+@api udp_server_receiver.proc(data, remote_ip, remote_port)
+
+@param1 data string
+表示接收到的数据;
+
+@param2 remote_ip string
+表示发送数据的client的IP地址;
+
+@param3 remote_port number
+表示发送数据的client的端口号;
+
+@return1 result bool
+表示处理结果,成功为true,失败为false
+
+@usage
+udp_server_receiver.proc(data, remote_ip, remote_port)
+]]
+function udp_server_receiver.proc(data, remote_ip, remote_port)
+    log.info("udp_server_receiver.proc", "收到数据", data, "来自", remote_ip, remote_port)
+    
+    -- -- 更新客户端信息
+    -- local info_changed = (client_info.ip ~= remote_ip or client_info.port ~= remote_port)
+    client_info.ip = remote_ip
+    client_info.port = remote_port
+    -- -- 在客户端信息发生变化时发布通知
+    -- if info_changed and not client_info_notified then
+    --     -- 发布消息通知其他模块客户端信息已更新
+    --     sys.publish("UDP_CLIENT_INFO_UPDATED")
+    --     client_info_notified = true
+    -- end
+
+    log.info("client_info", client_info.ip, client_info.port)
+
+    -- 将接收到的数据通过消息发布出去
+    sys.publish("RECV_DATA_FROM_CLIENT", data, remote_ip, remote_port)
+    
+    return true
+end
+
+return udp_server_receiver

+ 115 - 0
module/Air8000/demo/socket/server/udp/udp_server_sender.lua

@@ -0,0 +1,115 @@
+--[[
+@module  udp_server_sender
+@summary udp server socket数据发送应用功能模块 
+@version 1.0
+@date    2025.09.15
+@author  王世豪
+@usage
+本文件为udp server socket数据发送应用功能模块,核心业务逻辑为:
+1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列send_queue中;
+2、udp_server_main主任务调用udp_server_sender.proc接口,遍历队列send_queue,逐条发送数据到client;
+3、udp server socket如果出现异常,udp_server_main主任务调用udp_server_sender.exception_proc接口,丢弃掉队列send_queue中未发送的数据;
+4、任何一条数据无论发送成功还是失败,只要这条数据有回调函数,都会通过回调函数通知数据发送方;
+
+本文件的对外接口有3个:
+1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func):订阅"SEND_DATA_REQ"消息;
+   其他应用模块如果需要发送数据,直接sys.publish这个消息即可,将需要发送的数据、目标IP、目标端口以及回调函数和回调参数一起publish出去;
+2、udp_server_sender.proc:数据发送应用逻辑处理入口,在udp_server_main.lua中调用;
+3、udp_server_sender.exception_proc:数据发送应用逻辑异常处理入口,在udp_server_main.lua中调用;
+]]
+
+local udp_server_sender = {}
+
+--[[
+数据发送队列,数据结构为:
+{
+    [1] = {data="data1", ip="127.0.0.1", port=8888, cb={func=callback_function1, para=callback_para1}},
+    [2] = {data="data2", ip="127.0.0.1", port=8888, cb={func=callback_function2, para=callback_para2}},
+}
+data的内容为真正要发送的数据,必须存在;
+ip的内容为目标IP,必须存在;
+port的内容为目标端口,必须存在;
+func的内容为数据发送结果的用户回调函数,可以不存在
+para的内容为数据发送结果的用户回调函数的回调参数,可以不存在;
+]]
+local send_queue = {}
+
+-- "SEND_DATA_REQ"消息的处理函数
+local function send_data_req_proc_func(tag, data, ip, port, cb)
+    -- 将原始数据增加前缀,然后插入到发送队列send_queue中
+    table.insert(send_queue, {data="send from "..tag..": "..data, ip=ip, port=port, cb=cb}) 
+    log.info("send_queue", #send_queue)
+    -- 通知主任务:有数据待发送,唤醒阻塞
+    sys.publish("udp_server", "SEND_READY", nil, nil)  -- 后两个参数为 remote_ip 和 remote_port,这里置为 nil
+end
+
+--[[
+检查udp server是否需要发送数据,如果需要发送数据,读取并且发送完发送队列中的所有数据
+
+@api udp_server_sender.proc(udp_server)
+
+@param 
+表示由udpsrv.create接口创建的udp_server对象;
+必须传入,不允许为空或者nil;
+
+@return1 result bool
+表示处理结果,成功为true,失败为false
+
+@usage
+udp_server_sender.proc(udp_server)
+]]
+function udp_server_sender.proc(udp_server)
+    local send_item
+    local result
+
+    -- 遍历数据发送队列send_queue
+    while #send_queue>0 do
+        -- 取出来第一条数据赋值给send_item
+        -- 同时从队列send_queue中删除这一条数据
+        send_item = table.remove(send_queue,1)
+
+        result = udp_server:send(send_item.data, send_item.ip, send_item.port)
+
+        -- 发送失败
+        if not result then
+            log.error("udp_server_sender.proc", "udp_server:send error")
+
+            -- 如果当前发送的数据有用户回调函数,则执行用户回调函数
+            if send_item.cb and send_item.cb.func then
+                send_item.cb.func(false, send_item.cb.para)
+            end
+
+            return false
+        end
+
+        log.info("udp_server_sender.proc", "send success", send_item.ip, send_item.port)
+        -- 发送成功,如果当前发送的数据有用户回调函数,则执行用户回调函数
+        if send_item.cb and send_item.cb.func then
+            send_item.cb.func(true, send_item.cb.para)
+        end
+    end
+
+    return true
+end
+
+-- UDP服务器出现异常时,清空等待发送的数据,并且执行发送方的回调函数
+function udp_server_sender.exception_proc()
+    -- 遍历数据发送队列send_queue
+    while #send_queue>0 do
+        local send_item = table.remove(send_queue,1)
+        -- 发送失败,如果当前发送的数据有用户回调函数,则执行用户回调函数
+        if send_item.cb and send_item.cb.func then
+            send_item.cb.func(false, send_item.cb.para)
+        end
+    end
+end
+
+-- 订阅"SEND_DATA_REQ"消息;
+-- 其他应用模块如果需要发送数据,直接sys.publish这个消息即可,将需要发送的数据以及回调函数和回调参数一起publish出去;
+-- 参数格式: sys.publish("SEND_DATA_REQ", tag, data, ip, port, cb)
+-- tag: 发送方标识, data: 要发送的数据, ip: 目标IP, port: 目标端口, cb: 回调函数
+-- 例如: sys.publish("SEND_DATA_REQ", "app1", "hello client", "192.168.1.100", 50000)
+-- 本demo项目中uart_app.lua和timer_app.lua中publish了这个消息;
+sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)
+
+return udp_server_sender