init: Air780EPM 官方 LuatOS 项目代码基线

- 来源: 合宙 LuatOS 官方仓库 air780epm 模块完整代码
- 路径: luatos/air780epm/module/Air780EPM/demo 含官方 demo(含 mqtt/mqtts/socket/uart 等)
- 后续: 基于 demo 开发 UART<->MQTT 数据上报功能
This commit is contained in:
wangfq
2026-08-31 08:53:49 +08:00
commit 173dd6874f
813 changed files with 125484 additions and 0 deletions
@@ -0,0 +1,171 @@
## 功能模块介绍
1、main.lua:主程序入口;
2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的四种网卡(单4g网卡,单wifi网卡,单spi以太网卡,多网卡)中的任何一种网卡;
3、tcp文件夹:tcp client连接以及数据收发处理逻辑;
4、udp文件夹:udp client连接以及数据收发处理逻辑;
5、tcp_ssl文件夹:tcp ssl client连接以及数据收发处理逻辑;
6、tcp_ssl_ca文件夹:tcp ssl client单向认证连接以及数据收发处理逻辑;
7、network_watchdog.lua:网络环境检测看门狗;
8、timer_app.lua:通知四个client定时发送数据到服务器;
9、uart_app.lua:在四个client和uart外设之间透传数据;
## 系统消息介绍
1、"IP_READY":某种网卡已经获取到ip信息,仅仅获取到了ip信息,能否和外网连通还不确认;
2、"IP_LOSE":某种网卡已经掉网;
## 用户消息介绍
1、"RECV_DATA_FROM_SERVER"socket client收到服务器下发的数据后,通过此消息发布出去,给其他应用模块处理;
2、"SEND_DATA_REQ":其他应用模块发布此消息,通知socket client发送数据给服务器;
3、"FEED_NETWORK_WATCHDOG":网络环境检测看门狗的喂狗消息,在需要喂狗的地方发布此消息;
## 演示功能概述
1、创建四路socket连接,在目录中对应四个文件夹详情如下
- TCP文件夹功能为创建一个tcp client,连接tcp server
- UDP文件夹功能为创建一个udp client,连接udp server
- TCP_SSL文件夹功能为创建一个tcp ssl client,连接tcp ssl server,不做证书校验;
- TCP_SSL_CA文件夹功能为创建一个tcp ssl client,连接tcp ssl serverclient仅单向校验server的证书,server不校验client的证书和密钥文件;
2、每一路socket连接出现异常后,自动重连;
3、每一路socket连接,client按照以下几种逻辑发送数据给server
- 串口应用功能模块uart_app.lua,通过uart1接收到串口数据,将串口数据增加send from uart: 前缀后发送给server
- 定时器应用功能模块timer_app.lua,定时产生数据,将数据增加send from timer:前缀后发送给server
4、每一路socket连接,client收到server数据后,将数据增加recv from tcp/udp/tcp ssl/tcp ssl ca(四选一)server: 前缀后,通过uart1发送出去;
5、启动一个网络业务逻辑看门狗task,用来监控网络环境,如果连续长时间工作不正常,重启整个软件系统;
6、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
(1) netdrv_4g4G网卡
(2) netdrv_wifiWIFI STA网卡
(3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
(4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
## 演示硬件环境
![](https://docs.openluat.com/air780epm/luatos/app/driver/eth/image/RFSvb75NRoEWqYxfCRVcVrOKnsf.jpg)
1、Air780EPM 1.3版本开发板一块+可上网的sim卡一张+4g天线一根+网线一根:
- sim卡插入开发板的sim卡槽
- 天线装到开发板上
- 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口
2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air780EPM开发板和数据线的硬件接线方式为:
- Air780EPM开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
- USB转串口数据线,一般来说,白线连接开发板的UART1_TX,绿线连接开发板的UART1_RX,黑线连接核心板的GND,另外一端连接电脑USB口;
## 演示软件环境
1、Luatools下载调试工具
2、[780EPM V2008版本固件)](https://docs.openluat.com/air780epm/luatos/firmware/version/)(理论上,2025年7月26日之后发布的固件都可以)
3、PC端的串口工具,例如SSCOM、LLCOM等都可以;
4、PC端浏览器访问[合宙TCP/UDP web测试工具](https://iot.luatos.com/#/page6/netlab)
## 演示核心步骤
1、搭建好硬件环境
2、PC端浏览器访问[合宙TCP/UDP web测试工具](https://iot.luatos.com/#/page6/netlab),点击 打开TCP 按钮,会创建一个TCP server,将server的地址和端口赋值给tcp_client_main.lua中的SERVER_ADDR和SERVER_PORT两个变量
详细使用说明参考:[合宙 TCP/UDP web 测试工具使用说明](https://iot.luatos.com/#/p8000/netlab) 。
3、PC端浏览器访问[合宙TCP/UDP web测试工具](https://iot.luatos.com/#/page6/netlab),点击 打开UDP 按钮,会创建一个UDP server,将server的地址和端口赋值给udp_client_main.lua中的SERVER_ADDR和SERVER_PORT两个变量
详细使用说明参考:[合宙 TCP/UDP web 测试工具使用说明](https://iot.luatos.com/#/p8000/netlab) 。
4、PC端浏览器访问[合宙TCP/UDP web测试工具](https://iot.luatos.com/#/page6/netlab),点击 打开TCP SSL 按钮,会创建一个TCP SSL server,将server的地址和端口赋值给tcp_ssl_main.lua中的SERVER_ADDR和SERVER_PORT两个变量
详细使用说明参考:[合宙 TCP/UDP web 测试工具使用说明](https://iot.luatos.com/#/p8000/netlab) 。
5、demo脚本代码netdrv_device.lua中,按照自己的网卡需求启用对应的Lua文件
- 如果需要单4G网卡,打开require "netdrv_4g",其余注释掉
- 如果需要单WIFI STA网卡,打开require "netdrv_wifi",其余注释掉;同时netdrv_wifi.lua中的wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1),前两个参数,修改为自己测试时wifi热点的名称和密码;注意:仅支持2.4G的wifi,不支持5G的wifi
- 如果需要以太网卡,打开require "netdrv_eth_spi",其余注释掉
- 如果需要多网卡,打开require "netdrv_multiple",其余注释掉;同时netdrv_multiple.lua中的ssid = "茶室-降功耗,找合宙!", password = "Air123456", 修改为自己测试时wifi热点的名称和密码;注意:仅支持2.4G的wifi,不支持5G的wifi
6、Luatools烧录内核固件和修改后的demo脚本代码
7、烧录成功后,自动开机运行
8、[合宙TCP/UDP web测试工具](https://iot.luatos.com/#/page6/netlab)上创建的TCP server、UDP server、TCP SSL server,一共三个server,可以看到有设备连接上来,每隔5秒钟,会接收到一段类似于 send from timer: 1 的数据,最后面的数字每次加1,类似于以下效果:
``` lua
[2025-06-24 16:47:39.085]send from timer: 1
73656E642066726F6D2074696D65723A2031
[2025-06-24 16:47:43.247]send from timer: 2
73656E642066726F6D2074696D65723A2032
[2025-06-24 16:47:48.241]send from timer: 3
73656E642066726F6D2074696D65723A2033
```
9、打开PC端的串口工具,选择对应的端口,配置波特率115200,数据位8,停止位1,无奇偶校验位;
10、PC端的串口工具输入一段数据,点击发送,在[合宙TCP/UDP web测试工具](https://iot.luatos.com/#/page6/netlab)上的四个server页面都可以接收到数据,类似于以下效果:
``` lua
[2025-06-24 17:19:58.402]send from uart: kerjkjwr
73656E642066726F6D20756172743A206B65726A6B6A7772
```
11、在[合宙TCP/UDP web测试工具](https://iot.luatos.com/#/page6/netlab)的发送编辑框内,输入一段数据,点击发送,在PC端的串口工具上可以接收到这段数据,并且也能看到是哪一个server发送的,类似于以下效果:
``` lua
recv from tcp server: 123456798012345678901234567830
recv from udp server: 123456798012345678901234567830
recv from tcp_ssl server: 123456789901234
```
12、注意:第四路连接,连接的是baidu的https网站,连接成功后,Air780EPM每隔一段时间发数据给服务器,因为发送的不是http合法格式的数据,所以每隔一段时间服务器都会主动断开连接,断开连接后,Air780EPM会自动重连,如此循环,属于正常现象。
@@ -0,0 +1,91 @@
--[[
@module aircloud_data
@summary 网络驱动设备功能模块
@version 1.0
@date 2026年3月26日
@author 黄何
@usage
本文件为aircloud数据处理模块,核心业务逻辑为:根据aircloud服务器要求的格式,构建要发送的数据结构,并且定时发送给tcp client socket的发送功能模块;
1、构建要发送的数据结构,数据结构为一个table,table中每一个元素也是一个table,每个元素的字段含义如下:
field_meaning:字段含义,必须按照aircloud平台的要求进行定义,
aircloud平台要求的字段含义可以参考excloud中参数描述,使用时直接将excloud.FIELD_MEANINGS中定义的字段含义赋值给field_meaning即可;
如果需要发送自定义数据,无法在excloud.FIELD_MEANINGS中找到合适的字段含义,可以将field_meaning赋值为0,表示自定义数据,此时data_type和value的定义和格式也必须满足aircloud平台的要求;
data_type:数据类型,必须按照aircloud平台的要求进行定义,
aircloud平台要求的数据类型可以参考excloud.DATA_TYPES这个table中的定义,使用时直接将excloud.DATA_TYPES中定义的数据类型赋值给data_type即可;
value:数据值,按照field_meaning定义的字段含义和data_type定义的数据类型,构建对应的数据值;
2、定时发送构建好的数据结构给tcp client socket的发送功能模块;
本文件没有对外接口,使用时直接在main.lua中require "aircloud_data"就可以加载运行;
]]
local excloud = require "excloud"
-- 获取CPU温度的函数
local function cpu_temperature()
adc.open(adc.CH_CPU) -- 打开adc.CH_CPU通道
local cpu_temp = adc.get(adc.CH_CPU) -- 获取adc.CH_CPU计算值
adc.close(adc.CH_CPU) -- 关闭adc.CH_CPU通道
log.info("CPU TEMP", cpu_temp / 1000) -- 打印adc.CH_CPU计算值,单位:摄氏度
return cpu_temp / 1000 or 0 -- 如果获取到的值为nil,则返回0
end
-- 获取VBAT电压的函数
local function vabt_vaule()
adc.open(adc.CH_VBAT) -- 打开adc.CH_VBAT通道
local vabt_vaule = adc.get(adc.CH_VBAT) -- 获取adc.CH_VBAT计算值
adc.close(adc.CH_VBAT) -- 关闭adc.CH_VBAT通道
log.info("VBAT", vabt_vaule / 1000) -- 打印adc.CH_VBAT计算值,单位:毫伏
return vabt_vaule / 1000 or 3.300 -- 如果获取到的值为nil,则返回3300
end
-- 定时发送json数据的回调函数
local function send_aircloud_data()
sys.waitUntil("CONNECTION_SUCCESS") -- 等待服务器连接成功的消息,确保在连接成功后才开始发送数据
-- 按照aircloud平台的格式定义要发送的数据
-- 不需要的变量可以删除,或者注释掉;需要的变量可以增加,或者修改;只要保证field_meaning、data_type、value这三个字段的定义和格式满足aircloud平台的要求即可;
-- 具体的field_meaning赋值可以参考本目录下的FIELD_MEANINGS.md文件;
-- 具体的data_type赋值可以参考https://docs.openluat.com/osapi/ext/excloud/#31 中的数据类型常量;
local send_data = {{
field_meaning = excloud.FIELD_MEANINGS.SIGNAL_STRENGTH_4G, -- 字段含义:4G信号强度
data_type = excloud.DATA_TYPES.INTEGER, -- 数据类型:整数
value = mobile.csq() or 0 -- 信号强度
}, {
field_meaning = excloud.FIELD_MEANINGS.SIM_ICCID, -- 字段含义:SIM卡的ICCID号
data_type = excloud.DATA_TYPES.ASCII, -- 数据类型:ASCII字符串
value = mobile.iccid() or "" -- SIM卡ICCID
}, {
field_meaning = excloud.FIELD_MEANINGS.TIMESTAMP, -- 字段含义:时间戳
data_type = excloud.DATA_TYPES.INTEGER, -- 数据类型:整数
value = os.time() -- 当前时间戳
}, {
field_meaning = excloud.FIELD_MEANINGS.DEVICE_ID, -- 字段含义:设备号
data_type = excloud.DATA_TYPES.ASCII, -- 数据类型:ASCII字符串
value = mobile.imei() or "" -- 设备IMEI4G模块)
}, {
field_meaning = excloud.FIELD_MEANINGS.TEMPERATURE, -- 字段含义:温度
data_type = excloud.DATA_TYPES.FLOAT, -- 数据类型:浮点数
value = cpu_temperature() -- 温度值
}, {
field_meaning = excloud.FIELD_MEANINGS.VOLTAGE, -- 字段含义:电压
data_type = excloud.DATA_TYPES.INTEGER, -- 数据类型:整数
value = vabt_vaule() -- vbat电压值
}, {
field_meaning = 0, -- 自定义数据,使用0作为field_meaning
data_type = excloud.DATA_TYPES.UNICODE, -- 数据类型:Unicode字符串
value = "用户utf-8格式自定义数据"
}}
while 1 do
sys.wait(30000) -- 每隔30秒发送一次数据
-- 打印构建的数据结构,用于调试
log.info("send_data structure:", json.encode(send_data))
local send_data_string = json.encode(send_data)
sys.publish("SEND_DATA_REQ", "Aircloud_main", send_data_string)
end
end
sys.taskInit(send_aircloud_data)
@@ -0,0 +1,104 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本demo演示的核心功能为:
1、创建四路socket连接,详情如下
- 创建一个tcp client,连接tcp server
- 创建一个udp client,连接udp server
- 创建一个tcp ssl client,连接tcp ssl server,不做证书校验;
- 创建一个tcp ssl client,连接tcp ssl serverclient仅单向校验server的证书,server不校验client的证书和密钥文件;
2、每一路socket连接出现异常后,自动重连;
3、每一路socket连接,client按照以下几种逻辑发送数据给server
- 串口应用功能模块uart_app.lua,通过uart1接收到串口数据,将串口数据增加send from uart: 前缀后发送给server
- 定时器应用功能模块timer_app.lua,定时产生数据,将数据增加send from timer:前缀后发送给server
4、每一路socket连接,client收到server数据后,将数据增加recv from tcp/udp/tcp ssl/tcp ssl ca(四选一)server: 前缀后,通过uart1发送出去;
5、启动一个网络业务逻辑看门狗task,用来监控网络环境,如果连续长时间工作不正常,重启整个软件系统;
6、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(三选一)
(1) netdrv_4g4G网卡
(2) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
(3) netdrv_multiple:支持以上两种网卡,可以配置两种网卡的优先级
更多说明参考本目录下的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这三位数字必须存在,但是没有任何用处,可以一直写为999
如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
]]
PROJECT = "SOCKET_LONG_CONNECTION"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持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 "network_watchdog"
-- 加载网络驱动设备功能模块
require "netdrv_device"
-- 加载串口应用功能模块
require "uart_app"
-- 加载定时器应用功能模块
require "timer_app"
-- 加载aircloud数据处理模块
require "aircloud_data"
-- 加载tcp client socket主应用功能模块
require "tcp_client_main"
-- 加载udp client socket主应用功能模块
--require "udp_client_main"
-- 打开内核固件中ssl的调试日志(需要分析问题时再打开)
-- socket.sslLog(3)
-- 加载tcp ssl client socket主应用功能模块
-- require "tcp_ssl_main"
-- 加载tcp ssl ca client socket主应用功能模块
-- require "tcp_ssl_ca_main"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,43 @@
--[[
@module netdrv_4g
@summary “4G网卡”驱动模块
@version 1.0
@date 2025.07.01
@author 马梦阳
@usage
本文件为4G网卡驱动模块,核心业务逻辑为:
1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
]]
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_GP then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
end
end
local function ip_lose_func(adapter)
if adapter == socket.LWIP_GP then
log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
end
end
-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
-- 在Air780EPM上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
@@ -0,0 +1,77 @@
--[[
@module netdrv_eth_spi
@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
@version 1.0
@date 2025.07.24
@author 马梦阳
@usage
本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块,核心业务逻辑为:
1、打开CH390H芯片供电开关;
2、初始化spi0,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
3、以太网卡的连接状态发生变化时,在日志中进行打印;
直接使用Air780EPM V1.3版本开发板硬件测试即可;
本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
]]
local exnetif = require "exnetif"
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_ETH then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
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")
end
end
-- 以太网联网成功(成功连接路由器,并且获取到了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)
local function netdrv_eth_spi_task_func()
-- 配置SPI外接以太网芯片CH390H的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_ETH
-- 本demo使用Air780EPM V1.3版本开发板测试,开发板上的硬件配置为:
-- GPIO20为CH390H以太网芯片的供电使能控制引脚
-- 使用spi0,片选引脚使用GPIO8,中断引脚使用GPIO22
-- 如果使用的硬件和以上描述的环境不同,根据自己的硬件配置修改以下参数
exnetif.set_priority_order({
{
ETHERNET = {
pwrpin = 20,
tp = netdrv.CH390,
opts = {spi = 0, cs = 8, irq = 22}
}
}
})
end
-- 启动一个tasktask的处理函数为netdrv_eth_spi_task_func
-- 在处理函数中调用exnetif.set_priority_order设置网卡优先级
-- 因为exnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
sys.taskInit(netdrv_eth_spi_task_func)
@@ -0,0 +1,88 @@
--[[
@module netdrv_multiple
@summary 多网卡(4G网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块
@version 1.0
@date 2025.07.24
@author 马梦阳
@usage
本文件为多网卡驱动模块,核心业务逻辑为:
1、调用exnetif.set_priority_order配置多网卡的控制参数以及优先级;
直接使用Air780EPM V1.3版本开发板硬件测试即可;
本文件没有对外接口,直接在其他功能模块中require "netdrv_multiple"就可以加载运行;
]]
local exnetif = require "exnetif"
-- 网卡状态变化通知回调函数
-- 当exnetif中检测到网卡切换或者所有网卡都断网时,会触发调用此回调函数
-- 当网卡切换切换时:
-- net_typestring类型,表示当前使用的网卡字符串
-- adapternumber类型,表示当前使用的网卡id
-- 当所有网卡断网时:
-- net_type:为nil
-- adapternumber类型,为-1
local function netdrv_multiple_notify_cbfunc(net_type,adapter)
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
if type(net_type)=="string" then
log.info("netdrv_multiple_notify_cbfunc", "use new adapter", net_type, adapter)
elseif type(net_type)=="nil" then
log.warn("netdrv_multiple_notify_cbfunc", "no available adapter", net_type, adapter)
else
log.warn("netdrv_multiple_notify_cbfunc", "unknown status", net_type, adapter)
end
end
local function netdrv_multiple_task_func()
--设置网卡优先级
exnetif.set_priority_order(
{
-- “通过SPI外挂CH390H芯片”的以太网卡,使用Air780EPM V1.3版本开发板验证
{
ETHERNET = {
-- 供电使能GPIO
pwrpin = 20,
-- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
-- 如果没有传入此参数,exnetif会使用默认值10秒
ping_time = 3000,
-- 连通性检测ip(选填参数)
-- 如果没有传入ip地址,exnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
-- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
-- ping_ip = "填入可靠的并且可以ping通的ip地址",
-- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
tp = netdrv.CH390,
opts = {spi=0, cs=8, irq=22}
}
},
-- 4G网卡
{
LWIP_GP = true
}
}
)
end
-- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc
exnetif.notify_status(netdrv_multiple_notify_cbfunc)
-- 如果存在udp网络应用,并且udp网络应用中,根据应用层的心跳能够判断出来udp数据通信出现了异常;
-- 可以在判断出现异常的位置,调用一次exnetif.check_network_status()接口,强制对当前正式使用的网卡进行一次连通性检测;
-- 如果存在tcp网络应用,不需要用户调用exnetif.check_network_status()接口去控制,exnetif会在tcp网络应用通信异常时自动对当前使用的网卡进行连通性检测。
-- 启动一个tasktask的处理函数为netdrv_multiple_task_func
-- 在处理函数中调用exnetif.set_priority_order设置网卡优先级
-- 因为exnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
sys.taskInit(netdrv_multiple_task_func)
@@ -0,0 +1,45 @@
--[[
@module netdrv_pc
@summary “pc模拟器网卡”驱动模块
@version 1.0
@date 2025.07.01
@author 朱天华
@usage
本文件为pc模拟器网卡驱动模块,核心业务逻辑为:
1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
本文件没有对外接口,直接在其他功能模块中require "netdrv_pc"就可以加载运行;
]]
local function ip_ready_func(ip, adapter)
if adapter == socket.ETH0 then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
log.info("netdrv_pc.ip_ready_func", "IP_READY", socket.localIP(socket.ETH0))
end
end
local function ip_lose_func(adapter)
if adapter == socket.ETH0 then
log.warn("netdrv_pc.ip_lose_func", "IP_LOSE")
end
end
--此处订阅"IP_READY"和"IP_LOSE"两种消息
--在消息的处理函数中,仅仅打印了一些信息,便于实时观察pc模拟器网络的连接状态
--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
-- 设置默认网卡为socket.ETH0
-- pc模拟器上的默认网卡仍然需要使用接口(socket.ETH0)来设置,因为exnetif扩展库当前还不支持模拟器
socket.dft(socket.ETH0)
@@ -0,0 +1,30 @@
--[[
@module netdrv_device
@summary 网络驱动设备功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
1、netdrv_4gsocket.LWIP_GP4G网卡;
2、netdrv_eth_spisocket.LWIP_USER1,通过SPI外挂CH390H芯片的以太网卡;
3、netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
根据自己的项目需求,只需要require以上四种中的一种即可;
本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
]]
-- 根据自己的项目需求,只需要require以下四种中的一种即可;
-- 加载“4G网卡”驱动模块
-- require "netdrv_4g"
-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
require "netdrv_eth_spi"
-- 加载“可以配置优先级的多种网卡”驱动模块
-- require "netdrv_multiple"
@@ -0,0 +1,67 @@
--[[
@module network_watchdog
@summary 网络环境检测看门狗功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为网络环境检测看门狗功能模块,监控网络环境是否工作正常(设备和服务器双向通信正常,或者至少单向通信正常),核心业务逻辑为:
1、启动一个网络环境检测看门狗task,等待其他socket网络应用功能模块来喂狗,如果喂狗超时,则控制软件重启;
2、如何确定“喂狗超时时间”,一般来说,有以下几个原则;
(1) 先确定一个最小基准值T1,2分钟或者5分钟或者10分钟,这个取值取决于具体项目需求,但是不能太短,因为开机后,在网络环境不太好的地方,网络初始化可能需要比较长的时间,一般推荐这个值不能小于2分钟;
(2) 再确定一个和产品业务逻辑有关的一个值T2,这个值和产品的应用业务逻辑息息相关,假设你的产品业务中:
<1> 服务器会定时下发数据给设备,例如设备连接上业务服务器之后,每隔3分钟,设备都会给服务器发送一次心跳,然后服务器都会立即回复一个心跳应答包;
这种情况下,可以取3分钟的大于等于1的倍数(例如1倍,1.5倍,2倍等等)+一段时间(例如10秒钟,如果前面是1倍,则此处必须加一段时间,给网络数据传输过程留够充足的时间);
<2> 如果服务器不会定时下发数据给设备,但是socket使用的是tcp传输,并且设备会定时发送数据给服务器,例如设备连接上业务服务器之后,每隔2分钟,设备都会给服务器发送一次心跳;
这种情况下,可以取2分钟的大于等于1的倍数(例如1倍,1.5倍,2倍等等)+一段时间(例如10秒钟,如果前面是1倍,则此处必须加一段时间,给网络数据传输过程留够充足的时间);
<3> 如果服务器既不会定时或者至少一段时间下发应用数据给设备,设备也不会定时或者至少一段时间上传应用数据到服务器;
这种情况下,一般来说也不是长连接应用,一般来说也不需要网络业务逻辑看门狗,遇到这种情况再具体问题具体分析;
(3) 取T1和T2的最大值,就是“喂狗超时时间”
3、其他socket网络业务功能模块的喂狗时机,和上面2.2的描述相对应,一般来说,可以在以下几种时间点执行喂狗动作:
(1) 设备收到服务器下发的数据时
(2) tcp连接下,设备成功发送数据到服务器时
(3) tcp连接成功时(不到迫不得已,这种情况下不要喂狗,如果喂狗,可能会影响以上两点的判断;
因为长连接的收发数据失败会导致一直重连,重连成功喂狗就会掩盖收发数据异常,除非收发数据完全无规律,才可能在tcp连接成功时喂狗)
4、最重要的一点是:以上所说的原则,仅仅是建议,要根据自己的实际项目业务逻辑以及自己的需求最终确定看门狗方案
5、具体到本demo
(1) 产品业务逻辑为:
<1> 创建了一个tcp连接,设备每隔5秒钟发送一次数据到服务器,服务器何时下发数据给设备不确定;
<2> 创建了一个udp连接,设备每隔5秒钟发送一次数据到服务器,服务器何时下发数据给设备不确定;
<3> 创建了一个tcp ssl连接,设备每隔5秒钟发送一次数据到服务器,服务器何时下发数据给设备不确定;
<4> 创建了一个tcp ssl 单向校验证书连接,设备每隔5秒钟发送一次数据到服务器,服务器何时下发数据给设备不确定;
(2) 确定喂狗超时时间:
<1> 本demo支持单以太网、单4G网络连接外网,网络环境准备就绪预留2分钟的时间已经足够,所以最小基准值T1取值2分钟;
<2> 本demo中存在4路socket连接,但是这4路socket连接都没有定时或者至少一段时间,服务器下发数据给设备,所以无法基于服务器下发数据的业务逻辑来确定T2的值;
<3> 本demo中存在4路socket连接,每1路socket连接,设备都是5秒发送一次数据给服务器,因为5秒的时间太短,在网络环境波动的时候,数据发送延时会比较大;
在这个demo中,我能接受的延时发送时长是1分钟,能接受连续3次延时发送时长的失败,所以,T2取值3分钟;
<4> 取T1 2分钟和T2 3分钟的最大值,最终的喂狗超时时间就是3分钟;
(3) 确定喂狗时机:
<1> 4路连接中,任何1路收到服务器的下发数据时;
<2> tcp、tcp ssl、tcp ssl单向校验证书3路连接中,任何1路成功发送数据给服务器时;
6、本demo设计的网络环境检测看门狗功能模块,可以检测以下两种种的任意一种网络环境异常:
(1) 网络环境连续超过3分钟没有准备就绪
(2) tcp、tcp ssl、tcp ssl单向校验证书3路连接中,连续3分钟没有成功发送数据到服务器;并且4路连接中,连续3分钟没有收到服务器下发的数据;
本文件没有对外接口,直接在main.lua中require "network_watchdog"就可以加载运行;
外部功能模块喂狗时,直接调用sys.publish("FEED_NETWORK_WATCHDOG")
]]
-- 网络环境检测看门狗task处理函数
local function network_watchdog_task_func()
while true do
--如果等待180秒没有等到"FEED_NETWORK_WATCHDOG"消息,则看门狗超时
if not sys.waitUntil("FEED_NETWORK_WATCHDOG", 180000) then
log.error("network_watchdog_task_func timeout")
-- 等待3秒钟,然后软件重启
sys.wait(3000)
rtos.reboot()
end
end
end
--创建并且启动一个task
--运行这个task的处理函数network_watchdog_task_func
sys.taskInit(network_watchdog_task_func)
@@ -0,0 +1,144 @@
--[[
@module tcp_client_main
@summary tcp client socket主应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为tcp client socket主应用功能模块,核心业务逻辑为:
1、创建一个tcp client socket,连接server
2、处理连接异常,出现异常后执行重连动作;
3、调用tcp_client_receiver和tcp_client_sender中的外部接口,进行数据收发处理;
本文件没有对外接口,直接在main.lua中require "tcp_client_main"就可以加载运行;
]]
local libnet = require "libnet"
-- 加载tcp client socket数据接收功能模块
local tcp_client_receiver = require "tcp_client_receiver"
-- 加载tcp client socket数据发送功能模块
local tcp_client_sender = require "tcp_client_sender"
-- 电脑访问:https://iot.luatos.com/#/page6/netlab
-- 本工具使用方法可以参考:https://docs.openluat.com/common/TCPUDP_Test/
-- 点击 打开TCP 按钮,会创建一个TCP server
-- 将server的地址和端口赋值给下面这两个变量
local SERVER_ADDR = "112.125.89.8"
local SERVER_PORT = 46414
-- tcp_client_main的任务名
local TASK_NAME = tcp_client_sender.TASK_NAME
-- 处理未识别的消息
local function tcp_client_main_cbfunc(msg)
log.info("tcp_client_main_cbfunc", msg[1], msg[2], msg[3], msg[4])
end
-- tcp client socket的任务处理函数
local function tcp_client_main_task_func()
local socket_client
local result, para1, para2
while true do
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("sntp_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_client_main_task_func", "recv IP_READY", socket.dft())
-- 创建socket client对象
socket_client = socket.create(nil, TASK_NAME)
-- 如果创建socket client对象失败
if not socket_client then
log.error("tcp_client_main_task_func", "socket.create error")
goto EXCEPTION_PROC
end
-- 配置socket client对象为tcp client
result = socket.config(socket_client, nil, nil, nil, 300, 10, 3)
-- 如果配置失败
if not result then
log.error("tcp_client_main_task_func", "socket.config error")
goto EXCEPTION_PROC
end
-- 连接server
result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
-- 如果连接server失败
if not result then
log.error("tcp_client_main_task_func", "libnet.connect error")
goto EXCEPTION_PROC
end
log.info("tcp_client_main_task_func", "libnet.connect success")
-- 连接成功后,发布一个事件给aircloud_data文件,通知连接成功了
sys.publish("CONNECTION_SUCCESS")
-- 数据收发以及网络连接异常事件总处理逻辑
while true do
-- 数据接收处理(接收处理必须写在libnet.wait之前,因为老版本的内核固件要求必须这样,新版本的内核固件没这个要求,为了不出问题,写在libnet.wait之前就行了)
-- 如果处理失败,则退出循环
if not tcp_client_receiver.proc(socket_client) then
log.error("tcp_client_main_task_func", "tcp_client_receiver.proc error")
break
end
-- 数据发送处理
-- 如果处理失败,则退出循环
if not tcp_client_sender.proc(TASK_NAME, socket_client) then
log.error("tcp_client_main_task_func", "tcp_client_sender.proc error")
break
end
-- 阻塞等待socket.EVENT事件或者15秒钟超时
-- 以下三种业务逻辑会发布事件:
-- 1、socket client和server之间的连接出现异常(例如server主动断开,网络环境出现异常等),此时在内核固件中会发布事件socket.EVENT
-- 2、socket client接收到server发送过来的数据,此时在内核固件中会发布事件socket.EVENT
-- 3、socket client需要发送数据到server, 在tcp_client_sender.lua中会发布事件socket.EVENT
result, para1, para2 = libnet.wait(TASK_NAME, 15000, socket_client)
log.info("tcp_client_main_task_func", "libnet.wait", result, para1, para2)
-- 如果连接异常,则退出循环
if not result then
log.warn("tcp_client_main_task_func", "connection exception")
break
end
end
-- 出现异常
::EXCEPTION_PROC::
-- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
tcp_client_sender.exception_proc()
-- 如果存在socket client对象
if socket_client then
-- 关闭socket client连接
libnet.close(TASK_NAME, 5000, socket_client)
-- 释放socket client对象
socket.release(socket_client)
socket_client = nil
end
-- 5秒后跳转到循环体开始位置,自动发起重连
sys.wait(5000)
end
end
--创建并且启动一个task
--运行这个task的主函数tcp_client_main_task_func
sys.taskInitEx(tcp_client_main_task_func, TASK_NAME, tcp_client_main_cbfunc)
@@ -0,0 +1,90 @@
--[[
@module tcp_client_receiver
@summary tcp client socket数据接收应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为tcp client socket数据接收应用功能模块,核心业务逻辑为:
从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;
本文件的对外接口有2个:
1、tcp_client_receiver.proc(socket_client):数据接收应用逻辑处理入口,在tcp_client_main.lua中调用;
2、sys.publish("RECV_DATA_FROM_SERVER", "recv from tcp server: ", data)
将接收到的数据通过消息"RECV_DATA_FROM_SERVER"发布出去;
需要处理数据的应用功能模块订阅处理此消息即可,本demo项目中uart_app.lua中订阅处理了本消息;
]]
local tcp_client_receiver = {}
-- socket数据接收缓冲区
local recv_buff = nil
--[[
检查socket client是否收到数据,如果收到数据,读取并且处理完所有数据
@api tcp_client_receiver.proc(socket_client)
@param1 socket_client userdata
表示由socket.create接口创建的socket client对象;
必须传入,不允许为空或者nil
@return1 result bool
表示处理结果,成功为true,失败为false
@usage
--
tcp_client_receiver.proc(socket_client)
]]
function tcp_client_receiver.proc(socket_client)
-- 如果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中
-- 如果recv_buff的存储空间不足,会自动扩容
local result = socket.rx(socket_client, recv_buff)
-- 读取数据失败
-- 有两种情况:
-- 1、recv_buff扩容失败
-- 2、socket client和server之间的连接断开
if not result then
log.error("tcp_client_receiver.proc", "socket.rx error")
return false
end
-- 如果读取到了数据, used()就必然大于0, 进行处理
if recv_buff:used() > 0 then
log.info("tcp_client_receiver.proc", "recv data len", recv_buff:used())
-- 读取socket数据接收缓冲区中的数据,赋值给data
local data = recv_buff:query()
-- 将数据data通过"RECV_DATA_FROM_SERVER"消息publish出去,给其他应用模块处理
sys.publish("RECV_DATA_FROM_SERVER", "recv from tcp server: ", data)
-- 接收到数据,通知网络环境检测看门狗功能模块进行喂狗
sys.publish("FEED_NETWORK_WATCHDOG")
-- 清空socket数据接收缓冲区中的数据
recv_buff:del()
-- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
else
break
end
end
return true
end
return tcp_client_receiver
@@ -0,0 +1,138 @@
--[[
@module tcp_client_sender
@summary tcp client socket数据发送应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为tcp client socket数据发送应用功能模块,核心业务逻辑为:
1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列send_queue中;
2、tcp_client_main主任务调用tcp_client_sender.proc接口,遍历队列send_queue,逐条发送数据到server
3、tcp client socket和server之间的连接如果出现异常,tcp_client_main主任务调用tcp_client_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_client_sender.proc:数据发送应用逻辑处理入口,在tcp_client_main.lua中调用;
3、tcp_client_sender.exception_proc:数据发送应用逻辑异常处理入口,在tcp_client_main.lua中调用;
]]
local tcp_client_sender = {}
local libnet = require "libnet"
--[[
数据发送队列,数据结构为:
{
[1] = {data="data1", cb={func=callback_function1, para=callback_para1}},
[2] = {data="data2", cb={func=callback_function2, para=callback_para2}},
}
data的内容为真正要发送的数据,必须存在;
func的内容为数据发送结果的用户回调函数,可以不存在
para的内容为数据发送结果的用户回调函数的回调参数,可以不存在;
]]
local send_queue = {}
-- tcp_client_main的任务名
tcp_client_sender.TASK_NAME = "tcp_client_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_client_main主任务有数据需要发送
-- tcp_client_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
sys.sendMsg(tcp_client_sender.TASK_NAME, socket.EVENT, 0)
end
--[[
检查socket client是否需要发送数据,如果需要发送数据,读取并且发送完发送队列中的所有数据
@api tcp_client_sender.proc(task_name, socket_client)
@param1 task_name string
表示socket.create接口创建socket client对象时所处的task的name
必须传入,不允许为空或者nil
@param2 socket_client userdata
表示由socket.create接口创建的socket client对象;
必须传入,不允许为空或者nil
@return1 result bool
表示处理结果,成功为true,失败为false
@usage
tcp_client_sender.proc("tcp_client_main", socket_client)
]]
function tcp_client_sender.proc(task_name, socket_client)
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, socket_client, send_item.data)
-- 发送失败
if not result then
log.error("tcp_client_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_client_sender.proc", "send success")
-- 发送成功,如果当前发送的数据有用户回调函数,则执行用户回调函数
if send_item.cb and send_item.cb.func then
send_item.cb.func(true, send_item.cb.para)
end
-- 发送成功,通知网络环境检测看门狗功能模块进行喂狗
sys.publish("FEED_NETWORK_WATCHDOG")
end
return true
end
--[[
socket client连接出现异常时,清空等待发送的数据,并且执行发送方的回调函数
@api tcp_client_sender.exception_proc()
@usage
tcp_client_sender.exception_proc()
]]
function tcp_client_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_client_sender
@@ -0,0 +1,145 @@
--[[
@module tcp_ssl_main
@summary tcp_ssl client socket主应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为tcp_ssl client socket主应用功能模块,核心业务逻辑为:
1、创建一个tcp_ssl client socket,连接server
2、处理连接异常,出现异常后执行重连动作;
3、调用tcp_ssl_receiver和tcp_ssl_sender中的外部接口,进行数据收发处理;
本文件没有对外接口,直接在main.lua中require "tcp_ssl_main"就可以加载运行;
]]
local libnet = require "libnet"
-- 加载tcp_ssl client socket数据接收功能模块
local tcp_ssl_receiver = require "tcp_ssl_receiver"
-- 加载tcp_ssl client socket数据发送功能模块
local tcp_ssl_sender = require "tcp_ssl_sender"
-- 电脑访问:https://iot.luatos.com/#/page6/netlab
-- 本工具使用方法可以参考:https://docs.openluat.com/common/TCPUDP_Test/
-- 点击 打开TCP SSL 按钮,会创建一个TCP SSL server
-- 将server的地址和端口赋值给下面这两个变量
local SERVER_ADDR = "112.125.89.8"
local SERVER_PORT = 42284
-- tcp_ssl_main的任务名
local TASK_NAME = tcp_ssl_sender.TASK_NAME
-- 处理未识别的消息
local function tcp_ssl_main_cbfunc(msg)
log.info("tcp_ssl_main_cbfunc", msg[1], msg[2], msg[3], msg[4])
end
-- tcp_ssl client socket的任务处理函数
local function tcp_ssl_main_task_func()
local socket_client
local result, para1, para2
while true do
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("sntp_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_ssl_main_task_func", "recv IP_READY", socket.dft())
-- 创建socket client对象
socket_client = socket.create(nil, TASK_NAME)
-- 如果创建socket client对象失败
if not socket_client then
log.error("tcp_ssl_main_task_func", "socket.create error")
goto EXCEPTION_PROC
end
-- 配置socket client对象为tcp_ssl client
-- 不做证书校验
result = socket.config(socket_client, nil, nil, true, 300, 10, 3)
-- 如果配置失败
if not result then
log.error("tcp_ssl_main_task_func", "socket.config error")
goto EXCEPTION_PROC
end
-- 连接server
result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
-- 如果连接server失败
if not result then
log.error("tcp_ssl_main_task_func", "libnet.connect error")
goto EXCEPTION_PROC
end
log.info("tcp_ssl_main_task_func", "libnet.connect success")
-- 连接成功后,发布一个事件给aircloud_data文件,通知连接成功了
sys.publish("CONNECTION_SUCCESS")
-- 数据收发以及网络连接异常事件总处理逻辑
while true do
-- 数据接收处理(接收处理必须写在libnet.wait之前,因为老版本的内核固件要求必须这样,新版本的内核固件没这个要求,为了不出问题,写在libnet.wait之前就行了)
-- 如果处理失败,则退出循环
if not tcp_ssl_receiver.proc(socket_client) then
log.error("tcp_ssl_main_task_func", "tcp_ssl_receiver.proc error")
break
end
-- 数据发送处理
-- 如果处理失败,则退出循环
if not tcp_ssl_sender.proc(TASK_NAME, socket_client) then
log.error("tcp_ssl_main_task_func", "tcp_ssl_sender.proc error")
break
end
-- 阻塞等待socket.EVENT事件或者15秒钟超时
-- 以下三种业务逻辑会发布事件:
-- 1、socket client和server之间的连接出现异常(例如server主动断开,网络环境出现异常等),此时在内核固件中会发布事件socket.EVENT
-- 2、socket client接收到server发送过来的数据,此时在内核固件中会发布事件socket.EVENT
-- 3、socket client需要发送数据到server, 在tcp_ssl_sender.lua中会发布事件socket.EVENT
result, para1, para2 = libnet.wait(TASK_NAME, 15000, socket_client)
log.info("tcp_ssl_main_task_func", "libnet.wait", result, para1, para2)
-- 如果连接异常,则退出循环
if not result then
log.warn("tcp_ssl_main_task_func", "connection exception")
break
end
end
-- 出现异常
::EXCEPTION_PROC::
-- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
tcp_ssl_sender.exception_proc()
-- 如果存在socket client对象
if socket_client then
-- 关闭socket client连接
libnet.close(TASK_NAME, 5000, socket_client)
-- 释放socket client对象
socket.release(socket_client)
socket_client = nil
end
-- 5秒后跳转到循环体开始位置,自动发起重连
sys.wait(5000)
end
end
--创建并且启动一个task
--运行这个task的主函数tcp_ssl_main_task_func
sys.taskInitEx(tcp_ssl_main_task_func, TASK_NAME, tcp_ssl_main_cbfunc)
@@ -0,0 +1,96 @@
--[[
@module tcp_ssl_receiver
@summary tcp_ssl client socket数据接收应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为tcp_ssl client socket数据接收应用功能模块,核心业务逻辑为:
从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;
本文件的对外接口有2个:
1、tcp_ssl_receiver.proc(socket_client):数据接收应用逻辑处理入口,在tcp_ssl_main.lua中调用;
2、sys.publish("RECV_DATA_FROM_SERVER", "recv from tcp_ssl server: ", data)
将接收到的数据通过消息"RECV_DATA_FROM_SERVER"发布出去;
需要处理数据的应用功能模块订阅处理此消息即可,本demo项目中uart_app.lua中订阅处理了本消息;
]]
local tcp_ssl_receiver = {}
-- socket数据接收缓冲区
local recv_buff = nil
--[[
检查socket client是否收到数据,如果收到数据,读取并且处理完所有数据
@api tcp_ssl_receiver.proc(socket_client)
@param1 socket_client userdata
表示由socket.create接口创建的socket client对象;
必须传入,不允许为空或者nil
@return1 result bool
表示处理结果,成功为true,失败为false
@usage
--
tcp_ssl_receiver.proc(socket_client)
]]
function tcp_ssl_receiver.proc(socket_client)
-- 如果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中
-- 如果recv_buff的存储空间不足,会自动扩容
-- 如果使用netlab.luatos.com创建的tcl ssl server来配合测试,注意server存在一个问题,后续可能会解决,也可能没解决,问题如下:
-- netlab创建的tcp ssl server,第一次下发数据给client时,会截取输入的完整数据的一半数据做为一包发送,剩余一半做为第二包发送;
-- 以后server再次发送其他数据,一包的最大长度一直是第一次下发使用的长度;
-- 假设第一次在编辑框输入了12字节的数据,则会拆分成2包数据进行发送,每包6字节;
-- 假设第一次在编辑框输入了120字节的数据,则会拆分成20包数据进行发送,每包仍然6字节;
-- 如果出现了这个问题,不用担心,和client无关,最终你使用自己的server时只要保证自己的server没问题就行;
local result = socket.rx(socket_client, recv_buff)
-- 读取数据失败
-- 有两种情况:
-- 1、recv_buff扩容失败
-- 2、socket client和server之间的连接断开
if not result then
log.error("tcp_ssl_receiver.proc", "socket.rx error")
return false
end
-- 如果读取到了数据, used()就必然大于0, 进行处理
if recv_buff:used() > 0 then
log.info("tcp_ssl_receiver.proc", "recv data len", recv_buff:used())
-- 读取socket数据接收缓冲区中的数据,赋值给data
local data = recv_buff:query()
-- 将数据data通过"RECV_DATA_FROM_SERVER"消息publish出去,给其他应用模块处理
sys.publish("RECV_DATA_FROM_SERVER", "recv from tcp_ssl server: ", data)
-- 接收到数据,通知网络环境检测看门狗功能模块进行喂狗
sys.publish("FEED_NETWORK_WATCHDOG")
-- 清空socket数据接收缓冲区中的数据
recv_buff:del()
-- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
else
break
end
end
return true
end
return tcp_ssl_receiver
@@ -0,0 +1,138 @@
--[[
@module tcp_ssl_sender
@summary tcp_ssl client socket数据发送应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为tcp_ssl client socket数据发送应用功能模块,核心业务逻辑为:
1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列send_queue中;
2、tcp_ssl_main主任务调用tcp_ssl_sender.proc接口,遍历队列send_queue,逐条发送数据到server
3、tcp_ssl client socket和server之间的连接如果出现异常,tcp_ssl_main主任务调用tcp_ssl_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_ssl_sender.proc:数据发送应用逻辑处理入口,在tcp_ssl_main.lua中调用;
3、tcp_ssl_sender.exception_proc:数据发送应用逻辑异常处理入口,在tcp_ssl_main.lua中调用;
]]
local tcp_ssl_sender = {}
local libnet = require "libnet"
--[[
数据发送队列,数据结构为:
{
[1] = {data="data1", cb={func=callback_function1, para=callback_para1}},
[2] = {data="data2", cb={func=callback_function2, para=callback_para2}},
}
data的内容为真正要发送的数据,必须存在;
func的内容为数据发送结果的用户回调函数,可以不存在
para的内容为数据发送结果的用户回调函数的回调参数,可以不存在;
]]
local send_queue = {}
-- tcp_ssl_main的任务名
tcp_ssl_sender.TASK_NAME = "tcp_ssl_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_ssl_main主任务有数据需要发送
-- tcp_ssl_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
sys.sendMsg(tcp_ssl_sender.TASK_NAME, socket.EVENT, 0)
end
--[[
检查socket client是否需要发送数据,如果需要发送数据,读取并且发送完发送队列中的所有数据
@api tcp_ssl_sender.proc(task_name, socket_client)
@param1 task_name string
表示socket.create接口创建socket client对象时所处的task的name
必须传入,不允许为空或者nil
@param2 socket_client userdata
表示由socket.create接口创建的socket client对象;
必须传入,不允许为空或者nil
@return1 result bool
表示处理结果,成功为true,失败为false
@usage
tcp_ssl_sender.proc("tcp_client_main", socket_client)
]]
function tcp_ssl_sender.proc(task_name, socket_client)
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, socket_client, send_item.data)
-- 发送失败
if not result then
log.error("tcp_ssl_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_ssl_sender.proc", "buffer is full, wait for the next time")
table.insert(send_queue, 1, send_item)
return true
end
log.info("tcp_ssl_sender.proc", "send success")
-- 发送成功,如果当前发送的数据有用户回调函数,则执行用户回调函数
if send_item.cb and send_item.cb.func then
send_item.cb.func(true, send_item.cb.para)
end
-- 发送成功,通知网络环境检测看门狗功能模块进行喂狗
sys.publish("FEED_NETWORK_WATCHDOG")
end
return true
end
--[[
socket client连接出现异常时,清空等待发送的数据,并且执行发送方的回调函数
@api tcp_ssl_sender.exception_proc()
@usage
tcp_ssl_sender.exception_proc()
]]
function tcp_ssl_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_ssl_sender
@@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE-----
MIIETjCCAzagAwIBAgINAe5fIh38YjvUMzqFVzANBgkqhkiG9w0BAQsFADBMMSAw
HgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFs
U2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjAeFw0xODExMjEwMDAwMDBaFw0yODEx
MjEwMDAwMDBaMFAxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52
LXNhMSYwJAYDVQQDEx1HbG9iYWxTaWduIFJTQSBPViBTU0wgQ0EgMjAxODCCASIw
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKdaydUMGCEAI9WXD+uu3Vxoa2uP
UGATeoHLl+6OimGUSyZ59gSnKvuk2la77qCk8HuKf1UfR5NhDW5xUTolJAgvjOH3
idaSz6+zpz8w7bXfIa7+9UQX/dhj2S/TgVprX9NHsKzyqzskeU8fxy7quRU6fBhM
abO1IFkJXinDY+YuRluqlJBJDrnw9UqhCS98NE3QvADFBlV5Bs6i0BDxSEPouVq1
lVW9MdIbPYa+oewNEtssmSStR8JvA+Z6cLVwzM0nLKWMjsIYPJLJLnNvBhBWk0Cq
o8VS++XFBdZpaFwGue5RieGKDkFNm5KQConpFmvv73W+eka440eKHRwup08CAwEA
AaOCASkwggElMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0G
A1UdDgQWBBT473/yzXhnqN5vjySNiPGHAwKz6zAfBgNVHSMEGDAWgBSP8Et/qC5F
JK5NUPpjmove4t0bvDA+BggrBgEFBQcBAQQyMDAwLgYIKwYBBQUHMAGGImh0dHA6
Ly9vY3NwMi5nbG9iYWxzaWduLmNvbS9yb290cjMwNgYDVR0fBC8wLTAroCmgJ4Yl
aHR0cDovL2NybC5nbG9iYWxzaWduLmNvbS9yb290LXIzLmNybDBHBgNVHSAEQDA+
MDwGBFUdIAAwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly93d3cuZ2xvYmFsc2lnbi5j
b20vcmVwb3NpdG9yeS8wDQYJKoZIhvcNAQELBQADggEBAJmQyC1fQorUC2bbmANz
EdSIhlIoU4r7rd/9c446ZwTbw1MUcBQJfMPg+NccmBqixD7b6QDjynCy8SIwIVbb
0615XoFYC20UgDX1b10d65pHBf9ZjQCxQNqQmJYaumxtf4z1s4DfjGRzNpZ5eWl0
6r/4ngGPoJVpjemEuunl1Ig423g7mNA2eymw0lIYkN5SQwCuaifIFJ6GlazhgDEw
fpolu4usBCOmmQDo8dIm7A9+O4orkjgTHY+GzYZSR+Y0fFukAj6KYXwidlNalFMz
hriSqHKvoflShx8xpfywgVcvzfTO3PYkz6fiNJBonf6q8amaEsybwMbDqKWwIX7e
SPY=
-----END CERTIFICATE-----
@@ -0,0 +1,66 @@
--[[
@module sntp_app
@summary sntp时间同步应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为sntp时间同步应用功能模块,核心业务逻辑为:
1、连接ntp服务器进行时间同步;
2、如果同步成功,1小时之后重新发起同步动作;
3、如果同步失败,10秒钟之后重新发起同步动作;
本文件没有对外接口,直接在其他应用功能模块中require "sntp_app"就可以加载运行;
]]
-- sntp时间同步的任务处理函数
local function sntp_task_func()
while true do
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("sntp_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.warn("sntp_task_func", "recv IP_READY")
-- 发起ntp时间同步动作
socket.sntp()
-- 等待ntp时间同步结果,30秒超时失败,通常只需要几百毫秒就能成功
local ret = sys.waitUntil("NTP_UPDATE", 30000)
--同步成功
if ret then
-- 以下是获取/打印时间的演示,注意时区问题
log.info("sntp_task_func", "时间同步成功", "本地时间", os.date())
log.info("sntp_task_func", "时间同步成功", "UTC时间", os.date("!%c"))
log.info("sntp_task_func", "时间同步成功", "RTC时钟(UTC时间)", json.encode(rtc.get()))
log.info("sntp_task_func", "时间同步成功", "本地时间戳", os.time())
local t = os.date("*t")
log.info("sntp_task_func", "时间同步成功", "本地时间os.date() json格式", json.encode(t))
log.info("sntp_task_func", "时间同步成功", "本地时间os.date(os.time())", os.time(t))
-- 正常使用, 一小时一次, 已经足够了, 甚至1天一次也可以
sys.wait(3600000)
--同步失败
else
log.info("sntp_task_func", "时间同步失败")
-- 10秒后重新发起同步动作
sys.wait(10000)
end
end
end
--创建并且启动一个task
--运行这个task的主函数sntp_task_func
sys.taskInit(sntp_task_func)
@@ -0,0 +1,165 @@
--[[
@module tcp_ssl_ca_main
@summary tcp_ssl_ca client socket主应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为tcp_ssl_ca client socket主应用功能模块,核心业务逻辑为:
1、创建一个tcp_ssl_ca client socket,连接server
2、处理连接异常,出现异常后执行重连动作;
3、调用tcp_ssl_ca_receiver和tcp_ssl_ca_sender中的外部接口,进行数据收发处理;
本文件没有对外接口,直接在main.lua中require "tcp_ssl_ca_main"就可以加载运行;
]]
local libnet = require "libnet"
-- 加载sntp时间同步应用功能模块(ca证书校验的ssl socket需要时间同步功能)
require "sntp_app"
-- 加载tcp_ssl_ca client socket数据接收功能模块
local tcp_ssl_ca_receiver = require "tcp_ssl_ca_receiver"
-- 加载tcp_ssl_ca client socket数据发送功能模块
local tcp_ssl_ca_sender = require "tcp_ssl_ca_sender"
-- https://www.baidu.com网站服务器,地址为"www.baidu.com",端口为443
local SERVER_ADDR = "www.baidu.com"
local SERVER_PORT = 443
-- tcp_ssl_ca_main的任务名
local TASK_NAME = tcp_ssl_ca_sender.TASK_NAME
-- 处理未识别的消息
local function tcp_ssl_ca_main_cbfunc(msg)
log.info("tcp_ssl_ca_main_cbfunc", msg[1], msg[2], msg[3], msg[4])
end
-- tcp_ssl_ca client socket的任务处理函数
local function tcp_ssl_ca_main_task_func()
local socket_client
local result, para1, para2
-- 用来验证server证书是否合法的ca证书文件为baidu_parent_ca.crt
-- 此ca证书的有效期截止到2028年11月21日
-- 将这个ca证书文件的内容读取出来,赋值给server_ca_cert
-- 注意:此处的ca证书文件仅用来验证baidu网站的server证书
-- baidu网站的server证书有效期截止到2026年8月10日
-- 在有效期之前,baidu会更换server证书,如果server证书更换后,此处验证使用的baidu_parent_ca.crt也可能需要更换
-- 使用电脑上的网页浏览器访问https://www.baidu.com,可以实时看到baidu的server证书以及baidu_parent_ca.crt
-- 如果你使用的是自己的server,要替换为自己server证书对应的ca证书文件
local server_ca_cert = io.readFile("/luadb/baidu_parent_ca.crt")
while true do
-- 如果当前时间点设置的网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("tcp_ssl_ca_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_ssl_ca_main_task_func", "recv IP_READY", socket.dft())
-- 创建socket client对象
socket_client = socket.create(nil, TASK_NAME)
-- 如果创建socket client对象失败
if not socket_client then
log.error("tcp_ssl_ca_main_task_func", "socket.create error")
goto EXCEPTION_PROC
end
-- 配置socket client对象为tcp_ssl_ca client
-- client仅单向校验server的证书,server不校验client的证书和密钥文件
-- 如果做证书校验,需要特别注意以下几点:
-- 1、证书校验前,设备端必须同步为正确的时间,因为校验过程中会检查ca证书以及server证书中的有效期是否合法;本demo中的sntp_app.lua会同步时间;
-- 2、任何证书都有有效期,无论是ca证书还是server证书,必须在有效期截止之前,及时更换证书,延长有效期,否则证书校验会失败;
-- 3、如果要更换ca证书,需要在设备端远程升级,必须保证ca证书失效之前升级成功,否则校验失败,就无法连接server;
-- 综上所述,证书校验虽然安全,可以验证身份,但是后续维护成本比较高;除非有需要,否则可以不配置证书校验功能;
-- 另外,如果使用https://iot.luatos.com/#/page6/netlab创建的TCP SSL Server,使用的server证书有可能过了有效期;
-- 详细的使用说明参考:[合宙 TCP/UDP web 测试工具使用说明](https://iot.luatos.com/#/p8000/netlab) 。
-- 如果过了有效期,使用本文件无法连接成功tcp ssl ca server,遇到这种问题,可以在main.lua中打开socket.sslLog(3),观察Luatools的日志,如果出现类似于下面的日志
-- expires on : 2020-12-27 15:46:55
-- 表示证书有效期截止到2020-12-27 15:46:55,明显就是证书已经过了有效期
-- 遇到这种情况,可以反馈给合宙的技术人员;或者不再使用netlab server测试,使用你自己的tcp ssl server来测试,只要保证你的server证书合法就行;
result = socket.config(socket_client, nil, nil, true, 300, 10, 3, server_ca_cert)
-- 如果配置失败
if not result then
log.error("tcp_ssl_ca_main_task_func", "socket.config error")
goto EXCEPTION_PROC
end
-- 连接server
result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
-- 如果连接server失败
if not result then
log.error("tcp_ssl_ca_main_task_func", "libnet.connect error")
goto EXCEPTION_PROC
end
log.info("tcp_ssl_ca_main_task_func", "libnet.connect success")
-- 数据收发以及网络连接异常事件总处理逻辑
while true do
-- 数据接收处理(接收处理必须写在libnet.wait之前,因为老版本的内核固件要求必须这样,新版本的内核固件没这个要求,为了不出问题,写在libnet.wait之前就行了)
-- 如果处理失败,则退出循环
if not tcp_ssl_ca_receiver.proc(socket_client) then
log.error("tcp_ssl_ca_main_task_func", "tcp_ssl_ca_receiver.proc error")
break
end
-- 数据发送处理
-- 如果处理失败,则退出循环
if not tcp_ssl_ca_sender.proc(TASK_NAME, socket_client) then
log.error("tcp_ssl_ca_main_task_func", "tcp_ssl_ca_sender.proc error")
break
end
-- 阻塞等待socket.EVENT事件或者15秒钟超时
-- 以下三种业务逻辑会发布事件:
-- 1、socket client和server之间的连接出现异常(例如server主动断开,网络环境出现异常等),此时在内核固件中会发布事件socket.EVENT
-- 2、socket client接收到server发送过来的数据,此时在内核固件中会发布事件socket.EVENT
-- 3、socket client需要发送数据到server, 在tcp_ssl_ca_sender.lua中会发布事件socket.EVENT
result, para1, para2 = libnet.wait(TASK_NAME, 15000, socket_client)
log.info("tcp_ssl_ca_main_task_func", "libnet.wait", result, para1, para2)
-- 如果连接异常,则退出循环
if not result then
log.warn("tcp_ssl_ca_main_task_func", "connection exception")
break
end
end
-- 出现异常
::EXCEPTION_PROC::
-- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
tcp_ssl_ca_sender.exception_proc()
-- 如果存在socket client对象
if socket_client then
-- 关闭socket client连接
libnet.close(TASK_NAME, 5000, socket_client)
-- 释放socket client对象
socket.release(socket_client)
socket_client = nil
end
-- 5秒后跳转到循环体开始位置,自动发起重连
sys.wait(5000)
end
end
--创建并且启动一个task
--运行这个task的主函数tcp_ssl_ca_main_task_func
sys.taskInitEx(tcp_ssl_ca_main_task_func, TASK_NAME, tcp_ssl_ca_main_cbfunc)
@@ -0,0 +1,96 @@
--[[
@module tcp_ssl_ca_receiver
@summary tcp_ssl_ca client socket数据接收应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为tcp_ssl_ca client socket数据接收应用功能模块,核心业务逻辑为:
从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;
本文件的对外接口有2个:
1、tcp_ssl_ca_receiver.proc(socket_client):数据接收应用逻辑处理入口,在tcp_ssl_ca_main.lua中调用;
2、sys.publish("RECV_DATA_FROM_SERVER", "recv from tcp_ssl_ca server: ", data)
将接收到的数据通过消息"RECV_DATA_FROM_SERVER"发布出去;
需要处理数据的应用功能模块订阅处理此消息即可,本demo项目中uart_app.lua中订阅处理了本消息;
]]
local tcp_ssl_ca_receiver = {}
-- socket数据接收缓冲区
local recv_buff = nil
--[[
检查socket client是否收到数据,如果收到数据,读取并且处理完所有数据
@api tcp_ssl_ca_receiver.proc(socket_client)
@param1 socket_client userdata
表示由socket.create接口创建的socket client对象;
必须传入,不允许为空或者nil
@return1 result bool
表示处理结果,成功为true,失败为false
@usage
--
tcp_ssl_ca_receiver.proc(socket_client)
]]
function tcp_ssl_ca_receiver.proc(socket_client)
-- 如果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中
-- 如果recv_buff的存储空间不足,会自动扩容
-- 如果使用netlab.luatos.com创建的tcl ssl server来配合测试,注意server存在一个问题,后续可能会解决,也可能没解决,问题如下:
-- netlab创建的tcp ssl server,第一次下发数据给client时,会截取输入的完整数据的一半数据做为一包发送,剩余一半做为第二包发送;
-- 以后server再次发送其他数据,一包的最大长度一直是第一次下发使用的长度;
-- 假设第一次在编辑框输入了12字节的数据,则会拆分成2包数据进行发送,每包6字节;
-- 假设第一次在编辑框输入了120字节的数据,则会拆分成20包数据进行发送,每包仍然6字节;
-- 如果出现了这个问题,不用担心,和client无关,最终你使用自己的server时只要保证自己的server没问题就行;
local result = socket.rx(socket_client, recv_buff)
-- 读取数据失败
-- 有两种情况:
-- 1、recv_buff扩容失败
-- 2、socket client和server之间的连接断开
if not result then
log.error("tcp_ssl_ca_receiver.proc", "socket.rx error")
return false
end
-- 如果读取到了数据, used()就必然大于0, 进行处理
if recv_buff:used() > 0 then
log.info("tcp_ssl_ca_receiver.proc", "recv data len", recv_buff:used())
-- 读取socket数据接收缓冲区中的数据,赋值给data
local data = recv_buff:query()
-- 将数据data通过"RECV_DATA_FROM_SERVER"消息publish出去,给其他应用模块处理
sys.publish("RECV_DATA_FROM_SERVER", "recv from tcp_ssl_ca server: ", data)
-- 接收到数据,通知网络环境检测看门狗功能模块进行喂狗
sys.publish("FEED_NETWORK_WATCHDOG")
-- 清空socket数据接收缓冲区中的数据
recv_buff:del()
-- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
else
break
end
end
return true
end
return tcp_ssl_ca_receiver
@@ -0,0 +1,138 @@
--[[
@module tcp_ssl_ca_sender
@summary tcp_ssl_ca client socket数据发送应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为tcp_ssl_ca client socket数据发送应用功能模块,核心业务逻辑为:
1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列send_queue中;
2、tcp_ssl_ca_main主任务调用tcp_ssl_ca_sender.proc接口,遍历队列send_queue,逐条发送数据到server
3、tcp_ssl_ca client socket和server之间的连接如果出现异常,tcp_ssl_ca_main主任务调用tcp_ssl_ca_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_ssl_ca_sender.proc:数据发送应用逻辑处理入口,在tcp_ssl_ca_main.lua中调用;
3、tcp_ssl_ca_sender.exception_proc:数据发送应用逻辑异常处理入口,在tcp_ssl_ca_main.lua中调用;
]]
local tcp_ssl_ca_sender = {}
local libnet = require "libnet"
--[[
数据发送队列,数据结构为:
{
[1] = {data="data1", cb={func=callback_function1, para=callback_para1}},
[2] = {data="data2", cb={func=callback_function2, para=callback_para2}},
}
data的内容为真正要发送的数据,必须存在;
func的内容为数据发送结果的用户回调函数,可以不存在
para的内容为数据发送结果的用户回调函数的回调参数,可以不存在;
]]
local send_queue = {}
-- tcp_ssl_ca_main的任务名
tcp_ssl_ca_sender.TASK_NAME = "tcp_ssl_ca_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_ssl_ca_main主任务有数据需要发送
-- tcp_ssl_ca_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
sys.sendMsg(tcp_ssl_ca_sender.TASK_NAME, socket.EVENT, 0)
end
--[[
检查socket client是否需要发送数据,如果需要发送数据,读取并且发送完发送队列中的所有数据
@api tcp_ssl_ca_sender.proc(task_name, socket_client)
@param1 task_name string
表示socket.create接口创建socket client对象时所处的task的name
必须传入,不允许为空或者nil
@param2 socket_client userdata
表示由socket.create接口创建的socket client对象;
必须传入,不允许为空或者nil
@return1 result bool
表示处理结果,成功为true,失败为false
@usage
tcp_ssl_ca_sender.proc("tcp_client_main", socket_client)
]]
function tcp_ssl_ca_sender.proc(task_name, socket_client)
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, socket_client, send_item.data)
-- 发送失败
if not result then
log.error("tcp_ssl_ca_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_ssl_ca_sender.proc", "buffer is full, wait for the next time")
table.insert(send_queue, 1, send_item)
return true
end
log.info("tcp_ssl_ca_sender.proc", "send success")
-- 发送成功,如果当前发送的数据有用户回调函数,则执行用户回调函数
if send_item.cb and send_item.cb.func then
send_item.cb.func(true, send_item.cb.para)
end
-- 发送成功,通知网络环境检测看门狗功能模块进行喂狗
sys.publish("FEED_NETWORK_WATCHDOG")
end
return true
end
--[[
socket client连接出现异常时,清空等待发送的数据,并且执行发送方的回调函数
@api tcp_ssl_ca_sender.exception_proc()
@usage
tcp_ssl_ca_sender.exception_proc()
]]
function tcp_ssl_ca_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_ssl_ca_sender
@@ -0,0 +1,41 @@
--[[
@module timer_app
@summary 定时器应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为定时器应用功能模块,核心业务逻辑为:
创建一个5秒的循环定时器,每次产生一段数据,通知四个socket client进行处理;
本文件的对外接口有一个:
1、sys.publish("SEND_DATA_REQ", "timer", data, {func=send_data_cbfunc, para="timer"..data}),通过publish通知socket client数据发送功能模块发送data数据;
数据发送结果通过执行回调函数send_data_cbfunc通知本功能模块;
]]
local data = 1
-- 数据发送结果回调函数
-- result:发送结果,true为发送成功,false为发送失败
-- para:回调参数,sys.publish("SEND_DATA_REQ", "timer", data, {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为要发送的原始数据
-- 携带的第三个参数cb为发送结果回调(可以为空,如果为空,表示不关心socket client发送数据成功还是失败),其中:
-- cb.func为回调函数(可以为空,如果为空,表示不关心socket client发送数据成功还是失败)
-- cb.para为回调函数的第二个参数(可以为空),回调函数的第一个参数为发送结果(true表示成功,false表示失败)
sys.publish("SEND_DATA_REQ", "timer", data, {func=send_data_cbfunc, para="timer"..data})
data = data+1
end
-- 启动一个5秒的单次定时器
-- 时间到达后,执行一次send_data_req_timer_cbfunc函数
sys.timerStart(send_data_req_timer_cbfunc, 5000)
@@ -0,0 +1,79 @@
--[[
@module uart_app
@summary 串口应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为串口应用功能模块,核心业务逻辑为:
1、打开uart1,波特率115200,数据位8,停止位1,无奇偶校验位;
2、uart1和pc端的串口工具相连;
3、从uart1接收到pc端串口工具发送的数据后,通知四个socket client进行处理;
4、收到四个socket client从socket server接收到的数据后,将数据通过uart1发送到pc端串口工具;
本文件的对外接口有两个:
1、sys.publish("SEND_DATA_REQ", "uart", read_buf),通过publish通知socket client数据发送功能模块发送read_buf数据,不关心数据发送成功还是失败;
2、sys.subscribe("RECV_DATA_FROM_SERVER", recv_data_from_server_proc),订阅RECV_DATA_FROM_SERVER消息,处理消息携带的数据;
]]
-- 使用UART1
local UART_ID = 2
-- 串口接收数据缓冲区
local read_buf = ""
-- 将前缀prefix和数据data拼接
-- 然后末尾增加回车换行两个字符,通过uart发送出去,方便在PC端换行显示查看
local function recv_data_from_server_proc(prefix, data)
uart.write(UART_ID, prefix..data.."\r\n")
end
local function concat_timeout_func()
-- 如果存在尚未处理的串口缓冲区数据;
-- 将数据通过publish通知其他应用功能模块处理;
-- 然后清空本文件的串口缓冲区数据
if read_buf:len() > 0 then
sys.publish("SEND_DATA_REQ", "uart", read_buf)
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_SERVER"消息的处理函数recv_data_from_server_proc
-- 收到"RECV_DATA_FROM_SERVER"消息后,会执行函数recv_data_from_server_proc
sys.subscribe("RECV_DATA_FROM_SERVER", recv_data_from_server_proc)
@@ -0,0 +1,146 @@
--[[
@module udp_client_receiver
@summary udp client socket主应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为udp client socket主应用功能模块,核心业务逻辑为:
1、创建一个udp client socket,连接server
2、处理连接异常,出现异常后执行重连动作;
3、调用udp_client_receiver和udp_client_sender中的外部接口,进行数据收发处理;
本文件没有对外接口,直接在main.lua中require "udp_client_main"就可以加载运行;
]]
local libnet = require "libnet"
-- 加载udp client socket数据接收功能模块
local udp_client_receiver = require "udp_client_receiver"
-- 加载udp client socket数据发送功能模块
local udp_client_sender = require "udp_client_sender"
-- 电脑访问:https://iot.luatos.com/#/page6/netlab
-- 本工具使用方法可以参考:https://docs.openluat.com/common/TCPUDP_Test/
-- 点击 打开UDP 按钮,会创建一个UDP server
-- 将server的地址和端口赋值给下面这两个变量
local SERVER_ADDR = "112.125.89.8"
local SERVER_PORT = 43510
-- udp_client_main的任务名
local TASK_NAME = udp_client_sender.TASK_NAME
-- 处理未识别的消息
local function udp_client_main_cbfunc(msg)
log.info("udp_client_main_cbfunc", msg[1], msg[2], msg[3], msg[4])
end
-- udp client socket的任务处理函数
local function udp_client_main_task_func()
local socket_client
local result, para1, para2
while true do
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("sntp_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_client_main_task_func", "recv IP_READY", socket.dft())
-- 创建socket client对象
socket_client = socket.create(nil, TASK_NAME)
-- 如果创建socket client对象失败
if not socket_client then
log.error("udp_client_main_task_func", "socket.create error")
goto EXCEPTION_PROC
end
-- 配置socket client对象为udp client
result = socket.config(socket_client, nil, true)
-- 如果配置失败
if not result then
log.error("udp_client_main_task_func", "socket.config error")
goto EXCEPTION_PROC
end
-- 连接server
result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
-- 如果连接server失败
if not result then
log.error("udp_client_main_task_func", "libnet.connect error")
goto EXCEPTION_PROC
end
log.info("udp_client_main_task_func", "libnet.connect success")
-- 连接成功后,发布一个事件给aircloud_data文件,通知连接成功了
sys.publish("CONNECTION_SUCCESS")
-- 数据收发以及网络连接异常事件总处理逻辑
while true do
-- 数据接收处理(接收处理必须写在libnet.wait之前,因为老版本的内核固件要求必须这样,新版本的内核固件没这个要求,为了不出问题,写在libnet.wait之前就行了)
-- 如果处理失败,则退出循环
if not udp_client_receiver.proc(socket_client) then
log.error("udp_client_main_task_func", "udp_client_receiver.proc error")
break
end
-- 数据发送处理
-- 如果处理失败,则退出循环
if not udp_client_sender.proc(TASK_NAME, socket_client) then
log.error("udp_client_main_task_func", "udp_client_sender.proc error")
break
end
-- 阻塞等待socket.EVENT事件或者15秒钟超时
-- 以下三种业务逻辑会发布事件:
-- 1、socket client和server之间的连接出现异常(例如server主动断开,网络环境出现异常等),此时在内核固件中会发布事件socket.EVENT
-- 2、socket client接收到server发送过来的数据,此时在内核固件中会发布事件socket.EVENT
-- 3、socket client需要发送数据到server, 在udp_client_sender.lua中会发布事件socket.EVENT
result, para1, para2 = libnet.wait(TASK_NAME, 15000, socket_client)
log.info("udp_client_main_task_func", "libnet.wait", result, para1, para2)
-- 如果连接异常,则退出循环
if not result then
log.warn("udp_client_main_task_func", "connection exception")
break
end
end
-- 出现异常
::EXCEPTION_PROC::
-- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
udp_client_sender.exception_proc()
-- 如果存在socket client对象
if socket_client then
-- 关闭socket client连接
libnet.close(TASK_NAME, 5000, socket_client)
-- 释放socket client对象
socket.release(socket_client)
socket_client = nil
end
-- 5秒后跳转到循环体开始位置,自动发起重连
sys.wait(5000)
end
end
--创建并且启动一个task
--运行这个task的主函数udp_client_main_task_func
sys.taskInitEx(udp_client_main_task_func, TASK_NAME, udp_client_main_cbfunc)
@@ -0,0 +1,90 @@
--[[
@module udp_client_receiver
@summary udp client socket数据接收应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为udp client socket数据接收应用功能模块,核心业务逻辑为:
从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;
本文件的对外接口有2个:
1、udp_client_receiver.proc(socket_client):数据接收应用逻辑处理入口,在udp_client_main.lua中调用;
2、sys.publish("RECV_DATA_FROM_SERVER", "recv from udp server: ", data)
将接收到的数据通过消息"RECV_DATA_FROM_SERVER"发布出去;
需要处理数据的应用功能模块订阅处理此消息即可,本demo项目中uart_app.lua中订阅处理了本消息;
]]
local udp_client_receiver = {}
-- socket数据接收缓冲区
local recv_buff = nil
--[[
检查socket client是否收到数据,如果收到数据,读取并且处理完所有数据
@api udp_client_receiver.proc(socket_client)
@param1 socket_client userdata
表示由socket.create接口创建的socket client对象;
必须传入,不允许为空或者nil
@return1 result bool
表示处理结果,成功为true,失败为false
@usage
--
udp_client_receiver.proc(socket_client)
]]
function udp_client_receiver.proc(socket_client)
-- 如果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中
-- 如果recv_buff的存储空间不足,会自动扩容
local result = socket.rx(socket_client, recv_buff)
-- 读取数据失败
-- 有两种情况:
-- 1、recv_buff扩容失败
-- 2、socket client和server之间的连接断开
if not result then
log.error("udp_client_receiver.proc", "socket.rx error")
return false
end
-- 如果读取到了数据, used()就必然大于0, 进行处理
if recv_buff:used() > 0 then
log.info("udp_client_receiver.proc", "recv data len", recv_buff:used())
-- 读取socket数据接收缓冲区中的数据,赋值给data
local data = recv_buff:query()
-- 将数据data通过"RECV_DATA_FROM_SERVER"消息publish出去,给其他应用模块处理
sys.publish("RECV_DATA_FROM_SERVER", "recv from udp server: ", data)
-- 接收到数据,通知网络环境检测看门狗功能模块进行喂狗
sys.publish("FEED_NETWORK_WATCHDOG")
-- 清空socket数据接收缓冲区中的数据
recv_buff:del()
-- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
else
break
end
end
return true
end
return udp_client_receiver
@@ -0,0 +1,135 @@
--[[
@module udp_client_sender
@summary udp client socket数据发送应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为udp client socket数据发送应用功能模块,核心业务逻辑为:
1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列send_queue中;
2、udp_client_main主任务调用udp_client_sender.proc接口,遍历队列send_queue,逐条发送数据到server
3、udp client socket和server之间的连接如果出现异常,udp_client_main主任务调用udp_client_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、udp_client_sender.proc:数据发送应用逻辑处理入口,在udp_client_main.lua中调用;
3、udp_client_sender.exception_proc:数据发送应用逻辑异常处理入口,在udp_client_main.lua中调用;
]]
local udp_client_sender = {}
local libnet = require "libnet"
--[[
数据发送队列,数据结构为:
{
[1] = {data="data1", cb={func=callback_function1, para=callback_para1}},
[2] = {data="data2", cb={func=callback_function2, para=callback_para2}},
}
data的内容为真正要发送的数据,必须存在;
func的内容为数据发送结果的用户回调函数,可以不存在
para的内容为数据发送结果的用户回调函数的回调参数,可以不存在;
]]
local send_queue = {}
-- udp_client_main的任务名
udp_client_sender.TASK_NAME = "udp_client_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})
-- 通知udp_client_main主任务有数据需要发送
-- udp_client_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
sys.sendMsg(udp_client_sender.TASK_NAME, socket.EVENT, 0)
end
--[[
检查socket client是否需要发送数据,如果需要发送数据,读取并且发送完发送队列中的所有数据
@api udp_client_sender.proc(task_name, socket_client)
@param1 task_name string
表示socket.create接口创建socket client对象时所处的task的name
必须传入,不允许为空或者nil
@param2 socket_client userdata
表示由socket.create接口创建的socket client对象;
必须传入,不允许为空或者nil
@return1 result bool
表示处理结果,成功为true,失败为false
@usage
udp_client_sender.proc("tcp_client_main", socket_client)
]]
function udp_client_sender.proc(task_name, socket_client)
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, socket_client, send_item.data)
-- 发送失败
if not result then
log.error("udp_client_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("udp_client_sender.proc", "buffer is full, wait for the next time")
table.insert(send_queue, 1, send_item)
return true
end
log.info("udp_client_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 client连接出现异常时,清空等待发送的数据,并且执行发送方的回调函数
@api udp_client_sender.exception_proc()
@usage
udp_client_sender.exception_proc()
]]
function udp_client_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 udp_client_sender
@@ -0,0 +1,91 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.11.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_eth_spi:通过SPI外挂CH390H芯片的以太网卡
注意:
tcp文件夹下的tcp server仅支持一路client连接(一对一);
tcp_multi文件夹下的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这三位数字必须存在,但是没有任何用处,可以一直写为999
如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
]]
PROJECT = "tcp_udp_server_demo"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持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"
-- -- 如需一对多连接,注释掉上面的require "tcp_server_main",打开下面的require "tcp_multi_main"
-- require "tcp_multi_main"
-- -- 加载udp server socket主应用功能模块
-- require "udp_server_main"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,78 @@
--[[
@module netdrv_eth_spi
@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
@version 1.0
@date 2025.11.15
@author 王世豪
@usage
本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块 ,核心业务逻辑为:
1、打开CH390H芯片供电开关;
2、初始化spi0,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
3、以太网卡的连接状态发生变化时,在日志中进行打印;
直接使用Air780EPM V1.3版本开发板硬件测试即可;
本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
]]
local exnetif = require "exnetif"
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_ETH then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
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地址)后,内核固件会产生一个"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)
local function netdrv_eth_spi_task_func()
-- 配置SPI外接以太网芯片CH390H的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_ETH
-- 本demo使用Air780EPM V1.3版本开发板测试,开发板上的硬件配置为:
-- GPIO20为CH390H以太网芯片的供电使能控制引脚
-- 使用spi0,片选引脚使用GPIO8,中断引脚使用GPIO22
-- 如果使用的硬件和以上描述的环境不同,根据自己的硬件配置修改以下参数
exnetif.set_priority_order({
{
ETHERNET = {
pwrpin = 20,
tp = netdrv.CH390,
opts = {spi = 0, cs = 8, irq = 22}
}
}
})
end
-- 启动一个tasktask的处理函数为netdrv_eth_spi_task_func
-- 在处理函数中调用exnetif.set_priority_order设置网卡优先级
-- 因为exnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
sys.taskInit(netdrv_eth_spi_task_func)
@@ -0,0 +1,15 @@
--[[
@module netdrv_device
@summary 网络驱动设备功能模块
@version 1.0
@date 2025.11.15
@author 王世豪
@usage
本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
1、netdrv_eth_spisocket.LWIP_ETH,通过SPI外挂CH390H芯片的以太网卡;
本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
]]
-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
require "netdrv_eth_spi"
@@ -0,0 +1,177 @@
## 功能模块介绍
1、main.lua:主程序入口;
2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的单spi以太网卡;
3、tcp文件夹:tcp server(一对一)以及数据收发处理逻辑;
4、tcp_multi文件夹:tcp server(一对多)以及数据收发处理逻辑,支持多路client同时连接,支持广播和单播;
5、udp文件夹:udp server以及数据收发处理逻辑;
6、timer_app.lua:通知server定时发送数据给client
7、uart_app.luaserver和uart外设之间透传数据;
> 注意:
>
> tcp文件夹下的tcp server仅支持一路client连接(一对一);
>
> tcp_multi文件夹下的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连接;
- tcp_multi文件夹功能为创建一个tcp server(一对多),支持多路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_eth_spi:通过SPI外挂CH390H芯片的以太网卡
## 演示硬件环境
![](https://docs.openluat.com/air780epm/luatos/app/driver/eth/image/RFSvb75NRoEWqYxfCRVcVrOKnsf.jpg)
1、Air780EPM V1.3版本开发板一块+网线一根:
- 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口
2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air780EPM V1.3版本开发板和数据线的硬件接线方式为:
- Air780EPM V1.3版本开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
- USB转串口数据线,一般来说,白线连接开发板的UART1_TX,绿线连接开发板的UART1_RX,黑线连接核心板的GND,另外一端连接电脑USB口;
## 演示软件环境
1、Luatools下载调试工具
2、[Air780EPM V2016版本固件](https://docs.openluat.com/air780epm/luatos/firmware/version/)(理论上,2025年8月10日之后发布的固件都可以)
3、PC端的串口工具,建议使用SSCOM(SSCOM可以创建TCP客户端或UDP客户端,测试TCP/UDP 通信功能)
## 演示核心步骤
1、搭建好硬件环境
2、demo脚本代码netdrv_device.lua中,打开require "netdrv_eth_spi" (默认打开)
3、demo脚本代码中,测试TCP server和UDP server时,需要修改的地方如下:
- 测试TCP server(一对一)时,main.lua打开 require "tcp_server_main",注释掉 require "tcp_multi_main" 和 require "udp_server_main";同时timer_app.lua和uart_app.lua中的enable_tcp设为trueenable_udp设为false。
- 测试TCP server(一对多)时,main.lua打开 require "tcp_multi_main",注释掉 require "tcp_server_main" 和 require "udp_server_main";同时timer_app.lua和uart_app.lua中的enable_tcp设为trueenable_udp设为false。
- 测试UDP server时,main.lua打开 require "udp_server_main",注释掉 require "tcp_server_main" 和 require "tcp_multi_main";同时timer_app.lua和uart_app.lua中的enable_udp设为trueenable_tcp设为false。
4、Luatools烧录内核固件和修改后的demo脚本代码
5、烧录成功后,自动开机运行
6、TCP演示:
(1)根据烧录日志,找到TCP server的ip,此外 port 在示例代码中默认是50003
ip获取方式,是在每个netdrv网卡文件中的 ip_ready_func接口中,此处演示以太网卡的情况下,如何找到创建的TCP server的ip
```lua
local function ip_ready_func()
log.info("netdrv_eth_spi.ip_ready_func", "IP_READY: ", socket.localIP(socket.LWIP_ETH))
end
```
luatools日志打印如下:
![image](https://docs.openLuat.com/cdn/image/socket/Air780EPM_tcp_ip_ready.png)
(2)PC 端打开一个TCP客户端,连接到Air780EPM开发板创建的TCP server (本例使用SSCOM打开一个TCP客户端):
端口号:选择TCPCLient
远程:填写TCP server的ip地址 和TCP监听的port ,默认是50003
本地:填写本地PC端的IP地址
![image](https://docs.openLuat.com/cdn/image/socket/Air780EPM_tcp_client.png)
成功连接之后,即可收到TCP server主动发送的第一条消息:
![image](https://docs.openLuat.com/cdn/image/socket/Air780EPM_tcp_client1.png)
(3)另外再打开一个PC端的串口工具连接到Air780EPM开发板的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/Air780EPM_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()
log.info("netdrv_eth_spi.ip_ready_func", "IP_READY: ", socket.localIP(socket.LWIP_ETH))
end
```
luatools日志打印如下:
![image](https://docs.openLuat.com/cdn/image/socket/Air780EPM_udp_ip_ready.png)
(2)PC 端打开一个UDP客户端,连接到Air780EPM开发板创建的UDP server (本例使用SSCOM打开一个UDP客户端):
端口号:选择UDP
远程:填写UDP server的ip地址 和UDP监听的port ,默认是50003
本地:填写本地PC端的IP地址, 本例填写的port是50000
![image](https://docs.openLuat.com/cdn/image/socket/Air780EPM_udp_client.png)
成功连接之后,即可收到UDP server主动发送的第一条消息:
![image](https://docs.openLuat.com/cdn/image/socket/Air780EPM_udp_client1.png)
(3)另外再打开一个PC端的串口工具连接到Air780EPM开发板的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/Air780EPM_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连接。
@@ -0,0 +1,127 @@
--[[
@module tcp_server_main
@summary tcp server主应用功能模块
@version 1.0
@date 2025.11.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, nil, nil, 300, 10, 3)
-- 如果配置失败
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)
@@ -0,0 +1,87 @@
--[[
@module tcp_server_receiver
@summary tcp server socket数据接收应用功能模块
@version 1.0
@date 2025.11.15
@author 王世豪
@usage
本文件为tcp server 数据接收应用功能模块,核心业务逻辑为:
从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;
本文件的对外接口有2个:
1、tcp_server_receiver.proc(netc):数据接收应用逻辑处理入口,在tcp_server_main.lua中调用;
2、sys.publish("RECV_DATA_FROM_CLIENT", 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
@@ -0,0 +1,135 @@
--[[
@module tcp_server_sender
@summary tcp server socket数据发送应用功能模块
@version 1.0
@date 2025.11.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="data1", cb={func=callback_function1, para=callback_para1}},
[2] = {data="data2", cb={func=callback_function2, para=callback_para2}},
}
data的内容为真正要发送的数据,必须存在;
func的内容为数据发送结果的用户回调函数,可以不存在
para的内容为数据发送结果的用户回调函数的回调参数,可以不存在;
]]
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
@@ -0,0 +1,184 @@
--[[
@module tcp_multi_main
@summary tcp server主应用功能模块(一对多)
@version 1.0
@date 2026.08.02
@author 王世豪
@usage
本文件为tcp server主应用功能模块(一对多版本),核心业务逻辑为:
1、创建一个tcp server,通过libnet.listen(task, 0, netc, backlog)开启一对多监听;
2、循环调用libnet.listen阻塞等待客户端接入,再调用socket.accept派生独立client连接;
3、每个client通过独立的事件回调处理数据收发与断开事件,互不影响;
4、server socket只负责listen和accept,不再复用为client通信通道;
本文件没有对外接口,直接在main.lua中require "tcp_multi_main"就可以加载运行;
与tcp/目录下的一对一版本的区别:
1、libnet.listen增加backlog参数(默认8),允许排队等待accept的客户端数量;
2、socket.accept传入回调函数,派生独立client socket,每个client独立处理收发;
3、clients表管理所有已连接client,支持广播和单播;
4、server socket只负责listen/accept,不复用为client通信通道;
]]
local libnet = require "libnet"
-- 加载TCP服务器数据接收功能模块
local tcp_multi_receiver = require "tcp_multi_receiver"
-- 加载TCP服务器数据发送功能模块
local tcp_multi_sender = require "tcp_multi_sender"
-- tcp_multi_main的任务名
local TASK_NAME = tcp_multi_sender.TASK_NAME
-- 已连接client的socket列表,结构为{ [1]=client_netc, [2]=client_netc, ... }
-- 由tcp_multi_sender引用,用于广播发送
local clients = {}
tcp_multi_sender.clients = clients
-- 处理未识别的消息
local function tcp_multi_main_cbfunc(msg)
log.info("tcp_multi_main_cbfunc", msg[1], msg[2], msg[3], msg[4])
end
-- 从clients列表中移除指定client
local function remove_client(client)
for i, c in ipairs(clients) do
if tostring(c) == tostring(client) then
table.remove(clients, i)
log.info("tcp_multi_main.remove_client", "剩余连接数=", #clients)
return
end
end
end
-- 关闭并清理单个client
local function close_client(client)
if not client then return end
-- 先清理receiver为该client缓存的接收缓冲区
tcp_multi_receiver.cleanup(client)
-- 强制关闭client连接
socket.close(client)
-- 释放client对象
socket.release(client)
-- 从clients列表中移除
remove_client(client)
end
-- 单个client的事件回调函数,在socket.accept时注册
-- 每次该client有事件(数据到来/断开等)时,都会被回调
local function client_event_cb(client, event, param)
if event == socket.EVENT then
-- 有数据到来或连接异常,交给receiver处理
if not tcp_multi_receiver.proc(client) then
log.info("tcp_multi_main.client_event_cb", "client数据收发异常,关闭", client)
close_client(client)
end
elseif event == socket.CLOSED then
-- 客户端主动断开
log.info("tcp_multi_main.client_event_cb", "客户端断开", client)
close_client(client)
else
log.info("tcp_multi_main.client_event_cb", "其他事件", event)
end
end
-- tcp server socket的任务处理函数
local function tcp_multi_main_task_func()
local server_netc = nil
local result, code, client, ip, port
local listen_port = 50003 -- tcp server监听的端口号
local backlog = 8 -- 允许排队等待accept的客户端数量,0~8
while true do
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("tcp_multi_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_multi_main_task_func", "recv IP_READY", socket.dft())
server_netc = socket.create(socket.dft(), TASK_NAME)
if not server_netc then
log.error("tcp_multi_main_task_func", "socket.create失败")
goto EXCEPTION_PROC
end
socket.debug(server_netc, true)
-- 配置socker server 对象为tcp server
result = socket.config(server_netc, listen_port, nil, nil, 300, 10, 3)
-- 如果配置失败
if not result then
log.error("tcp_multi_main_task_func", "socket.config失败")
goto EXCEPTION_PROC
end
log.info("tcp_multi_main_task_func", "准备监听", socket.localIP(socket.dft()), listen_port)
-- 主循环:不断监听并accept新的客户端
while true do
-- 阻塞等待客户端连接请求到来,libnet.listen内部开启一对多监听模式
-- backlog表示最多允许排队等待accept的客户端数量
result, code = libnet.listen(TASK_NAME, 0, server_netc, backlog)
log.info("tcp_multi_main_task_func", "listen返回", result, code)
-- 监听失败或超时,退出主循环,进入异常处理
if not result then
log.error("tcp_multi_main_task_func", "监听失败或超时")
break
end
-- 有客户端连接请求到来,accept派生一个新的独立client连接
-- 第二个参数传入事件回调函数,之后该client的数据/断开事件都会进入client_event_cb
result, client, ip, port = socket.accept(server_netc, client_event_cb)
log.info("tcp_multi_main_task_func", "accept结果", result, client, ip, port)
if result and client then
-- accept成功,记录client信息到clients列表
socket.debug(client, true)
table.insert(clients, client)
log.info("tcp_multi_main_task_func", "新客户端接入", "IP=", ip, "port=", port, "当前连接数=", #clients)
-- 客户端连上了, 发一条数据给客户端(非阻塞发送)
socket.tx(client, "TCP server is UP! you are client-"..#clients)
else
log.warn("tcp_multi_main_task_func", "accept失败,稍后重新监听")
end
end
-- 出现异常
::EXCEPTION_PROC::
-- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
tcp_multi_sender.exception_proc()
-- 关闭并清理所有已连接的client
log.info("tcp_multi_main_task_func", "异常处理:关闭所有client,数量=", #clients)
while #clients > 0 do
close_client(clients[1])
end
-- 如果存在socket server对象
if server_netc then
-- 关闭socket server连接
libnet.close(TASK_NAME, 5000, server_netc)
-- 释放socket server对象
socket.release(server_netc)
server_netc = nil
end
-- 等待5秒后,再次尝试创建新的连接
sys.wait(5000)
end
end
--创建并且启动一个task
--运行这个task的主函数tcp_multi_main_task_func
sys.taskInitEx(tcp_multi_main_task_func, TASK_NAME, tcp_multi_main_cbfunc)
@@ -0,0 +1,112 @@
--[[
@module tcp_multi_receiver
@summary tcp server socket数据接收应用功能模块(一对多)
@version 1.0
@date 2026.08.02
@author 王世豪
@usage
本文件为tcp server 数据接收应用功能模块(一对多版本),核心业务逻辑为:
从指定的client socket读取内核缓冲区的数据,然后将数据发布给其他应用功能模块做进一步处理;
每个client拥有独立的接收缓冲区,互不影响。
本文件的对外接口有3个:
1、tcp_multi_receiver.proc(client):数据接收应用逻辑处理入口,在tcp_multi_main.lua的client事件回调中调用;
2、tcp_multi_receiver.cleanup(client):清理指定client的接收缓冲区,在client关闭时调用;
3、sys.publish("RECV_DATA_FROM_CLIENT", data, client)
将接收到的数据通过消息"RECV_DATA_FROM_CLIENT"发布出去;
第二个参数data为接收到的数据;
第三个参数client为数据来源的client句柄,业务模块可以基于它做单播回复;
需要处理数据的应用功能模块订阅处理此消息即可,本demo项目中uart_app.lua中订阅处理了本消息;
]]
local tcp_multi_receiver = {}
-- 每个client各自独立的socket数据接收缓冲区
-- key为tostring(client)value为zbuff
local recv_buffs = {}
--[[
检查指定的client socket是否收到数据,如果收到数据,读取并且处理完所有数据
@api tcp_multi_receiver.proc(client)
@param1 client userdata or lightuserdata
表示由socket.accept接口派生的client socket对象;
必须传入,不允许为空或者nil
@return1 result bool
表示处理结果,成功为true,失败为false(表示该client连接异常)
@usage
-- 示例:处理指定client接收数据
tcp_multi_receiver.proc(client)
]]
function tcp_multi_receiver.proc(client)
-- 以tostring(client)为key,获取该client专用的接收缓冲区
-- userdata与lightuserdata的tostring结果一致,均指向同一底层socket对象
local key = tostring(client)
if recv_buffs[key]==nil then
recv_buffs[key] = zbuff.create(1024)
-- 当recv_buff不再使用时,不需要主动调用recv_buff:free()去释放
-- 因为Lua的垃圾处理器会自动释放recv_buff所申请的内存空间
-- 如果等不及垃圾处理器自动处理,在确定以后不会再使用recv_buff时,则可以主动调用recv_buff:free()释放内存空间
end
local recv_buff = recv_buffs[key]
-- 循环从内核的缓冲区读取接收到的数据
-- 如果读取失败,返回false,退出循环
-- 如果读取成功,处理数据,并且继续循环读取
-- 如果读取成功,并且读出来的数据为空,表示已经没有数据可读,返回true,退出循环
while true do
-- 从内核的缓冲区中读取数据到recv_buff中
local succ, param = socket.rx(client, recv_buff)
-- 读取数据失败
-- 有两种情况:
-- 1、recv_buff扩容失败
-- 2、socket server和client之间的连接断开
if not succ then
log.info("tcp_multi_receiver.proc", "socket.rx error", param)
return false
end
-- 如果读取到了数据, used()就必然大于0, 进行处理
if recv_buff:used() > 0 then
log.info("tcp_multi_receiver.proc", "recv data len", recv_buff:used())
-- 读取socket数据接收缓冲区中的数据,赋值给data
local data = recv_buff:query()
log.info("tcp_multi_receiver.proc", "recv data", data)
-- 将数据以及来源client通过"RECV_DATA_FROM_CLIENT"消息publish出去,给其他应用模块处理
-- 业务模块可以通过第二个参数client,对数据来源的client做单播回复
sys.publish("RECV_DATA_FROM_CLIENT", data, client)
-- 清空socket数据接收缓冲区中的数据
recv_buff:del()
else
-- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
break
end
end
return true
end
--[[
清理指定client的接收缓冲区
@api tcp_multi_receiver.cleanup(client)
@param1 client userdata or lightuserdata
表示由socket.accept接口派生的client socket对象;
必须传入,不允许为空或者nil
@usage
-- 示例:client关闭时清理其缓冲区
tcp_multi_receiver.cleanup(client)
]]
function tcp_multi_receiver.cleanup(client)
recv_buffs[tostring(client)] = nil
end
return tcp_multi_receiver
@@ -0,0 +1,130 @@
--[[
@module tcp_multi_sender
@summary tcp server socket数据发送应用功能模块(一对多)
@version 1.0
@date 2026.08.02
@author 王世豪
@usage
本文件为tcp server socket数据发送应用功能模块(一对多版本),核心业务逻辑为:
1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息;
2、收到发送请求后,直接遍历clients列表,通过socket.tx向所有client广播数据;
3、也提供send_to(client, data)单播接口,向指定client发送数据;
4、tcp server socket如果出现异常,tcp_multi_main主任务调用exception_proc接口做异常处理;
本文件的对外接口有4个:
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_multi_sender.proc:兼容保留,一对多场景下广播由消息订阅直接完成;
3、tcp_multi_sender.send_to:向指定client单播发送数据;
4、tcp_multi_sender.exception_proc:兼容保留,一对多场景下无队列需要清空;
]]
local tcp_multi_sender = {}
-- 已连接client列表,由tcp_multi_main.lua注入,结构为{ [1]=client_netc, [2]=client_netc, ... }
tcp_multi_sender.clients = {}
-- tcp_multi_main的任务名
tcp_multi_sender.TASK_NAME = "tcp_multi_main"
--[[
"SEND_DATA_REQ"消息的处理函数
收到发送请求后,直接遍历clients列表广播
@param1 tag string
消息来源标识,例如"uart"、"timer",会作为数据前缀使用;
@param2 data string
需要发送的原始数据;
@param3 cb table
发送结果的用户回调函数及参数,可以为空;
格式:{func=callback_function, para=callback_para}
cb.func收到两个参数:result(true成功/false失败)、cb.para
]]
local function send_data_req_proc_func(tag, data, cb)
local msg = "send from "..tag..": "..data
-- 无client连接时直接回调失败
if #tcp_multi_sender.clients == 0 then
log.warn("tcp_multi_sender", "无client连接,丢弃数据")
if cb and cb.func then
cb.func(false, cb.para)
end
return
end
-- 向所有client广播,倒序遍历方便处理断开的client
local send_ok = false
for i = #tcp_multi_sender.clients, 1, -1 do
local client = tcp_multi_sender.clients[i]
-- 使用socket.tx非阻塞发送,不使用libnet.tx
-- 因为accept派生的client走回调模式,没有task_namelibnet.tx会等待task消息导致超时
local succ, buff_full = socket.tx(client, msg)
if succ and not buff_full then
send_ok = true
elseif not succ then
-- 发送失败,client可能已断开,由client_event_cb的CLOSED事件兜底清理
log.error("tcp_multi_sender", "发送失败,client可能断开", i)
elseif buff_full then
-- 内核缓冲区满,丢弃本条数据
log.warn("tcp_multi_sender", "缓冲区满,丢弃数据", i)
end
end
-- 只要有一条client发送成功,就认为发送成功
if cb and cb.func then
cb.func(send_ok, cb.para)
end
end
--[[
数据发送应用逻辑处理入口(兼容保留)
一对多场景下,数据广播在"SEND_DATA_REQ"消息订阅中直接完成,本接口不再需要实现发送逻辑
@api tcp_multi_sender.proc()
]]
function tcp_multi_sender.proc()
return true
end
--[[
向指定client单播发送数据
@api tcp_multi_sender.send_to(client, data)
@param1 client userdata or lightuserdata
表示由socket.accept接口派生的client socket对象;
@param2 data string
需要发送的数据;
@return1 succ bool
发送是否成功
]]
function tcp_multi_sender.send_to(client, data)
if not client then
return false
end
return socket.tx(client, data)
end
--[[
socket server连接出现异常时的处理(兼容保留)
一对多场景下无发送队列需要清空,本接口为空实现
@api tcp_multi_sender.exception_proc()
@usage
tcp_multi_sender.exception_proc()
]]
function tcp_multi_sender.exception_proc()
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_multi_sender
@@ -0,0 +1,71 @@
--[[
@module timer_app
@summary 定时器应用功能模块
@version 1.0
@date 2025.11.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 = false, -- 是否启用UDP发送
enable_tcp = true -- 是否启用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)
@@ -0,0 +1,101 @@
--[[
@module uart_app
@summary 串口应用功能模块
@version 1.0
@date 2025.11.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 = false, -- 是否启用UDP发送
enable_tcp = true -- 是否启用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)
@@ -0,0 +1,112 @@
--[[
@module udp_server_main
@summary udp server 主应用功能模块
@version 1.0
@date 2025.11.15
@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)
@@ -0,0 +1,71 @@
--[[
@module udp_server_receiver
@summary udp server socket数据接收应用功能模块
@version 1.0
@date 2025.11.15
@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 = {}
-- 获取客户端信息
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
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)
client_info.ip = remote_ip
client_info.port = remote_port
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
@@ -0,0 +1,115 @@
--[[
@module udp_server_sender
@summary udp server socket数据发送应用功能模块
@version 1.0
@date 2025.11.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