跳转至

06 WebSocket

一、websocket

什么是 websocket?

WebSocket 是 HTML5 下一种新的协议(本质上是一个基于 TCP 的协议),主要解决传统 HTTP 协议在 “实时通信” 场景下的效率问题。它实现了浏览器与服务器之间的全双工通信,能够节省服务器资源和带宽,达到实时通讯的目的。WebSocket 协议通过握手机制,允许客户端和服务器之间建立一个类似 TCP 的连接,从而方便它们之间的通信。

  • 在线聊天应用:允许用户实时发送和接收消息,而无需页面刷新或轮询服务器。
  • 实时协作应用:支持多用户实时编辑文档或共享白板等场景。
  • 实时游戏:允许多个玩家之间进行实时的游戏交互。
  • 实时数据展示:用于显示实时数据,如股票市场变化、天气预报更新等。
  • 实时通知和提醒:用于向用户发送实时的通知消息,如新邮件提醒、社交媒体通知等。
  • 在线会议和视频通话:支持实时的音视频通信。

websocket 的核心工作机制

全双工通信模式:

  • WebSocket 提供真正的全双工通信通道,允许客户端和服务器之间同时发送和接收数据。
  • 一旦连接建立,客户端和服务器可以主动向对方发送消息,而不需要像 HTTP 一样每次都要发起请求。
  • 这种模式实现了实时通信,适用于需要低延迟和高频率数据交换的场景。
  • 协议标识:非加密连接:ws://、加密连接:wss://

持久化连接与握手过程:

  • WebSocket 连接通过一次 HTTP 握手升级而来。客户端发送一个 HTTP 请求,包含 Upgrade: websocketConnection: Upgrade 等头部。
  • 服务器同意升级后,连接即转变为 WebSocket 连接,此后通信不再遵循 HTTP 协议。
  • 连接建立后保持打开状态,避免了 HTTP 频繁建立和断开连接的开销。

WebSocket 的主要优势

  • 实时性:支持服务器主动推送,延迟低,适用于实时应用。
  • 减少带宽消耗:相比 HTTP 轮询,WebSocket 减少了不必要的 HTTP 头开销和连接建立断开的开销。
  • 双向通信:客户端和服务器可以平等地发送消息。
  • 兼容性好:现代浏览器和大多数后端语言都支持 WebSocket。
  • 安全性:WebSocket 支持使用 TLS 加密(即 wss 协议),提供安全通信。

典型应用场景

  • 工业设备远程监控:实时传输设备状态、运行参数和告警信息。
  • 智能家居控制:家电状态实时同步和远程控制指令下发。
  • 能源管理系统:实时监控电表、水表、燃气表数据。
  • 环境监测与农业物联网:环境监测站、灾害预警系统、智慧农业远程控制等。
  • 安防与应急系统:消防监测系统、应急响应设备、智能安防系统。
  • 智慧城市:停车管理系统、公共设施监控、智能路灯控制等。

二、演示功能概述

  1. 创建 WebSocket 连接,详情如下:

注意:代码中的 WebSocket 服务器地址和端口会不定期重启或维护,仅能用作测试用途,不可商用,说不定哪一天就关闭了。用户开发项目时,需要替换为自己的商用服务器地址和端口。

  • 创建一个 WebSocket client,连接 WebSocket server;
  • 支持 wss 加密连接;

  • WebSocket 连接出现异常后,自动重连;

  • WebSocket client 按照以下几种逻辑发送数据给 server:

  • 串口应用功能模块 uart_app.lua,通过 uart1 接收到串口数据,将串口数据转发给 server;

  • 定时器应用功能模块 timer_app.lua,定时产生数据,将数据发送给 server;
  • 特殊命令处理:当收到"echo"命令时,会发送包含时间信息的 JSON 数据;

  • WebSocket client 收到 server 数据后,将数据增加"收到 WebSocket 服务器数据: "前缀后,通过 uart1 发送出去;

  • 启动一个网络业务逻辑看门狗 task,用来监控网络环境,如果连续长时间工作不正常,重启整个软件系统;
  • netdrv_device:配置连接外网使用的网卡,目前支持以下三种选择(三选一)

(1) netdrv_4g:4G 网卡

(2) netdrv_eth_spi:通过 SPI 外挂 CH390H 芯片的以太网卡

(3) netdrv_multiple:支持以上两种网卡,可以配置两种网卡的优先级

三、准备硬件环境

1、Air780EXX 核心板一块

2、TYPE-C USB 数据线一根

3、USB 转串口数据线一根

4、Air780EXX 核心板和数据线的硬件接线方式为

  • Air780EXX 核心板通过 TYPE-C USB 口供电;
  • 如果测试发现软件频繁重启,重启原因值为:poweron reason 0,可能是供电不足,此时再通过直流稳压电源对核心板的 vbat 管脚进行 4V 供电,或者 5V 管脚进行 5V 供电;
  • TYPE-C USB 数据线直接插到核心板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;
  • USB 转串口数据线,一般来说,白线连接核心板的 18/U1TXD,绿线连接核心板的 17/U1RXD,黑线连接核心板的 gnd,另外一端连接电脑 USB 口;

5、可选 AirPHY_1000 配件板一块,Air780EXX 核心板和 AirPHY_1000 配件板的硬件接线方式为:

四、演示软件环境

1.软件环境

1.1 烧录工具:

Luatools 下载调试工具

1.2 内核固件:

内核固件:Air780EHM V2012 版本固件Air780EHV V2012 版本固件Air780EGH V2012 版本固件(理论上,2025 年 7 月 26 日之后发布的固件都可以)

1.3 脚本文件:

https://gitee.com/openLuat/LuatOS/tree/master/module/Air780EHM_Air780EHV_Air780EGH/demo/WebSocket

1.4 PC 端串口工具:

  • LLCOM 的下载链接: LLCOM
  • LLCOM 配置:

串口工具中的更多设置需要设置数据位 8,停止位 1,无奇偶校验位。

1.5 LuatOS 运行所需要的 lib 文件:

使用 Luatools 烧录时,勾选 添加默认 lib 选项,使用默认 lib 脚本文件。

准备好软件环境之后,接下来查看如何烧录项目文件到 Air780EHV 核心板中,将本篇文章中演示使用的项目文件烧录到 Ai780EHM/EHV/EGH 核心板 r8000 开发板中。

2. API 介绍

sys 库:https://docs.openluat.com/osapi/core/sys/

websocket 库:https://docs.openluat.com/osapi/core/websocket/

五、核心模块代码详解

1. 主程序 (main.lua)

主程序文件 main.lua 是整个项目的入口点。它负责初始化系统环境。

1.1 初始化流程

1.1.1 项目和版本定义

定义 PROJECTVERSION 变量。

1.1.2 日志记录

使用 log.info``("main", PROJECT, VERSION) 在日志中打印项目名和版本号。

1.1.3 看门狗初始化(如果支持):

配置并启动硬件看门狗,防止程序死循环卡死。

1.1.4 加载功能模块

(1)加载网络环境检测看门狗模块(network_watchdog)。

(2)加载网络驱动设备模块(netdrv_device)。

(3)加载串口应用模块(uart_app)。

(4)加载定时器应用模块(timer_app)。

(5)加载 MQTT 客户端主模块(websocket_mainwebsocket_receiver.luawebsocket_sender.lua)。

1.1.5 启动任务调度器
  • 调用 sys.run() 启动 LuatOS 的任务调度器,开始执行各个任务。
--[[
@module  main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑 
@version 1.0
@date    2025.08.29
@author  朱天华
@usage
本demo演示的核心功能为:
1、创建一个WebSocket连接,连接WebSocket server;
2、WebSocket连接出现异常后,自动重连;
3、WebSocket连接,client按照以下几种逻辑发送数据给server
- 串口应用功能模块uart_app.lua,通过uart1接收到串口数据,将串口数据增加send from uart: 前缀后发送给server;
- 定时器应用功能模块timer_app.lua,定时产生数据,将数据增加send from timer:前缀后发送给server;
4、WebSocket连接,client收到server数据后,将数据增加recv from websocket server: 前缀后,通过uart1发送出去;
5、启动一个网络业务逻辑看门狗task,用来监控网络环境,如果连续长时间工作不正常,重启整个软件系统;
6、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
   (1) netdrv_4g:4G网卡
   (2) netdrv_wifi:WIFI STA网卡
   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
   (4) 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这三位数字必须存在,但是没有任何用处,可以一直写为000
        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
]]
PROJECT = "WEBSOCKET_LONG_CONNECTION"
VERSION = "001.000.000"

-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)

-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
if wdt then
     --配置喂狗超时时间为9秒钟
    wdt.init(9000)
    --启动一个循环定时器,每隔3秒钟喂一次狗
    sys.timerLoopStart(wdt.feed, 3000)
end

-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
--     errDump.config(true, 600)
-- end

-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用

-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
--     log.info("mem.lua", rtos.meminfo())
--     log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)

-- 加载网络环境检测看门狗功能模块
require "network_watchdog"

-- 加载网络驱动设备功能模块
require "netdrv_device"

-- 加载串口应用功能模块
require "uart_app"

-- 加载定时器应用功能模块
require "timer_app"

-- 加载WebSocket client主应用功能模块
require "websocket_main"

-- 用户代码已结束---------------------------------------------
sys.run()

2. 网络驱动 (netdrv/)

网络驱动模块负责初始化和管理不同的网络连接方式,如 4G 和以太网。

--[[
@module  netdrv_device
@summary 网络驱动设备功能模块
@version 1.0
@date    2025.07.24
@author  马梦阳
@usage
本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
1、netdrv_4g:socket.LWIP_GP,4G网卡;
2、netdrv_ethernet_spi:socket.LWIP_ETH,通过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"

2.1 4G 网络驱动 (netdrv_4g.lua)

  • 监听 IP_READYIP_LOSE 消息,监控网络连接状态。
  • 设置默认网卡为 socket.LWIP_GP
--[[
@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
        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)

-- 设置默认网卡为socket.LWIP_GP
-- 在Air780EPM上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
-- 在单4G网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
socket.dft(socket.LWIP_GP)

2.2 以太网网络驱动(netdrv_eth_spi.lua)

  • 通过 SPI 接口外挂 CH390H 芯片实现以太网。
  • 通过控制 GPIO140 引脚使能芯片供电。
  • 配置 SPI1 接口参数,用于与 CH390H 芯片通信。
  • 通过 netdrv.setup 函数配置以太网卡,并开启 DHCP 动态获取 IP 地址。
  • 设置默认网卡为 socket.LWIP_ETH
--[[
@module  netdrv_eth_spi
@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
@version 1.0
@date    2025.07.24
@author  马梦阳
@usage
本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块,核心业务逻辑为:
1、打开CH390H芯片供电开关;
2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
3、以太网卡的连接状态发生变化时,在日志中进行打印;

直接使用Air780EPM V1.3版本开发板硬件测试即可;

本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
]]

local function ip_ready_func(ip, adapter)
    if adapter == socket.LWIP_ETH then
        log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
    end
end

local function ip_lose_func(adapter)
    if adapter == socket.LWIP_ETH then
        log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
    end
end


-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI外挂CH390H芯片的以太网卡”的连接状态
-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)


-- 设置默认网卡为socket.LWIP_ETH
socket.dft(socket.LWIP_ETH)


-- 本demo测试使用的是Air780EPM V1.3版本开发板
-- GPIO20为CH390H以太网芯片的供电使能控制引脚
gpio.setup(20, 1, gpio.PULLUP)

-- 这个task的核心业务逻辑是:初始化SPI,初始化以太网卡,并在以太网卡上开启动态主机配置协议
local function netdrv_eth_spi_task_func()
    -- 初始化SPI1
    local result = spi.setup(
        0,--spi_id
        nil,
        0,--CPHA
        0,--CPOL
        8,--数据宽度
        25600000--,--频率
        -- spi.MSB,--高低位顺序    可选,默认高位在前
        -- spi.master,--主模式     可选,默认主
        -- spi.full--全双工       可选,默认全双工
    )
    log.info("netdrv_eth_spi", "spi open result", result)
    --返回值为0,表示打开成功
    if result ~= 0 then
        log.error("netdrv_eth_spi", "spi open error",result)
        return
    end

    --初始化以太网卡

    --以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
    --各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
    --也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功

    --以太网断网后,内核固件会产生一个"IP_LOSE"消息
    --各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
    --也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功

    -- socket.LWIP_ETH 指定网络适配器编号
    -- netdrv.CH390外挂CH390
    -- SPI ID 0, 片选 GPIO8
    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spi=0, cs=8})

    -- 在以太网上开启动态主机配置协议
    netdrv.dhcp(socket.LWIP_ETH, true)
end

-- 创建并且启动一个task
-- task的处理函数为netdrv_eth_spi_task_func
sys.taskInit(netdrv_eth_spi_task_func)

2.3 多网络驱动管理 (netdrv_multiple.lua)

  • 管理多个网络驱动实例,根据配置选择合适的网络连接方式。
  • 通过 exnetif.set_priority_order 函数配置多网卡的控制参数以及优先级。
  • 通过 exnetif.notify_status 函数设置网卡状态变化通知回调函数。
--[[
@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_type:string类型,表示当前使用的网卡字符串
--     adapter:number类型,表示当前使用的网卡id
-- 当所有网卡断网时:
--     net_type:为nil
--     adapter:number类型,为-1
local function netdrv_multiple_notify_cbfunc(net_type,adapter)
    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}
                }
            },

            -- 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网络应用通信异常时自动对当前使用的网卡进行连通性检测。


-- 启动一个task,task的处理函数为netdrv_multiple_task_func
-- 在处理函数中调用exnetif.set_priority_order设置网卡优先级
-- 因为exnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
sys.taskInit(netdrv_multiple_task_func)

3. websocket 客户端

websocket 客户端目录包含三个核心文件:websocket_mainwebsocket_receiver.lua和websocket_sender.lua,分别负责客户端的初始化、数据接收和数据发送。

3.1 客户端初始化

本文件为 WebSocket client 主应用功能模块

--[[
@module  websocket_main
@summary WebSocket client 主应用功能模块
@version 1.1
@date    2025.08.24
@author  陈媛媛
@usage
本文件为WebSocket client 主应用功能模块,核心业务逻辑为:
1、创建一个WebSocket client,连接server;
2、处理连接/异常逻辑,出现异常后执行重连动作;
3、调用websocket_receiver的外部接口websocket_receiver.proc,对接收到的数据进行处理;
4、调用sysplus.sendMsg接口,发送"CONNECT OK"和"DISCONNECTED"两种类型的"WEBSOCKET_EVENT"消息到websocket_sender的task,控制数据发送逻辑;
5、收到WebSocket数据后,执行sys.publish("FEED_NETWORK_WATCHDOG") 对网络环境检测看门狗功能模块进行喂狗;

本文件没有对外接口,直接在main.lua中require "websocket_main"就可以加载运行;
]]

-- 加载WebSocket client数据接收功能模块
local websocket_receiver = require "websocket_receiver"
-- 加载WebSocket client数据发送功能模块
local websocket_sender = require "websocket_sender"

-- WebSocket服务器地址和端口
-- 这里使用的地址和端口,会不定期重启或维护,仅能用作测试用途,不可商用,说不定哪一天就关闭了
-- 用户开发项目时,替换为自己的商用服务器地址和端口
-- 加密TCP链接 wss 表示加密
local SERVER_URL = "wss://echo.airtun.air32.cn/ws/echo"
-- 这是另外一个测试服务, 能响应websocket的二进制帧
--local SERVER_URL = "ws://echo.airtun.air32.cn/ws/echo2"

-- websocket_main的任务名
local TASK_NAME = websocket_sender.TASK_NAME_PREFIX.."main"

-- WebSocket client的事件回调函数
local function websocket_client_event_cbfunc(ws_client, event, data, fin, opcode)
    log.info("WebSocket事件回调", ws_client, event, data, fin, opcode)

    -- WebSocket连接成功
    if event == "conack" then
        sysplus.sendMsg(TASK_NAME, "WEBSOCKET_EVENT", "CONNECT", true)
        -- 连接成功,通知网络环境检测看门狗功能模块进行喂狗
        sys.publish("FEED_NETWORK_WATCHDOG")

    -- 接收到服务器下发的数据
    -- data:string类型,表示接收到的数据
    -- fin:number类型,1表示是最后一个数据包,0表示还有后续数据包
    -- opcode:number类型,表示数据包类型(1-文本,2-二进制)
    elseif event == "recv" then
        -- 对接收到的数据处理
        websocket_receiver.proc(data, fin, opcode)

    -- 发送成功数据
    -- data:number类型,表示发送状态(通常为nil或0)
    elseif event == "sent" then
        log.info("WebSocket事件回调", "数据发送成功,发送确认")
        -- 发送消息通知 websocket sender task
        sysplus.sendMsg(websocket_sender.TASK_NAME, "WEBSOCKET_EVENT", "SEND_OK", data)

    -- 服务器断开WebSocket连接
    elseif event == "disconnect" then
        -- 发送消息通知 websocket main task
        sysplus.sendMsg(TASK_NAME, "WEBSOCKET_EVENT", "DISCONNECTED", false)

    -- 严重异常,本地会主动断开连接
    -- data:string类型,表示具体的异常,有以下几种:
    --       "connect":tcp连接失败
    --       "tx":数据发送失败
    --       "other":其他异常
    elseif event == "error" then
        if data == "connect" then
            -- 发送消息通知 websocket main task,连接失败
            sysplus.sendMsg(TASK_NAME, "WEBSOCKET_EVENT", "CONNECT", false)
        elseif data == "other" or data == "tx" then
            -- 发送消息通知 websocket main task,出现异常
            sysplus.sendMsg(TASK_NAME, "WEBSOCKET_EVENT", "ERROR")
        end
    end
end

-- websocket main task 的任务处理函数
local function websocket_client_main_task_func()
    local ws_client
    local result, msg

    while true do
        -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
        while not socket.adapter(socket.dft()) do
            log.warn("WebSocket主任务", "等待网络就绪", 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("WebSocket主任务", "收到网络就绪消息", socket.dft())

        -- 清空此task绑定的消息队列中的未处理的消息
        sysplus.cleanMsg(TASK_NAME)

        -- 创建WebSocket client对象
        ws_client = websocket.create(nil, SERVER_URL)
        -- 如果创建WebSocket client对象失败
        if not ws_client then
            log.error("WebSocket主任务", "WebSocket创建失败")
            goto EXCEPTION_PROC
        end

        -- 设置自定义请求头
        --如果有需要,根据自己的实际需求,在此处配置请求头并打开注释。
        --if ws_client.headers then
           --ws_client:headers({Auth="Basic ABCDEGG"})
        --end

        -- 注册WebSocket client对象的事件回调函数
        ws_client:on(websocket_client_event_cbfunc)

        -- 连接server
        result = ws_client:connect()
        -- 如果连接server失败
        if not result then
            log.error("WebSocket主任务", "WebSocket连接失败")
            goto EXCEPTION_PROC
        end

        -- 连接、断开连接、异常等各种事件的处理调度逻辑
        while true do
            -- 等待"WEBSOCKET_EVENT"消息
            msg = sysplus.waitMsg(TASK_NAME, "WEBSOCKET_EVENT")
            log.info("WebSocket主任务等待消息", msg[2], msg[3], msg[4])

            -- connect连接结果
            -- msg[3]表示连接结果,true为连接成功,false为连接失败
            if msg[2] == "CONNECT" then
                -- WebSocket连接成功
                if msg[3] then
                    log.info("WebSocket主任务", "连接成功")
                    -- 通知websocket sender数据发送应用模块的task,WebSocket连接成功
                    sysplus.sendMsg(websocket_sender.TASK_NAME, "WEBSOCKET_EVENT", "CONNECT_OK", ws_client)
                -- WebSocket连接失败
                else
                    log.info("WebSocket主任务", "连接失败")
                    -- 退出循环,发起重连
                    break
                end

            -- 需要主动关闭WebSocket连接
            -- 用户需要主动关闭WebSocket连接时,可以调用sysplus.sendMsg(TASK_NAME, "WEBSOCKET_EVENT", "CLOSE")
            elseif msg[2] == "CLOSE" then
                -- 主动断开WebSocket client连接
                ws_client:disconnect()
                -- 发送disconnect之后,此处延时1秒,给数据发送预留一点儿时间
                sys.wait(1000)
                break

            -- 被动关闭了WebSocket连接
            -- 被网络或者服务器断开了连接
            elseif msg[2] == "DISCONNECTED" then
                break

            -- 出现了其他异常
            elseif msg[2] == "ERROR" then
                break
            end
        end

        -- 出现异常
        ::EXCEPTION_PROC::

        -- 清空此task绑定的消息队列中的未处理的消息
        sysplus.cleanMsg(TASK_NAME)

        -- 通知websocket sender数据发送应用模块的task,WebSocket连接已经断开
        sysplus.sendMsg(websocket_sender.TASK_NAME, "WEBSOCKET_EVENT", "DISCONNECTED")

        -- 如果存在WebSocket client对象
        if ws_client then
            -- 关闭WebSocket client,并且释放WebSocket client对象
            ws_client:close()
            ws_client = nil
        end

        -- 5秒后跳转到循环体开始位置,自动发起重连(与MQTT保持一致)
        sys.wait(5000)
    end
end

--创建并且启动一个task
sysplus.taskInitEx(websocket_client_main_task_func, TASK_NAME)

3.2 接收数据

--[[
@module  websocket_receiver
@summary WebSocket client数据接收处理应用功能模块
@version 1.0
@date    2025.08.24
@author  陈媛媛
@usage
本文件为WebSocket client 数据接收应用功能模块,核心业务逻辑为:
处理接收到的数据,同时将数据发送给其他应用功能模块做进一步处理;

本文件的对外接口有2个:
1、websocket_receiver.proc(data, fin, opcode):数据处理入口,在websocket_main.lua中调用;
2、sys.publish("RECV_DATA_FROM_SERVER", "recv from websocket server: ", data):
   将接收到的数据通过消息"RECV_DATA_FROM_SERVER"发布出去;
   需要处理数据的应用功能模块订阅处理此消息即可,本demo项目中uart_app.lua中订阅处理了本消息;
]]

local websocket_receiver = {}

-- 接收数据缓冲区
local recv_data_buff = ""

--[[
处理接收到的数据

@api websocket_receiver.proc(data, fin, opcode)

@param1 data string
表示接收到的数据

@param2 fin number
表示是否为最后一个数据包,1表示是最后一个,0表示还有后续

@param3 opcode number
表示数据包类型,1-文本,2-二进制

@return1 result nil

@usage
websocket_receiver.proc(data, fin, opcode)
]]
function websocket_receiver.proc(data, fin, opcode)
    log.info("WebSocket接收处理", "收到数据", data, "是否结束", fin, "操作码", opcode)

    -- 接收到数据,通知网络环境检测看门狗功能模块进行喂狗
    sys.publish("FEED_NETWORK_WATCHDOG")

    -- 将数据拼接到缓冲区
    recv_data_buff = recv_data_buff .. data

    -- 如果收到完整消息(fin=1)并且缓冲区有数据,则处理
    if fin == 1 and #recv_data_buff > 0 then
        local processed_data = recv_data_buff
        recv_data_buff = "" -- 清空缓冲区

        -- 尝试解析JSON格式数据
        local json_data, result, errinfo = json.decode(processed_data)
        if result and type(json_data) == "table" then
            log.info("WebSocket接收处理", "收到JSON格式数据")
            -- 如果是JSON格式,提取有用信息
            if json_data.action == "echo" and json_data.msg then
                processed_data = json_data.msg
                log.info("WebSocket接收处理", "提取echo消息", processed_data)
            end
            -- 其他JSON格式数据处理逻辑可以在这里添加
        else
            log.info("WebSocket接收处理", "收到非JSON格式数据")
        end

        -- 将处理后的数据通过"RECV_DATA_FROM_SERVER"消息publish出去,给其他应用模块处理
        sys.publish("RECV_DATA_FROM_SERVER", "收到WebSocket服务器数据: ", processed_data)
    else
        log.info("WebSocket接收处理", "收到部分数据,等待后续数据包")
    end
end

return websocket_receiver

3.3 发送数据

--[[
@module  websocket_sender
@summary WebSocket client数据发送应用功能模块
@version 1.0
@date    2025.08.25
@author  陈媛媛
@usage
本文件为WebSocket client 数据发送应用功能模块,核心业务逻辑为:
1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列send_queue中;
2、websocket sender task接收"CONNECT_OK"、"SEND_REQ"、"SEND_OK"三种类型的"WEBSOCKET_EVENT"消息,遍历队列send_queue,逐条发送数据到server;
3、websocket sender task接收"DISCONNECTED"类型的"WEBSOCKET_EVENT"消息,丢弃掉队列send_queue中未发送的数据;
4、任何一条数据无论发送成功还是失败,只要这条数据有回调函数,都会通过回调函数通知数据发送方;

本文件的对外接口有1个:
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了这个消息;
]]

local websocket_sender = {}

--[[
数据发送队列,数据结构为:
{
    [1] = {data="data1", cb={func=callback_function1, para=callback_para1}},
    [2] = {data="data2", cb={func=callback_function2, para=callback_para2}},
    [3] = {data="data3", cb={func=callback_function3, para=callback_para3}},
}
data的内容为要发送的数据,string类型,必须存在;
cb.func的内容为数据发送结果的用户回调函数,可以不存在;
cb.para的内容为数据发送结果的用户回调函数的回调参数,可以不存在;
]]

local send_queue = {}

-- WebSocket client的任务名前缀
websocket_sender.TASK_NAME_PREFIX = "websocket_"

-- websocket_client_sender的任务名
websocket_sender.TASK_NAME = websocket_sender.TASK_NAME_PREFIX.."sender"

-- "SEND_DATA_REQ"消息的处理函数
local function send_data_req_proc_func(tag, data, cb)
    -- 确保data是字符串类型
    local data_str = tostring(data)

    -- 检查是否是"echo"命令
    if data_str == '"echo"' then
        log.info("WebSocket发送处理", "收到echo命令,发送数据")
        -- 创建JSON格式的echo响应
        local response = json.encode({
            action = "echo",
            msg = os.date("%a %Y-%m-%d %H:%M:%S") -- %a表示星期几缩写
        })
        -- 将echo响应插入到发送队列send_queue中
        table.insert(send_queue, {data=response, cb=cb})
        log.info("准备发送数据到服务器,长度", #response)
        log.info("原始数据:", response)
    else
        -- 根据tag类型输出日志
        if tag == "timer" then
            -- 对于timer数据,修改日志为"发送心跳"
            log.info("发送心跳", "长度", #data_str)
            log.info("原始数据:", data_str)
            table.insert(send_queue, {data=data_str, cb=cb})
        else
            -- 其他数据(如uart)
            log.info("准备发送数据到服务器,长度", #data_str)
            log.info("原始数据:", data_str)
            log.info("UART发送到服务器的数据包类型", type(data_str))
            log.info("转发普通数据")
            table.insert(send_queue, {data=data_str, cb=cb})
        end
    end

    -- 发送消息通知 websocket sender task,有新数据等待发送
    sysplus.sendMsg(websocket_sender.TASK_NAME, "WEBSOCKET_EVENT", "SEND_REQ")
end

-- 按照顺序发送send_queue中的数据
-- 如果调用send接口成功,则返回当前正在发送的数据项
-- 如果调用send接口失败,通知回调函数发送失败后,继续发送下一条数据
local function send_item(ws_client)
    local item
    -- 如果发送队列中有数据等待发送
    while #send_queue > 0 do
        -- 取出来第一条数据赋值给item
        -- 同时从队列send_queue中删除这一条数据
        item = table.remove(send_queue, 1)

        -- 检查WebSocket连接状态
        if not ws_client or not ws_client:ready() then
            log.warn("WebSocket发送处理", "WebSocket连接未就绪,无法发送")
            -- 如果当前发送的数据有用户回调函数,则执行用户回调函数
            if item.cb and item.cb.func then
                item.cb.func(false, item.cb.para)
            end
            -- 触发重连
            sysplus.sendMsg(websocket_sender.TASK_NAME, "WEBSOCKET_EVENT", "DISCONNECTED")
            return nil
        end

        -- send数据
        -- result表示调用send接口的同步结果,返回值有以下几种:
        -- 如果失败,返回false
        -- 如果成功,返回true
        result = ws_client:send(item.data)

        -- send接口调用成功
        if result then
            -- 根据数据内容修改日志输出
            if item.data:match("^%d+$") then -- 如果数据是纯数字(来自timer)
                log.info("wbs_sender", "发送心跳成功", "长度", #item.data)
            else
                log.info("wbs_sender", "发送成功", "长度", #item.data)
            end

            -- 由于sent事件可能不会触发,我们直接认为发送成功
            if item.cb and item.cb.func then
                item.cb.func(true, item.cb.para)
            end
            -- 发送成功,通知网络环境检测看门狗功能模块进行喂狗
            -- 使用来自定时器的数据作为心跳
            if item.data:match("^%d+$") then -- 如果数据是纯数字(来自timer)
                sys.publish("FEED_NETWORK_WATCHDOG")
            end
            return item
        -- send接口调用失败
        else
            log.warn("WebSocket发送处理", "数据发送失败")
            -- 如果当前发送的数据有用户回调函数,则执行用户回调函数
            if item.cb and item.cb.func then
                item.cb.func(false, item.cb.para)
            end
            -- 触发重连
            sysplus.sendMsg(websocket_sender.TASK_NAME, "WEBSOCKET_EVENT", "DISCONNECTED")
            return nil
        end
    end
    return nil
end

-- websocket client sender的任务处理函数
local function websocket_client_sender_task_func()
    local ws_client
    local send_item_obj
    local result, msg

    while true do
        -- 等待"WEBSOCKET_EVENT"消息
        msg = sysplus.waitMsg(websocket_sender.TASK_NAME, "WEBSOCKET_EVENT")
        log.info("WebSocket发送任务等待消息", msg[2], msg[3])

        -- WebSocket连接成功
        -- msg[3]表示WebSocket client对象
        if msg[2] == "CONNECT_OK" then
            ws_client = msg[3]
            log.info("WebSocket发送任务", "WebSocket连接成功")
            -- 发送send_queue中的所有数据
            while #send_queue > 0 do
                send_item_obj = send_item(ws_client)
                if not send_item_obj then
                    break
                end
            end

        -- WebSocket send数据请求
        elseif msg[2] == "SEND_REQ" then
            log.info("WebSocket发送任务", "收到发送请求")
            -- 如果WebSocket client对象存在
            if ws_client then
                send_item_obj = send_item(ws_client)
            end

        -- WebSocket send数据成功
        elseif msg[2] == "SEND_OK" then
            log.info("WebSocket发送任务", "数据发送成功")
            -- 继续发送send_queue中的数据
            send_item_obj = send_item(ws_client)

        -- WebSocket断开连接
        elseif msg[2] == "DISCONNECTED" then
            log.info("WebSocket发送任务", "WebSocket连接断开")
            -- 清空WebSocket client对象
            ws_client = nil
            -- 如果发送队列中有数据等待发送
            while #send_queue > 0 do
                -- 取出来第一条数据赋值给send_item_obj
                -- 同时从队列send_queue中删除这一条数据
                send_item_obj = table.remove(send_queue, 1)
                -- 如果当前发送的数据有用户回调函数,则执行用户回调函数
                if send_item_obj.cb and send_item_obj.cb.func then
                    send_item_obj.cb.func(false, send_item_obj.cb.para)
                end
            end
            -- 当前没有正在等待发送结果的发送项
            send_item_obj = nil
        end
    end
end

-- 订阅"SEND_DATA_REQ"消息;
sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)

--创建并且启动一个task
sysplus.taskInitEx(websocket_client_sender_task_func, websocket_sender.TASK_NAME)

return websocket_sender

4. 应用功能 (timer_app.lua, uart_app.lua)

应用功能模块负责生成测试数据和处理串口通信。

4.1 定时器应用 (timer_app.lua)

  • 创建一个 5 秒循环的定时器。
  • 定时生成递增的测试数据。
  • 通过 sys.publish("SEND_DATA_REQ", data) 发布发送请求消息。
  • 实现发送结果回调,根据发送结果决定是否重发数据。
  • 送结果回调,根据发送结果决定是否重发数据。
--[[
@module  timer_app
@summary 定时器应用功能模块 
@version 1.0
@date    2025.07.01
@author  朱天华
@usage
本文件为定时器应用功能模块,核心业务逻辑为:
创建一个5秒的循环定时器,每次产生一段数据,通知WebSocket client进行处理;

本文件的对外接口有一个:
1、sys.publish("SEND_DATA_REQ", "timer", data, {func=send_data_cbfunc, para="timer"..data}),通过publish通知WebSocket client数据发送功能模块发送data数据;
   数据发送结果通过执行回调函数send_data_cbfunc通知本功能模块;
]]

local data = 1

-- 数据发送结果回调函数
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"
    sys.publish("SEND_DATA_REQ", "timer", data, {func=send_data_cbfunc, para="timer"..data})
    data = data+1
end

-- 启动一个5秒的单次定时器
sys.timerStart(send_data_req_timer_cbfunc, 5000)

4.2串口应用 (uart_app.lua)

  • 配置 UART1,波特率为 115200。
  • 接收来自 PC 的数据,并通过 MQTT 发送。
  • 将 MQTT 接收到的数据通过串口输出到 PC。
  • 实现数据缓冲和超时处理。
--[[
@module  uart_app
@summary 串口应用功能模块 
@version 1.0
@date    2025.07.01
@author  朱天华
@usage
本文件为串口应用功能模块,核心业务逻辑为:
1、打开uart1,波特率115200,数据位8,停止位1,无奇偶校验位;
2、uart1和pc端的串口工具相连;
3、从uart1接收到pc端串口工具发送的数据后,通知WebSocket client进行处理;
4、收到WebSocket client从WebSocket server接收到的数据后,将数据通过uart1发送到pc端串口工具;

本文件的对外接口有两个:
1、sys.publish("SEND_DATA_REQ", "uart", read_buf),通过publish通知WebSocket client数据发送功能模块发送read_buf数据,不关心数据发送成功还是失败;
2、sys.subscribe("RECV_DATA_FROM_SERVER", recv_data_from_server_proc),订阅RECV_DATA_FROM_SERVER消息,处理消息携带的数据;
]]

-- 使用UART1
local UART_ID = 1
-- 串口接收数据缓冲区
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)
        log.info("uart_app", "Sending data length:", read_buf:len())
        log.info("uart_app", "Sending data (hex):", read_buf:toHex())
        read_buf = ""
    end
end

-- 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毫秒内没收到新的数据,则处理当前收到的所有数据
            sys.timerStart(concat_timeout_func, 50)
            break
        end

        log.info("uart_app.read len", s:len())
        -- 将本次从串口读到的数据拼接到串口缓冲区read_buf中
        read_buf = read_buf..s
    end
end

-- 初始化UART1,波特率115200,数据位8,停止位1
uart.setup(UART_ID, 115200, 8, 1)

-- 注册UART1的数据接收中断处理函数
uart.on(UART_ID, "receive", read)

-- 订阅"RECV_DATA_FROM_SERVER"消息的处理函数recv_data_from_server_proc
sys.subscribe("RECV_DATA_FROM_SERVER", recv_data_from_server_proc)

4.3 时间同步应用(sntp_app.lua)

本文件为 sntp 时间同步应用功能模块,核心业务逻辑为:

1、连接 ntp 服务器进行时间同步;

2、如果同步成功,1 小时之后重新发起同步动作;

3、如果同步失败,10 秒钟之后重新发起同步动作;

--[[
@module  sntp_app
@summary sntp时间同步应用功能模块
@version 1.0
@date    2025.07.01
@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)

5. 网络环境检测看门狗 (network_watchdog.lua)

网络看门狗模块负责监控网络连接状态和数据收发情况,确保系统在网络异常时能够自动恢复。

5.1 设计原则

  • 看门狗超时时间应大于任意一个 MQTT 连接的发送间隔。
  • 通过接收 FEED_NETWORK_WATCHDOG 消息来喂狗。
  • 超时未收到喂狗消息时,系统自动重启。

5.2 实现细节

  • 创建 network_watchdog_task_func 任务函数。
  • 任务函数循环等待 FEED_NETWORK_WATCHDOG 消息,超时时间为 5 分钟。
  • 超时则调用 sys.restart("network timeout") 重启系统。
--[[
@module  network_watchdog
@summary 网络环境检测看门狗功能模块 
@version 1.0
@date    2025.07.23
@author  朱天华
@usage
本文件为网络环境检测看门狗功能模块,监控网络环境是否工作正常(设备和服务器双向通信正常,或者至少单向通信正常),核心业务逻辑为:
1、启动一个网络环境检测看门狗task,等待其他WebSocket网络应用功能模块来喂狗,如果喂狗超时,则控制软件重启;
2、如何确定"喂狗超时时间",一般来说,有以下几个原则;
   (1) 先确定一个最小基准值T1,2分钟或者5分钟或者10分钟,这个取值取决于具体项目需求,但是不能太短,因为开机后,在网络环境不太好的地方,网络初始化可能需要比较长的时间,一般推荐这个值不能小于2分钟;
   (2) 再确定一个和产品业务逻辑有关的一个值T2,这个值和产品的应用业务逻辑息息相关,假设你的产品业务中:
       <1> 服务器会定时下发数据给设备,例如设备连接上业务服务器之后,每隔3分钟,设备都会给服务器发送一次心跳,然后服务器都会立即回复一个心跳应答包;
           这种情况下,可以取3分钟的大于等于1的倍数(例如1倍,1.5倍,2倍等等)+一段时间(例如10秒钟,如果前面是1倍,则此处必须加一段时间,给网络数据传输过程留够充足的时间);
       <2> 如果服务器不会定时下发数据给设备,但是WebSocket使用的是长连接,并且设备会定时发送数据给服务器,例如设备连接上业务服务器之后,每隔2分钟,设备都会给服务器发送一次心跳;
           这种情况下,可以取2分钟的大于等于1的倍数(例如1倍,1.5倍,2倍等等)+一段时间(例如10秒钟,如果前面是1倍,则此处必须加一段时间,给网络数据传输过程留够充足的时间);       
       <3> 如果服务器既不会定时或者至少一段时间下发应用数据给设备,设备也不会定时或者至少一段时间上传应用数据到服务器;
           这种情况下,一般来说也不是长连接应用,一般来说也不需要网络业务逻辑看门狗,遇到这种情况再具体问题具体分析;
    (3) 取T1和T2的最大值,就是"喂狗超时时间"
3、其他WebSocket网络业务功能模块的喂狗时机,和上面2.2的描述相对应,一般来说,可以在以下几种时间点执行喂狗动作:
   (1) 设备收到服务器下发的数据时
   (2) WebSocket连接下,设备成功发送数据到服务器时
   (3) WebSocket连接成功时(不到迫不得已,这种情况下不要喂狗,如果喂狗,可能会影响以上两点的判断;
                因为长连接的收发数据失败会导致一直重连,重连成功喂狗就会掩盖收发数据异常,除非收发数据完全无规律,才可能在WebSocket连接成功时喂狗)
4、最重要的一点是:以上所说的原则,仅仅是建议,要根据自己的实际项目业务逻辑以及自己的需求最终确定看门狗方案

5、具体到本demo
   (1) 产品业务逻辑为:
       <1> 创建了一个WebSocket连接,设备会定时发送数据到服务器,服务器何时下发数据给设备不确定;
   (2) 确定喂狗超时时间:
       <1> 本demo支持单WIFI、单以太网、单4G网络连接外网,网络环境准备就绪预留2分钟的时间已经足够,所以最小基准值T1取值2分钟;
       <2> 本demo中存在1路WebSocket连接,设备会定时发送数据给服务器,定时发送的时间间隔为5秒,在网络环境波动的时候,数据发送延时会比较大;
           在这个demo中,我能接受的延时发送时长是1分钟,能接受连续3次延时发送时长的失败,所以,T2取值3分钟;
       <3> 取T1 2分钟和T2 3分钟的最大值,最终的喂狗超时时间就是3分钟;
   (3) 确定喂狗时机:
       <1> WebSocket连接中,收到服务器的下发数据时;       
       <2> WebSocket连接中,成功发送数据给服务器时;

本文件没有对外接口,直接在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
sys.taskInit(network_watchdog_task_func)

六、系统与用户消息类型

1. 系统消息

  • IP_READY:网络 IP 地址已准备好。
  • IP_LOSE:网络 IP 地址丢失。
  • NTP_UPDATE:SNTP 时间同步完成。

2.用户消息

  • RECV_DATA_FROM_SERVER:从 websocket 服务器接收到数据。
  • SEND_DATA_REQ:请求发送数据。
  • FEED_NETWORK_WATCHDOG:网络看门狗喂狗消息。

七、演示功能

1. 不同网卡切换

Air780EHM/EHV/EGHX8000 工业引擎支持单 4g 网卡,单 wifi 网卡,单 spi 以太网卡,多网卡。

切换网卡为 4G 网卡:

netdrv_device.lua 模块里只打开 netdrv_4g 模块。netdrv_4g.lua 模块中的代码不需要修改。

LuaTools 工具日志打印:

如下图所示,如出现类似 I/user.netdrv_4g.ip_ready_func IP_READY 10.200.191.34 255.255.255.255 0.0.0.0 nil 的日志,则表示 4g 网卡连接成功。

切换网卡为以太网卡:

注意:Air780EHM/EHV/EGH8000 的以太网卡是通过 SPI 外挂 CH390H 芯片实现的。

netdrv_device.lua 模块里只打开 netdrv_eth_spi 模块。如果是使用合宙官方的开发板,netdrv_eth_spi 模块中的代码不需要修改。

luatools 日志打印:

如出现类似 I/user.netdrv_eth_spi.ip_ready_func IP_READY 192.168.0.110 255.255.255.0 192.168.0.1 nil 的日志,则表示以太网卡联网成功。

多网卡自动切换:

如果需要多网卡,打开 require "netdrv_multiple",其余注释掉;同时 netdrv_multiple.lua 中的 ssid = "茶室-降功耗,找合宙!", password = "Air123456", 修改为自己测试时 wifi 热点的名称和密码;注意:仅支持 2.4G 的 wifi,不支持 5G 的 wifi。 可根据自己的需求调整网卡的优先级,以下示例设置为以太网卡是最高优先级。

首先在 netdrv_device.lua 文件中只打开 netdrv_multiple 模块。

默认以太网卡进行连接

拔掉网线后,网络切换为 4g 网卡

2. websocket 通信实操

客户端数据发送与接收:

下图为 Air780EXX 建立的 websocket 客户端发送数据成功后的日志打印。

当收到的是心跳包或普通数据消息时,代码会直接将数据透传给服务器。

当收到的是"echo"命令时,则会生成一个包含当前时间的 JSON 格式响应消息并发送给服务器。

八、总结

至此,我们演示了使用不同网卡进行 websocket 通信的全过程,相信聪明的你已经完全领悟 websocket 通信的逻辑了,快来实际操作一下吧!