Преглед изворни кода

Merge branch 'master' of gitee.com:openLuat/LuatOS

Wendal Chen пре 6 месеци
родитељ
комит
d4e9d54056
100 измењених фајлова са 10104 додато и 301 уклоњено
  1. 142 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/README.md
  2. 119 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/air_srv_fota.lua
  3. 91 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/main.lua
  4. 33 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/netdrv/netdrv_4g.lua
  5. 85 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/netdrv/netdrv_eth_spi.lua
  6. 95 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/netdrv/netdrv_multiple.lua
  7. 30 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/netdrv_device.lua
  8. 121 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/psm_power_fota.lua
  9. 140 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/tcp_iot/tcp_iot_main.lua
  10. 111 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/tcp_iot/tcp_iot_receiver.lua
  11. 139 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/tcp_iot/tcp_iot_sender.lua
  12. 101 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/update.lua
  13. 0 101
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/main.lua
  14. 155 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/README.md
  15. 139 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/customer_srv_fota.lua
  16. 108 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/main.lua
  17. 33 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/netdrv/netdrv_4g.lua
  18. 85 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/netdrv/netdrv_eth_spi.lua
  19. 95 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/netdrv/netdrv_multiple.lua
  20. 30 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/netdrv_device.lua
  21. 131 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/psm_power_fota.lua
  22. 140 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/tcp_self_server/tcp_self_main.lua
  23. 126 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/tcp_self_server/tcp_self_receiver.lua
  24. 137 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/tcp_self_server/tcp_self_sender.lua
  25. 128 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/update.lua
  26. 128 0
      module/Air780EPM/demo/fota2/iot_server/README.md
  27. 119 0
      module/Air780EPM/demo/fota2/iot_server/air_srv_fota.lua
  28. 91 0
      module/Air780EPM/demo/fota2/iot_server/main.lua
  29. 33 0
      module/Air780EPM/demo/fota2/iot_server/netdrv/netdrv_4g.lua
  30. 85 0
      module/Air780EPM/demo/fota2/iot_server/netdrv/netdrv_eth_spi.lua
  31. 95 0
      module/Air780EPM/demo/fota2/iot_server/netdrv/netdrv_multiple.lua
  32. 30 0
      module/Air780EPM/demo/fota2/iot_server/netdrv_device.lua
  33. 121 0
      module/Air780EPM/demo/fota2/iot_server/psm_power_fota.lua
  34. 140 0
      module/Air780EPM/demo/fota2/iot_server/tcp_iot/tcp_iot_main.lua
  35. 111 0
      module/Air780EPM/demo/fota2/iot_server/tcp_iot/tcp_iot_receiver.lua
  36. 139 0
      module/Air780EPM/demo/fota2/iot_server/tcp_iot/tcp_iot_sender.lua
  37. 101 0
      module/Air780EPM/demo/fota2/iot_server/update.lua
  38. 0 99
      module/Air780EPM/demo/fota2/main.lua
  39. 141 0
      module/Air780EPM/demo/fota2/self_server/README.md
  40. 139 0
      module/Air780EPM/demo/fota2/self_server/customer_srv_fota.lua
  41. 108 0
      module/Air780EPM/demo/fota2/self_server/main.lua
  42. 33 0
      module/Air780EPM/demo/fota2/self_server/netdrv/netdrv_4g.lua
  43. 85 0
      module/Air780EPM/demo/fota2/self_server/netdrv/netdrv_eth_spi.lua
  44. 95 0
      module/Air780EPM/demo/fota2/self_server/netdrv/netdrv_multiple.lua
  45. 30 0
      module/Air780EPM/demo/fota2/self_server/netdrv_device.lua
  46. 130 0
      module/Air780EPM/demo/fota2/self_server/psm_power_fota.lua
  47. 140 0
      module/Air780EPM/demo/fota2/self_server/tcp_self_server/tcp_self_main.lua
  48. 126 0
      module/Air780EPM/demo/fota2/self_server/tcp_self_server/tcp_self_receiver.lua
  49. 137 0
      module/Air780EPM/demo/fota2/self_server/tcp_self_server/tcp_self_sender.lua
  50. 128 0
      module/Air780EPM/demo/fota2/self_server/update.lua
  51. 128 0
      module/Air8000/demo/fota2/iot_server/README.md
  52. 119 0
      module/Air8000/demo/fota2/iot_server/air_srv_fota.lua
  53. 91 0
      module/Air8000/demo/fota2/iot_server/main.lua
  54. 33 0
      module/Air8000/demo/fota2/iot_server/netdrv/netdrv_4g.lua
  55. 85 0
      module/Air8000/demo/fota2/iot_server/netdrv/netdrv_eth_spi.lua
  56. 95 0
      module/Air8000/demo/fota2/iot_server/netdrv/netdrv_multiple.lua
  57. 50 0
      module/Air8000/demo/fota2/iot_server/netdrv/netdrv_wifi.lua
  58. 33 0
      module/Air8000/demo/fota2/iot_server/netdrv_device.lua
  59. 122 0
      module/Air8000/demo/fota2/iot_server/psm_power_fota.lua
  60. 140 0
      module/Air8000/demo/fota2/iot_server/tcp_iot/tcp_iot_main.lua
  61. 111 0
      module/Air8000/demo/fota2/iot_server/tcp_iot/tcp_iot_receiver.lua
  62. 139 0
      module/Air8000/demo/fota2/iot_server/tcp_iot/tcp_iot_sender.lua
  63. 101 0
      module/Air8000/demo/fota2/iot_server/update.lua
  64. 0 101
      module/Air8000/demo/fota2/main.lua
  65. 141 0
      module/Air8000/demo/fota2/self_server/README.md
  66. 138 0
      module/Air8000/demo/fota2/self_server/customer_srv_fota.lua
  67. 108 0
      module/Air8000/demo/fota2/self_server/main.lua
  68. 33 0
      module/Air8000/demo/fota2/self_server/netdrv/netdrv_4g.lua
  69. 85 0
      module/Air8000/demo/fota2/self_server/netdrv/netdrv_eth_spi.lua
  70. 95 0
      module/Air8000/demo/fota2/self_server/netdrv/netdrv_multiple.lua
  71. 50 0
      module/Air8000/demo/fota2/self_server/netdrv/netdrv_wifi.lua
  72. 33 0
      module/Air8000/demo/fota2/self_server/netdrv_device.lua
  73. 131 0
      module/Air8000/demo/fota2/self_server/psm_power_fota.lua
  74. 140 0
      module/Air8000/demo/fota2/self_server/tcp_self_server/tcp_self_main.lua
  75. 128 0
      module/Air8000/demo/fota2/self_server/tcp_self_server/tcp_self_receiver.lua
  76. 137 0
      module/Air8000/demo/fota2/self_server/tcp_self_server/tcp_self_sender.lua
  77. 128 0
      module/Air8000/demo/fota2/self_server/update.lua
  78. 182 0
      module/Air8101/demo/fota2/iot_server/README.md
  79. 119 0
      module/Air8101/demo/fota2/iot_server/air_srv_fota.lua
  80. 91 0
      module/Air8101/demo/fota2/iot_server/main.lua
  81. 118 0
      module/Air8101/demo/fota2/iot_server/netdrv/netdrv_4g.lua
  82. 77 0
      module/Air8101/demo/fota2/iot_server/netdrv/netdrv_eth_rmii.lua
  83. 102 0
      module/Air8101/demo/fota2/iot_server/netdrv/netdrv_eth_spi.lua
  84. 145 0
      module/Air8101/demo/fota2/iot_server/netdrv/netdrv_multiple.lua
  85. 55 0
      module/Air8101/demo/fota2/iot_server/netdrv/netdrv_wifi.lua
  86. 37 0
      module/Air8101/demo/fota2/iot_server/netdrv_device.lua
  87. 119 0
      module/Air8101/demo/fota2/iot_server/psm_power_fota.lua
  88. 140 0
      module/Air8101/demo/fota2/iot_server/tcp_iot/tcp_iot_main.lua
  89. 111 0
      module/Air8101/demo/fota2/iot_server/tcp_iot/tcp_iot_receiver.lua
  90. 139 0
      module/Air8101/demo/fota2/iot_server/tcp_iot/tcp_iot_sender.lua
  91. 101 0
      module/Air8101/demo/fota2/iot_server/update.lua
  92. 194 0
      module/Air8101/demo/fota2/self_server/README.md
  93. 139 0
      module/Air8101/demo/fota2/self_server/customer_srv_fota.lua
  94. 108 0
      module/Air8101/demo/fota2/self_server/main.lua
  95. 118 0
      module/Air8101/demo/fota2/self_server/netdrv/netdrv_4g.lua
  96. 77 0
      module/Air8101/demo/fota2/self_server/netdrv/netdrv_eth_rmii.lua
  97. 102 0
      module/Air8101/demo/fota2/self_server/netdrv/netdrv_eth_spi.lua
  98. 145 0
      module/Air8101/demo/fota2/self_server/netdrv/netdrv_multiple.lua
  99. 55 0
      module/Air8101/demo/fota2/self_server/netdrv/netdrv_wifi.lua
  100. 37 0
      module/Air8101/demo/fota2/self_server/netdrv_device.lua

+ 142 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/README.md

@@ -0,0 +1,142 @@
+## 功能模块介绍
+
+### iot服务器fota功能演示
+
+1、main.lua:主程序入口;
+
+2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的四种网卡(单4g网卡,单wifi网卡,单spi以太网卡,多网卡)中的任何一种网卡;
+
+3、update.lua:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
+
+4、tcp_iot文件夹:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备启动air_srv_fota功能模块,使用合宙iot服务器进行升级;
+
+5、air_srv_fota.lua:合宙服务器升级功能模块;
+
+6、psm_power_fota.lua:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。需要注意的是此场景与上面两种场景不能同时使用;
+
+
+## 系统消息介绍
+
+1、"IP_READY":某种网卡已经获取到ip信息,仅仅获取到了ip信息,能否和外网连通还不确认;
+
+2、"IP_LOSE":某种网卡已经掉网;
+
+
+
+## 用户消息介绍
+
+1、"RECV_DATA_FROM_SERVER":socket client收到服务器下发的数据后,通过此消息发布出去,给其他应用模块处理;
+
+2、"SEND_DATA_REQ":其他应用模块发布此消息,通知socket client发送数据给服务器;
+
+
+
+## 演示功能概述
+
+1、此demo演示了三种场景:
+
+   (1) fota升级简单演示:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
+
+   (2) tcp服务器下发升级指令:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块;
+
+   (3) psm低功耗fota:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子;
+
+2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
+
+   (1) netdrv_4g:4G网卡
+
+   (2) netdrv_wifi:WIFI STA网卡
+
+   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
+
+   (4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
+
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/air780ehv/luatos/common/hwenv/image/Air780EHV.png)
+
+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、可选AirETH_1000配件板一块,Air780EXX核心板和AirETH_1000配件板的硬件接线方式为:
+
+| Air780EXX核心板  |  AirETH_1000配件板 |
+| --------------- | ----------------- |
+| 3V3             | 3.3v              |
+| gnd             | gnd               |
+| 86/SPI0CLK      | SCK               |
+| 83/SPI0CS       | CSS               |
+| 84/SPI0MISO     | SDO               |
+| 85/SPI0MOSI     | SDI               |
+| 107/GPIO21      | INT               |
+
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air780EHM V2012版本固件](https://docs.openluat.com/air780epm/luatos/firmware/version/#air780ehmluatos)、[Air780EHV V2012版本固件](https://docs.openluat.com/air780ehv/luatos/firmware/version/)、[Air780EGH V2012版本固件](https://docs.openluat.com/air780egh/luatos/firmware/version/)(理论上,2025年7月26日之后发布的固件都可以)
+
+3、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/);
+
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/),点击 打开TCP 按钮,会创建一个TCP server,将server的地址和端口赋值给tcp_iot_main.lua中的SERVER_ADDR和SERVER_PORT两个变量
+
+4、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
+
+5、Luatools烧录内核固件和修改后的demo脚本代码
+
+6、使用Luatools制作升级包,先把新旧版本分别生成量产文件,然后再制作升级包,工具上栏 luatOS->固件工具->差分包/整包升级包制作,将制作好的升级包配置到合宙iot服务器自己项目下,或上传到自建服务器上面;
+
+7、烧录成功后,自动开机运行
+
+8、可以看到升级过程如下,不管是什么场景下升级,基本都是如下日志情况:
+
+``` lua
+--没有升级之前可以看到如下打印
+I/user.fota 脚本版本号 001.000.000 core版本号 V2010
+
+I/user.fota_task_func recv IP_READY 1
+I/user.开始检查升级
+
+I/user.升级包下载成功,重启模块
+
+
+--升级之后可以看到如下打印
+I/user.fota 脚本版本号 001.000.001 core版本号 V2012
+--升级重启之后还是会检查升级,所以会有如下打印属于正常情况,其中"code": 27 是合宙iot服务器返回的状态码,意思是已经是最新版本了。
+I/user.fota -9 {"code": 27, "msg": "\u5df2\u662f\u6700\u65b0\u7248\u672c"}
+I/user.使用合宙服务器,接下来解析body里的code
+I/user.已是最新版本 1.设备的固件/脚本版本高于或等于云平台上的版本号 2.用户项目升级配置中未添加该设备 3.云平台升级配置中,是否升级配置为否
+I/user.fota 4
+
+
+```
+9、对于psm休眠状态下的升级的场景,可以通过iot平台查看是否成功,在iot平台的升级日志页面搜索模组的imei,可以看到有两条升级结果“成功”和“已是最新版本”。模组升级成功后会自动进入psm休眠状态。可以通过电流查看休眠情况。

+ 119 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/air_srv_fota.lua

@@ -0,0 +1,119 @@
+--[[
+@module  air_srv_fota
+@summary 使用合宙iot平台远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、接收 AIR_SRV_FOTA 系统消息,触发升级;
+2、判断网卡是否连接成功;
+3、初始化fota2模块;
+4、调用fota2模块的升级函数;
+5、根据升级结果进行处理;
+]]
+
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("降功耗 找合宙")
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+
+sys.timerLoopStart(get_version, 3000)
+
+
+-- fota升级标志:true 表示当前正有 FOTA 流程在跑
+local fota_running = false
+
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    -- fota结束,无论成功还是失败,都释放fota_running标志
+    fota_running = false
+
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
+            "3) 已经是最新版本,无需升级")
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local opts = {}
+
+local function air_fota_func(data)
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+    while true do
+        -- 阻塞等待外部事件:"AIR_SRV_FOTA"
+        sys.waitUntil("AIR_SRV_FOTA")
+
+        -- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
+        if "123" == _G.PRODUCT_KEY then
+            while 1 do
+                sys.wait(1000)
+                log.info("fota", "请修改正确的PRODUCT_KEY")
+            end
+        end
+
+        if fota_running then
+            log.warn("fota_task", "FOTA 正在运行,跳过本次请求")
+        else
+            -- 标记FOTA正在运行
+            -- 注意:这里只是标记,实际的FOTA流程还没有开始
+            fota_running = true
+            log.info("开始检查升级")
+            libfota2.request(fota_cb, opts)
+        end
+    end
+end
+-- 初始化FOTA任务
+sys.taskInit(air_fota_func)
+
+
+-- 演示定时自动升级, 每隔4小时自动检查一次,可以根据需求打开
+-- sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, opts)

+ 91 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/main.lua

@@ -0,0 +1,91 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本demo演示的核心功能为:
+1、此demo演示了三种场景:
+   (1)fota升级简单演示:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
+   (2)tcp服务器下发升级指令:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块;
+   (3)psm低功耗fota:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子;
+2、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 = "FOTA2_DEMO"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+
+-- 加载网络驱动设备功能模块
+require "netdrv_device"
+
+-- 加载远程升级功能模块,场景1
+require "update"
+---------------------------------------------------------------------------
+-- 加载tcp client iot socket主应用功能模块,通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块,场景二
+-- require "tcp_iot_main"
+-- 加载合宙iot平台远程升级功能模块
+-- require "air_srv_fota"
+---------------------------------------------------------------------------
+-- 加载psm+低功耗模式升级功能模块,场景三
+-- require "psm_power_fota"
+
+
+
+
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

+ 33 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/netdrv/netdrv_4g.lua

@@ -0,0 +1,33 @@
+--[[
+@module  netdrv_4g
+@summary “4G网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为4G网卡驱动模块,核心业务逻辑为:
+1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
+end
+
+
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+-- 设置默认网卡为socket.LWIP_GP
+-- 在Air8000上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
+-- 在单4G网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
+socket.dft(socket.LWIP_GP)

+ 85 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/netdrv/netdrv_eth_spi.lua

@@ -0,0 +1,85 @@
+--[[
+@module  netdrv_eth_spi
+@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块 ,核心业务逻辑为:
+1、打开CH390H芯片供电开关;
+2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
+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测试使用的是Air8000开发板
+--GPIO140为CH390H以太网芯片的供电使能控制引脚
+gpio.setup(140, 1, gpio.PULLUP)
+
+--这个task的核心业务逻辑是:初始化SPI,初始化以太网卡,并在以太网卡上开启动态主机配置协议
+local function netdrv_eth_spi_task_func()
+    -- 初始化SPI1
+    local result = spi.setup(
+        1,--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 1, 片选 GPIO12
+    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spi=1, cs=12})
+
+    --在以太上开启动态主机配置协议
+    netdrv.dhcp(socket.LWIP_ETH, true)
+end
+
+--创建并且启动一个task
+--task的处理函数为netdrv_eth_spi_task_func
+sys.taskInit(netdrv_eth_spi_task_func)

+ 95 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/netdrv/netdrv_multiple.lua

@@ -0,0 +1,95 @@
+--[[
+@module  netdrv_multiple
+@summary 多网卡(4G网卡、WIFI STA网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为多网卡驱动模块 ,核心业务逻辑为:
+1、调用libnetif.set_priority_order配置多网卡的控制参数以及优先级;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_multiple"就可以加载运行;
+]]
+
+
+local exnetif = require "exnetif"
+
+-- 网卡状态变化通知回调函数
+-- 当libnetif中检测到网卡切换或者所有网卡都断网时,会触发调用此回调函数
+-- 当网卡切换切换时:
+--     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芯片”的以太网卡,使用Air8000开发板验证
+            {
+                ETHERNET = {
+                    -- 供电使能GPIO
+                    pwrpin = 140,
+                    -- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
+                    -- 如果没有传入此参数,libnetif会使用默认值10秒
+                    ping_time = 3000,
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+
+                    -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
+                    tp = netdrv.CH390,
+                    opts = {spi=1, cs=12}
+                }
+            },
+
+            -- WIFI STA网卡
+            {
+                WIFI = {
+                    -- 要连接的WIFI路由器名称
+                    ssid = "茶室-降功耗,找合宙!",
+                    -- 要连接的WIFI路由器密码
+                    password = "Air123456",
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+                }
+            },
+
+            -- 4G网卡
+            {
+                LWIP_GP = true
+            }
+        }
+    )
+end
+
+-- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc
+exnetif.notify_status(netdrv_multiple_notify_cbfunc)
+
+-- 如果存在udp网络应用,并且udp网络应用中,根据应用层的心跳能够判断出来udp数据通信出现了异常;
+-- 可以在判断出现异常的位置,调用一次libnetif.check_network_status()接口,强制对当前正式使用的网卡进行一次连通性检测;
+-- 如果存在tcp网络应用,不需要用户调用libnetif.check_network_status()接口去控制,libnetif会在tcp网络应用通信异常时自动对当前使用的网卡进行连通性检测。
+
+
+-- 启动一个task,task的处理函数为netdrv_multiple_task_func
+-- 在处理函数中调用libnetif.set_priority_order设置网卡优先级
+-- 因为libnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
+sys.taskInit(netdrv_multiple_task_func)

+ 30 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/netdrv_device.lua

@@ -0,0 +1,30 @@
+--[[
+@module  netdrv_device
+@summary 网络驱动设备功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
+1、netdrv_4g:socket.LWIP_GP,4G网卡;
+2、netdrv_wifi:socket.LWIP_STA,WIFI STA网卡;
+3、netdrv_ethernet_spi:socket.LWIP_USER1,通过SPI外挂CH390H芯片的以太网卡;
+4、netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
+
+根据自己的项目需求,只需要require以上四种中的一种即可;
+
+
+本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
+]]
+
+
+-- 根据自己的项目需求,只需要require以下四种中的一种即可;
+
+-- 加载“4G网卡”驱动模块
+require "netdrv_4g"
+
+-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
+-- require "netdrv_eth_spi"
+
+-- 加载“可以配置优先级的多种网卡”驱动模块
+-- require "netdrv_multiple"

+ 121 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/psm_power_fota.lua

@@ -0,0 +1,121 @@
+--[[
+@module  psm+_power_fota
+@summary psm+超低功耗模式下升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为psm+超低功耗模式下升级功能模块,核心设计思路
+1.升级触发机制 :
+   - 定时器唤醒升级 :设备定期从PSM模式唤醒,主动检查是否有新固件版本。
+   - 外部中断唤醒升级 :通过特定GPIO中断或网络消息唤醒设备进行升级。
+2.防止升级过程中进入休眠 :
+   - 在开始FOTA升级前,禁用PSM模式进入。
+   - 升级完成后,根据结果决定是否重启设备或重新进入PSM模式。
+
+]]
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "BnYk2BlYO30DiWra7q27wUmEarOiipHO" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+
+
+
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    --升级结束,触发升级回调,发布消息升级结束,可以进入休眠模式
+    sys.publish("FOTA_END")
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
+            "3) 已经是最新版本,无需升级")
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local ota_opts = {}
+
+
+function psm_fota_task_func()
+    -- 如果是被定时器唤醒,因为上次进入PSM+时是开启了飞行模式,所以在唤醒后第一时间关闭飞行模式。
+    mobile.flymode(0, false)
+    log.info("开始测试PSM+模式功耗。")
+
+    -- 打印版本号, 方便看版本号变化, 非必须
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, ota_opts)
+
+
+
+    -- 等待下载升级包结束, 发布消息"FOTA_END",
+    -- 如果15秒内没有收到消息,则15秒的时长到达后进入PSM+模式。
+    -- 需要注意的是在fota_cb回调函数中,升级包下载成功后,会立马重启并升级模组。如果还有其他事情要做不想立马重启升级,需自行决定reboot的时机
+    -- 升级包下载成功后,本demo默认是立即自动重启并且将升级包更新到模组中,更新成功后,会再次走到这里
+    -- 再次走到这里后,合宙iot平台会返回“已经是最新版本,不需要升级”,fota_cb回调函数中会发布消息"FOTA_END"
+    -- 至此,才会继续向下执行代码,进入PSM+模式
+    sys.waitUntil("FOTA_END", 15000)
+
+    log.info("升级结束,进入PSM模式")
+
+
+
+    -- 定时检查升级 (每4小时唤醒一次)
+    pm.dtimerStart(2, 4 * 3600000)
+    -- 启动飞行模式,规避可能会出现的网络问题
+    mobile.flymode(0, true)
+    -- 进入PSM模式
+    pm.power(pm.WORK_MODE, 3)
+    -- 防御机制:15秒后如果未进入PSM则重启
+    sys.wait(15000)
+    log.info("进入PSM+失败,重启")
+    rtos.reboot()
+end
+
+sys.taskInit(psm_fota_task_func)

+ 140 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/tcp_iot/tcp_iot_main.lua

@@ -0,0 +1,140 @@
+--[[
+@module  tcp_client_main
+@summary tcp client socket主应用功能模块
+@version 1.0
+@date    2025.08.12
+@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_iot_receiver"
+-- 加载tcp client socket数据发送功能模块
+local tcp_client_sender = require "tcp_iot_sender"
+
+-- 电脑访问:https://netlab.luatos.com/
+-- 点击 打开TCP 按钮,会创建一个TCP server
+-- 将server的地址和端口赋值给下面这两个变量
+local SERVER_ADDR = "112.125.89.8"
+local SERVER_PORT = 42662
+
+-- 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毫秒超时不要修改的更长;
+            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+            -- 当libnetif.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)
+        -- 如果配置失败
+        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")
+
+        -- 数据收发以及网络连接异常事件总处理逻辑
+        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
+sysplus.taskInitEx(tcp_client_main_task_func, TASK_NAME, tcp_client_main_cbfunc)

+ 111 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/tcp_iot/tcp_iot_receiver.lua

@@ -0,0 +1,111 @@
+--[[
+@module  tcp_client_receiver
+@summary tcp client socket数据接收应用功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为tcp client socket数据接收应用功能模块,核心业务逻辑为:
+从内核读取接收到的数据,然后将数据处理后发送给其他应用功能模块做进一步处理;
+
+本文件的对外接口有2个:
+1、tcp_client_receiver.proc(socket_client):数据接收应用逻辑处理入口,在tcp_client_main.lua中调用;
+2、sys.publish("RECV_DATA_FROM_SERVER", data):
+   将接收到的数据通过消息"RECV_DATA_FROM_SERVER"发布出去处理;
+   处理后通过消息"AIR_SRV_FOTA"发布到air_srv_fota.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_IOT_DATA_SERVER"消息publish出去,给其他应用模块处理
+            sys.publish("RECV_IOT_DATA_SERVER", data)
+
+            -- 清空socket数据接收缓冲区中的数据
+            recv_buff:del()
+            -- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
+        else
+            break
+        end
+    end
+
+    return true
+end
+
+--解析接受到的json参数并发布到消息"AIR_SRV_FOTA"中
+--定义一个json格式如下,具体可以根据实际情况定义,使用iot升级的话只定义了一个参数即可:
+--{
+--    "fota": "true"
+--}
+
+local function data_processing(data)
+    -- 解析json数据
+    local json_data = json.decode(data)
+    -- 如果解析失败
+    if not json_data then
+        log.error("data_processing", "json.decode error")
+        return
+    end
+    -- 如果fota为true
+    if json_data.fota == "true" then
+        -- 发布消息"AIR_SRV_FOTA"
+        sys.publish("AIR_SRV_FOTA")
+    end
+end
+
+-- 订阅"RECV_IOT_DATA_SERVER"消息
+sys.subscribe("RECV_IOT_DATA_SERVER", data_processing)
+
+return tcp_client_receiver

+ 139 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/tcp_iot/tcp_iot_sender.lua

@@ -0,0 +1,139 @@
+--[[
+@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"消息,将其他应用模块需要发送的数据存储到队列iot_send_queue中;
+2、tcp_client_main主任务调用tcp_client_sender.proc接口,遍历队列iot_send_queue,逐条发送数据到server;
+3、tcp client socket和server之间的连接如果出现异常,tcp_client_main主任务调用tcp_client_sender.exception_proc接口,丢弃掉队列iot_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 iot_send_queue = {}
+
+-- tcp_client_main的任务名
+tcp_client_sender.TASK_NAME = "iot_tcp_client_main"
+
+-- "SEND_DATA_REQ"消息的处理函数
+local function send_data_req_proc_func(data, cb)
+    -- 将原始数据增加前缀,然后插入到发送队列iot_send_queue中
+    table.insert(iot_send_queue, { data =  data, cb = cb })
+    -- 通知tcp_client_main主任务有数据需要发送
+    -- tcp_client_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
+    sysplus.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
+
+    -- 遍历数据发送队列iot_send_queue
+    while #iot_send_queue > 0 do
+        -- 取出来第一条数据赋值给send_item
+        -- 同时从队列iot_send_queue中删除这一条数据
+        send_item = table.remove(iot_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再次插入到iot_send_queue的队首位置,等待下次尝试发送
+        if buff_full then
+            log.error("tcp_client_sender.proc", "buffer is full, wait for the next time")
+            table.insert(iot_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()
+    -- 遍历数据发送队列iot_send_queue
+    while #iot_send_queue > 0 do
+        local send_item = table.remove(iot_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
+

+ 101 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/iot_server/update.lua

@@ -0,0 +1,101 @@
+--[[
+@module  update
+@summary 远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、判断网卡是否连接成功;
+2、初始化fota2模块;
+3、配置fota2模块的参数;
+4、调用fota2模块的升级函数;
+5、在升级结果的回调函数中,根据升级结果进行处理;
+]]
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+
+libfota2 = require "libfota2"
+
+
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("降功耗 找合宙")
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+sys.timerLoopStart(get_version, 3000)
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
+            "3) 已经是最新版本,无需升级" )
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local ota_opts = {}
+
+function fota_task_func()
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+
+    -- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
+    if "123" == _G.PRODUCT_KEY  then
+        while 1 do
+            sys.wait(1000)
+            log.info("fota", "请修改正确的PRODUCT_KEY")
+        end
+    end
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, ota_opts)
+end
+
+--创建并且启动一个task
+--运行这个task的主函数fota_task_func
+sys.taskInit(fota_task_func)
+-- 演示定时自动升级, 每隔4小时自动检查一次
+sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, ota_opts)
+
+

+ 0 - 101
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/main.lua

@@ -1,101 +0,0 @@
-
--- LuaTools需要PROJECT和VERSION这两个信息
-PROJECT = "fotademo"
--- iot限制,只能上传xxx.yyy.zzz格式的三位数的版本号,但实际上现在只用了XXX和ZZZ,中间yyy暂未使用
--- 需要注意的是,因为yyy不生效,所以111.222.333版本和111.444.333版本,对iot平台来说都一样,所以建议中间那一位永远写000
-VERSION = "001.000.000"
-
--- 使用合宙iot平台时需要这个参数
-PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
-
-sys = require "sys"
-libfota2 = require "libfota2"
-
--- 联网函数, 可自行删减
-sys.taskInit(function()
-    -- 默认都等到联网成功
-    sys.waitUntil("IP_READY")
-    log.info("4G网络链接成功")
-    sys.publish("net_ready")
-end)
-
--- 循环打印版本号, 方便看版本号变化, 非必须
-sys.taskInit(function()
-    while 1 do
-        sys.wait(5000)
-        log.info("降功耗 找合宙")
-        -- log.info("fota", "脚本版本号", VERSION)
-        log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
-    end
-end)
-
--- 升级结果的回调函数
--- 功能:获取fota的回调函数
--- 参数:
--- result:number类型
---   0表示成功
---   1表示连接失败
---   2表示url错误
---   3表示服务器断开
---   4表示接收报文错误
---   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
-local function fota_cb(ret)
-    log.info("fota", ret)
-    if ret == 0 then
-        log.info("升级包下载成功,重启模块")
-        rtos.reboot()
-    elseif ret == 1 then
-        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
-    elseif ret == 2 then
-        log.info("url错误", "检查url拼写")
-    elseif ret == 3 then
-        log.info("服务器断开", "检查服务器白名单配置")
-    elseif ret == 4 then
-        log.info("接收报文错误", "检查模块固件或升级包内文件是否正常")
-    elseif ret == 5 then
-        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
-    else
-        log.info("不是上面几种情况 ret为", ret)
-    end
-end
-
-local ota_opts = {}
-
--- 使用合宙iot平台进行升级,不需要管下面这段代码
--- 使用第三方服务器时打开下面这段代码
---[[local ota_opts = {
-    url = "", 
-    -- 合宙IOT平台的默认升级URL, 不填就是这个默认值
-    -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
-    -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ### 
-    -- 如果不加###,则默认会上传如下参数
-    -- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
-    -- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
-    -- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
-    -- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
-    -- 5. opts.firmware_name string 底层版本号
-    -- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
-    version = ""
-    -- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
-}]]--
-sys.taskInit(function()
-    -- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
-    if "123" == _G.PRODUCT_KEY and not ota_opts.url then
-        while 1 do
-            sys.wait(1000)
-            log.info("fota", "请修改正确的PRODUCT_KEY")
-        end
-    end
-    -- 等待网络就行后开始检查升级
-    sys.waitUntil("net_ready")
-    log.info("开始检查升级")
-    sys.wait(500)
-    libfota2.request(fota_cb, ota_opts)
-end)
--- 演示定时自动升级, 每隔4小时自动检查一次
-sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, ota_opts)
-
--- 用户代码已结束---------------------------------------------
--- 结尾总是这一句
-sys.run()
--- sys.run()之后后面不要加任何语句!!!!!

+ 155 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/README.md

@@ -0,0 +1,155 @@
+## 功能模块介绍
+
+### 自建服务器fota功能演示
+
+1、main.lua:主程序入口;
+
+2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的四种网卡(单4g网卡,单wifi网卡,单spi以太网卡,多网卡)中的任何一种网卡;
+
+3、update.lua:使用自建服务器进行远程升级功能模块,简单升级演示;
+
+4、tcp_self_server文件夹:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备启动customer_srv_fota功能模块,使用自建服务器进行升级;
+
+5、customer_srv_fota.lua:自建服务器升级功能模块;
+
+6、psm_power_fota.lua:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。需要注意的是此场景与上面两种场景不能同时使用;
+
+
+## 系统消息介绍
+
+1、"IP_READY":某种网卡已经获取到ip信息,仅仅获取到了ip信息,能否和外网连通还不确认;
+
+2、"IP_LOSE":某种网卡已经掉网;
+
+
+
+## 用户消息介绍
+
+1、"RECV_DATA_FROM_SERVER":socket client收到服务器下发的数据后,通过此消息发布出去,给其他应用模块处理;
+
+2、"SEND_DATA_REQ":其他应用模块发布此消息,通知socket client发送数据给服务器;
+
+
+
+## 演示功能概述
+
+1、combination文件夹下的demo会有三个演示场景,在main.lua中选择要使用的场景:
+
+    (1) 使用自建服务器升级,演示最简单的升级逻辑。
+
+    (2) 使用自建服务器升级,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。
+
+    (3) 休眠状态下升级,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。
+
+2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
+
+   (1) netdrv_4g:4G网卡
+
+   (2) netdrv_wifi:WIFI STA网卡
+
+   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
+
+   (4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
+
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/air780ehv/luatos/common/hwenv/image/Air780EHV.png)
+
+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、可选AirETH_1000配件板一块,Air780EXX核心板和AirETH_1000配件板的硬件接线方式为:
+
+| Air780EXX核心板  |  AirETH_1000配件板 |
+| --------------- | ----------------- |
+| 3V3             | 3.3v              |
+| gnd             | gnd               |
+| 86/SPI0CLK      | SCK               |
+| 83/SPI0CS       | CSS               |
+| 84/SPI0MISO     | SDO               |
+| 85/SPI0MOSI     | SDI               |
+| 107/GPIO21      | INT               |
+
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air780EHM V2012版本固件](https://docs.openluat.com/air780epm/luatos/firmware/version/#air780ehmluatos)、[Air780EHV V2012版本固件](https://docs.openluat.com/air780ehv/luatos/firmware/version/)、[Air780EGH V2012版本固件](https://docs.openluat.com/air780egh/luatos/firmware/version/)(理论上,2025年7月26日之后发布的固件都可以)
+
+3、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/);
+
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/),点击 打开TCP 按钮,会创建一个TCP server,将server的地址和端口赋值给tcp_client_self_main.lua中的SERVER_ADDR和SERVER_PORT两个变量
+
+3、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
+
+4、Luatools烧录内核固件和修改后的demo脚本代码
+
+5、使用Luatools制作升级包,先把新旧版本分别生成量产文件,然后再制作升级包,工具上栏 luatOS->固件工具->差分包/整包升级包制作,将制作好的升级包配置到合宙iot服务器自己项目下,或上传到自建服务器上面;
+
+6、烧录成功后,自动开机运行
+
+7、[合宙TCP/UDP web测试工具](https://netlab.luatos.com/)上创建的两个TCP server可以看到有设备连接上来,然后可以下发下面字符串触发升级:
+
+
+``` lua
+
+--自建服务器下发这个指令,下发之前需要在服务器上面配置好升级包,然后吧url给到字符串
+--定义一个json格式如下,具体可以根据实际情况定义:
+-- {"fota": "true","url": "http://airtest.openluat.com:2900/download/FOTA2_DEMO_2012.001.001_LuatOS-SoC_Air780EHM.bin"}
+
+```
+
+8、可以看到升级过程如下,不管是什么场景下升级,基本都是如下情况:
+
+``` lua
+--没有升级之前可以看到如下打印
+I/user.fota 脚本版本号 001.000.000 core版本号 V2010
+
+I/user.fota_task_func recv IP_READY 1
+I/user.开始检查升级
+
+I/user.升级包下载成功,重启模块
+
+
+--升级之后可以看到如下打印
+I/user.fota 脚本版本号 001.000.001 core版本号 V2012
+--升级重启之后还是会检查升级,所以会有如下打印属于正常情况,其中"code": 27 是合宙iot服务器返回的状态码,意思是已经是最新版本了。自建服务器如果没做设置会再下载一遍升级包。
+
+I/user.fota -9 {"code": 27, "msg": "\u5df2\u662f\u6700\u65b0\u7248\u672c"}
+I/user.使用合宙服务器,接下来解析body里的code
+I/user.已是最新版本 1.设备的固件/脚本版本高于或等于云平台上的版本号 2.用户项目升级配置中未添加该设备 3.云平台升级配置中,是否升级配置为否
+I/user.fota 4
+
+
+```
+
+9、对于psm休眠状态下的升级的场景,打印的版本来判断是否是最新版本。模组升级成功后会自动进入psm休眠状态。可以通过电流查看休眠情况。

+ 139 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/customer_srv_fota.lua

@@ -0,0 +1,139 @@
+--[[
+@module  air_srv_fota
+@summary 使用自建服务器远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、接收 CUSTOMER_SRV_FOTA 系统消息,触发升级;
+2、判断网卡是否连接成功;
+3、初始化fota2模块;
+4、调用fota2模块的升级函数;
+5、根据升级结果进行处理;
+]]
+
+
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("降功耗 找合宙")
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+
+sys.timerLoopStart(get_version, 3000)
+
+-- fota升级标志:true 表示当前正有 FOTA 流程在跑
+local fota_running = false
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    -- fota结束,无论成功还是失败,都释放fota_running标志
+    fota_running = false
+
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
+            "3) 已经是最新版本,无需升级")
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+
+-- 使用第三方服务器,配置ota_opts参数
+--[[
+-- opts参数说明, 所有参数都是可选的
+-- 1. opts.url string 升级所需要的URL, 若使用合宙iot平台,则不需要填
+-- 2. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+-- 3. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+-- 4. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+-- 5. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+-- 6. opts.firmware_name string 固件名称,默认是 _G.PROJECT.. "_LuatOS-SoC_" .. rtos.bsp()
+-- 7. opts.server_cert string 服务器证书, 默认不使用
+-- 8. opts.client_cert string 客户端证书, 默认不使用
+-- 9. opts.client_key string 客户端私钥, 默认不使用
+-- 10. opts.client_password string 客户端私钥口令, 默认不使用
+-- 11. opts.method string 请求方法, 默认是GET
+-- 12. opts.headers table 额外添加的请求头,默认不需要
+-- 13. opts.body string 额外添加的请求body,默认不需要
+]]
+local opts = {
+    url = "",
+    -- 合宙IOT平台的默认升级URL, 不填就是这个默认值
+    -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
+    -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
+    -- 如果不加###,则默认会上传如下参数
+    -- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+    -- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+    -- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+    -- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+    -- 5. opts.firmware_name string 底层版本号
+
+    -- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
+    version = ""
+    -- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
+}
+
+
+local function air_fota_func(data)
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+    while true do
+        -- 阻塞等待外部事件:"CUSTOMER_SRV_FOTA"
+        local result, data = sys.waitUntil("CUSTOMER_SRV_FOTA")
+        if result then
+            log.info("接收到数据", "date", #data)
+
+            if fota_running then
+                log.warn("fota_task", "FOTA 正在运行,跳过本次请求")
+            else
+                -- 标记FOTA正在运行
+                -- 注意:这里只是标记,实际的FOTA流程还没有开始
+                opts.url = data.url
+                opts.version = data.version
+                fota_running = true
+                log.info("开始检查升级")
+                libfota2.request(fota_cb, opts)
+            end
+        end
+    end
+end
+-- 初始化FOTA任务
+sys.taskInit(air_fota_func)

+ 108 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/main.lua

@@ -0,0 +1,108 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本demo演示的核心功能为:
+1、这个demo会有三个演示场景:
+    (1)使用自建服务器升级,演示最简单的升级逻辑。
+    (2)使用自建服务器升级,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。
+    (3)休眠状态下升级,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。
+2、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 = "FOTA2_DEMO"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+
+sys.timerLoopStart(get_version, 3000)
+
+
+-- 加载网络驱动设备功能模块
+require "netdrv_device"
+
+
+
+--两种tcp下发指令升级的场景和psm+低功耗模式升级不能同时使用,需要根据自己场景选择其中一种
+
+--两种tcp下发指令升级的场景可以启用一种,也可以启用两种,启用两种时,注意控制不要一个在fota的过程中,另外一个再fota。
+
+
+-- 加载远程升级功能模块,场景一
+require "update"
+---------------------------------------------------------------------------
+-- 加载tcp client self socket主应用功能模块,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。场景二
+-- require "tcp_self_main"
+-- 加载自建服务器远程升级功能模块
+-- require "customer_srv_fota"
+---------------------------------------------------------------------------
+-- 加载psm+低功耗模式升级功能模块,场景三
+-- require "psm_power_fota"
+
+
+
+
+
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

+ 33 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/netdrv/netdrv_4g.lua

@@ -0,0 +1,33 @@
+--[[
+@module  netdrv_4g
+@summary “4G网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为4G网卡驱动模块,核心业务逻辑为:
+1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
+end
+
+
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+-- 设置默认网卡为socket.LWIP_GP
+-- 在Air8000上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
+-- 在单4G网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
+socket.dft(socket.LWIP_GP)

+ 85 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/netdrv/netdrv_eth_spi.lua

@@ -0,0 +1,85 @@
+--[[
+@module  netdrv_eth_spi
+@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块 ,核心业务逻辑为:
+1、打开CH390H芯片供电开关;
+2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
+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测试使用的是Air8000开发板
+--GPIO140为CH390H以太网芯片的供电使能控制引脚
+gpio.setup(140, 1, gpio.PULLUP)
+
+--这个task的核心业务逻辑是:初始化SPI,初始化以太网卡,并在以太网卡上开启动态主机配置协议
+local function netdrv_eth_spi_task_func()
+    -- 初始化SPI1
+    local result = spi.setup(
+        1,--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 1, 片选 GPIO12
+    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spi=1, cs=12})
+
+    --在以太上开启动态主机配置协议
+    netdrv.dhcp(socket.LWIP_ETH, true)
+end
+
+--创建并且启动一个task
+--task的处理函数为netdrv_eth_spi_task_func
+sys.taskInit(netdrv_eth_spi_task_func)

+ 95 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/netdrv/netdrv_multiple.lua

@@ -0,0 +1,95 @@
+--[[
+@module  netdrv_multiple
+@summary 多网卡(4G网卡、WIFI STA网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为多网卡驱动模块 ,核心业务逻辑为:
+1、调用libnetif.set_priority_order配置多网卡的控制参数以及优先级;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_multiple"就可以加载运行;
+]]
+
+
+local exnetif = require "exnetif"
+
+-- 网卡状态变化通知回调函数
+-- 当libnetif中检测到网卡切换或者所有网卡都断网时,会触发调用此回调函数
+-- 当网卡切换切换时:
+--     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芯片”的以太网卡,使用Air8000开发板验证
+            {
+                ETHERNET = {
+                    -- 供电使能GPIO
+                    pwrpin = 140,
+                    -- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
+                    -- 如果没有传入此参数,libnetif会使用默认值10秒
+                    ping_time = 3000,
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+
+                    -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
+                    tp = netdrv.CH390,
+                    opts = {spi=1, cs=12}
+                }
+            },
+
+            -- WIFI STA网卡
+            {
+                WIFI = {
+                    -- 要连接的WIFI路由器名称
+                    ssid = "茶室-降功耗,找合宙!",
+                    -- 要连接的WIFI路由器密码
+                    password = "Air123456",
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+                }
+            },
+
+            -- 4G网卡
+            {
+                LWIP_GP = true
+            }
+        }
+    )
+end
+
+-- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc
+exnetif.notify_status(netdrv_multiple_notify_cbfunc)
+
+-- 如果存在udp网络应用,并且udp网络应用中,根据应用层的心跳能够判断出来udp数据通信出现了异常;
+-- 可以在判断出现异常的位置,调用一次libnetif.check_network_status()接口,强制对当前正式使用的网卡进行一次连通性检测;
+-- 如果存在tcp网络应用,不需要用户调用libnetif.check_network_status()接口去控制,libnetif会在tcp网络应用通信异常时自动对当前使用的网卡进行连通性检测。
+
+
+-- 启动一个task,task的处理函数为netdrv_multiple_task_func
+-- 在处理函数中调用libnetif.set_priority_order设置网卡优先级
+-- 因为libnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
+sys.taskInit(netdrv_multiple_task_func)

+ 30 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/netdrv_device.lua

@@ -0,0 +1,30 @@
+--[[
+@module  netdrv_device
+@summary 网络驱动设备功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
+1、netdrv_4g:socket.LWIP_GP,4G网卡;
+2、netdrv_wifi:socket.LWIP_STA,WIFI STA网卡;
+3、netdrv_ethernet_spi:socket.LWIP_USER1,通过SPI外挂CH390H芯片的以太网卡;
+4、netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
+
+根据自己的项目需求,只需要require以上四种中的一种即可;
+
+
+本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
+]]
+
+
+-- 根据自己的项目需求,只需要require以下四种中的一种即可;
+
+-- 加载“4G网卡”驱动模块
+require "netdrv_4g"
+
+-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
+-- require "netdrv_eth_spi"
+
+-- 加载“可以配置优先级的多种网卡”驱动模块
+-- require "netdrv_multiple"

+ 131 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/psm_power_fota.lua

@@ -0,0 +1,131 @@
+--[[
+@module  psm+_power_fota
+@summary psm+超低功耗模式下升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为psm+超低功耗模式下升级功能模块,核心设计思路
+1.升级触发机制 :
+   - 定时器唤醒升级 :设备定期从PSM模式唤醒,主动检查是否有新固件版本。
+   - 外部中断唤醒升级 :通过特定GPIO中断或网络消息唤醒设备进行升级。
+2.防止升级过程中进入休眠 :
+   - 在开始FOTA升级前,禁用PSM模式进入。
+   - 升级完成后,根据结果决定是否重启设备或重新进入PSM模式。
+
+]]
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "BnYk2BlYO30DiWra7q27wUmEarOiipHO" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    --升级结束,触发升级回调,发布消息升级结束,可以进入休眠模式
+    sys.publish("FOTA_END")
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
+            "3) 已经是最新版本,无需升级" )
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local opts = {
+    url = "###http://cdn.openluat-backend.openluat.com/upgrade_firmware/fotademo_2008.001.001_LuatOS-SoC_Air8000.bin_20250623184110381812",
+    -- 合宙IOT平台的默认升级URL, 不填就是这个默认值
+    -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
+    -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
+    -- 如果不加###,则默认会上传如下参数
+    -- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+    -- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+    -- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+    -- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+    -- 5. opts.firmware_name string 底层版本号
+
+    -- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
+    -- version = ""
+    -- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
+}
+
+
+function psm_fota_task_func()
+    -- 如果是被定时器唤醒,因为上次进入PSM+时是开启了飞行模式,所以在唤醒后第一时间关闭飞行模式。
+    mobile.flymode(0, false)
+    log.info("开始测试PSM+模式功耗。")
+
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, opts)
+
+    -- 打印版本号, 方便看版本号变化, 非必须
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+
+
+    -- 等待下载升级包结束, 发布消息"FOTA_END",
+    -- 如果15秒内没有收到消息,则15秒的时长到达后进入PSM+模式。
+    -- 需要注意的是在fota_cb回调函数中,升级包下载成功后,会立马重启并升级模组。如果还有其他事情要做不想立马重启升级,需自行决定reboot的时机
+    -- 升级包下载成功后,本demo默认是立即自动重启并且将升级包更新到模组中,更新成功后,会再次走到这里
+    -- 再次走到这里后,合宙iot平台会返回“已经是最新版本,不需要升级”,fota_cb回调函数中会发布消息"FOTA_END"
+    -- 至此,才会继续向下执行代码,进入PSM+模式
+    sys.waitUntil("FOTA_END", 15000)
+
+    log.info("升级结束,进入PSM模式")
+
+
+
+    -- 定时检查升级 (每4小时唤醒一次)
+    pm.dtimerStart(2, 4 * 3600000)
+    -- 启动飞行模式,规避可能会出现的网络问题
+    mobile.flymode(0, true)
+    -- 进入PSM模式
+    pm.power(pm.WORK_MODE, 3)
+    -- 防御机制:15秒后如果未进入PSM则重启
+    sys.wait(15000)
+    log.info("进入PSM+失败,重启")
+    rtos.reboot()
+end
+
+sys.taskInit(psm_fota_task_func)

+ 140 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/tcp_self_server/tcp_self_main.lua

@@ -0,0 +1,140 @@
+--[[
+@module  tcp_client_main
+@summary tcp client socket主应用功能模块
+@version 1.0
+@date    2025.08.12
+@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_self_receiver"
+-- 加载tcp client socket数据发送功能模块
+local tcp_client_sender = require "tcp_self_sender"
+
+-- 电脑访问:https://netlab.luatos.com/
+-- 点击 打开TCP 按钮,会创建一个TCP server
+-- 将server的地址和端口赋值给下面这两个变量
+local SERVER_ADDR = "112.125.89.8"
+local SERVER_PORT = 45433
+
+-- 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毫秒超时不要修改的更长;
+            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+            -- 当libnetif.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)
+        -- 如果配置失败
+        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")
+
+        -- 数据收发以及网络连接异常事件总处理逻辑
+        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
+sysplus.taskInitEx(tcp_client_main_task_func, TASK_NAME, tcp_client_main_cbfunc)

+ 126 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/tcp_self_server/tcp_self_receiver.lua

@@ -0,0 +1,126 @@
+--[[
+@module  tcp_client_receiver
+@summary tcp client socket数据接收应用功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为tcp client socket数据接收应用功能模块,核心业务逻辑为:
+从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;
+
+本文件的对外接口有2个:
+1、tcp_client_receiver.proc(socket_client):数据接收应用逻辑处理入口,在tcp_client_main.lua中调用;
+2、sys.publish("RECV_DATA_FROM_SERVER",  data):数据接收应用逻辑处理入口,在tcp_client_main
+   将接收到的数据通过消息"RECV_DATA_FROM_SERVER"发布出去处理;
+   处理后通过消息"CUSTOMER_SRV_FOTA"发布到customer_srv_fota.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_SELF_DATA_SERVER"消息publish出去,给其他应用模块处理
+            sys.publish("RECV_SELF_DATA_SERVER",  data)
+
+            -- 清空socket数据接收缓冲区中的数据
+            recv_buff:del()
+            -- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
+        else
+            break
+        end
+    end
+
+    return true
+end
+--解析接受到的json参数并发布到消息"AIR_SRV_FOTA"中
+--定义一个json格式如下,具体可以根据实际情况定义:
+--{
+--    "fota": "true",
+--    "url": "http://192.168.1.100:8080/firmware.bin"
+--}
+
+local function data_processing(data)
+    -- 解析json数据
+    local json_data = json.decode(data)
+
+    -- 如果解析失败
+    if not json_data then
+        log.error("data_processing", "json.decode error")
+        return
+    end
+    -- 解析fota参数
+    local fota = json_data.fota
+    -- 如果fota参数不存在
+    if not fota then
+        log.error("data_processing", "fota error")
+        return
+    end
+    -- 解析url参数
+    local url = json_data.url
+    -- 如果url参数不存在
+    if not url then
+        log.error("data_processing", "url error")
+        return
+    end
+    -- 如果fota为true
+    if fota == "true" then
+        -- -- 发布消息"CUSTOMER_SRV_FOTA"
+        sys.publish("CUSTOMER_SRV_FOTA",json_data)
+    end
+end
+
+-- 订阅"RECV_SELF_DATA_SERVER"消息
+sys.subscribe("RECV_SELF_DATA_SERVER", data_processing)
+
+return tcp_client_receiver

+ 137 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/tcp_self_server/tcp_self_sender.lua

@@ -0,0 +1,137 @@
+--[[
+@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"消息,将其他应用模块需要发送的数据存储到队列self_send_queue中;
+2、tcp_client_main主任务调用tcp_client_sender.proc接口,遍历队列self_send_queue,逐条发送数据到server;
+3、tcp client socket和server之间的连接如果出现异常,tcp_client_main主任务调用tcp_client_sender.exception_proc接口,丢弃掉队列self_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 self_send_queue = {}
+
+-- tcp_client_main的任务名
+tcp_client_sender.TASK_NAME = "self_tcp_client_main"
+
+-- "SEND_DATA_REQ"消息的处理函数
+local function send_data_req_proc_func(data, cb)
+    -- 将原始数据增加前缀,然后插入到发送队列self_send_queue中
+    table.insert(self_send_queue, {data=data, cb=cb})
+    -- 通知tcp_client_main主任务有数据需要发送
+    -- tcp_client_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
+    sysplus.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
+    -- 遍历数据发送队列self_send_queue
+    while #self_send_queue>0 do
+        -- 取出来第一条数据赋值给send_item
+        -- 同时从队列self_send_queue中删除这一条数据
+        send_item = table.remove(self_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再次插入到self_send_queue的队首位置,等待下次尝试发送
+        if buff_full then
+            log.error("tcp_client_sender.proc", "buffer is full, wait for the next time")
+            table.insert(self_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()
+    -- 遍历数据发送队列self_send_queue
+    while #self_send_queue>0 do
+        local send_item = table.remove(self_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("SELF_SEND_DATA_REQ", send_data_req_proc_func)
+
+return tcp_client_sender

+ 128 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fota2/self_server/update.lua

@@ -0,0 +1,128 @@
+--[[
+@module  update
+@summary 远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、判断网卡是否连接成功;
+2、初始化fota2模块;
+3、配置fota2模块的参数;
+4、调用fota2模块的升级函数;
+5、在升级结果的回调函数中,根据升级结果进行处理;
+]]
+
+libfota2 = require "libfota2"
+
+
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+sys.timerLoopStart(get_version, 3000)
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
+            "3) 已经是最新版本,无需升级" )
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+-- 使用第三方服务器,配置ota_opts参数
+--[[
+-- opts参数说明, 所有参数都是可选的
+-- 1. opts.url string 升级所需要的URL, 若使用合宙iot平台,则不需要填
+-- 2. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+-- 3. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+-- 4. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+-- 5. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+-- 6. opts.firmware_name string 固件名称,默认是 _G.PROJECT.. "_LuatOS-SoC_" .. rtos.bsp()
+-- 7. opts.server_cert string 服务器证书, 默认不使用
+-- 8. opts.client_cert string 客户端证书, 默认不使用
+-- 9. opts.client_key string 客户端私钥, 默认不使用
+-- 10. opts.client_password string 客户端私钥口令, 默认不使用
+-- 11. opts.method string 请求方法, 默认是GET
+-- 12. opts.headers table 额外添加的请求头,默认不需要
+-- 13. opts.body string 额外添加的请求body,默认不需要
+]]
+local opts = {
+    url = "###http://cdn.openluat-backend.openluat.com/upgrade_firmware/fotademo_2008.001.001_LuatOS-SoC_Air8000.bin_20250623184110381812",
+    -- 合宙IOT平台的默认升级URL, 不填就是这个默认值
+    -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
+    -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
+    -- 如果不加###,则默认会上传如下参数
+    -- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+    -- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+    -- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+    -- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+    -- 5. opts.firmware_name string 底层版本号
+
+    -- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
+    -- version = ""
+    -- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
+}
+
+function fota_task_func()
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+    ----这个判断是提醒要设置url的,且不要使用本文中的测试服务器,实际生产请删除
+    if not opts.url or string.find(opts.url,"airtest.openluat.com") then
+        while 1 do
+            sys.wait(1000)
+            log.info("fota", "当前URL",opts.url,"请修改正确的url")
+        end
+    end
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, opts)
+end
+
+--创建并且启动一个task
+--运行这个task的主函数fota_task_func
+sys.taskInit(fota_task_func)
+-- 演示定时自动升级, 每隔4小时自动检查一次
+sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, opts)
+
+

+ 128 - 0
module/Air780EPM/demo/fota2/iot_server/README.md

@@ -0,0 +1,128 @@
+## 功能模块介绍
+
+### iot服务器fota功能演示
+
+1、main.lua:主程序入口;
+
+2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的四种网卡(单4g网卡,单wifi网卡,单spi以太网卡,多网卡)中的任何一种网卡;
+
+3、update.lua:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
+
+4、tcp_iot文件夹:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备启动air_srv_fota功能模块,使用合宙iot服务器进行升级;
+
+5、air_srv_fota.lua:合宙服务器升级功能模块;
+
+6、psm_power_fota.lua:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。需要注意的是此场景与上面两种场景不能同时使用;
+
+
+## 系统消息介绍
+
+1、"IP_READY":某种网卡已经获取到ip信息,仅仅获取到了ip信息,能否和外网连通还不确认;
+
+2、"IP_LOSE":某种网卡已经掉网;
+
+
+
+## 用户消息介绍
+
+1、"RECV_DATA_FROM_SERVER":socket client收到服务器下发的数据后,通过此消息发布出去,给其他应用模块处理;
+
+2、"SEND_DATA_REQ":其他应用模块发布此消息,通知socket client发送数据给服务器;
+
+
+
+## 演示功能概述
+
+1、此demo演示了三种场景:
+
+   (1) fota升级简单演示:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
+
+   (2) tcp服务器下发升级指令:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块;
+
+   (3) psm低功耗fota:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子;
+
+2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
+
+   (1) netdrv_4g:4G网卡
+
+   (2) netdrv_wifi:WIFI STA网卡
+
+   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
+
+   (4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
+
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/air780epm/luatos/app/driver/eth/image/RFSvb75NRoEWqYxfCRVcVrOKnsf.jpg)
+
+1、Air780EPM V1.3版本开发板一块+可上网的sim卡一张+4g天线一根+网线一根:
+
+- sim卡插入开发板的sim卡槽
+
+- 天线装到开发板上
+
+- 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口
+
+2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air780EPM V1.3版本开发板和数据线的硬件接线方式为:
+
+- Air780EPM V1.3版本开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air780EPM V2012版本固件)](https://docs.openluat.com/air780epm/luatos/firmware/version/)
+
+3、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/);
+
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/),点击 打开TCP 按钮,会创建一个TCP server,将server的地址和端口赋值给tcp_iot_main.lua中的SERVER_ADDR和SERVER_PORT两个变量
+
+4、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
+
+5、Luatools烧录内核固件和修改后的demo脚本代码
+
+6、使用Luatools制作升级包,先把新旧版本分别生成量产文件,然后再制作升级包,工具上栏 luatOS->固件工具->差分包/整包升级包制作,将制作好的升级包配置到合宙iot服务器自己项目下,或上传到自建服务器上面;
+
+7、烧录成功后,自动开机运行
+
+8、可以看到升级过程如下,不管是什么场景下升级,基本都是如下日志情况:
+
+``` lua
+--没有升级之前可以看到如下打印
+I/user.fota 脚本版本号 001.000.000 core版本号 V2010
+
+I/user.fota_task_func recv IP_READY 1
+I/user.开始检查升级
+
+I/user.升级包下载成功,重启模块
+
+
+--升级之后可以看到如下打印
+I/user.fota 脚本版本号 001.000.001 core版本号 V2012
+--升级重启之后还是会检查升级,所以会有如下打印属于正常情况,其中"code": 27 是合宙iot服务器返回的状态码,意思是已经是最新版本了。
+I/user.fota -9 {"code": 27, "msg": "\u5df2\u662f\u6700\u65b0\u7248\u672c"}
+I/user.使用合宙服务器,接下来解析body里的code
+I/user.已是最新版本 1.设备的固件/脚本版本高于或等于云平台上的版本号 2.用户项目升级配置中未添加该设备 3.云平台升级配置中,是否升级配置为否
+I/user.fota 4
+
+
+```
+9、对于psm休眠状态下的升级的场景,可以通过iot平台查看是否成功,在iot平台的升级日志页面搜索模组的imei,可以看到有两条升级结果“成功”和“已是最新版本”。模组升级成功后会自动进入psm休眠状态。可以通过电流查看休眠情况。

+ 119 - 0
module/Air780EPM/demo/fota2/iot_server/air_srv_fota.lua

@@ -0,0 +1,119 @@
+--[[
+@module  air_srv_fota
+@summary 使用合宙iot平台远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、接收 AIR_SRV_FOTA 系统消息,触发升级;
+2、判断网卡是否连接成功;
+3、初始化fota2模块;
+4、调用fota2模块的升级函数;
+5、根据升级结果进行处理;
+]]
+
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("降功耗 找合宙")
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+
+sys.timerLoopStart(get_version, 3000)
+
+
+-- fota升级标志:true 表示当前正有 FOTA 流程在跑
+local fota_running = false
+
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    -- fota结束,无论成功还是失败,都释放fota_running标志
+    fota_running = false
+
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
+            "3) 已经是最新版本,无需升级")
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local opts = {}
+
+local function air_fota_func(data)
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+    while true do
+        -- 阻塞等待外部事件:"AIR_SRV_FOTA"
+        sys.waitUntil("AIR_SRV_FOTA")
+
+        -- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
+        if "123" == _G.PRODUCT_KEY then
+            while 1 do
+                sys.wait(1000)
+                log.info("fota", "请修改正确的PRODUCT_KEY")
+            end
+        end
+
+        if fota_running then
+            log.warn("fota_task", "FOTA 正在运行,跳过本次请求")
+        else
+            -- 标记FOTA正在运行
+            -- 注意:这里只是标记,实际的FOTA流程还没有开始
+            fota_running = true
+            log.info("开始检查升级")
+            libfota2.request(fota_cb, opts)
+        end
+    end
+end
+-- 初始化FOTA任务
+sys.taskInit(air_fota_func)
+
+
+-- 演示定时自动升级, 每隔4小时自动检查一次,可以根据需求打开
+-- sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, opts)

+ 91 - 0
module/Air780EPM/demo/fota2/iot_server/main.lua

@@ -0,0 +1,91 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本demo演示的核心功能为:
+1、此demo演示了三种场景:
+   (1)fota升级简单演示:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
+   (2)tcp服务器下发升级指令:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块;
+   (3)psm低功耗fota:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子;
+2、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 = "FOTA2_DEMO"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+
+-- 加载网络驱动设备功能模块
+require "netdrv_device"
+
+-- 加载远程升级功能模块,场景1
+require "update"
+---------------------------------------------------------------------------
+-- 加载tcp client iot socket主应用功能模块,通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块,场景二
+-- require "tcp_iot_main"
+-- 加载合宙iot平台远程升级功能模块
+-- require "air_srv_fota"
+---------------------------------------------------------------------------
+-- 加载psm+低功耗模式升级功能模块,场景三
+-- require "psm_power_fota"
+
+
+
+
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

+ 33 - 0
module/Air780EPM/demo/fota2/iot_server/netdrv/netdrv_4g.lua

@@ -0,0 +1,33 @@
+--[[
+@module  netdrv_4g
+@summary “4G网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为4G网卡驱动模块,核心业务逻辑为:
+1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
+end
+
+
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+-- 设置默认网卡为socket.LWIP_GP
+-- 在Air8000上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
+-- 在单4G网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
+socket.dft(socket.LWIP_GP)

+ 85 - 0
module/Air780EPM/demo/fota2/iot_server/netdrv/netdrv_eth_spi.lua

@@ -0,0 +1,85 @@
+--[[
+@module  netdrv_eth_spi
+@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块 ,核心业务逻辑为:
+1、打开CH390H芯片供电开关;
+2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
+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测试使用的是Air8000开发板
+--GPIO140为CH390H以太网芯片的供电使能控制引脚
+gpio.setup(140, 1, gpio.PULLUP)
+
+--这个task的核心业务逻辑是:初始化SPI,初始化以太网卡,并在以太网卡上开启动态主机配置协议
+local function netdrv_eth_spi_task_func()
+    -- 初始化SPI1
+    local result = spi.setup(
+        1,--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 1, 片选 GPIO12
+    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spi=1, cs=12})
+
+    --在以太上开启动态主机配置协议
+    netdrv.dhcp(socket.LWIP_ETH, true)
+end
+
+--创建并且启动一个task
+--task的处理函数为netdrv_eth_spi_task_func
+sys.taskInit(netdrv_eth_spi_task_func)

+ 95 - 0
module/Air780EPM/demo/fota2/iot_server/netdrv/netdrv_multiple.lua

@@ -0,0 +1,95 @@
+--[[
+@module  netdrv_multiple
+@summary 多网卡(4G网卡、WIFI STA网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为多网卡驱动模块 ,核心业务逻辑为:
+1、调用libnetif.set_priority_order配置多网卡的控制参数以及优先级;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_multiple"就可以加载运行;
+]]
+
+
+local exnetif = require "exnetif"
+
+-- 网卡状态变化通知回调函数
+-- 当libnetif中检测到网卡切换或者所有网卡都断网时,会触发调用此回调函数
+-- 当网卡切换切换时:
+--     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芯片”的以太网卡,使用Air8000开发板验证
+            {
+                ETHERNET = {
+                    -- 供电使能GPIO
+                    pwrpin = 140,
+                    -- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
+                    -- 如果没有传入此参数,libnetif会使用默认值10秒
+                    ping_time = 3000,
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+
+                    -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
+                    tp = netdrv.CH390,
+                    opts = {spi=1, cs=12}
+                }
+            },
+
+            -- WIFI STA网卡
+            {
+                WIFI = {
+                    -- 要连接的WIFI路由器名称
+                    ssid = "茶室-降功耗,找合宙!",
+                    -- 要连接的WIFI路由器密码
+                    password = "Air123456",
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+                }
+            },
+
+            -- 4G网卡
+            {
+                LWIP_GP = true
+            }
+        }
+    )
+end
+
+-- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc
+exnetif.notify_status(netdrv_multiple_notify_cbfunc)
+
+-- 如果存在udp网络应用,并且udp网络应用中,根据应用层的心跳能够判断出来udp数据通信出现了异常;
+-- 可以在判断出现异常的位置,调用一次libnetif.check_network_status()接口,强制对当前正式使用的网卡进行一次连通性检测;
+-- 如果存在tcp网络应用,不需要用户调用libnetif.check_network_status()接口去控制,libnetif会在tcp网络应用通信异常时自动对当前使用的网卡进行连通性检测。
+
+
+-- 启动一个task,task的处理函数为netdrv_multiple_task_func
+-- 在处理函数中调用libnetif.set_priority_order设置网卡优先级
+-- 因为libnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
+sys.taskInit(netdrv_multiple_task_func)

+ 30 - 0
module/Air780EPM/demo/fota2/iot_server/netdrv_device.lua

@@ -0,0 +1,30 @@
+--[[
+@module  netdrv_device
+@summary 网络驱动设备功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
+1、netdrv_4g:socket.LWIP_GP,4G网卡;
+2、netdrv_wifi:socket.LWIP_STA,WIFI STA网卡;
+3、netdrv_ethernet_spi:socket.LWIP_USER1,通过SPI外挂CH390H芯片的以太网卡;
+4、netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
+
+根据自己的项目需求,只需要require以上四种中的一种即可;
+
+
+本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
+]]
+
+
+-- 根据自己的项目需求,只需要require以下四种中的一种即可;
+
+-- 加载“4G网卡”驱动模块
+require "netdrv_4g"
+
+-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
+-- require "netdrv_eth_spi"
+
+-- 加载“可以配置优先级的多种网卡”驱动模块
+-- require "netdrv_multiple"

+ 121 - 0
module/Air780EPM/demo/fota2/iot_server/psm_power_fota.lua

@@ -0,0 +1,121 @@
+--[[
+@module  psm+_power_fota
+@summary psm+超低功耗模式下升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为psm+超低功耗模式下升级功能模块,核心设计思路
+1.升级触发机制 :
+   - 定时器唤醒升级 :设备定期从PSM模式唤醒,主动检查是否有新固件版本。
+   - 外部中断唤醒升级 :通过特定GPIO中断或网络消息唤醒设备进行升级。
+2.防止升级过程中进入休眠 :
+   - 在开始FOTA升级前,禁用PSM模式进入。
+   - 升级完成后,根据结果决定是否重启设备或重新进入PSM模式。
+
+]]
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "BnYk2BlYO30DiWra7q27wUmEarOiipHO" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+
+
+
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    --升级结束,触发升级回调,发布消息升级结束,可以进入休眠模式
+    sys.publish("FOTA_END")
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
+            "3) 已经是最新版本,无需升级")
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local ota_opts = {}
+
+
+function psm_fota_task_func()
+    -- 如果是被定时器唤醒,因为上次进入PSM+时是开启了飞行模式,所以在唤醒后第一时间关闭飞行模式。
+    mobile.flymode(0, false)
+    log.info("开始测试PSM+模式功耗。")
+
+    -- 打印版本号, 方便看版本号变化, 非必须
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, ota_opts)
+
+
+
+    -- 等待下载升级包结束, 发布消息"FOTA_END",
+    -- 如果15秒内没有收到消息,则15秒的时长到达后进入PSM+模式。
+    -- 需要注意的是在fota_cb回调函数中,升级包下载成功后,会立马重启并升级模组。如果还有其他事情要做不想立马重启升级,需自行决定reboot的时机
+    -- 升级包下载成功后,本demo默认是立即自动重启并且将升级包更新到模组中,更新成功后,会再次走到这里
+    -- 再次走到这里后,合宙iot平台会返回“已经是最新版本,不需要升级”,fota_cb回调函数中会发布消息"FOTA_END"
+    -- 至此,才会继续向下执行代码,进入PSM+模式
+    sys.waitUntil("FOTA_END", 15000)
+
+    log.info("升级结束,进入PSM模式")
+
+
+
+    -- 定时检查升级 (每4小时唤醒一次)
+    pm.dtimerStart(2, 4 * 3600000)
+    -- 启动飞行模式,规避可能会出现的网络问题
+    mobile.flymode(0, true)
+    -- 进入PSM模式
+    pm.power(pm.WORK_MODE, 3)
+    -- 防御机制:15秒后如果未进入PSM则重启
+    sys.wait(15000)
+    log.info("进入PSM+失败,重启")
+    rtos.reboot()
+end
+
+sys.taskInit(psm_fota_task_func)

+ 140 - 0
module/Air780EPM/demo/fota2/iot_server/tcp_iot/tcp_iot_main.lua

@@ -0,0 +1,140 @@
+--[[
+@module  tcp_client_main
+@summary tcp client socket主应用功能模块
+@version 1.0
+@date    2025.08.12
+@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_iot_receiver"
+-- 加载tcp client socket数据发送功能模块
+local tcp_client_sender = require "tcp_iot_sender"
+
+-- 电脑访问:https://netlab.luatos.com/
+-- 点击 打开TCP 按钮,会创建一个TCP server
+-- 将server的地址和端口赋值给下面这两个变量
+local SERVER_ADDR = "112.125.89.8"
+local SERVER_PORT = 42662
+
+-- 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毫秒超时不要修改的更长;
+            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+            -- 当libnetif.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)
+        -- 如果配置失败
+        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")
+
+        -- 数据收发以及网络连接异常事件总处理逻辑
+        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
+sysplus.taskInitEx(tcp_client_main_task_func, TASK_NAME, tcp_client_main_cbfunc)

+ 111 - 0
module/Air780EPM/demo/fota2/iot_server/tcp_iot/tcp_iot_receiver.lua

@@ -0,0 +1,111 @@
+--[[
+@module  tcp_client_receiver
+@summary tcp client socket数据接收应用功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为tcp client socket数据接收应用功能模块,核心业务逻辑为:
+从内核读取接收到的数据,然后将数据处理后发送给其他应用功能模块做进一步处理;
+
+本文件的对外接口有2个:
+1、tcp_client_receiver.proc(socket_client):数据接收应用逻辑处理入口,在tcp_client_main.lua中调用;
+2、sys.publish("RECV_DATA_FROM_SERVER", data):
+   将接收到的数据通过消息"RECV_DATA_FROM_SERVER"发布出去处理;
+   处理后通过消息"AIR_SRV_FOTA"发布到air_srv_fota.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_IOT_DATA_SERVER"消息publish出去,给其他应用模块处理
+            sys.publish("RECV_IOT_DATA_SERVER", data)
+
+            -- 清空socket数据接收缓冲区中的数据
+            recv_buff:del()
+            -- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
+        else
+            break
+        end
+    end
+
+    return true
+end
+
+--解析接受到的json参数并发布到消息"AIR_SRV_FOTA"中
+--定义一个json格式如下,具体可以根据实际情况定义,使用iot升级的话只定义了一个参数即可:
+--{
+--    "fota": "true"
+--}
+
+local function data_processing(data)
+    -- 解析json数据
+    local json_data = json.decode(data)
+    -- 如果解析失败
+    if not json_data then
+        log.error("data_processing", "json.decode error")
+        return
+    end
+    -- 如果fota为true
+    if json_data.fota == "true" then
+        -- 发布消息"AIR_SRV_FOTA"
+        sys.publish("AIR_SRV_FOTA")
+    end
+end
+
+-- 订阅"RECV_IOT_DATA_SERVER"消息
+sys.subscribe("RECV_IOT_DATA_SERVER", data_processing)
+
+return tcp_client_receiver

+ 139 - 0
module/Air780EPM/demo/fota2/iot_server/tcp_iot/tcp_iot_sender.lua

@@ -0,0 +1,139 @@
+--[[
+@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"消息,将其他应用模块需要发送的数据存储到队列iot_send_queue中;
+2、tcp_client_main主任务调用tcp_client_sender.proc接口,遍历队列iot_send_queue,逐条发送数据到server;
+3、tcp client socket和server之间的连接如果出现异常,tcp_client_main主任务调用tcp_client_sender.exception_proc接口,丢弃掉队列iot_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 iot_send_queue = {}
+
+-- tcp_client_main的任务名
+tcp_client_sender.TASK_NAME = "iot_tcp_client_main"
+
+-- "SEND_DATA_REQ"消息的处理函数
+local function send_data_req_proc_func(data, cb)
+    -- 将原始数据增加前缀,然后插入到发送队列iot_send_queue中
+    table.insert(iot_send_queue, { data =  data, cb = cb })
+    -- 通知tcp_client_main主任务有数据需要发送
+    -- tcp_client_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
+    sysplus.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
+
+    -- 遍历数据发送队列iot_send_queue
+    while #iot_send_queue > 0 do
+        -- 取出来第一条数据赋值给send_item
+        -- 同时从队列iot_send_queue中删除这一条数据
+        send_item = table.remove(iot_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再次插入到iot_send_queue的队首位置,等待下次尝试发送
+        if buff_full then
+            log.error("tcp_client_sender.proc", "buffer is full, wait for the next time")
+            table.insert(iot_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()
+    -- 遍历数据发送队列iot_send_queue
+    while #iot_send_queue > 0 do
+        local send_item = table.remove(iot_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
+

+ 101 - 0
module/Air780EPM/demo/fota2/iot_server/update.lua

@@ -0,0 +1,101 @@
+--[[
+@module  update
+@summary 远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、判断网卡是否连接成功;
+2、初始化fota2模块;
+3、配置fota2模块的参数;
+4、调用fota2模块的升级函数;
+5、在升级结果的回调函数中,根据升级结果进行处理;
+]]
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+
+libfota2 = require "libfota2"
+
+
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("降功耗 找合宙")
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+sys.timerLoopStart(get_version, 3000)
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
+            "3) 已经是最新版本,无需升级" )
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local ota_opts = {}
+
+function fota_task_func()
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+
+    -- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
+    if "123" == _G.PRODUCT_KEY  then
+        while 1 do
+            sys.wait(1000)
+            log.info("fota", "请修改正确的PRODUCT_KEY")
+        end
+    end
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, ota_opts)
+end
+
+--创建并且启动一个task
+--运行这个task的主函数fota_task_func
+sys.taskInit(fota_task_func)
+-- 演示定时自动升级, 每隔4小时自动检查一次
+sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, ota_opts)
+
+

+ 0 - 99
module/Air780EPM/demo/fota2/main.lua

@@ -1,99 +0,0 @@
--- LuaTools需要PROJECT和VERSION这两个信息
-PROJECT = "fotademo"
--- iot限制,只能上传xxx.yyy.zzz格式的三位数的版本号,但实际上现在只用了XXX和ZZZ,中间yyy暂未使用
--- 需要注意的是,因为yyy不生效,所以111.222.333版本和111.444.333版本,对iot平台来说都一样,所以建议中间那一位永远写000
-VERSION = "001.000.000"
-
--- 使用合宙iot平台时需要这个参数
-PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
-
-sys = require "sys"
-libfota2 = require "libfota2"
-
--- 联网函数, 可自行删减
-sys.taskInit(function()
-    -- 默认都等到联网成功
-    sys.waitUntil("IP_READY")
-    log.info("4G网络链接成功")
-    sys.publish("net_ready")
-end)
-
--- 循环打印版本号, 方便看版本号变化, 非必须
-sys.taskInit(function()
-    while 1 do
-        sys.wait(5000)
-        log.info("降功耗 找合宙")
-        -- log.info("fota", "脚本版本号", VERSION)
-        log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
-    end
-end)
-
--- 升级结果的回调函数
--- 功能:获取fota的回调函数
--- 参数:
--- result:number类型
---   0表示成功
---   1表示连接失败
---   2表示url错误
---   3表示服务器断开
---   4表示接收报文错误
---   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
-local function fota_cb(ret)
-    log.info("fota", ret)
-    if ret == 0 then
-        log.info("升级包下载成功,重启模块")
-        rtos.reboot()
-    elseif ret == 1 then
-        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
-    elseif ret == 2 then
-        log.info("url错误", "检查url拼写")
-    elseif ret == 3 then
-        log.info("服务器断开", "检查服务器白名单配置")
-    elseif ret == 4 then
-        log.info("接收报文错误", "检查模块固件或升级包内文件是否正常")
-    elseif ret == 5 then
-        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
-    else
-        log.info("不是上面几种情况 ret为", ret)
-    end
-end
-local ota_opts = {}
-
--- 使用合宙iot平台进行升级,不需要管下面这段代码
--- 使用第三方服务器时打开下面这段代码
---[[local ota_opts = {
-    url = "", 
-    -- 合宙IOT平台的默认升级URL, 不填就是这个默认值
-    -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
-    -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ### 
-    -- 如果不加###,则默认会上传如下参数
-    -- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
-    -- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
-    -- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
-    -- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
-    -- 5. opts.firmware_name string 底层版本号
-    -- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
-    version = ""
-    -- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
-}]]--
-sys.taskInit(function()
-    -- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
-    if "123" == _G.PRODUCT_KEY and not ota_opts.url then
-        while 1 do
-            sys.wait(1000)
-            log.info("fota", "请修改正确的PRODUCT_KEY")
-        end
-    end
-    -- 等待网络就行后开始检查升级
-    sys.waitUntil("net_ready")
-    log.info("开始检查升级")
-    sys.wait(500)
-    libfota2.request(fota_cb, ota_opts)
-end)
--- 演示定时自动升级, 每隔4小时自动检查一次
-sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, ota_opts)
-
--- 用户代码已结束---------------------------------------------
--- 结尾总是这一句
-sys.run()
--- sys.run()之后后面不要加任何语句!!!!!

+ 141 - 0
module/Air780EPM/demo/fota2/self_server/README.md

@@ -0,0 +1,141 @@
+## 功能模块介绍
+
+### 自建服务器fota功能演示
+
+1、main.lua:主程序入口;
+
+2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的四种网卡(单4g网卡,单wifi网卡,单spi以太网卡,多网卡)中的任何一种网卡;
+
+3、update.lua:使用自建服务器进行远程升级功能模块,简单升级演示;
+
+4、tcp_self_server文件夹:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备启动customer_srv_fota功能模块,使用自建服务器进行升级;
+
+5、customer_srv_fota.lua:自建服务器升级功能模块;
+
+6、psm_power_fota.lua:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。需要注意的是此场景与上面两种场景不能同时使用;
+
+
+## 系统消息介绍
+
+1、"IP_READY":某种网卡已经获取到ip信息,仅仅获取到了ip信息,能否和外网连通还不确认;
+
+2、"IP_LOSE":某种网卡已经掉网;
+
+
+
+## 用户消息介绍
+
+1、"RECV_DATA_FROM_SERVER":socket client收到服务器下发的数据后,通过此消息发布出去,给其他应用模块处理;
+
+2、"SEND_DATA_REQ":其他应用模块发布此消息,通知socket client发送数据给服务器;
+
+
+
+## 演示功能概述
+
+1、combination文件夹下的demo会有三个演示场景,在main.lua中选择要使用的场景:
+
+    (1) 使用自建服务器升级,演示最简单的升级逻辑。
+
+    (2) 使用自建服务器升级,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。
+
+    (3) 休眠状态下升级,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。
+
+2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
+
+   (1) netdrv_4g:4G网卡
+
+   (2) netdrv_wifi:WIFI STA网卡
+
+   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
+
+   (4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
+
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/air780epm/luatos/app/driver/eth/image/RFSvb75NRoEWqYxfCRVcVrOKnsf.jpg)
+
+1、Air780EPM V1.3版本开发板一块+可上网的sim卡一张+4g天线一根+网线一根:
+
+- sim卡插入开发板的sim卡槽
+
+- 天线装到开发板上
+
+- 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口
+
+2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air780EPM V1.3版本开发板和数据线的硬件接线方式为:
+
+- Air780EPM V1.3版本开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air780EPM V2012版本固件)](https://docs.openluat.com/air780epm/luatos/firmware/version/)
+
+3、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/);
+
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/),点击 打开TCP 按钮,会创建一个TCP server,将server的地址和端口赋值给tcp_client_self_main.lua中的SERVER_ADDR和SERVER_PORT两个变量
+
+3、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
+
+4、Luatools烧录内核固件和修改后的demo脚本代码
+
+5、使用Luatools制作升级包,先把新旧版本分别生成量产文件,然后再制作升级包,工具上栏 luatOS->固件工具->差分包/整包升级包制作,将制作好的升级包配置到合宙iot服务器自己项目下,或上传到自建服务器上面;
+
+6、烧录成功后,自动开机运行
+
+7、[合宙TCP/UDP web测试工具](https://netlab.luatos.com/)上创建的两个TCP server可以看到有设备连接上来,然后可以下发下面字符串触发升级:
+
+
+``` lua
+
+--自建服务器下发这个指令,下发之前需要在服务器上面配置好升级包,然后吧url给到字符串
+--定义一个json格式如下,具体可以根据实际情况定义:
+-- {"fota": "true","url": "http://airtest.openluat.com:2900/download/FOTA2_DEMO_2012.001.001_LuatOS-SoC_Air780EPM.bin"}
+
+```
+
+8、可以看到升级过程如下,不管是什么场景下升级,基本都是如下情况:
+
+``` lua
+--没有升级之前可以看到如下打印
+I/user.fota 脚本版本号 001.000.000 core版本号 V2010
+
+I/user.fota_task_func recv IP_READY 1
+I/user.开始检查升级
+
+I/user.升级包下载成功,重启模块
+
+
+--升级之后可以看到如下打印
+I/user.fota 脚本版本号 001.000.001 core版本号 V2012
+--升级重启之后还是会检查升级,所以会有如下打印属于正常情况,其中"code": 27 是合宙iot服务器返回的状态码,意思是已经是最新版本了。自建服务器如果没做设置会再下载一遍升级包。
+
+I/user.fota -9 {"code": 27, "msg": "\u5df2\u662f\u6700\u65b0\u7248\u672c"}
+I/user.使用合宙服务器,接下来解析body里的code
+I/user.已是最新版本 1.设备的固件/脚本版本高于或等于云平台上的版本号 2.用户项目升级配置中未添加该设备 3.云平台升级配置中,是否升级配置为否
+I/user.fota 4
+
+
+```
+
+9、对于psm休眠状态下的升级的场景,打印的版本来判断是否是最新版本。模组升级成功后会自动进入psm休眠状态。可以通过电流查看休眠情况。

+ 139 - 0
module/Air780EPM/demo/fota2/self_server/customer_srv_fota.lua

@@ -0,0 +1,139 @@
+--[[
+@module  air_srv_fota
+@summary 使用自建服务器远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、接收 CUSTOMER_SRV_FOTA 系统消息,触发升级;
+2、判断网卡是否连接成功;
+3、初始化fota2模块;
+4、调用fota2模块的升级函数;
+5、根据升级结果进行处理;
+]]
+
+
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("降功耗 找合宙")
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+
+sys.timerLoopStart(get_version, 3000)
+
+-- fota升级标志:true 表示当前正有 FOTA 流程在跑
+local fota_running = false
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    -- fota结束,无论成功还是失败,都释放fota_running标志
+    fota_running = false
+
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
+            "3) 已经是最新版本,无需升级")
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+
+-- 使用第三方服务器,配置ota_opts参数
+--[[
+-- opts参数说明, 所有参数都是可选的
+-- 1. opts.url string 升级所需要的URL, 若使用合宙iot平台,则不需要填
+-- 2. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+-- 3. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+-- 4. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+-- 5. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+-- 6. opts.firmware_name string 固件名称,默认是 _G.PROJECT.. "_LuatOS-SoC_" .. rtos.bsp()
+-- 7. opts.server_cert string 服务器证书, 默认不使用
+-- 8. opts.client_cert string 客户端证书, 默认不使用
+-- 9. opts.client_key string 客户端私钥, 默认不使用
+-- 10. opts.client_password string 客户端私钥口令, 默认不使用
+-- 11. opts.method string 请求方法, 默认是GET
+-- 12. opts.headers table 额外添加的请求头,默认不需要
+-- 13. opts.body string 额外添加的请求body,默认不需要
+]]
+local opts = {
+    url = "",
+    -- 合宙IOT平台的默认升级URL, 不填就是这个默认值
+    -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
+    -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
+    -- 如果不加###,则默认会上传如下参数
+    -- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+    -- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+    -- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+    -- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+    -- 5. opts.firmware_name string 底层版本号
+
+    -- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
+    version = ""
+    -- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
+}
+
+
+local function air_fota_func(data)
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+    while true do
+        -- 阻塞等待外部事件:"CUSTOMER_SRV_FOTA"
+        local result, data = sys.waitUntil("CUSTOMER_SRV_FOTA")
+        if result then
+            log.info("接收到数据", "date", #data)
+
+            if fota_running then
+                log.warn("fota_task", "FOTA 正在运行,跳过本次请求")
+            else
+                -- 标记FOTA正在运行
+                -- 注意:这里只是标记,实际的FOTA流程还没有开始
+                opts.url = data.url
+                opts.version = data.version
+                fota_running = true
+                log.info("开始检查升级")
+                libfota2.request(fota_cb, opts)
+            end
+        end
+    end
+end
+-- 初始化FOTA任务
+sys.taskInit(air_fota_func)

+ 108 - 0
module/Air780EPM/demo/fota2/self_server/main.lua

@@ -0,0 +1,108 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本demo演示的核心功能为:
+1、这个demo会有三个演示场景:
+    (1)使用自建服务器升级,演示最简单的升级逻辑。
+    (2)使用自建服务器升级,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。
+    (3)休眠状态下升级,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。
+2、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 = "FOTA2_DEMO"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+
+sys.timerLoopStart(get_version, 3000)
+
+
+-- 加载网络驱动设备功能模块
+require "netdrv_device"
+
+
+
+--两种tcp下发指令升级的场景和psm+低功耗模式升级不能同时使用,需要根据自己场景选择其中一种
+
+--两种tcp下发指令升级的场景可以启用一种,也可以启用两种,启用两种时,注意控制不要一个在fota的过程中,另外一个再fota。
+
+
+-- 加载远程升级功能模块,场景一
+require "update"
+---------------------------------------------------------------------------
+-- 加载tcp client self socket主应用功能模块,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。场景二
+-- require "tcp_self_main"
+-- 加载自建服务器远程升级功能模块
+-- require "customer_srv_fota"
+---------------------------------------------------------------------------
+-- 加载psm+低功耗模式升级功能模块,场景三
+-- require "psm_power_fota"
+
+
+
+
+
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

+ 33 - 0
module/Air780EPM/demo/fota2/self_server/netdrv/netdrv_4g.lua

@@ -0,0 +1,33 @@
+--[[
+@module  netdrv_4g
+@summary “4G网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为4G网卡驱动模块,核心业务逻辑为:
+1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
+end
+
+
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+-- 设置默认网卡为socket.LWIP_GP
+-- 在Air8000上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
+-- 在单4G网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
+socket.dft(socket.LWIP_GP)

+ 85 - 0
module/Air780EPM/demo/fota2/self_server/netdrv/netdrv_eth_spi.lua

@@ -0,0 +1,85 @@
+--[[
+@module  netdrv_eth_spi
+@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块 ,核心业务逻辑为:
+1、打开CH390H芯片供电开关;
+2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
+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测试使用的是Air8000开发板
+--GPIO140为CH390H以太网芯片的供电使能控制引脚
+gpio.setup(140, 1, gpio.PULLUP)
+
+--这个task的核心业务逻辑是:初始化SPI,初始化以太网卡,并在以太网卡上开启动态主机配置协议
+local function netdrv_eth_spi_task_func()
+    -- 初始化SPI1
+    local result = spi.setup(
+        1,--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 1, 片选 GPIO12
+    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spi=1, cs=12})
+
+    --在以太上开启动态主机配置协议
+    netdrv.dhcp(socket.LWIP_ETH, true)
+end
+
+--创建并且启动一个task
+--task的处理函数为netdrv_eth_spi_task_func
+sys.taskInit(netdrv_eth_spi_task_func)

+ 95 - 0
module/Air780EPM/demo/fota2/self_server/netdrv/netdrv_multiple.lua

@@ -0,0 +1,95 @@
+--[[
+@module  netdrv_multiple
+@summary 多网卡(4G网卡、WIFI STA网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为多网卡驱动模块 ,核心业务逻辑为:
+1、调用libnetif.set_priority_order配置多网卡的控制参数以及优先级;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_multiple"就可以加载运行;
+]]
+
+
+local exnetif = require "exnetif"
+
+-- 网卡状态变化通知回调函数
+-- 当libnetif中检测到网卡切换或者所有网卡都断网时,会触发调用此回调函数
+-- 当网卡切换切换时:
+--     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芯片”的以太网卡,使用Air8000开发板验证
+            {
+                ETHERNET = {
+                    -- 供电使能GPIO
+                    pwrpin = 140,
+                    -- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
+                    -- 如果没有传入此参数,libnetif会使用默认值10秒
+                    ping_time = 3000,
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+
+                    -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
+                    tp = netdrv.CH390,
+                    opts = {spi=1, cs=12}
+                }
+            },
+
+            -- WIFI STA网卡
+            {
+                WIFI = {
+                    -- 要连接的WIFI路由器名称
+                    ssid = "茶室-降功耗,找合宙!",
+                    -- 要连接的WIFI路由器密码
+                    password = "Air123456",
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+                }
+            },
+
+            -- 4G网卡
+            {
+                LWIP_GP = true
+            }
+        }
+    )
+end
+
+-- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc
+exnetif.notify_status(netdrv_multiple_notify_cbfunc)
+
+-- 如果存在udp网络应用,并且udp网络应用中,根据应用层的心跳能够判断出来udp数据通信出现了异常;
+-- 可以在判断出现异常的位置,调用一次libnetif.check_network_status()接口,强制对当前正式使用的网卡进行一次连通性检测;
+-- 如果存在tcp网络应用,不需要用户调用libnetif.check_network_status()接口去控制,libnetif会在tcp网络应用通信异常时自动对当前使用的网卡进行连通性检测。
+
+
+-- 启动一个task,task的处理函数为netdrv_multiple_task_func
+-- 在处理函数中调用libnetif.set_priority_order设置网卡优先级
+-- 因为libnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
+sys.taskInit(netdrv_multiple_task_func)

+ 30 - 0
module/Air780EPM/demo/fota2/self_server/netdrv_device.lua

@@ -0,0 +1,30 @@
+--[[
+@module  netdrv_device
+@summary 网络驱动设备功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
+1、netdrv_4g:socket.LWIP_GP,4G网卡;
+2、netdrv_wifi:socket.LWIP_STA,WIFI STA网卡;
+3、netdrv_ethernet_spi:socket.LWIP_USER1,通过SPI外挂CH390H芯片的以太网卡;
+4、netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
+
+根据自己的项目需求,只需要require以上四种中的一种即可;
+
+
+本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
+]]
+
+
+-- 根据自己的项目需求,只需要require以下四种中的一种即可;
+
+-- 加载“4G网卡”驱动模块
+require "netdrv_4g"
+
+-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
+-- require "netdrv_eth_spi"
+
+-- 加载“可以配置优先级的多种网卡”驱动模块
+-- require "netdrv_multiple"

+ 130 - 0
module/Air780EPM/demo/fota2/self_server/psm_power_fota.lua

@@ -0,0 +1,130 @@
+--[[
+@module  psm+_power_fota
+@summary psm+超低功耗模式下升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为psm+超低功耗模式下升级功能模块,核心设计思路
+1.升级触发机制 :
+   - 定时器唤醒升级 :设备定期从PSM模式唤醒,主动检查是否有新固件版本。
+   - 外部中断唤醒升级 :通过特定GPIO中断或网络消息唤醒设备进行升级。
+2.防止升级过程中进入休眠 :
+   - 在开始FOTA升级前,禁用PSM模式进入。
+   - 升级完成后,根据结果决定是否重启设备或重新进入PSM模式。
+
+]]
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "BnYk2BlYO30DiWra7q27wUmEarOiipHO" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    --升级结束,触发升级回调,发布消息升级结束,可以进入休眠模式
+    sys.publish("FOTA_END")
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
+            "3) 已经是最新版本,无需升级" )
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local opts = {
+    url = "###http://cdn.openluat-backend.openluat.com/upgrade_firmware/fotademo_2008.001.001_LuatOS-SoC_Air8000.bin_20250623184110381812",
+    -- 合宙IOT平台的默认升级URL, 不填就是这个默认值
+    -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
+    -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
+    -- 如果不加###,则默认会上传如下参数
+    -- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+    -- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+    -- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+    -- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+    -- 5. opts.firmware_name string 底层版本号
+
+    -- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
+    -- version = ""
+    -- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
+}
+
+
+function psm_fota_task_func()
+    -- 如果是被定时器唤醒,因为上次进入PSM+时是开启了飞行模式,所以在唤醒后第一时间关闭飞行模式。
+    mobile.flymode(0, false)
+    log.info("开始测试PSM+模式功耗。")
+
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, opts)
+
+    -- 打印版本号, 方便看版本号变化, 非必须
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+
+
+    -- 等待下载升级包结束, 发布消息"FOTA_END",
+    -- 如果15秒内没有收到消息,则15秒的时长到达后进入PSM+模式。
+    -- 需要注意的是在fota_cb回调函数中,升级包下载成功后,会立马重启并升级模组。如果还有其他事情要做不想立马重启升级,需自行决定reboot的时机
+    -- 升级包下载成功后,本demo默认是立即自动重启并且将升级包更新到模组中,更新成功后,会再次走到这里
+    -- 再次走到这里后,合宙iot平台会返回“已经是最新版本,不需要升级”,fota_cb回调函数中会发布消息"FOTA_END"
+    -- 至此,才会继续向下执行代码,进入PSM+模式
+    sys.waitUntil("FOTA_END", 15000)
+
+    log.info("升级结束,进入PSM模式")
+
+
+    -- 定时检查升级 (每4小时唤醒一次)
+    pm.dtimerStart(2, 4 * 3600000)
+    -- 启动飞行模式,规避可能会出现的网络问题
+    mobile.flymode(0, true)
+    -- 进入PSM模式
+    pm.power(pm.WORK_MODE, 3)
+    -- 防御机制:15秒后如果未进入PSM则重启
+    sys.wait(15000)
+    log.info("进入PSM+失败,重启")
+    rtos.reboot()
+end
+
+sys.taskInit(psm_fota_task_func)

+ 140 - 0
module/Air780EPM/demo/fota2/self_server/tcp_self_server/tcp_self_main.lua

@@ -0,0 +1,140 @@
+--[[
+@module  tcp_client_main
+@summary tcp client socket主应用功能模块
+@version 1.0
+@date    2025.08.12
+@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_self_receiver"
+-- 加载tcp client socket数据发送功能模块
+local tcp_client_sender = require "tcp_self_sender"
+
+-- 电脑访问:https://netlab.luatos.com/
+-- 点击 打开TCP 按钮,会创建一个TCP server
+-- 将server的地址和端口赋值给下面这两个变量
+local SERVER_ADDR = "112.125.89.8"
+local SERVER_PORT = 45433
+
+-- 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毫秒超时不要修改的更长;
+            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+            -- 当libnetif.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)
+        -- 如果配置失败
+        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")
+
+        -- 数据收发以及网络连接异常事件总处理逻辑
+        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
+sysplus.taskInitEx(tcp_client_main_task_func, TASK_NAME, tcp_client_main_cbfunc)

+ 126 - 0
module/Air780EPM/demo/fota2/self_server/tcp_self_server/tcp_self_receiver.lua

@@ -0,0 +1,126 @@
+--[[
+@module  tcp_client_receiver
+@summary tcp client socket数据接收应用功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为tcp client socket数据接收应用功能模块,核心业务逻辑为:
+从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;
+
+本文件的对外接口有2个:
+1、tcp_client_receiver.proc(socket_client):数据接收应用逻辑处理入口,在tcp_client_main.lua中调用;
+2、sys.publish("RECV_DATA_FROM_SERVER",  data):数据接收应用逻辑处理入口,在tcp_client_main
+   将接收到的数据通过消息"RECV_DATA_FROM_SERVER"发布出去处理;
+   处理后通过消息"CUSTOMER_SRV_FOTA"发布到customer_srv_fota.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_SELF_DATA_SERVER"消息publish出去,给其他应用模块处理
+            sys.publish("RECV_SELF_DATA_SERVER",  data)
+
+            -- 清空socket数据接收缓冲区中的数据
+            recv_buff:del()
+            -- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
+        else
+            break
+        end
+    end
+
+    return true
+end
+--解析接受到的json参数并发布到消息"AIR_SRV_FOTA"中
+--定义一个json格式如下,具体可以根据实际情况定义:
+--{
+--    "fota": "true",
+--    "url": "http://192.168.1.100:8080/firmware.bin"
+--}
+
+local function data_processing(data)
+    -- 解析json数据
+    local json_data = json.decode(data)
+
+    -- 如果解析失败
+    if not json_data then
+        log.error("data_processing", "json.decode error")
+        return
+    end
+    -- 解析fota参数
+    local fota = json_data.fota
+    -- 如果fota参数不存在
+    if not fota then
+        log.error("data_processing", "fota error")
+        return
+    end
+    -- 解析url参数
+    local url = json_data.url
+    -- 如果url参数不存在
+    if not url then
+        log.error("data_processing", "url error")
+        return
+    end
+    -- 如果fota为true
+    if fota == "true" then
+        -- -- 发布消息"CUSTOMER_SRV_FOTA"
+        sys.publish("CUSTOMER_SRV_FOTA",json_data)
+    end
+end
+
+-- 订阅"RECV_SELF_DATA_SERVER"消息
+sys.subscribe("RECV_SELF_DATA_SERVER", data_processing)
+
+return tcp_client_receiver

+ 137 - 0
module/Air780EPM/demo/fota2/self_server/tcp_self_server/tcp_self_sender.lua

@@ -0,0 +1,137 @@
+--[[
+@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"消息,将其他应用模块需要发送的数据存储到队列self_send_queue中;
+2、tcp_client_main主任务调用tcp_client_sender.proc接口,遍历队列self_send_queue,逐条发送数据到server;
+3、tcp client socket和server之间的连接如果出现异常,tcp_client_main主任务调用tcp_client_sender.exception_proc接口,丢弃掉队列self_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 self_send_queue = {}
+
+-- tcp_client_main的任务名
+tcp_client_sender.TASK_NAME = "self_tcp_client_main"
+
+-- "SEND_DATA_REQ"消息的处理函数
+local function send_data_req_proc_func(data, cb)
+    -- 将原始数据增加前缀,然后插入到发送队列self_send_queue中
+    table.insert(self_send_queue, {data=data, cb=cb})
+    -- 通知tcp_client_main主任务有数据需要发送
+    -- tcp_client_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
+    sysplus.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
+    -- 遍历数据发送队列self_send_queue
+    while #self_send_queue>0 do
+        -- 取出来第一条数据赋值给send_item
+        -- 同时从队列self_send_queue中删除这一条数据
+        send_item = table.remove(self_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再次插入到self_send_queue的队首位置,等待下次尝试发送
+        if buff_full then
+            log.error("tcp_client_sender.proc", "buffer is full, wait for the next time")
+            table.insert(self_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()
+    -- 遍历数据发送队列self_send_queue
+    while #self_send_queue>0 do
+        local send_item = table.remove(self_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("SELF_SEND_DATA_REQ", send_data_req_proc_func)
+
+return tcp_client_sender

+ 128 - 0
module/Air780EPM/demo/fota2/self_server/update.lua

@@ -0,0 +1,128 @@
+--[[
+@module  update
+@summary 远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、判断网卡是否连接成功;
+2、初始化fota2模块;
+3、配置fota2模块的参数;
+4、调用fota2模块的升级函数;
+5、在升级结果的回调函数中,根据升级结果进行处理;
+]]
+
+libfota2 = require "libfota2"
+
+
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+sys.timerLoopStart(get_version, 3000)
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
+            "3) 已经是最新版本,无需升级" )
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+-- 使用第三方服务器,配置ota_opts参数
+--[[
+-- opts参数说明, 所有参数都是可选的
+-- 1. opts.url string 升级所需要的URL, 若使用合宙iot平台,则不需要填
+-- 2. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+-- 3. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+-- 4. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+-- 5. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+-- 6. opts.firmware_name string 固件名称,默认是 _G.PROJECT.. "_LuatOS-SoC_" .. rtos.bsp()
+-- 7. opts.server_cert string 服务器证书, 默认不使用
+-- 8. opts.client_cert string 客户端证书, 默认不使用
+-- 9. opts.client_key string 客户端私钥, 默认不使用
+-- 10. opts.client_password string 客户端私钥口令, 默认不使用
+-- 11. opts.method string 请求方法, 默认是GET
+-- 12. opts.headers table 额外添加的请求头,默认不需要
+-- 13. opts.body string 额外添加的请求body,默认不需要
+]]
+local opts = {
+    url = "###http://cdn.openluat-backend.openluat.com/upgrade_firmware/fotademo_2008.001.001_LuatOS-SoC_Air8000.bin_20250623184110381812",
+    -- 合宙IOT平台的默认升级URL, 不填就是这个默认值
+    -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
+    -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
+    -- 如果不加###,则默认会上传如下参数
+    -- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+    -- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+    -- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+    -- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+    -- 5. opts.firmware_name string 底层版本号
+
+    -- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
+    -- version = ""
+    -- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
+}
+
+function fota_task_func()
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+    ----这个判断是提醒要设置url的,且不要使用本文中的测试服务器,实际生产请删除
+    if not opts.url or string.find(opts.url,"airtest.openluat.com") then
+        while 1 do
+            sys.wait(1000)
+            log.info("fota", "当前URL",opts.url,"请修改正确的url")
+        end
+    end
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, opts)
+end
+
+--创建并且启动一个task
+--运行这个task的主函数fota_task_func
+sys.taskInit(fota_task_func)
+-- 演示定时自动升级, 每隔4小时自动检查一次
+sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, opts)
+
+

+ 128 - 0
module/Air8000/demo/fota2/iot_server/README.md

@@ -0,0 +1,128 @@
+## 功能模块介绍
+
+### iot服务器fota功能演示
+
+1、main.lua:主程序入口;
+
+2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的四种网卡(单4g网卡,单wifi网卡,单spi以太网卡,多网卡)中的任何一种网卡;
+
+3、update.lua:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
+
+4、tcp_iot文件夹:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备启动air_srv_fota功能模块,使用合宙iot服务器进行升级;
+
+5、air_srv_fota.lua:合宙服务器升级功能模块;
+
+6、psm_power_fota.lua:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。需要注意的是此场景与上面两种场景不能同时使用;
+
+
+## 系统消息介绍
+
+1、"IP_READY":某种网卡已经获取到ip信息,仅仅获取到了ip信息,能否和外网连通还不确认;
+
+2、"IP_LOSE":某种网卡已经掉网;
+
+
+
+## 用户消息介绍
+
+1、"RECV_DATA_FROM_SERVER":socket client收到服务器下发的数据后,通过此消息发布出去,给其他应用模块处理;
+
+2、"SEND_DATA_REQ":其他应用模块发布此消息,通知socket client发送数据给服务器;
+
+
+
+## 演示功能概述
+
+1、此demo演示了三种场景:
+
+   (1) fota升级简单演示:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
+
+   (2) tcp服务器下发升级指令:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块;
+
+   (3) psm低功耗fota:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子;
+
+2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
+
+   (1) netdrv_4g:4G网卡
+
+   (2) netdrv_wifi:WIFI STA网卡
+
+   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
+
+   (4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
+
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/air8000/luatos/app/image/netdrv_multi.jpg)
+
+1、Air8000开发板一块+可上网的sim卡一张+4g天线一根+wifi天线一根+网线一根:
+
+- sim卡插入开发板的sim卡槽
+
+- 天线装到开发板上
+
+- 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口
+
+2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air8000开发板和数据线的硬件接线方式为:
+
+- Air8000开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air8000 V2012版本固件)](https://docs.openluat.com/air8000/luatos/firmware/)
+
+3、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/);
+
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/),点击 打开TCP 按钮,会创建一个TCP server,将server的地址和端口赋值给tcp_iot_main.lua中的SERVER_ADDR和SERVER_PORT两个变量
+
+4、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
+
+5、Luatools烧录内核固件和修改后的demo脚本代码
+
+6、使用Luatools制作升级包,先把新旧版本分别生成量产文件,然后再制作升级包,工具上栏 luatOS->固件工具->差分包/整包升级包制作,将制作好的升级包配置到合宙iot服务器自己项目下,或上传到自建服务器上面;
+
+7、烧录成功后,自动开机运行
+
+8、可以看到升级过程如下,不管是什么场景下升级,基本都是如下日志情况:
+
+``` lua
+--没有升级之前可以看到如下打印
+I/user.fota 脚本版本号 001.000.000 core版本号 V2010
+
+I/user.fota_task_func recv IP_READY 1
+I/user.开始检查升级
+
+I/user.升级包下载成功,重启模块
+
+
+--升级之后可以看到如下打印
+I/user.fota 脚本版本号 001.000.001 core版本号 V2012
+--升级重启之后还是会检查升级,所以会有如下打印属于正常情况,其中"code": 27 是合宙iot服务器返回的状态码,意思是已经是最新版本了。
+I/user.fota -9 {"code": 27, "msg": "\u5df2\u662f\u6700\u65b0\u7248\u672c"}
+I/user.使用合宙服务器,接下来解析body里的code
+I/user.已是最新版本 1.设备的固件/脚本版本高于或等于云平台上的版本号 2.用户项目升级配置中未添加该设备 3.云平台升级配置中,是否升级配置为否
+I/user.fota 4
+
+
+```
+9、对于psm休眠状态下的升级的场景,可以通过iot平台查看是否成功,在iot平台的升级日志页面搜索模组的imei,可以看到有两条升级结果“成功”和“已是最新版本”。模组升级成功后会自动进入psm休眠状态。可以通过电流查看休眠情况。

+ 119 - 0
module/Air8000/demo/fota2/iot_server/air_srv_fota.lua

@@ -0,0 +1,119 @@
+--[[
+@module  air_srv_fota
+@summary 使用合宙iot平台远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、接收 AIR_SRV_FOTA 系统消息,触发升级;
+2、判断网卡是否连接成功;
+3、初始化fota2模块;
+4、调用fota2模块的升级函数;
+5、根据升级结果进行处理;
+]]
+
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("降功耗 找合宙")
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+
+sys.timerLoopStart(get_version, 3000)
+
+
+-- fota升级标志:true 表示当前正有 FOTA 流程在跑
+local fota_running = false
+
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    -- fota结束,无论成功还是失败,都释放fota_running标志
+    fota_running = false
+
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
+            "3) 已经是最新版本,无需升级")
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local opts = {}
+
+local function air_fota_func(data)
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+    while true do
+        -- 阻塞等待外部事件:"AIR_SRV_FOTA"
+        sys.waitUntil("AIR_SRV_FOTA")
+
+        -- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
+        if "123" == _G.PRODUCT_KEY then
+            while 1 do
+                sys.wait(1000)
+                log.info("fota", "请修改正确的PRODUCT_KEY")
+            end
+        end
+
+        if fota_running then
+            log.warn("fota_task", "FOTA 正在运行,跳过本次请求")
+        else
+            -- 标记FOTA正在运行
+            -- 注意:这里只是标记,实际的FOTA流程还没有开始
+            fota_running = true
+            log.info("开始检查升级")
+            libfota2.request(fota_cb, opts)
+        end
+    end
+end
+-- 初始化FOTA任务
+sys.taskInit(air_fota_func)
+
+
+-- 演示定时自动升级, 每隔4小时自动检查一次,可以根据需求打开
+-- sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, opts)

+ 91 - 0
module/Air8000/demo/fota2/iot_server/main.lua

@@ -0,0 +1,91 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本demo演示的核心功能为:
+1、此demo演示了三种场景:
+   (1)fota升级简单演示:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
+   (2)tcp服务器下发升级指令:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块;
+   (3)psm低功耗fota:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子;
+2、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 = "FOTA2_DEMO"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+
+-- 加载网络驱动设备功能模块
+require "netdrv_device"
+
+-- 加载远程升级功能模块,场景1
+require "update"
+---------------------------------------------------------------------------
+-- 加载tcp client iot socket主应用功能模块,通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块,场景二
+-- require "tcp_iot_main"
+-- 加载合宙iot平台远程升级功能模块
+-- require "air_srv_fota"
+---------------------------------------------------------------------------
+-- 加载psm+低功耗模式升级功能模块,场景三
+-- require "psm_power_fota"
+
+
+
+
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

+ 33 - 0
module/Air8000/demo/fota2/iot_server/netdrv/netdrv_4g.lua

@@ -0,0 +1,33 @@
+--[[
+@module  netdrv_4g
+@summary “4G网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为4G网卡驱动模块,核心业务逻辑为:
+1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
+end
+
+
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+-- 设置默认网卡为socket.LWIP_GP
+-- 在Air8000上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
+-- 在单4G网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
+socket.dft(socket.LWIP_GP)

+ 85 - 0
module/Air8000/demo/fota2/iot_server/netdrv/netdrv_eth_spi.lua

@@ -0,0 +1,85 @@
+--[[
+@module  netdrv_eth_spi
+@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块 ,核心业务逻辑为:
+1、打开CH390H芯片供电开关;
+2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
+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测试使用的是Air8000开发板
+--GPIO140为CH390H以太网芯片的供电使能控制引脚
+gpio.setup(140, 1, gpio.PULLUP)
+
+--这个task的核心业务逻辑是:初始化SPI,初始化以太网卡,并在以太网卡上开启动态主机配置协议
+local function netdrv_eth_spi_task_func()
+    -- 初始化SPI1
+    local result = spi.setup(
+        1,--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 1, 片选 GPIO12
+    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spi=1, cs=12})
+
+    --在以太上开启动态主机配置协议
+    netdrv.dhcp(socket.LWIP_ETH, true)
+end
+
+--创建并且启动一个task
+--task的处理函数为netdrv_eth_spi_task_func
+sys.taskInit(netdrv_eth_spi_task_func)

+ 95 - 0
module/Air8000/demo/fota2/iot_server/netdrv/netdrv_multiple.lua

@@ -0,0 +1,95 @@
+--[[
+@module  netdrv_multiple
+@summary 多网卡(4G网卡、WIFI STA网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为多网卡驱动模块 ,核心业务逻辑为:
+1、调用libnetif.set_priority_order配置多网卡的控制参数以及优先级;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_multiple"就可以加载运行;
+]]
+
+
+local exnetif = require "exnetif"
+
+-- 网卡状态变化通知回调函数
+-- 当libnetif中检测到网卡切换或者所有网卡都断网时,会触发调用此回调函数
+-- 当网卡切换切换时:
+--     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芯片”的以太网卡,使用Air8000开发板验证
+            {
+                ETHERNET = {
+                    -- 供电使能GPIO
+                    pwrpin = 140,
+                    -- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
+                    -- 如果没有传入此参数,libnetif会使用默认值10秒
+                    ping_time = 3000,
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+
+                    -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
+                    tp = netdrv.CH390,
+                    opts = {spi=1, cs=12}
+                }
+            },
+
+            -- WIFI STA网卡
+            {
+                WIFI = {
+                    -- 要连接的WIFI路由器名称
+                    ssid = "茶室-降功耗,找合宙!",
+                    -- 要连接的WIFI路由器密码
+                    password = "Air123456",
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+                }
+            },
+
+            -- 4G网卡
+            {
+                LWIP_GP = true
+            }
+        }
+    )
+end
+
+-- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc
+exnetif.notify_status(netdrv_multiple_notify_cbfunc)
+
+-- 如果存在udp网络应用,并且udp网络应用中,根据应用层的心跳能够判断出来udp数据通信出现了异常;
+-- 可以在判断出现异常的位置,调用一次libnetif.check_network_status()接口,强制对当前正式使用的网卡进行一次连通性检测;
+-- 如果存在tcp网络应用,不需要用户调用libnetif.check_network_status()接口去控制,libnetif会在tcp网络应用通信异常时自动对当前使用的网卡进行连通性检测。
+
+
+-- 启动一个task,task的处理函数为netdrv_multiple_task_func
+-- 在处理函数中调用libnetif.set_priority_order设置网卡优先级
+-- 因为libnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
+sys.taskInit(netdrv_multiple_task_func)

+ 50 - 0
module/Air8000/demo/fota2/iot_server/netdrv/netdrv_wifi.lua

@@ -0,0 +1,50 @@
+--[[
+@module  netdrv_wifi
+@summary “WIFI STA网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为WIFI STA网卡驱动模块,核心业务逻辑为:
+1、初始化WIFI网络;
+2、连接WIFI路由器;
+3、和WIFI路由器之间的连接状态发生变化时,在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_wifi"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_wifi.ip_ready_func", "IP_READY", json.encode(wlan.getInfo()))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_wifi.ip_lose_func", "IP_LOSE")
+end
+
+
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察WIFI的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 设置默认网卡为socket.LWIP_STA
+socket.dft(socket.LWIP_STA)
+
+
+wlan.init()
+--连接WIFI热点,连接结果会通过"IP_READY"或者"IP_LOSE"消息通知
+--Air8000仅支持2.4G的WIFI,不支持5G的WIFI
+--此处前两个参数表示WIFI热点名称以及密码,更换为自己测试时的真实参数即可
+--第三个参数1表示WIFI连接异常时,内核固件会自动重连
+wlan.connect("kfyy123", "kfyy123456", 1)
+
+--WIFI联网成功(做为STATION成功连接AP,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
+--各个功能模块可以订阅"IP_READY"消息实时处理WIFI联网成功的事件
+--也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功
+
+--WIFI断网后,内核固件会产生一个"IP_LOSE"消息
+--各个功能模块可以订阅"IP_LOSE"消息实时处理WIFI断网的事件
+--也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功

+ 33 - 0
module/Air8000/demo/fota2/iot_server/netdrv_device.lua

@@ -0,0 +1,33 @@
+--[[
+@module  netdrv_device
+@summary 网络驱动设备功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
+1、netdrv_4g:socket.LWIP_GP,4G网卡;
+2、netdrv_wifi:socket.LWIP_STA,WIFI STA网卡;
+3、netdrv_ethernet_spi:socket.LWIP_USER1,通过SPI外挂CH390H芯片的以太网卡;
+4、netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
+
+根据自己的项目需求,只需要require以上四种中的一种即可;
+
+
+本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
+]]
+
+
+-- 根据自己的项目需求,只需要require以下四种中的一种即可;
+
+-- 加载“4G网卡”驱动模块
+require "netdrv_4g"
+
+-- 加载“WIFI STA网卡”驱动模块
+-- require "netdrv_wifi"
+
+-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
+-- require "netdrv_eth_spi"
+
+-- 加载“可以配置优先级的多种网卡”驱动模块
+-- require "netdrv_multiple"

+ 122 - 0
module/Air8000/demo/fota2/iot_server/psm_power_fota.lua

@@ -0,0 +1,122 @@
+--[[
+@module  psm+_power_fota
+@summary psm+超低功耗模式下升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为psm+超低功耗模式下升级功能模块,核心设计思路
+1.升级触发机制 :
+   - 定时器唤醒升级 :设备定期从PSM模式唤醒,主动检查是否有新固件版本。
+   - 外部中断唤醒升级 :通过特定GPIO中断或网络消息唤醒设备进行升级。
+2.防止升级过程中进入休眠 :
+   - 在开始FOTA升级前,禁用PSM模式进入。
+   - 升级完成后,根据结果决定是否重启设备或重新进入PSM模式。
+
+]]
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "BnYk2BlYO30DiWra7q27wUmEarOiipHO" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+
+
+
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    --升级结束,触发升级回调,发布消息升级结束,可以进入休眠模式
+    sys.publish("FOTA_END")
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
+            "3) 已经是最新版本,无需升级")
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local ota_opts = {}
+
+
+function psm_fota_task_func()
+    -- 如果是被定时器唤醒,因为上次进入PSM+时是开启了飞行模式,所以在唤醒后第一时间关闭飞行模式。
+    mobile.flymode(0, false)
+    log.info("开始测试PSM+模式功耗。")
+
+    -- 打印版本号, 方便看版本号变化, 非必须
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, ota_opts)
+
+
+
+    -- 等待下载升级包结束, 发布消息"FOTA_END",
+    -- 如果15秒内没有收到消息,则15秒的时长到达后进入PSM+模式。
+    -- 需要注意的是在fota_cb回调函数中,升级包下载成功后,会立马重启并升级模组。如果还有其他事情要做不想立马重启升级,需自行决定reboot的时机
+    -- 升级包下载成功后,本demo默认是立即自动重启并且将升级包更新到模组中,更新成功后,会再次走到这里
+    -- 再次走到这里后,合宙iot平台会返回“已经是最新版本,不需要升级”,fota_cb回调函数中会发布消息"FOTA_END"
+    -- 至此,才会继续向下执行代码,进入PSM+模式
+    sys.waitUntil("FOTA_END", 15000)
+
+    log.info("升级结束,进入PSM模式")
+
+    -- 关闭gps备电以及gsensor供电使能,防止休眠模式下漏电导致功耗增加
+    gpio.close(24)
+
+    -- 定时检查升级 (每4小时唤醒一次)
+    pm.dtimerStart(2, 4 * 3600000)
+    -- 启动飞行模式,规避可能会出现的网络问题
+    mobile.flymode(0, true)
+    -- 进入PSM模式
+    pm.power(pm.WORK_MODE, 3)
+    -- 防御机制:15秒后如果未进入PSM则重启
+    sys.wait(15000)
+    log.info("进入PSM+失败,重启")
+    rtos.reboot()
+end
+
+sys.taskInit(psm_fota_task_func)

+ 140 - 0
module/Air8000/demo/fota2/iot_server/tcp_iot/tcp_iot_main.lua

@@ -0,0 +1,140 @@
+--[[
+@module  tcp_client_main
+@summary tcp client socket主应用功能模块
+@version 1.0
+@date    2025.08.12
+@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_iot_receiver"
+-- 加载tcp client socket数据发送功能模块
+local tcp_client_sender = require "tcp_iot_sender"
+
+-- 电脑访问:https://netlab.luatos.com/
+-- 点击 打开TCP 按钮,会创建一个TCP server
+-- 将server的地址和端口赋值给下面这两个变量
+local SERVER_ADDR = "112.125.89.8"
+local SERVER_PORT = 42662
+
+-- 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毫秒超时不要修改的更长;
+            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+            -- 当libnetif.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)
+        -- 如果配置失败
+        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")
+
+        -- 数据收发以及网络连接异常事件总处理逻辑
+        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
+sysplus.taskInitEx(tcp_client_main_task_func, TASK_NAME, tcp_client_main_cbfunc)

+ 111 - 0
module/Air8000/demo/fota2/iot_server/tcp_iot/tcp_iot_receiver.lua

@@ -0,0 +1,111 @@
+--[[
+@module  tcp_client_receiver
+@summary tcp client socket数据接收应用功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为tcp client socket数据接收应用功能模块,核心业务逻辑为:
+从内核读取接收到的数据,然后将数据处理后发送给其他应用功能模块做进一步处理;
+
+本文件的对外接口有2个:
+1、tcp_client_receiver.proc(socket_client):数据接收应用逻辑处理入口,在tcp_client_main.lua中调用;
+2、sys.publish("RECV_DATA_FROM_SERVER", data):
+   将接收到的数据通过消息"RECV_DATA_FROM_SERVER"发布出去处理;
+   处理后通过消息"AIR_SRV_FOTA"发布到air_srv_fota.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_IOT_DATA_SERVER"消息publish出去,给其他应用模块处理
+            sys.publish("RECV_IOT_DATA_SERVER", data)
+
+            -- 清空socket数据接收缓冲区中的数据
+            recv_buff:del()
+            -- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
+        else
+            break
+        end
+    end
+
+    return true
+end
+
+--解析接受到的json参数并发布到消息"AIR_SRV_FOTA"中
+--定义一个json格式如下,具体可以根据实际情况定义,使用iot升级的话只定义了一个参数即可:
+--{
+--    "fota": "true"
+--}
+
+local function data_processing(data)
+    -- 解析json数据
+    local json_data = json.decode(data)
+    -- 如果解析失败
+    if not json_data then
+        log.error("data_processing", "json.decode error")
+        return
+    end
+    -- 如果fota为true
+    if json_data.fota == "true" then
+        -- 发布消息"AIR_SRV_FOTA"
+        sys.publish("AIR_SRV_FOTA")
+    end
+end
+
+-- 订阅"RECV_IOT_DATA_SERVER"消息
+sys.subscribe("RECV_IOT_DATA_SERVER", data_processing)
+
+return tcp_client_receiver

+ 139 - 0
module/Air8000/demo/fota2/iot_server/tcp_iot/tcp_iot_sender.lua

@@ -0,0 +1,139 @@
+--[[
+@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"消息,将其他应用模块需要发送的数据存储到队列iot_send_queue中;
+2、tcp_client_main主任务调用tcp_client_sender.proc接口,遍历队列iot_send_queue,逐条发送数据到server;
+3、tcp client socket和server之间的连接如果出现异常,tcp_client_main主任务调用tcp_client_sender.exception_proc接口,丢弃掉队列iot_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 iot_send_queue = {}
+
+-- tcp_client_main的任务名
+tcp_client_sender.TASK_NAME = "iot_tcp_client_main"
+
+-- "SEND_DATA_REQ"消息的处理函数
+local function send_data_req_proc_func(data, cb)
+    -- 将原始数据增加前缀,然后插入到发送队列iot_send_queue中
+    table.insert(iot_send_queue, { data =  data, cb = cb })
+    -- 通知tcp_client_main主任务有数据需要发送
+    -- tcp_client_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
+    sysplus.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
+
+    -- 遍历数据发送队列iot_send_queue
+    while #iot_send_queue > 0 do
+        -- 取出来第一条数据赋值给send_item
+        -- 同时从队列iot_send_queue中删除这一条数据
+        send_item = table.remove(iot_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再次插入到iot_send_queue的队首位置,等待下次尝试发送
+        if buff_full then
+            log.error("tcp_client_sender.proc", "buffer is full, wait for the next time")
+            table.insert(iot_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()
+    -- 遍历数据发送队列iot_send_queue
+    while #iot_send_queue > 0 do
+        local send_item = table.remove(iot_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
+

+ 101 - 0
module/Air8000/demo/fota2/iot_server/update.lua

@@ -0,0 +1,101 @@
+--[[
+@module  update
+@summary 远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、判断网卡是否连接成功;
+2、初始化fota2模块;
+3、配置fota2模块的参数;
+4、调用fota2模块的升级函数;
+5、在升级结果的回调函数中,根据升级结果进行处理;
+]]
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+
+libfota2 = require "libfota2"
+
+
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("降功耗 找合宙")
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+sys.timerLoopStart(get_version, 3000)
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
+            "3) 已经是最新版本,无需升级" )
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local ota_opts = {}
+
+function fota_task_func()
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+
+    -- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
+    if "123" == _G.PRODUCT_KEY  then
+        while 1 do
+            sys.wait(1000)
+            log.info("fota", "请修改正确的PRODUCT_KEY")
+        end
+    end
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, ota_opts)
+end
+
+--创建并且启动一个task
+--运行这个task的主函数fota_task_func
+sys.taskInit(fota_task_func)
+-- 演示定时自动升级, 每隔4小时自动检查一次
+sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, ota_opts)
+
+

+ 0 - 101
module/Air8000/demo/fota2/main.lua

@@ -1,101 +0,0 @@
-
--- LuaTools需要PROJECT和VERSION这两个信息
-PROJECT = "fotademo"
--- iot限制,只能上传xxx.yyy.zzz格式的三位数的版本号,但实际上现在只用了XXX和ZZZ,中间yyy暂未使用
--- 需要注意的是,因为yyy不生效,所以111.222.333版本和111.444.333版本,对iot平台来说都一样,所以建议中间那一位永远写000
-VERSION = "001.000.000"
-
--- 使用合宙iot平台时需要这个参数
-PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
-
-sys = require "sys"
-libfota2 = require "libfota2"
-
--- 联网函数, 可自行删减
-sys.taskInit(function()
-    -- 默认都等到联网成功
-    sys.waitUntil("IP_READY")
-    log.info("4G网络链接成功")
-    sys.publish("net_ready")
-end)
-
--- 循环打印版本号, 方便看版本号变化, 非必须
-sys.taskInit(function()
-    while 1 do
-        sys.wait(5000)
-        log.info("降功耗 找合宙")
-        -- log.info("fota", "脚本版本号", VERSION)
-        log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
-    end
-end)
-
--- 升级结果的回调函数
--- 功能:获取fota的回调函数
--- 参数:
--- result:number类型
---   0表示成功
---   1表示连接失败
---   2表示url错误
---   3表示服务器断开
---   4表示接收报文错误
---   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
-local function fota_cb(ret)
-    log.info("fota", ret)
-    if ret == 0 then
-        log.info("升级包下载成功,重启模块")
-        rtos.reboot()
-    elseif ret == 1 then
-        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
-    elseif ret == 2 then
-        log.info("url错误", "检查url拼写")
-    elseif ret == 3 then
-        log.info("服务器断开", "检查服务器白名单配置")
-    elseif ret == 4 then
-        log.info("接收报文错误", "检查模块固件或升级包内文件是否正常")
-    elseif ret == 5 then
-        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
-    else
-        log.info("不是上面几种情况 ret为", ret)
-    end
-end
-
-local ota_opts = {}
-
--- 使用合宙iot平台进行升级,不需要管下面这段代码
--- 使用第三方服务器时打开下面这段代码
---[[local ota_opts = {
-    url = "", 
-    -- 合宙IOT平台的默认升级URL, 不填就是这个默认值
-    -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
-    -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ### 
-    -- 如果不加###,则默认会上传如下参数
-    -- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
-    -- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
-    -- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
-    -- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
-    -- 5. opts.firmware_name string 底层版本号
-    -- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
-    version = ""
-    -- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
-}]]--
-sys.taskInit(function()
-    -- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
-    if "123" == _G.PRODUCT_KEY and not ota_opts.url then
-        while 1 do
-            sys.wait(1000)
-            log.info("fota", "请修改正确的PRODUCT_KEY")
-        end
-    end
-    -- 等待网络就行后开始检查升级
-    sys.waitUntil("net_ready")
-    log.info("开始检查升级")
-    sys.wait(500)
-    libfota2.request(fota_cb, ota_opts)
-end)
--- 演示定时自动升级, 每隔4小时自动检查一次
-sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, ota_opts)
-
--- 用户代码已结束---------------------------------------------
--- 结尾总是这一句
-sys.run()
--- sys.run()之后后面不要加任何语句!!!!!

+ 141 - 0
module/Air8000/demo/fota2/self_server/README.md

@@ -0,0 +1,141 @@
+## 功能模块介绍
+
+### 自建服务器fota功能演示
+
+1、main.lua:主程序入口;
+
+2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的四种网卡(单4g网卡,单wifi网卡,单spi以太网卡,多网卡)中的任何一种网卡;
+
+3、update.lua:使用自建服务器进行远程升级功能模块,简单升级演示;
+
+4、tcp_self_server文件夹:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备启动customer_srv_fota功能模块,使用自建服务器进行升级;
+
+5、customer_srv_fota.lua:自建服务器升级功能模块;
+
+6、psm_power_fota.lua:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。需要注意的是此场景与上面两种场景不能同时使用;
+
+
+## 系统消息介绍
+
+1、"IP_READY":某种网卡已经获取到ip信息,仅仅获取到了ip信息,能否和外网连通还不确认;
+
+2、"IP_LOSE":某种网卡已经掉网;
+
+
+
+## 用户消息介绍
+
+1、"RECV_DATA_FROM_SERVER":socket client收到服务器下发的数据后,通过此消息发布出去,给其他应用模块处理;
+
+2、"SEND_DATA_REQ":其他应用模块发布此消息,通知socket client发送数据给服务器;
+
+
+
+## 演示功能概述
+
+1、combination文件夹下的demo会有三个演示场景,在main.lua中选择要使用的场景:
+
+    (1) 使用自建服务器升级,演示最简单的升级逻辑。
+
+    (2) 使用自建服务器升级,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。
+
+    (3) 休眠状态下升级,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。
+
+2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
+
+   (1) netdrv_4g:4G网卡
+
+   (2) netdrv_wifi:WIFI STA网卡
+
+   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
+
+   (4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
+
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/air8000/luatos/app/image/netdrv_multi.jpg)
+
+1、Air8000开发板一块+可上网的sim卡一张+4g天线一根+wifi天线一根+网线一根:
+
+- sim卡插入开发板的sim卡槽
+
+- 天线装到开发板上
+
+- 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口
+
+2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air8000开发板和数据线的硬件接线方式为:
+
+- Air8000开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air8000 V2012版本固件)](https://docs.openluat.com/air8000/luatos/firmware/)
+
+3、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/);
+
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/),点击 打开TCP 按钮,会创建一个TCP server,将server的地址和端口赋值给tcp_client_self_main.lua中的SERVER_ADDR和SERVER_PORT两个变量
+
+3、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
+
+4、Luatools烧录内核固件和修改后的demo脚本代码
+
+5、使用Luatools制作升级包,先把新旧版本分别生成量产文件,然后再制作升级包,工具上栏 luatOS->固件工具->差分包/整包升级包制作,将制作好的升级包配置到合宙iot服务器自己项目下,或上传到自建服务器上面;
+
+6、烧录成功后,自动开机运行
+
+7、[合宙TCP/UDP web测试工具](https://netlab.luatos.com/)上创建的两个TCP server可以看到有设备连接上来,然后可以下发下面字符串触发升级:
+
+
+``` lua
+
+--自建服务器下发这个指令,下发之前需要在服务器上面配置好升级包,然后吧url给到字符串
+--定义一个json格式如下,具体可以根据实际情况定义:
+-- {"fota": "true","url": "http://airtest.openluat.com:2900/download/FOTA2_DEMO_2012.001.001_LuatOS-SoC_Air8000.bin"}
+
+```
+
+8、可以看到升级过程如下,不管是什么场景下升级,基本都是如下情况:
+
+``` lua
+--没有升级之前可以看到如下打印
+I/user.fota 脚本版本号 001.000.000 core版本号 V2010
+
+I/user.fota_task_func recv IP_READY 1
+I/user.开始检查升级
+
+I/user.升级包下载成功,重启模块
+
+
+--升级之后可以看到如下打印
+I/user.fota 脚本版本号 001.000.001 core版本号 V2012
+--升级重启之后还是会检查升级,所以会有如下打印属于正常情况,其中"code": 27 是合宙iot服务器返回的状态码,意思是已经是最新版本了。自建服务器如果没做设置会再下载一遍升级包。
+
+I/user.fota -9 {"code": 27, "msg": "\u5df2\u662f\u6700\u65b0\u7248\u672c"}
+I/user.使用合宙服务器,接下来解析body里的code
+I/user.已是最新版本 1.设备的固件/脚本版本高于或等于云平台上的版本号 2.用户项目升级配置中未添加该设备 3.云平台升级配置中,是否升级配置为否
+I/user.fota 4
+
+
+```
+
+9、对于psm休眠状态下的升级的场景,打印的版本来判断是否是最新版本。模组升级成功后会自动进入psm休眠状态。可以通过电流查看休眠情况。

+ 138 - 0
module/Air8000/demo/fota2/self_server/customer_srv_fota.lua

@@ -0,0 +1,138 @@
+--[[
+@module  air_srv_fota
+@summary 使用自建服务器远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、接收 CUSTOMER_SRV_FOTA 系统消息,触发升级;
+2、判断网卡是否连接成功;
+3、初始化fota2模块;
+4、调用fota2模块的升级函数;
+5、根据升级结果进行处理;
+]]
+
+
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("降功耗 找合宙")
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+
+sys.timerLoopStart(get_version, 3000)
+
+-- fota升级标志:true 表示当前正有 FOTA 流程在跑
+local fota_running = false
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    -- fota结束,无论成功还是失败,都释放fota_running标志
+    fota_running = false
+
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
+            "3) 已经是最新版本,无需升级")
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+
+-- 使用第三方服务器,配置ota_opts参数
+--[[
+-- opts参数说明, 所有参数都是可选的
+-- 1. opts.url string 升级所需要的URL, 若使用合宙iot平台,则不需要填
+-- 2. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+-- 3. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+-- 4. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+-- 5. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+-- 6. opts.firmware_name string 固件名称,默认是 _G.PROJECT.. "_LuatOS-SoC_" .. rtos.bsp()
+-- 7. opts.server_cert string 服务器证书, 默认不使用
+-- 8. opts.client_cert string 客户端证书, 默认不使用
+-- 9. opts.client_key string 客户端私钥, 默认不使用
+-- 10. opts.client_password string 客户端私钥口令, 默认不使用
+-- 11. opts.method string 请求方法, 默认是GET
+-- 12. opts.headers table 额外添加的请求头,默认不需要
+-- 13. opts.body string 额外添加的请求body,默认不需要
+]]
+local opts = {
+    url = "",
+    -- 合宙IOT平台的默认升级URL, 不填就是这个默认值
+    -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
+    -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
+    -- 如果不加###,则默认会上传如下参数
+    -- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+    -- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+    -- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+    -- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+    -- 5. opts.firmware_name string 底层版本号
+
+    -- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
+    version = ""
+    -- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
+}
+
+
+local function air_fota_func(data)
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+    while true do
+        -- 阻塞等待外部事件:"CUSTOMER_SRV_FOTA"
+        local result, data = sys.waitUntil("CUSTOMER_SRV_FOTA")
+        if result then
+            log.info("接收到数据", "date",data.url )
+
+            if fota_running then
+                log.warn("fota_task", "FOTA 正在运行,跳过本次请求")
+            else
+                -- 标记FOTA正在运行
+                -- 注意:这里只是标记,实际的FOTA流程还没有开始
+                opts.url = "###" .. data.url
+                fota_running = true
+                log.info("开始检查升级")
+                libfota2.request(fota_cb, opts)
+            end
+        end
+    end
+end
+-- 初始化FOTA任务
+sys.taskInit(air_fota_func)

+ 108 - 0
module/Air8000/demo/fota2/self_server/main.lua

@@ -0,0 +1,108 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本demo演示的核心功能为:
+1、这个demo会有三个演示场景:
+    (1)使用自建服务器升级,演示最简单的升级逻辑。
+    (2)使用自建服务器升级,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。
+    (3)休眠状态下升级,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。
+2、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 = "FOTA2_DEMO"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+
+sys.timerLoopStart(get_version, 3000)
+
+
+-- 加载网络驱动设备功能模块
+require "netdrv_device"
+
+
+
+--两种tcp下发指令升级的场景和psm+低功耗模式升级不能同时使用,需要根据自己场景选择其中一种
+
+--两种tcp下发指令升级的场景可以启用一种,也可以启用两种,启用两种时,注意控制不要一个在fota的过程中,另外一个再fota。
+
+
+-- 加载远程升级功能模块,场景一
+-- require "update"
+---------------------------------------------------------------------------
+-- 加载tcp client self socket主应用功能模块,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。场景二
+require "tcp_self_main"
+-- 加载自建服务器远程升级功能模块
+require "customer_srv_fota"
+---------------------------------------------------------------------------
+-- 加载psm+低功耗模式升级功能模块,场景三
+-- require "psm_power_fota"
+
+
+
+
+
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

+ 33 - 0
module/Air8000/demo/fota2/self_server/netdrv/netdrv_4g.lua

@@ -0,0 +1,33 @@
+--[[
+@module  netdrv_4g
+@summary “4G网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为4G网卡驱动模块,核心业务逻辑为:
+1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
+end
+
+
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+-- 设置默认网卡为socket.LWIP_GP
+-- 在Air8000上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
+-- 在单4G网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
+socket.dft(socket.LWIP_GP)

+ 85 - 0
module/Air8000/demo/fota2/self_server/netdrv/netdrv_eth_spi.lua

@@ -0,0 +1,85 @@
+--[[
+@module  netdrv_eth_spi
+@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块 ,核心业务逻辑为:
+1、打开CH390H芯片供电开关;
+2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
+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测试使用的是Air8000开发板
+--GPIO140为CH390H以太网芯片的供电使能控制引脚
+gpio.setup(140, 1, gpio.PULLUP)
+
+--这个task的核心业务逻辑是:初始化SPI,初始化以太网卡,并在以太网卡上开启动态主机配置协议
+local function netdrv_eth_spi_task_func()
+    -- 初始化SPI1
+    local result = spi.setup(
+        1,--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 1, 片选 GPIO12
+    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spi=1, cs=12})
+
+    --在以太上开启动态主机配置协议
+    netdrv.dhcp(socket.LWIP_ETH, true)
+end
+
+--创建并且启动一个task
+--task的处理函数为netdrv_eth_spi_task_func
+sys.taskInit(netdrv_eth_spi_task_func)

+ 95 - 0
module/Air8000/demo/fota2/self_server/netdrv/netdrv_multiple.lua

@@ -0,0 +1,95 @@
+--[[
+@module  netdrv_multiple
+@summary 多网卡(4G网卡、WIFI STA网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为多网卡驱动模块 ,核心业务逻辑为:
+1、调用libnetif.set_priority_order配置多网卡的控制参数以及优先级;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_multiple"就可以加载运行;
+]]
+
+
+local exnetif = require "exnetif"
+
+-- 网卡状态变化通知回调函数
+-- 当libnetif中检测到网卡切换或者所有网卡都断网时,会触发调用此回调函数
+-- 当网卡切换切换时:
+--     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芯片”的以太网卡,使用Air8000开发板验证
+            {
+                ETHERNET = {
+                    -- 供电使能GPIO
+                    pwrpin = 140,
+                    -- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
+                    -- 如果没有传入此参数,libnetif会使用默认值10秒
+                    ping_time = 3000,
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+
+                    -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
+                    tp = netdrv.CH390,
+                    opts = {spi=1, cs=12}
+                }
+            },
+
+            -- WIFI STA网卡
+            {
+                WIFI = {
+                    -- 要连接的WIFI路由器名称
+                    ssid = "茶室-降功耗,找合宙!",
+                    -- 要连接的WIFI路由器密码
+                    password = "Air123456",
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+                }
+            },
+
+            -- 4G网卡
+            {
+                LWIP_GP = true
+            }
+        }
+    )
+end
+
+-- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc
+exnetif.notify_status(netdrv_multiple_notify_cbfunc)
+
+-- 如果存在udp网络应用,并且udp网络应用中,根据应用层的心跳能够判断出来udp数据通信出现了异常;
+-- 可以在判断出现异常的位置,调用一次libnetif.check_network_status()接口,强制对当前正式使用的网卡进行一次连通性检测;
+-- 如果存在tcp网络应用,不需要用户调用libnetif.check_network_status()接口去控制,libnetif会在tcp网络应用通信异常时自动对当前使用的网卡进行连通性检测。
+
+
+-- 启动一个task,task的处理函数为netdrv_multiple_task_func
+-- 在处理函数中调用libnetif.set_priority_order设置网卡优先级
+-- 因为libnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
+sys.taskInit(netdrv_multiple_task_func)

+ 50 - 0
module/Air8000/demo/fota2/self_server/netdrv/netdrv_wifi.lua

@@ -0,0 +1,50 @@
+--[[
+@module  netdrv_wifi
+@summary “WIFI STA网卡”驱动模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为WIFI STA网卡驱动模块,核心业务逻辑为:
+1、初始化WIFI网络;
+2、连接WIFI路由器;
+3、和WIFI路由器之间的连接状态发生变化时,在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_wifi"就可以加载运行;
+]]
+
+local function ip_ready_func()
+    log.info("netdrv_wifi.ip_ready_func", "IP_READY", json.encode(wlan.getInfo()))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_wifi.ip_lose_func", "IP_LOSE")
+end
+
+
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察WIFI的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 设置默认网卡为socket.LWIP_STA
+socket.dft(socket.LWIP_STA)
+
+
+wlan.init()
+--连接WIFI热点,连接结果会通过"IP_READY"或者"IP_LOSE"消息通知
+--Air8000仅支持2.4G的WIFI,不支持5G的WIFI
+--此处前两个参数表示WIFI热点名称以及密码,更换为自己测试时的真实参数即可
+--第三个参数1表示WIFI连接异常时,内核固件会自动重连
+wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1)
+
+--WIFI联网成功(做为STATION成功连接AP,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
+--各个功能模块可以订阅"IP_READY"消息实时处理WIFI联网成功的事件
+--也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功
+
+--WIFI断网后,内核固件会产生一个"IP_LOSE"消息
+--各个功能模块可以订阅"IP_LOSE"消息实时处理WIFI断网的事件
+--也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功

+ 33 - 0
module/Air8000/demo/fota2/self_server/netdrv_device.lua

@@ -0,0 +1,33 @@
+--[[
+@module  netdrv_device
+@summary 网络驱动设备功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
+1、netdrv_4g:socket.LWIP_GP,4G网卡;
+2、netdrv_wifi:socket.LWIP_STA,WIFI STA网卡;
+3、netdrv_ethernet_spi:socket.LWIP_USER1,通过SPI外挂CH390H芯片的以太网卡;
+4、netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
+
+根据自己的项目需求,只需要require以上四种中的一种即可;
+
+
+本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
+]]
+
+
+-- 根据自己的项目需求,只需要require以下四种中的一种即可;
+
+-- 加载“4G网卡”驱动模块
+require "netdrv_4g"
+
+-- 加载“WIFI STA网卡”驱动模块
+-- require "netdrv_wifi"
+
+-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
+-- require "netdrv_eth_spi"
+
+-- 加载“可以配置优先级的多种网卡”驱动模块
+-- require "netdrv_multiple"

+ 131 - 0
module/Air8000/demo/fota2/self_server/psm_power_fota.lua

@@ -0,0 +1,131 @@
+--[[
+@module  psm+_power_fota
+@summary psm+超低功耗模式下升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为psm+超低功耗模式下升级功能模块,核心设计思路
+1.升级触发机制 :
+   - 定时器唤醒升级 :设备定期从PSM模式唤醒,主动检查是否有新固件版本。
+   - 外部中断唤醒升级 :通过特定GPIO中断或网络消息唤醒设备进行升级。
+2.防止升级过程中进入休眠 :
+   - 在开始FOTA升级前,禁用PSM模式进入。
+   - 升级完成后,根据结果决定是否重启设备或重新进入PSM模式。
+
+]]
+
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    --升级结束,触发升级回调,发布消息升级结束,可以进入休眠模式
+    sys.publish("FOTA_END")
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
+            "3) 已经是最新版本,无需升级" )
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local opts = {
+    url = "###http://airtest.openluat.com:2900/download/FOTA2_DEMO_2012.001.001_LuatOS-SoC_Air8000.bin",
+    -- 合宙IOT平台的默认升级URL, 不填就是这个默认值
+    -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
+    -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
+    -- 如果不加###,则默认会上传如下参数
+    -- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+    -- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+    -- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+    -- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+    -- 5. opts.firmware_name string 底层版本号
+
+    -- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
+    -- version = ""
+    -- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
+}
+
+
+function psm_fota_task_func()
+    -- 如果是被定时器唤醒,因为上次进入PSM+时是开启了飞行模式,所以在唤醒后第一时间关闭飞行模式。
+    mobile.flymode(0, false)
+    log.info("开始测试PSM+模式功耗。")
+
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, opts)
+
+    -- 打印版本号, 方便看版本号变化, 非必须
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+
+
+    -- 等待下载升级包结束, 发布消息"FOTA_END",
+    -- 如果15秒内没有收到消息,则15秒的时长到达后进入PSM+模式。
+    -- 需要注意的是在fota_cb回调函数中,升级包下载成功后,会立马重启并升级模组。如果还有其他事情要做不想立马重启升级,需自行决定reboot的时机
+    -- 升级包下载成功后,本demo默认是立即自动重启并且将升级包更新到模组中,更新成功后,会再次走到这里
+    -- 再次走到这里后,合宙iot平台会返回“已经是最新版本,不需要升级”,fota_cb回调函数中会发布消息"FOTA_END"
+    -- 至此,才会继续向下执行代码,进入PSM+模式
+    sys.waitUntil("FOTA_END", 15000)
+
+    log.info("升级结束,进入PSM模式")
+
+    -- 关闭gps备电以及gsensor供电使能,防止休眠模式下漏电导致功耗增加
+    gpio.close(24)
+
+    -- 定时检查升级 (每4小时唤醒一次)
+    pm.dtimerStart(2, 4 * 3600000)
+    -- 启动飞行模式,规避可能会出现的网络问题
+    mobile.flymode(0, true)
+    -- 进入PSM模式
+    pm.power(pm.WORK_MODE, 3)
+    -- 防御机制:15秒后如果未进入PSM则重启
+    sys.wait(15000)
+    log.info("进入PSM+失败,重启")
+    rtos.reboot()
+end
+
+sys.taskInit(psm_fota_task_func)

+ 140 - 0
module/Air8000/demo/fota2/self_server/tcp_self_server/tcp_self_main.lua

@@ -0,0 +1,140 @@
+--[[
+@module  tcp_client_main
+@summary tcp client socket主应用功能模块
+@version 1.0
+@date    2025.08.12
+@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_self_receiver"
+-- 加载tcp client socket数据发送功能模块
+local tcp_client_sender = require "tcp_self_sender"
+
+-- 电脑访问:https://netlab.luatos.com/
+-- 点击 打开TCP 按钮,会创建一个TCP server
+-- 将server的地址和端口赋值给下面这两个变量
+local SERVER_ADDR = "112.125.89.8"
+local SERVER_PORT = 44893
+
+-- 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毫秒超时不要修改的更长;
+            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+            -- 当libnetif.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)
+        -- 如果配置失败
+        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")
+
+        -- 数据收发以及网络连接异常事件总处理逻辑
+        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
+sysplus.taskInitEx(tcp_client_main_task_func, TASK_NAME, tcp_client_main_cbfunc)

+ 128 - 0
module/Air8000/demo/fota2/self_server/tcp_self_server/tcp_self_receiver.lua

@@ -0,0 +1,128 @@
+--[[
+@module  tcp_client_receiver
+@summary tcp client socket数据接收应用功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为tcp client socket数据接收应用功能模块,核心业务逻辑为:
+从内核读取接收到的数据,然后将数据发送给其他应用功能模块做进一步处理;
+
+本文件的对外接口有2个:
+1、tcp_client_receiver.proc(socket_client):数据接收应用逻辑处理入口,在tcp_client_main.lua中调用;
+2、sys.publish("RECV_DATA_FROM_SERVER",  data):数据接收应用逻辑处理入口,在tcp_client_main
+   将接收到的数据通过消息"RECV_DATA_FROM_SERVER"发布出去处理;
+   处理后通过消息"CUSTOMER_SRV_FOTA"发布到customer_srv_fota.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_SELF_DATA_SERVER"消息publish出去,给其他应用模块处理
+            sys.publish("RECV_SELF_DATA_SERVER",  data)
+
+            -- 清空socket数据接收缓冲区中的数据
+            recv_buff:del()
+            -- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
+        else
+            break
+        end
+    end
+
+    return true
+end
+--解析接受到的json参数并发布到消息"AIR_SRV_FOTA"中
+--定义一个json格式如下,具体可以根据实际情况定义:
+--{
+--    "fota": "true",
+--    "url": "http://192.168.1.100:8080/firmware.bin"
+--}
+local json_data
+local function data_processing(data)
+    -- 解析json数据
+    json_data = json.decode(data)
+
+    -- 如果解析失败
+    if not json_data then
+        log.error("data_processing", "json.decode error")
+        return
+    end
+    -- 解析fota参数
+    local fota = json_data.fota
+    -- 如果fota参数不存在
+    if not fota then
+        log.error("data_processing", "fota error")
+        return
+    end
+    -- 解析url参数
+    local url = json_data.url
+    -- 如果url参数不存在
+    if not url then
+        log.error("data_processing", "url error")
+        return
+    end
+    -- 如果fota为true
+    if fota == "true" then
+        -- -- 发布消息"CUSTOMER_SRV_FOTA"
+        log.info("发布数据", "fota", fota, "url", url)
+
+        sys.publish("CUSTOMER_SRV_FOTA",json_data)
+    end
+end
+
+-- 订阅"RECV_SELF_DATA_SERVER"消息
+sys.subscribe("RECV_SELF_DATA_SERVER", data_processing)
+
+return tcp_client_receiver

+ 137 - 0
module/Air8000/demo/fota2/self_server/tcp_self_server/tcp_self_sender.lua

@@ -0,0 +1,137 @@
+--[[
+@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"消息,将其他应用模块需要发送的数据存储到队列self_send_queue中;
+2、tcp_client_main主任务调用tcp_client_sender.proc接口,遍历队列self_send_queue,逐条发送数据到server;
+3、tcp client socket和server之间的连接如果出现异常,tcp_client_main主任务调用tcp_client_sender.exception_proc接口,丢弃掉队列self_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 self_send_queue = {}
+
+-- tcp_client_main的任务名
+tcp_client_sender.TASK_NAME = "self_tcp_client_main"
+
+-- "SEND_DATA_REQ"消息的处理函数
+local function send_data_req_proc_func(data, cb)
+    -- 将原始数据增加前缀,然后插入到发送队列self_send_queue中
+    table.insert(self_send_queue, {data=data, cb=cb})
+    -- 通知tcp_client_main主任务有数据需要发送
+    -- tcp_client_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
+    sysplus.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
+    -- 遍历数据发送队列self_send_queue
+    while #self_send_queue>0 do
+        -- 取出来第一条数据赋值给send_item
+        -- 同时从队列self_send_queue中删除这一条数据
+        send_item = table.remove(self_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再次插入到self_send_queue的队首位置,等待下次尝试发送
+        if buff_full then
+            log.error("tcp_client_sender.proc", "buffer is full, wait for the next time")
+            table.insert(self_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()
+    -- 遍历数据发送队列self_send_queue
+    while #self_send_queue>0 do
+        local send_item = table.remove(self_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("SELF_SEND_DATA_REQ", send_data_req_proc_func)
+
+return tcp_client_sender

+ 128 - 0
module/Air8000/demo/fota2/self_server/update.lua

@@ -0,0 +1,128 @@
+--[[
+@module  update
+@summary 远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、判断网卡是否连接成功;
+2、初始化fota2模块;
+3、配置fota2模块的参数;
+4、调用fota2模块的升级函数;
+5、在升级结果的回调函数中,根据升级结果进行处理;
+]]
+
+libfota2 = require "libfota2"
+
+
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+sys.timerLoopStart(get_version, 3000)
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
+            "3) 已经是最新版本,无需升级" )
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+-- 使用第三方服务器,配置ota_opts参数
+--[[
+-- opts参数说明, 所有参数都是可选的
+-- 1. opts.url string 升级所需要的URL, 若使用合宙iot平台,则不需要填
+-- 2. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+-- 3. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+-- 4. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+-- 5. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+-- 6. opts.firmware_name string 固件名称,默认是 _G.PROJECT.. "_LuatOS-SoC_" .. rtos.bsp()
+-- 7. opts.server_cert string 服务器证书, 默认不使用
+-- 8. opts.client_cert string 客户端证书, 默认不使用
+-- 9. opts.client_key string 客户端私钥, 默认不使用
+-- 10. opts.client_password string 客户端私钥口令, 默认不使用
+-- 11. opts.method string 请求方法, 默认是GET
+-- 12. opts.headers table 额外添加的请求头,默认不需要
+-- 13. opts.body string 额外添加的请求body,默认不需要
+]]
+local opts = {
+    url = "###http://airtest.openluat.com:2900/download/FOTA2_DEMO_2012.001.001_LuatOS-SoC_Air8000.bin",
+    -- 合宙IOT平台的默认升级URL, 不填就是这个默认值
+    -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
+    -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
+    -- 如果不加###,则默认会上传如下参数
+    -- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+    -- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+    -- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+    -- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+    -- 5. opts.firmware_name string 底层版本号
+
+    -- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
+    -- version = ""
+    -- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
+}
+
+function fota_task_func()
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+    ----这个判断是提醒要设置url的,且不要使用本文中的测试服务器,实际生产请删除
+    -- if not opts.url or string.find(opts.url,"airtest.openluat.com") then
+    --     while 1 do
+    --         sys.wait(1000)
+    --         log.info("fota", "当前URL",opts.url,"请修改正确的url")
+    --     end
+    -- end
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, opts)
+end
+
+--创建并且启动一个task
+--运行这个task的主函数fota_task_func
+sys.taskInit(fota_task_func)
+-- 演示定时自动升级, 每隔4小时自动检查一次
+sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, opts)
+
+

+ 182 - 0
module/Air8101/demo/fota2/iot_server/README.md

@@ -0,0 +1,182 @@
+## 功能模块介绍
+
+### iot服务器fota功能演示
+
+1、main.lua:主程序入口;
+
+2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的四种网卡(单4g网卡,单wifi网卡,单spi以太网卡,多网卡)中的任何一种网卡;
+
+3、update.lua:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
+
+4、tcp_iot文件夹:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备启动air_srv_fota功能模块,使用合宙iot服务器进行升级;
+
+5、air_srv_fota.lua:合宙服务器升级功能模块;
+
+6、psm_power_fota.lua:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。需要注意的是此场景与上面两种场景不能同时使用;
+
+
+## 系统消息介绍
+
+1、"IP_READY":某种网卡已经获取到ip信息,仅仅获取到了ip信息,能否和外网连通还不确认;
+
+2、"IP_LOSE":某种网卡已经掉网;
+
+
+
+## 用户消息介绍
+
+1、"RECV_DATA_FROM_SERVER":socket client收到服务器下发的数据后,通过此消息发布出去,给其他应用模块处理;
+
+2、"SEND_DATA_REQ":其他应用模块发布此消息,通知socket client发送数据给服务器;
+
+
+
+## 演示功能概述
+
+1、此demo演示了三种场景:
+
+   (1) fota升级简单演示:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
+
+   (2) tcp服务器下发升级指令:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块;
+
+   (3) psm低功耗fota:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子;
+
+2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
+
+   (1) netdrv_4g:4G网卡
+
+   (2) netdrv_wifi:WIFI STA网卡
+
+   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
+
+   (4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
+
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/air8101/luatos/app/image/netdrv_multi.jpg)
+
+1、Air8101核心板一块
+
+2、TYPE-C USB数据线一根
+
+3、USB转串口数据线一根
+
+4、Air8101核心板和数据线的硬件接线方式为
+
+- Air8101核心板通过TYPE-C USB口供电;(核心板背面的功耗测试开关拨到OFF一端)
+
+- 如果测试发现软件频繁重启,重启原因值为:poweron reason 0,可能是供电不足,此时再通过直流稳压电源对核心板的vbat管脚进行4V供电,或者VIN管脚进行5V供电;
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+- USB转串口数据线,一般来说,白线连接核心板的12/U1TX,绿线连接核心板的11/U1RX,黑线连接核心板的gnd,另外一端连接电脑USB口;
+
+5、可选AirPHY_1000配件板一块,Air8101核心板和AirPHY_1000配件板的硬件接线方式为:
+
+| Air8101核心板 | AirPHY_1000配件板 |
+| ------------- | ----------------- |
+| 59/3V3        | 3.3v              |
+| gnd           | gnd               |
+| 5/D2          | RX1               |
+| 72/D1         | RX0               |
+| 71/D3         | CRS               |
+| 4/D0          | MDIO              |
+| 6/D4          | TX0               |
+| 74/PCK        | MDC               |
+| 70/D5         | TX1               |
+| 7/D6          | TXEN              |
+| 不接          | NC                |
+| 69/D7         | CLK               |
+
+6、可选AirETH_1000配件板一块,Air8101核心板和AirETH_1000配件板的硬件接线方式为:
+
+| Air8101核心板 | AirETH_1000配件板 |
+| ------------- | ----------------- |
+| 59/3V3        | 3.3v              |
+| gnd           | gnd               |
+| 28/DCLK       | SCK               |
+| 54/DISP       | CSS               |
+| 55/HSYN       | SDO               |
+| 57/DE         | SDI               |
+| 14/GPIO8      | INT               |
+
+7、可选Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板或者开发板一块,Air8101核心板和Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板或者开发板的硬件接线方式为:
+
+| Air8101核心板 | Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板 |
+| ------------- | --------------------------------------------- |
+| gnd           | GND                                           |
+| 54/DISP       | 83/SPI0CS                                     |
+| 55/HSYN       | 84/SPI0MISO                                   |
+| 57/DE         | 85/SPI0MOSI                                   |
+| 28/DCLK       | 86/SPI0CLK                                    |
+| 43/R2         | 19/GPIO22                                     |
+| 75/GPIO28     | 22/GPIO1                                      |
+
+
+| Air8101核心板 | Air780EHM/Air780EHV/Air780EGH/Air780EPM开发板 |
+| ------------- | --------------------------------------------- |
+| gnd           | GND                                           |
+| 54/DISP       | SPI_CS                                        |
+| 55/HSYN       | SPI_MISO                                      |
+| 57/DE         | SPI_MOSI                                      |
+| 28/DCLK       | SPI_CLK                                       |
+| 43/R2         | GPIO22                                        |
+| 75/GPIO28     | GPIO1                                         |
+
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air8101 V1005版本固件](https://docs.openluat.com/air8101/luatos/firmware/)(理论上,2025年7月26日之后发布的固件都可以)
+
+3、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/);
+
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/),点击 打开TCP 按钮,会创建一个TCP server,将server的地址和端口赋值给tcp_iot_main.lua中的SERVER_ADDR和SERVER_PORT两个变量
+
+4、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
+
+5、Luatools烧录内核固件和修改后的demo脚本代码
+
+6、使用Luatools制作升级包,先把新旧版本分别生成量产文件,然后再制作升级包,工具上栏 luatOS->固件工具->差分包/整包升级包制作,将制作好的升级包配置到合宙iot服务器自己项目下,或上传到自建服务器上面;
+
+7、烧录成功后,自动开机运行
+
+8、可以看到升级过程如下,不管是什么场景下升级,基本都是如下日志情况:
+
+``` lua
+--没有升级之前可以看到如下打印
+I/user.fota 脚本版本号 001.000.000 core版本号 V2010
+
+I/user.fota_task_func recv IP_READY 1
+I/user.开始检查升级
+
+I/user.升级包下载成功,重启模块
+
+
+--升级之后可以看到如下打印
+I/user.fota 脚本版本号 001.000.001 core版本号 V2012
+--升级重启之后还是会检查升级,所以会有如下打印属于正常情况,其中"code": 27 是合宙iot服务器返回的状态码,意思是已经是最新版本了。
+I/user.fota -9 {"code": 27, "msg": "\u5df2\u662f\u6700\u65b0\u7248\u672c"}
+I/user.使用合宙服务器,接下来解析body里的code
+I/user.已是最新版本 1.设备的固件/脚本版本高于或等于云平台上的版本号 2.用户项目升级配置中未添加该设备 3.云平台升级配置中,是否升级配置为否
+I/user.fota 4
+
+
+```
+9、对于psm休眠状态下的升级的场景,可以通过iot平台查看是否成功,在iot平台的升级日志页面搜索模组的imei,可以看到有两条升级结果“成功”和“已是最新版本”。模组升级成功后会自动进入psm休眠状态。可以通过电流查看休眠情况。

+ 119 - 0
module/Air8101/demo/fota2/iot_server/air_srv_fota.lua

@@ -0,0 +1,119 @@
+--[[
+@module  air_srv_fota
+@summary 使用合宙iot平台远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、接收 AIR_SRV_FOTA 系统消息,触发升级;
+2、判断网卡是否连接成功;
+3、初始化fota2模块;
+4、调用fota2模块的升级函数;
+5、根据升级结果进行处理;
+]]
+
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("降功耗 找合宙")
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+
+sys.timerLoopStart(get_version, 3000)
+
+
+-- fota升级标志:true 表示当前正有 FOTA 流程在跑
+local fota_running = false
+
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    -- fota结束,无论成功还是失败,都释放fota_running标志
+    fota_running = false
+
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
+            "3) 已经是最新版本,无需升级")
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local opts = {}
+
+local function air_fota_func(data)
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+    while true do
+        -- 阻塞等待外部事件:"AIR_SRV_FOTA"
+        sys.waitUntil("AIR_SRV_FOTA")
+
+        -- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
+        if "123" == _G.PRODUCT_KEY then
+            while 1 do
+                sys.wait(1000)
+                log.info("fota", "请修改正确的PRODUCT_KEY")
+            end
+        end
+
+        if fota_running then
+            log.warn("fota_task", "FOTA 正在运行,跳过本次请求")
+        else
+            -- 标记FOTA正在运行
+            -- 注意:这里只是标记,实际的FOTA流程还没有开始
+            fota_running = true
+            log.info("开始检查升级")
+            libfota2.request(fota_cb, opts)
+        end
+    end
+end
+-- 初始化FOTA任务
+sys.taskInit(air_fota_func)
+
+
+-- 演示定时自动升级, 每隔4小时自动检查一次,可以根据需求打开
+-- sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, opts)

+ 91 - 0
module/Air8101/demo/fota2/iot_server/main.lua

@@ -0,0 +1,91 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本demo演示的核心功能为:
+1、此demo演示了三种场景:
+   (1)fota升级简单演示:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
+   (2)tcp服务器下发升级指令:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块;
+   (3)psm低功耗fota:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子;
+2、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 = "FOTA2_DEMO"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+
+-- 加载网络驱动设备功能模块
+require "netdrv_device"
+
+-- 加载远程升级功能模块,场景1
+require "update"
+---------------------------------------------------------------------------
+-- 加载tcp client iot socket主应用功能模块,通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块,场景二
+-- require "tcp_iot_main"
+-- 加载合宙iot平台远程升级功能模块
+-- require "air_srv_fota"
+---------------------------------------------------------------------------
+-- 加载psm+低功耗模式升级功能模块,场景三
+-- require "psm_power_fota"
+
+
+
+
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

+ 118 - 0
module/Air8101/demo/fota2/iot_server/netdrv/netdrv_4g.lua

@@ -0,0 +1,118 @@
+--[[
+@module  netdrv_4g
+@summary “通过SPI接口外挂4G模组(Air780EHM/Air780EHV/Air780EGH/Air780EPM)的4G网卡”驱动模块
+@version 1.0
+@date    2025.07.27
+@author  朱天华
+@usage
+本文件为 “通过SPI接口外挂4G模组(Air780EHM/Air780EHV/Air780EGH/Air780EPM)的4G网卡”驱动模块,核心业务逻辑为:
+1、初始化和外部4G网卡的配置(初始化AirLINK、配置桥接网络、配置SPI、静态配置IP地址/子网掩码/网关);
+2、4G网卡的连接状态发生变化时,在日志中进行打印;
+
+
+硬件环境使用以下两种环境中的一种即可:
+1、Air8101核心板+Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板
+2、Air8101核心板+Air780EHM/Air780EHV/Air780EGH/Air780EPM开发板
+
+一、当使用第1种硬件环境时,Air8101核心板和Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板的硬件接线方式为:
+
+1、Air8101核心板:
+- 核心板通过TYPE-C USB口供电(核心板背面的功耗测试开关拨到OFF一端);
+- 如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的VIN管脚进行5V供电;
+
+2、Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板:
+- 核心板通过TYPE-C USB口供电(TYPE-C USB口旁边的ON/OFF拨动开关拨到ON一端);
+- 如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的5V管脚进行5V供电;
+
+3、
+| Air8101核心板 |  Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板  |
+| ------------ | ---------------------------------------------- |
+|     gnd      |                     GND                        |
+|  54/DISP     |                     83/SPI0CS                  |
+|  55/HSYN     |                     84/SPI0MISO                |
+|    57/DE     |                     85/SPI0MOSI                |
+|  28/DCLK     |                     86/SPI0CLK                 |
+|    43/R2     |                     19/GPIO22                  |
+|  75/GPIO28   |                     22/GPIO1                   |
+
+二、当使用第2种硬件环境时,Air8101核心板和Air780EHM/Air780EHV/Air780EGH/Air780EPM开发板的硬件接线方式为:
+
+1、Air8101核心板:
+- 核心板通过TYPE-C USB口供电(核心板背面的功耗测试开关拨到OFF一端);
+- 如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的VIN管脚进行5V供电;
+
+2、Air780EHM/Air780EHV/Air780EGH/Air780EPM开发板:
+- 核心板通过TYPE-C USB口供电(外部供电/USB供电拨动开关拨到USB供电一端);
+- 如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对开发板的5V管脚进行5V供电;
+
+3、
+| Air8101核心板 | Air780EHM/Air780EHV/Air780EGH/Air780EPM开发板  |
+| ------------ | ---------------------------------------------- |
+|     gnd      |                     GND                        |
+|  54/DISP     |                     SPI_CS                     |
+|  55/HSYN     |                     SPI_MISO                   |
+|    57/DE     |                     SPI_MOSI                   |
+|  28/DCLK     |                     SPI_CLK                    |
+|    43/R2     |                     GPIO22                     |
+|  75/GPIO28   |                     GPIO1                      |
+
+三、以上两种硬件环境,Air8101使用的SPI0默认的一组引脚,也可以使用SPI1;使用SPI1时,硬件连接说明的更多资料参考:
+https://docs.openluat.com/air8101/luatos/hardware/design/4gnet/
+软件代码需要做以下配置:
+airlink.config(airlink.CONF_SPI_ID, 1)
+airlink.config(airlink.CONF_SPI_CS, 10)
+
+四、测试本功能模块时,Air780EHM/Air780EHV/Air780EGH/Air780EPM需要烧录以下软件:
+1、最新版本的内核固件
+2、脚本:https://gitee.com/openLuat/LuatOS/tree/master/module/Air8101/demo/multi_network/WIFI_4G_ETH/Air8101_Air780EPM/Air780EPM_master
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
+]]
+
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_USER0 then
+        log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_USER0))
+    end
+end
+
+local function ip_lose_func(adapter)
+    if adapter == socket.LWIP_USER0 then
+        log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
+    end
+end
+
+
+-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
+-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI接口外挂4G模组(Air780EHM/Air780EHV/Air780EGH/Air780EPM)的4G网卡”的连接状态
+-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 设置默认网卡为socket.LWIP_USER0
+socket.dft(socket.LWIP_USER0)
+
+
+-- 初始化airlink,Air8101和4G网卡之间,在spi之上,基于airlink协议通信
+airlink.init()
+-- 创建桥接网络设备
+-- 此处第一个参数必须是socket.LWIP_USER0,是因为Air780EHM/Air780EHV/Air780EGH/Air780EPM使用的也是socket.LWIP_USER0,双方是点对点通讯的对等网络
+-- 此处第二个参数必须是netdrv.WHALE,表示虚拟网卡的实现方式
+netdrv.setup(socket.LWIP_USER0, netdrv.WHALE)
+-- 启动airlink,配置Air8101作为SPI从机模式。
+airlink.start(airlink.MODE_SPI_SLAVE)
+
+-- 静态配置IPv4地址
+-- 本地ip地址为"192.168.111.1",网关ip地址为"192.168.111.2",子网掩码为"255.255.255.0"
+-- 此处设置的本地ip地址要和Air780EHM/Air780EHV/Air780EGH/Air780EPM中设置的网关ip地址完全一样
+-- 此处设置的网关ip地址要和Air780EHM/Air780EHV/Air780EGH/Air780EPM中设置的本地ip地址完全一样
+-- 此处设置的子网掩码要和Air780EHM/Air780EHV/Air780EGH/Air780EPM中设置的子网掩码完全一样
+netdrv.ipv4(socket.LWIP_USER0, "192.168.111.1", "255.255.255.0", "192.168.111.2")
+
+-- 4G联网成功后,内核固件会产生一个"IP_READY"消息
+-- 各个功能模块可以订阅"IP_READY"消息实时处理4G联网成功的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_USER0)来获取4G网络是否连接成功
+
+-- 4G断网后,内核固件会产生一个"IP_LOSE"消息
+-- 各个功能模块可以订阅"IP_LOSE"消息实时处理4G网络断网的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_USER0)来获取4G网络是否连接成功

+ 77 - 0
module/Air8101/demo/fota2/iot_server/netdrv/netdrv_eth_rmii.lua

@@ -0,0 +1,77 @@
+--[[
+@module  netdrv_eth_rmii
+@summary “通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡”驱动模块 
+@version 1.0
+@date    2025.07.24
+@author  朱天华
+@usage
+本文件为“通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡”驱动模块 ,核心业务逻辑为:
+1、打开PHY芯片供电开关;
+2、初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+Air8101核心板和AirPHY_1000配件板的硬件接线方式为:
+Air8101核心板通过TYPE-C USB口供电(核心板背面的功耗测试开关拨到OFF一端);
+如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的VIN管脚进行5V供电;
+| Air8101核心板 |  AirPHY_1000配件板  |
+| ------------ | ------------------ |
+|    59/3V3    |         3.3v       |
+|     gnd      |         gnd        |
+|     5/D2     |         RX1        |
+|    72/D1     |         RX0        |
+|    71/D3     |         CRS        |
+|     4/D0     |         MDIO       |
+|     6/D4     |         TX0        |
+|    74/PCK    |         MDC        |
+|    70/D5     |         TX1        |
+|     7/D6     |         TXEN       |
+|     不接     |          NC        |
+|    69/D7     |         CLK        |
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_rmii"就可以加载运行;
+]]
+
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_ETH then
+        log.info("netdrv_eth_rmii.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_rmii.ip_lose_func", "IP_LOSE")
+    end
+end
+
+
+-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
+-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡”的连接状态
+-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 设置默认网卡为socket.LWIP_ETH
+socket.dft(socket.LWIP_ETH)
+
+
+-- 本demo测试使用的是核心板的VDD 3V3引脚对AirPHY_1000配件板进行供电
+-- VDD 3V3引脚是Air8101内部的LDO输出引脚,最大输出电流300mA
+-- GPIO13在Air8101内部使能控制这个LDO的输出
+-- 所以在此处GPIO13输出高电平打开这个LDO
+gpio.setup(13, 1, gpio.PULLUP)
+
+
+--初始化以太网卡
+
+--以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
+--各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
+--也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+
+--以太网断网后,内核固件会产生一个"IP_LOSE"消息
+--各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
+--也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+netdrv.setup(socket.LWIP_ETH)
+
+--在以太网卡上开启动态主机配置协议
+netdrv.dhcp(socket.LWIP_ETH, true)

+ 102 - 0
module/Air8101/demo/fota2/iot_server/netdrv/netdrv_eth_spi.lua

@@ -0,0 +1,102 @@
+--[[
+@module  netdrv_eth_spi
+@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
+@version 1.0
+@date    2025.07.24
+@author  朱天华
+@usage
+本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块 ,核心业务逻辑为:
+1、打开AirETH_1000配件板供电开关;
+2、初始化spi0,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+Air8101核心板和AirETH_1000配件板的硬件接线方式为:
+Air8101核心板通过TYPE-C USB口供电(核心板背面的功耗测试开关拨到OFF一端);
+如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的VIN管脚进行5V供电;
+| Air8101核心板    | AirETH_1000配件板  |
+| --------------- | ----------------- |
+| 59/3V3          | 3.3v              |
+| gnd             | gnd               |
+| 28/DCLK         | SCK               |
+| 54/DISP         | CSS               |
+| 55/HSYN         | SDO               |
+| 57/DE           | SDI               |
+| 14/GPIO8        | INT               |
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
+]]
+
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_USER1 then
+        log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_USER1))
+    end
+end
+
+local function ip_lose_func(adapter)
+    if adapter == socket.LWIP_USER1 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_USER1
+socket.dft(socket.LWIP_USER1)
+
+
+-- 本demo测试使用的是核心板的VDD 3V3引脚对AirETH_1000配件板进行供电
+-- VDD 3V3引脚是Air8101内部的LDO输出引脚,最大输出电流300mA
+-- GPIO13在Air8101内部使能控制这个LDO的输出
+-- 所以在此处GPIO13输出高电平打开这个LDO
+gpio.setup(13, 1, gpio.PULLUP)
+
+
+-- 这个task的核心业务逻辑是:初始化SPI,初始化以太网卡,并在以太网卡上开启动态主机配置协议
+local function netdrv_eth_spi_task_func()
+    -- 初始化SPI0
+    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_USER1)来获取以太网是否连接成功
+
+    -- 以太网断网后,内核固件会产生一个"IP_LOSE"消息
+    -- 各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
+    -- 也可以在任何时刻调用socket.adapter(socket.LWIP_USER1)来获取以太网是否连接成功
+
+    -- socket.LWIP_USER1 指定网络适配器编号
+    -- netdrv.CH390外挂CH390
+    -- SPI ID 0, 片选 GPIO15
+    netdrv.setup(socket.LWIP_USER1, netdrv.CH390, {spi=0, cs=15})
+
+    -- 在以太上开启动态主机配置协议
+    netdrv.dhcp(socket.LWIP_USER1, true)
+end
+
+-- 创建并且启动一个task
+-- task的处理函数为netdrv_eth_spi_task_func
+sys.taskInit(netdrv_eth_spi_task_func)

+ 145 - 0
module/Air8101/demo/fota2/iot_server/netdrv/netdrv_multiple.lua

@@ -0,0 +1,145 @@
+--[[
+@module  netdrv_multiple
+@summary 多网卡(WIFI STA网卡、通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡、通过SPI外挂CH390H芯片的以太网卡、通过SPI外挂4G模组的4G网卡)驱动模块
+@version 1.0
+@date    2025.07.24
+@author  朱天华
+@usage
+本文件为多网卡驱动模块 ,核心业务逻辑为:
+1、调用exnetif.set_priority_order配置多网卡的控制参数以及优先级;
+
+
+通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡:
+Air8101核心板和AirPHY_1000配件板的硬件接线方式为:
+Air8101核心板通过TYPE-C USB口供电(核心板背面的功耗测试开关拨到OFF一端);
+如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的VIN管脚进行5V供电;
+| Air8101核心板 | AirPHY_1000配件板  |
+| ------------ | ------------------ |
+|    59/3V3    |         3.3v       |
+|     gnd      |         gnd        |
+|     5/D2     |         RX1        |
+|    72/D1     |         RX0        |
+|    71/D3     |         CRS        |
+|     4/D0     |         MDIO       |
+|     6/D4     |         TX0        |
+|    74/PCK    |         MDC        |
+|    70/D5     |         TX1        |
+|     7/D6     |         TXEN       |
+|     不接     |          NC        |
+|    69/D7     |         CLK        |
+
+
+通过SPI外挂CH390H芯片的以太网卡(此网卡和4G网卡硬件连接有冲突,如果使用以太网,可以优先使用rmii接口的以太网卡,如果必须使用spi以太网卡,注意更换以太网或者4G网卡使用的spi,不要冲突):
+Air8101核心板和AirETH_1000配件板的硬件接线方式为:
+Air8101核心板通过TYPE-C USB口供电(核心板背面的功耗测试开关拨到OFF一端);
+如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的VIN管脚进行5V供电;
+| Air8101核心板   |  AirETH_1000配件板 |
+| --------------- | ----------------- |
+| 59/3V3          | 3.3v              |
+| gnd             | gnd               |
+| 28/DCLK         | SCK               |
+| 54/DISP         | CSS               |
+| 55/HSYN         | SDO               |
+| 57/DE           | SDI               |
+| 14/GPIO8        | INT               |
+
+
+通过SPI接口外挂4G模组(Air780EHM/Air780EHV/Air780EGH/Air780EPM)的4G网卡:
+Air8101核心板和Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板或者开发板的硬件接线方式,参考netdrv_4g.lua的文件头注释;
+
+
+本文件没有对外接口,直接在其他功能模块中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(
+        {
+            -- “通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)”的以太网卡,可以使用Air8101核心板+AirPHY_1000配件板验证
+            {
+                ETHERNET = {
+                    -- 供电使能GPIO,此demo使用的59脚3V3供电,受GPIO13控制
+                    pwrpin = 13,
+                    -- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
+                    -- 如果没有传入此参数,exnetif会使用默认值10秒
+                    ping_time = 3000,
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,exnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+                }
+            },
+
+            -- “通过SPI外挂CH390H芯片”的以太网卡,可以使用Air8101核心板+AirETH_1000配件板验证
+            -- {
+            --     ETHUSER1 = {
+            --         -- 供电使能GPIO,此demo使用的59脚3V3供电,受GPIO13控制
+            --         pwrpin = 13,
+            --         -- 设置的多个“已经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=15}
+            --     }
+            -- },
+
+            -- WIFI STA网卡
+            {
+                WIFI = {
+                    -- 要连接的WIFI路由器名称
+                    ssid = "茶室-降功耗,找合宙!",
+                    -- 要连接的WIFI路由器密码
+                    password = "Air123456", 
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,exnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+                }
+            }
+        }
+    )    
+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)

+ 55 - 0
module/Air8101/demo/fota2/iot_server/netdrv/netdrv_wifi.lua

@@ -0,0 +1,55 @@
+--[[
+@module  netdrv_wifi
+@summary “WIFI STA网卡”驱动模块
+@version 1.0
+@date    2025.07.01
+@author  朱天华
+@usage
+本文件为WIFI STA网卡驱动模块,核心业务逻辑为:
+1、初始化WIFI网络;
+2、连接WIFI路由器;
+3、和WIFI路由器之间的连接状态发生变化时,在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_wifi"就可以加载运行;
+]]
+
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_STA then
+        log.info("netdrv_wifi.ip_ready_func", "IP_READY", json.encode(wlan.getInfo()))
+    end
+end
+
+local function ip_lose_func(adapter)
+    if adapter == socket.LWIP_STA then
+        log.warn("netdrv_wifi.ip_lose_func", "IP_LOSE")
+    end
+end
+
+
+-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
+-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察WIFI的连接状态
+-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 设置默认网卡为socket.LWIP_STA
+-- 在Air8101上,内核固件运行起来之后,默认网卡就是socket.LWIP_STA
+-- 在单socket.LWIP_STA网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
+socket.dft(socket.LWIP_STA)
+
+
+wlan.init()
+-- 连接WIFI热点,连接结果会通过"IP_READY"或者"IP_LOSE"消息通知
+-- Air8101仅支持2.4G的WIFI,不支持5G的WIFI
+-- 此处前两个参数表示WIFI热点名称以及密码,更换为自己测试时的真实参数即可
+-- 第三个参数1表示WIFI连接异常时,内核固件会自动重连
+wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1)
+
+-- WIFI联网成功(做为STATION成功连接AP,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
+-- 各个功能模块可以订阅"IP_READY"消息实时处理WIFI联网成功的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功
+
+-- WIFI断网后,内核固件会产生一个"IP_LOSE"消息
+-- 各个功能模块可以订阅"IP_LOSE"消息实时处理WIFI断网的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功

+ 37 - 0
module/Air8101/demo/fota2/iot_server/netdrv_device.lua

@@ -0,0 +1,37 @@
+--[[
+@module  netdrv_device
+@summary 网络驱动设备功能模块
+@version 1.0
+@date    2025.07.24
+@author  朱天华
+@usage
+本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
+1、netdrv_wifi:socket.LWIP_STA,WIFI STA网卡;
+2、netdrv_ethernet_rmii:socket.LWIP_ETH,通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡;
+3、netdrv_ethernet_spi:socket.LWIP_USER1,通过SPI外挂CH390H芯片的以太网卡;
+4、netdrv_4g:socket.LWIP_USER0,通过SPI外挂4G模组的4G网卡;
+5、netdrv_multi_network:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
+
+根据自己的项目需求,只需要require以上五种中的一种即可;
+
+
+本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
+]]
+
+
+-- 根据自己的项目需求,只需要require以下五种中的一种即可;
+
+-- 加载“WIFI STA网卡”驱动模块
+require "netdrv_wifi"
+
+-- 加载“通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡”驱动模块
+-- require "netdrv_eth_rmii"
+
+-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
+-- require "netdrv_eth_spi"
+
+-- 加载“通过SPI外挂4G模组的4G网卡”驱动模块
+-- require "netdrv_4G"
+
+-- 加载“可以配置优先级的多种网卡”驱动模块
+-- require "netdrv_multiple"

+ 119 - 0
module/Air8101/demo/fota2/iot_server/psm_power_fota.lua

@@ -0,0 +1,119 @@
+--[[
+@module  psm+_power_fota
+@summary psm+超低功耗模式下升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为psm+超低功耗模式下升级功能模块,核心设计思路
+1.升级触发机制 :
+   - 定时器唤醒升级 :设备定期从PSM模式唤醒,主动检查是否有新固件版本。
+   - 外部中断唤醒升级 :通过特定GPIO中断或网络消息唤醒设备进行升级。
+2.防止升级过程中进入休眠 :
+   - 在开始FOTA升级前,禁用PSM模式进入。
+   - 升级完成后,根据结果决定是否重启设备或重新进入PSM模式。
+
+]]
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "BnYk2BlYO30DiWra7q27wUmEarOiipHO" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+
+
+
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    --升级结束,触发升级回调,发布消息升级结束,可以进入休眠模式
+    sys.publish("FOTA_END")
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
+            "3) 已经是最新版本,无需升级")
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local ota_opts = {}
+
+
+function psm_fota_task_func()
+
+    log.info("开始测试PSM+模式功耗。")
+
+    -- 打印版本号, 方便看版本号变化, 非必须
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, ota_opts)
+
+
+
+    -- 等待下载升级包结束, 发布消息"FOTA_END",
+    -- 如果15秒内没有收到消息,则15秒的时长到达后进入PSM+模式。
+    -- 需要注意的是在fota_cb回调函数中,升级包下载成功后,会立马重启并升级模组。如果还有其他事情要做不想立马重启升级,需自行决定reboot的时机
+    -- 升级包下载成功后,本demo默认是立即自动重启并且将升级包更新到模组中,更新成功后,会再次走到这里
+    -- 再次走到这里后,合宙iot平台会返回“已经是最新版本,不需要升级”,fota_cb回调函数中会发布消息"FOTA_END"
+    -- 至此,才会继续向下执行代码,进入PSM+模式
+    sys.waitUntil("FOTA_END", 15000)
+
+    log.info("升级结束,进入PSM模式")
+
+
+    -- 进入PSM+前需要手动断开AP链接,不然无法正常进入PSM+
+    wlan.disconnect()
+    -- 定时检查升级 (每4小时唤醒一次)
+    pm.dtimerStart(2, 4 * 3600000)
+    -- 进入PSM模式
+    pm.power(pm.WORK_MODE, 3)
+    -- 防御机制:15秒后如果未进入PSM则重启
+    sys.wait(15000)
+    log.info("进入PSM+失败,重启")
+    rtos.reboot()
+end
+
+sys.taskInit(psm_fota_task_func)

+ 140 - 0
module/Air8101/demo/fota2/iot_server/tcp_iot/tcp_iot_main.lua

@@ -0,0 +1,140 @@
+--[[
+@module  tcp_client_main
+@summary tcp client socket主应用功能模块
+@version 1.0
+@date    2025.08.12
+@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_iot_receiver"
+-- 加载tcp client socket数据发送功能模块
+local tcp_client_sender = require "tcp_iot_sender"
+
+-- 电脑访问:https://netlab.luatos.com/
+-- 点击 打开TCP 按钮,会创建一个TCP server
+-- 将server的地址和端口赋值给下面这两个变量
+local SERVER_ADDR = "112.125.89.8"
+local SERVER_PORT = 42662
+
+-- 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毫秒超时不要修改的更长;
+            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+            -- 当libnetif.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)
+        -- 如果配置失败
+        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")
+
+        -- 数据收发以及网络连接异常事件总处理逻辑
+        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
+sysplus.taskInitEx(tcp_client_main_task_func, TASK_NAME, tcp_client_main_cbfunc)

+ 111 - 0
module/Air8101/demo/fota2/iot_server/tcp_iot/tcp_iot_receiver.lua

@@ -0,0 +1,111 @@
+--[[
+@module  tcp_client_receiver
+@summary tcp client socket数据接收应用功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本文件为tcp client socket数据接收应用功能模块,核心业务逻辑为:
+从内核读取接收到的数据,然后将数据处理后发送给其他应用功能模块做进一步处理;
+
+本文件的对外接口有2个:
+1、tcp_client_receiver.proc(socket_client):数据接收应用逻辑处理入口,在tcp_client_main.lua中调用;
+2、sys.publish("RECV_DATA_FROM_SERVER", data):
+   将接收到的数据通过消息"RECV_DATA_FROM_SERVER"发布出去处理;
+   处理后通过消息"AIR_SRV_FOTA"发布到air_srv_fota.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_IOT_DATA_SERVER"消息publish出去,给其他应用模块处理
+            sys.publish("RECV_IOT_DATA_SERVER", data)
+
+            -- 清空socket数据接收缓冲区中的数据
+            recv_buff:del()
+            -- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
+        else
+            break
+        end
+    end
+
+    return true
+end
+
+--解析接受到的json参数并发布到消息"AIR_SRV_FOTA"中
+--定义一个json格式如下,具体可以根据实际情况定义,使用iot升级的话只定义了一个参数即可:
+--{
+--    "fota": "true"
+--}
+
+local function data_processing(data)
+    -- 解析json数据
+    local json_data = json.decode(data)
+    -- 如果解析失败
+    if not json_data then
+        log.error("data_processing", "json.decode error")
+        return
+    end
+    -- 如果fota为true
+    if json_data.fota == "true" then
+        -- 发布消息"AIR_SRV_FOTA"
+        sys.publish("AIR_SRV_FOTA")
+    end
+end
+
+-- 订阅"RECV_IOT_DATA_SERVER"消息
+sys.subscribe("RECV_IOT_DATA_SERVER", data_processing)
+
+return tcp_client_receiver

+ 139 - 0
module/Air8101/demo/fota2/iot_server/tcp_iot/tcp_iot_sender.lua

@@ -0,0 +1,139 @@
+--[[
+@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"消息,将其他应用模块需要发送的数据存储到队列iot_send_queue中;
+2、tcp_client_main主任务调用tcp_client_sender.proc接口,遍历队列iot_send_queue,逐条发送数据到server;
+3、tcp client socket和server之间的连接如果出现异常,tcp_client_main主任务调用tcp_client_sender.exception_proc接口,丢弃掉队列iot_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 iot_send_queue = {}
+
+-- tcp_client_main的任务名
+tcp_client_sender.TASK_NAME = "iot_tcp_client_main"
+
+-- "SEND_DATA_REQ"消息的处理函数
+local function send_data_req_proc_func(data, cb)
+    -- 将原始数据增加前缀,然后插入到发送队列iot_send_queue中
+    table.insert(iot_send_queue, { data =  data, cb = cb })
+    -- 通知tcp_client_main主任务有数据需要发送
+    -- tcp_client_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
+    sysplus.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
+
+    -- 遍历数据发送队列iot_send_queue
+    while #iot_send_queue > 0 do
+        -- 取出来第一条数据赋值给send_item
+        -- 同时从队列iot_send_queue中删除这一条数据
+        send_item = table.remove(iot_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再次插入到iot_send_queue的队首位置,等待下次尝试发送
+        if buff_full then
+            log.error("tcp_client_sender.proc", "buffer is full, wait for the next time")
+            table.insert(iot_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()
+    -- 遍历数据发送队列iot_send_queue
+    while #iot_send_queue > 0 do
+        local send_item = table.remove(iot_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
+

+ 101 - 0
module/Air8101/demo/fota2/iot_server/update.lua

@@ -0,0 +1,101 @@
+--[[
+@module  update
+@summary 远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、判断网卡是否连接成功;
+2、初始化fota2模块;
+3、配置fota2模块的参数;
+4、调用fota2模块的升级函数;
+5、在升级结果的回调函数中,根据升级结果进行处理;
+]]
+-- 使用合宙iot平台时需要这个参数
+PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+
+libfota2 = require "libfota2"
+
+
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("降功耗 找合宙")
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+sys.timerLoopStart(get_version, 3000)
+
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
+            "3) 已经是最新版本,无需升级" )
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
+local ota_opts = {}
+
+function fota_task_func()
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+
+    -- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
+    if "123" == _G.PRODUCT_KEY  then
+        while 1 do
+            sys.wait(1000)
+            log.info("fota", "请修改正确的PRODUCT_KEY")
+        end
+    end
+
+    log.info("开始检查升级")
+    libfota2.request(fota_cb, ota_opts)
+end
+
+--创建并且启动一个task
+--运行这个task的主函数fota_task_func
+sys.taskInit(fota_task_func)
+-- 演示定时自动升级, 每隔4小时自动检查一次
+sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, ota_opts)
+
+

+ 194 - 0
module/Air8101/demo/fota2/self_server/README.md

@@ -0,0 +1,194 @@
+## 功能模块介绍
+
+### 自建服务器fota功能演示
+
+1、main.lua:主程序入口;
+
+2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的四种网卡(单4g网卡,单wifi网卡,单spi以太网卡,多网卡)中的任何一种网卡;
+
+3、update.lua:使用自建服务器进行远程升级功能模块,简单升级演示;
+
+4、tcp_self_server文件夹:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备启动customer_srv_fota功能模块,使用自建服务器进行升级;
+
+5、customer_srv_fota.lua:自建服务器升级功能模块;
+
+6、psm_power_fota.lua:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。需要注意的是此场景与上面两种场景不能同时使用;
+
+
+## 系统消息介绍
+
+1、"IP_READY":某种网卡已经获取到ip信息,仅仅获取到了ip信息,能否和外网连通还不确认;
+
+2、"IP_LOSE":某种网卡已经掉网;
+
+
+
+## 用户消息介绍
+
+1、"RECV_DATA_FROM_SERVER":socket client收到服务器下发的数据后,通过此消息发布出去,给其他应用模块处理;
+
+2、"SEND_DATA_REQ":其他应用模块发布此消息,通知socket client发送数据给服务器;
+
+
+
+## 演示功能概述
+
+1、combination文件夹下的demo会有三个演示场景,在main.lua中选择要使用的场景:
+
+    (1) 使用自建服务器升级,演示最简单的升级逻辑。
+
+    (2) 使用自建服务器升级,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。
+
+    (3) 休眠状态下升级,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。
+
+2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
+
+   (1) netdrv_4g:4G网卡
+
+   (2) netdrv_wifi:WIFI STA网卡
+
+   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
+
+   (4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
+
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/air8101/luatos/app/image/netdrv_multi.jpg)
+
+1、Air8101核心板一块
+
+2、TYPE-C USB数据线一根
+
+3、USB转串口数据线一根
+
+4、Air8101核心板和数据线的硬件接线方式为
+
+- Air8101核心板通过TYPE-C USB口供电;(核心板背面的功耗测试开关拨到OFF一端)
+
+- 如果测试发现软件频繁重启,重启原因值为:poweron reason 0,可能是供电不足,此时再通过直流稳压电源对核心板的vbat管脚进行4V供电,或者VIN管脚进行5V供电;
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+- USB转串口数据线,一般来说,白线连接核心板的12/U1TX,绿线连接核心板的11/U1RX,黑线连接核心板的gnd,另外一端连接电脑USB口;
+
+5、可选AirPHY_1000配件板一块,Air8101核心板和AirPHY_1000配件板的硬件接线方式为:
+
+| Air8101核心板 | AirPHY_1000配件板 |
+| ------------- | ----------------- |
+| 59/3V3        | 3.3v              |
+| gnd           | gnd               |
+| 5/D2          | RX1               |
+| 72/D1         | RX0               |
+| 71/D3         | CRS               |
+| 4/D0          | MDIO              |
+| 6/D4          | TX0               |
+| 74/PCK        | MDC               |
+| 70/D5         | TX1               |
+| 7/D6          | TXEN              |
+| 不接          | NC                |
+| 69/D7         | CLK               |
+
+6、可选AirETH_1000配件板一块,Air8101核心板和AirETH_1000配件板的硬件接线方式为:
+
+| Air8101核心板 | AirETH_1000配件板 |
+| ------------- | ----------------- |
+| 59/3V3        | 3.3v              |
+| gnd           | gnd               |
+| 28/DCLK       | SCK               |
+| 54/DISP       | CSS               |
+| 55/HSYN       | SDO               |
+| 57/DE         | SDI               |
+| 14/GPIO8      | INT               |
+
+7、可选Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板或者开发板一块,Air8101核心板和Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板或者开发板的硬件接线方式为:
+
+| Air8101核心板 | Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板 |
+| ------------- | --------------------------------------------- |
+| gnd           | GND                                           |
+| 54/DISP       | 83/SPI0CS                                     |
+| 55/HSYN       | 84/SPI0MISO                                   |
+| 57/DE         | 85/SPI0MOSI                                   |
+| 28/DCLK       | 86/SPI0CLK                                    |
+| 43/R2         | 19/GPIO22                                     |
+| 75/GPIO28     | 22/GPIO1                                      |
+
+
+| Air8101核心板 | Air780EHM/Air780EHV/Air780EGH/Air780EPM开发板 |
+| ------------- | --------------------------------------------- |
+| gnd           | GND                                           |
+| 54/DISP       | SPI_CS                                        |
+| 55/HSYN       | SPI_MISO                                      |
+| 57/DE         | SPI_MOSI                                      |
+| 28/DCLK       | SPI_CLK                                       |
+| 43/R2         | GPIO22                                        |
+| 75/GPIO28     | GPIO1                                         |
+
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air8101 V1005版本固件](https://docs.openluat.com/air8101/luatos/firmware/)(理论上,2025年7月26日之后发布的固件都可以)
+
+3、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/);
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、PC端浏览器访问[合宙TCP/UDP web测试工具](https://netlab.luatos.com/),点击 打开TCP 按钮,会创建一个TCP server,将server的地址和端口赋值给tcp_client_self_main.lua中的SERVER_ADDR和SERVER_PORT两个变量
+
+3、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
+
+4、Luatools烧录内核固件和修改后的demo脚本代码
+
+5、使用Luatools制作升级包,先把新旧版本分别生成量产文件,然后再制作升级包,工具上栏 luatOS->固件工具->差分包/整包升级包制作,将制作好的升级包配置到合宙iot服务器自己项目下,或上传到自建服务器上面;
+
+6、烧录成功后,自动开机运行
+
+7、[合宙TCP/UDP web测试工具](https://netlab.luatos.com/)上创建的两个TCP server可以看到有设备连接上来,然后可以下发下面字符串触发升级:
+
+
+``` lua
+
+--自建服务器下发这个指令,下发之前需要在服务器上面配置好升级包,然后吧url给到字符串
+--定义一个json格式如下,具体可以根据实际情况定义:
+-- {"fota": "true","url": "http://airtest.openluat.com:2900/download/FOTA2_DEMO_2012.001.001_LuatOS-SoC_Air8000.bin"}
+
+```
+
+8、可以看到升级过程如下,不管是什么场景下升级,基本都是如下情况:
+
+``` lua
+--没有升级之前可以看到如下打印
+I/user.fota 脚本版本号 001.000.000 core版本号 V2010
+
+I/user.fota_task_func recv IP_READY 1
+I/user.开始检查升级
+
+I/user.升级包下载成功,重启模块
+
+
+--升级之后可以看到如下打印
+I/user.fota 脚本版本号 001.000.001 core版本号 V2012
+--升级重启之后还是会检查升级,所以会有如下打印属于正常情况,其中"code": 27 是合宙iot服务器返回的状态码,意思是已经是最新版本了。自建服务器如果没做设置会再下载一遍升级包。
+
+I/user.fota -9 {"code": 27, "msg": "\u5df2\u662f\u6700\u65b0\u7248\u672c"}
+I/user.使用合宙服务器,接下来解析body里的code
+I/user.已是最新版本 1.设备的固件/脚本版本高于或等于云平台上的版本号 2.用户项目升级配置中未添加该设备 3.云平台升级配置中,是否升级配置为否
+I/user.fota 4
+
+
+```
+
+9、对于psm休眠状态下的升级的场景,打印的版本来判断是否是最新版本。模组升级成功后会自动进入psm休眠状态。可以通过电流查看休眠情况。

+ 139 - 0
module/Air8101/demo/fota2/self_server/customer_srv_fota.lua

@@ -0,0 +1,139 @@
+--[[
+@module  air_srv_fota
+@summary 使用自建服务器远程升级功能模块
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+实现远程升级功能,具体流程如下:
+1、接收 CUSTOMER_SRV_FOTA 系统消息,触发升级;
+2、判断网卡是否连接成功;
+3、初始化fota2模块;
+4、调用fota2模块的升级函数;
+5、根据升级结果进行处理;
+]]
+
+
+--加在libfota2扩展库
+libfota2 = require "libfota2"
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("降功耗 找合宙")
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+
+sys.timerLoopStart(get_version, 3000)
+
+-- fota升级标志:true 表示当前正有 FOTA 流程在跑
+local fota_running = false
+
+-- 升级结果的回调函数
+-- 功能:获取fota的回调函数
+-- 参数:
+-- result:number类型
+--   0表示成功
+--   1表示连接失败
+--   2表示url错误
+--   3表示服务器断开
+--   4表示接收报文错误
+--   5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
+local function fota_cb(ret)
+    log.info("fota", ret)
+    -- fota结束,无论成功还是失败,都释放fota_running标志
+    fota_running = false
+
+    if ret == 0 then
+        log.info("升级包下载成功,重启模块")
+        rtos.reboot()
+    elseif ret == 1 then
+        log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
+    elseif ret == 2 then
+        log.info("url错误", "检查url拼写")
+    elseif ret == 3 then
+        log.info("服务器断开", "检查服务器白名单配置")
+    elseif ret == 4 then
+        log.error("FOTA 失败",
+            "原因可能有:\n" ..
+            "1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
+            "2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
+            "3) 已经是最新版本,无需升级")
+    elseif ret == 5 then
+        log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
+    else
+        log.info("不是上面几种情况 ret为", ret)
+    end
+end
+
+
+-- 使用第三方服务器,配置ota_opts参数
+--[[
+-- opts参数说明, 所有参数都是可选的
+-- 1. opts.url string 升级所需要的URL, 若使用合宙iot平台,则不需要填
+-- 2. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+-- 3. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+-- 4. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+-- 5. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+-- 6. opts.firmware_name string 固件名称,默认是 _G.PROJECT.. "_LuatOS-SoC_" .. rtos.bsp()
+-- 7. opts.server_cert string 服务器证书, 默认不使用
+-- 8. opts.client_cert string 客户端证书, 默认不使用
+-- 9. opts.client_key string 客户端私钥, 默认不使用
+-- 10. opts.client_password string 客户端私钥口令, 默认不使用
+-- 11. opts.method string 请求方法, 默认是GET
+-- 12. opts.headers table 额外添加的请求头,默认不需要
+-- 13. opts.body string 额外添加的请求body,默认不需要
+]]
+local opts = {
+    url = "",
+    -- 合宙IOT平台的默认升级URL, 不填就是这个默认值
+    -- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
+    -- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
+    -- 如果不加###,则默认会上传如下参数
+    -- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
+    -- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
+    -- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
+    -- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
+    -- 5. opts.firmware_name string 底层版本号
+
+    -- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
+    version = ""
+    -- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
+}
+
+
+local function air_fota_func(data)
+    -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+    while not socket.adapter(socket.dft()) do
+        log.warn("fota_task_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+    -- 检测到了IP_READY消息
+    log.info("fota_task_func", "recv IP_READY", socket.dft())
+    while true do
+        -- 阻塞等待外部事件:"CUSTOMER_SRV_FOTA"
+        local result, data = sys.waitUntil("CUSTOMER_SRV_FOTA")
+        if result then
+            log.info("接收到数据", "date", #data)
+
+            if fota_running then
+                log.warn("fota_task", "FOTA 正在运行,跳过本次请求")
+            else
+                -- 标记FOTA正在运行
+                -- 注意:这里只是标记,实际的FOTA流程还没有开始
+                opts.url = data.url
+                opts.version = data.version
+                fota_running = true
+                log.info("开始检查升级")
+                libfota2.request(fota_cb, opts)
+            end
+        end
+    end
+end
+-- 初始化FOTA任务
+sys.taskInit(air_fota_func)

+ 108 - 0
module/Air8101/demo/fota2/self_server/main.lua

@@ -0,0 +1,108 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.08.12
+@author  孟伟
+@usage
+本demo演示的核心功能为:
+1、这个demo会有三个演示场景:
+    (1)使用自建服务器升级,演示最简单的升级逻辑。
+    (2)使用自建服务器升级,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。
+    (3)休眠状态下升级,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。
+2、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 = "FOTA2_DEMO"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function get_version()
+    log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
+end
+
+sys.timerLoopStart(get_version, 3000)
+
+
+-- 加载网络驱动设备功能模块
+require "netdrv_device"
+
+
+
+--两种tcp下发指令升级的场景和psm+低功耗模式升级不能同时使用,需要根据自己场景选择其中一种
+
+--两种tcp下发指令升级的场景可以启用一种,也可以启用两种,启用两种时,注意控制不要一个在fota的过程中,另外一个再fota。
+
+
+-- 加载远程升级功能模块,场景一
+require "update"
+---------------------------------------------------------------------------
+-- 加载tcp client self socket主应用功能模块,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。场景二
+-- require "tcp_self_main"
+-- 加载自建服务器远程升级功能模块
+-- require "customer_srv_fota"
+---------------------------------------------------------------------------
+-- 加载psm+低功耗模式升级功能模块,场景三
+-- require "psm_power_fota"
+
+
+
+
+
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

+ 118 - 0
module/Air8101/demo/fota2/self_server/netdrv/netdrv_4g.lua

@@ -0,0 +1,118 @@
+--[[
+@module  netdrv_4g
+@summary “通过SPI接口外挂4G模组(Air780EHM/Air780EHV/Air780EGH/Air780EPM)的4G网卡”驱动模块
+@version 1.0
+@date    2025.07.27
+@author  朱天华
+@usage
+本文件为 “通过SPI接口外挂4G模组(Air780EHM/Air780EHV/Air780EGH/Air780EPM)的4G网卡”驱动模块,核心业务逻辑为:
+1、初始化和外部4G网卡的配置(初始化AirLINK、配置桥接网络、配置SPI、静态配置IP地址/子网掩码/网关);
+2、4G网卡的连接状态发生变化时,在日志中进行打印;
+
+
+硬件环境使用以下两种环境中的一种即可:
+1、Air8101核心板+Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板
+2、Air8101核心板+Air780EHM/Air780EHV/Air780EGH/Air780EPM开发板
+
+一、当使用第1种硬件环境时,Air8101核心板和Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板的硬件接线方式为:
+
+1、Air8101核心板:
+- 核心板通过TYPE-C USB口供电(核心板背面的功耗测试开关拨到OFF一端);
+- 如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的VIN管脚进行5V供电;
+
+2、Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板:
+- 核心板通过TYPE-C USB口供电(TYPE-C USB口旁边的ON/OFF拨动开关拨到ON一端);
+- 如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的5V管脚进行5V供电;
+
+3、
+| Air8101核心板 |  Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板  |
+| ------------ | ---------------------------------------------- |
+|     gnd      |                     GND                        |
+|  54/DISP     |                     83/SPI0CS                  |
+|  55/HSYN     |                     84/SPI0MISO                |
+|    57/DE     |                     85/SPI0MOSI                |
+|  28/DCLK     |                     86/SPI0CLK                 |
+|    43/R2     |                     19/GPIO22                  |
+|  75/GPIO28   |                     22/GPIO1                   |
+
+二、当使用第2种硬件环境时,Air8101核心板和Air780EHM/Air780EHV/Air780EGH/Air780EPM开发板的硬件接线方式为:
+
+1、Air8101核心板:
+- 核心板通过TYPE-C USB口供电(核心板背面的功耗测试开关拨到OFF一端);
+- 如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的VIN管脚进行5V供电;
+
+2、Air780EHM/Air780EHV/Air780EGH/Air780EPM开发板:
+- 核心板通过TYPE-C USB口供电(外部供电/USB供电拨动开关拨到USB供电一端);
+- 如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对开发板的5V管脚进行5V供电;
+
+3、
+| Air8101核心板 | Air780EHM/Air780EHV/Air780EGH/Air780EPM开发板  |
+| ------------ | ---------------------------------------------- |
+|     gnd      |                     GND                        |
+|  54/DISP     |                     SPI_CS                     |
+|  55/HSYN     |                     SPI_MISO                   |
+|    57/DE     |                     SPI_MOSI                   |
+|  28/DCLK     |                     SPI_CLK                    |
+|    43/R2     |                     GPIO22                     |
+|  75/GPIO28   |                     GPIO1                      |
+
+三、以上两种硬件环境,Air8101使用的SPI0默认的一组引脚,也可以使用SPI1;使用SPI1时,硬件连接说明的更多资料参考:
+https://docs.openluat.com/air8101/luatos/hardware/design/4gnet/
+软件代码需要做以下配置:
+airlink.config(airlink.CONF_SPI_ID, 1)
+airlink.config(airlink.CONF_SPI_CS, 10)
+
+四、测试本功能模块时,Air780EHM/Air780EHV/Air780EGH/Air780EPM需要烧录以下软件:
+1、最新版本的内核固件
+2、脚本:https://gitee.com/openLuat/LuatOS/tree/master/module/Air8101/demo/multi_network/WIFI_4G_ETH/Air8101_Air780EPM/Air780EPM_master
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
+]]
+
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_USER0 then
+        log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_USER0))
+    end
+end
+
+local function ip_lose_func(adapter)
+    if adapter == socket.LWIP_USER0 then
+        log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
+    end
+end
+
+
+-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
+-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI接口外挂4G模组(Air780EHM/Air780EHV/Air780EGH/Air780EPM)的4G网卡”的连接状态
+-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 设置默认网卡为socket.LWIP_USER0
+socket.dft(socket.LWIP_USER0)
+
+
+-- 初始化airlink,Air8101和4G网卡之间,在spi之上,基于airlink协议通信
+airlink.init()
+-- 创建桥接网络设备
+-- 此处第一个参数必须是socket.LWIP_USER0,是因为Air780EHM/Air780EHV/Air780EGH/Air780EPM使用的也是socket.LWIP_USER0,双方是点对点通讯的对等网络
+-- 此处第二个参数必须是netdrv.WHALE,表示虚拟网卡的实现方式
+netdrv.setup(socket.LWIP_USER0, netdrv.WHALE)
+-- 启动airlink,配置Air8101作为SPI从机模式。
+airlink.start(airlink.MODE_SPI_SLAVE)
+
+-- 静态配置IPv4地址
+-- 本地ip地址为"192.168.111.1",网关ip地址为"192.168.111.2",子网掩码为"255.255.255.0"
+-- 此处设置的本地ip地址要和Air780EHM/Air780EHV/Air780EGH/Air780EPM中设置的网关ip地址完全一样
+-- 此处设置的网关ip地址要和Air780EHM/Air780EHV/Air780EGH/Air780EPM中设置的本地ip地址完全一样
+-- 此处设置的子网掩码要和Air780EHM/Air780EHV/Air780EGH/Air780EPM中设置的子网掩码完全一样
+netdrv.ipv4(socket.LWIP_USER0, "192.168.111.1", "255.255.255.0", "192.168.111.2")
+
+-- 4G联网成功后,内核固件会产生一个"IP_READY"消息
+-- 各个功能模块可以订阅"IP_READY"消息实时处理4G联网成功的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_USER0)来获取4G网络是否连接成功
+
+-- 4G断网后,内核固件会产生一个"IP_LOSE"消息
+-- 各个功能模块可以订阅"IP_LOSE"消息实时处理4G网络断网的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_USER0)来获取4G网络是否连接成功

+ 77 - 0
module/Air8101/demo/fota2/self_server/netdrv/netdrv_eth_rmii.lua

@@ -0,0 +1,77 @@
+--[[
+@module  netdrv_eth_rmii
+@summary “通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡”驱动模块 
+@version 1.0
+@date    2025.07.24
+@author  朱天华
+@usage
+本文件为“通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡”驱动模块 ,核心业务逻辑为:
+1、打开PHY芯片供电开关;
+2、初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+Air8101核心板和AirPHY_1000配件板的硬件接线方式为:
+Air8101核心板通过TYPE-C USB口供电(核心板背面的功耗测试开关拨到OFF一端);
+如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的VIN管脚进行5V供电;
+| Air8101核心板 |  AirPHY_1000配件板  |
+| ------------ | ------------------ |
+|    59/3V3    |         3.3v       |
+|     gnd      |         gnd        |
+|     5/D2     |         RX1        |
+|    72/D1     |         RX0        |
+|    71/D3     |         CRS        |
+|     4/D0     |         MDIO       |
+|     6/D4     |         TX0        |
+|    74/PCK    |         MDC        |
+|    70/D5     |         TX1        |
+|     7/D6     |         TXEN       |
+|     不接     |          NC        |
+|    69/D7     |         CLK        |
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_rmii"就可以加载运行;
+]]
+
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_ETH then
+        log.info("netdrv_eth_rmii.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_rmii.ip_lose_func", "IP_LOSE")
+    end
+end
+
+
+-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
+-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡”的连接状态
+-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 设置默认网卡为socket.LWIP_ETH
+socket.dft(socket.LWIP_ETH)
+
+
+-- 本demo测试使用的是核心板的VDD 3V3引脚对AirPHY_1000配件板进行供电
+-- VDD 3V3引脚是Air8101内部的LDO输出引脚,最大输出电流300mA
+-- GPIO13在Air8101内部使能控制这个LDO的输出
+-- 所以在此处GPIO13输出高电平打开这个LDO
+gpio.setup(13, 1, gpio.PULLUP)
+
+
+--初始化以太网卡
+
+--以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
+--各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
+--也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+
+--以太网断网后,内核固件会产生一个"IP_LOSE"消息
+--各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
+--也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+netdrv.setup(socket.LWIP_ETH)
+
+--在以太网卡上开启动态主机配置协议
+netdrv.dhcp(socket.LWIP_ETH, true)

+ 102 - 0
module/Air8101/demo/fota2/self_server/netdrv/netdrv_eth_spi.lua

@@ -0,0 +1,102 @@
+--[[
+@module  netdrv_eth_spi
+@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
+@version 1.0
+@date    2025.07.24
+@author  朱天华
+@usage
+本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块 ,核心业务逻辑为:
+1、打开AirETH_1000配件板供电开关;
+2、初始化spi0,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+Air8101核心板和AirETH_1000配件板的硬件接线方式为:
+Air8101核心板通过TYPE-C USB口供电(核心板背面的功耗测试开关拨到OFF一端);
+如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的VIN管脚进行5V供电;
+| Air8101核心板    | AirETH_1000配件板  |
+| --------------- | ----------------- |
+| 59/3V3          | 3.3v              |
+| gnd             | gnd               |
+| 28/DCLK         | SCK               |
+| 54/DISP         | CSS               |
+| 55/HSYN         | SDO               |
+| 57/DE           | SDI               |
+| 14/GPIO8        | INT               |
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
+]]
+
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_USER1 then
+        log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_USER1))
+    end
+end
+
+local function ip_lose_func(adapter)
+    if adapter == socket.LWIP_USER1 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_USER1
+socket.dft(socket.LWIP_USER1)
+
+
+-- 本demo测试使用的是核心板的VDD 3V3引脚对AirETH_1000配件板进行供电
+-- VDD 3V3引脚是Air8101内部的LDO输出引脚,最大输出电流300mA
+-- GPIO13在Air8101内部使能控制这个LDO的输出
+-- 所以在此处GPIO13输出高电平打开这个LDO
+gpio.setup(13, 1, gpio.PULLUP)
+
+
+-- 这个task的核心业务逻辑是:初始化SPI,初始化以太网卡,并在以太网卡上开启动态主机配置协议
+local function netdrv_eth_spi_task_func()
+    -- 初始化SPI0
+    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_USER1)来获取以太网是否连接成功
+
+    -- 以太网断网后,内核固件会产生一个"IP_LOSE"消息
+    -- 各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
+    -- 也可以在任何时刻调用socket.adapter(socket.LWIP_USER1)来获取以太网是否连接成功
+
+    -- socket.LWIP_USER1 指定网络适配器编号
+    -- netdrv.CH390外挂CH390
+    -- SPI ID 0, 片选 GPIO15
+    netdrv.setup(socket.LWIP_USER1, netdrv.CH390, {spi=0, cs=15})
+
+    -- 在以太上开启动态主机配置协议
+    netdrv.dhcp(socket.LWIP_USER1, true)
+end
+
+-- 创建并且启动一个task
+-- task的处理函数为netdrv_eth_spi_task_func
+sys.taskInit(netdrv_eth_spi_task_func)

+ 145 - 0
module/Air8101/demo/fota2/self_server/netdrv/netdrv_multiple.lua

@@ -0,0 +1,145 @@
+--[[
+@module  netdrv_multiple
+@summary 多网卡(WIFI STA网卡、通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡、通过SPI外挂CH390H芯片的以太网卡、通过SPI外挂4G模组的4G网卡)驱动模块
+@version 1.0
+@date    2025.07.24
+@author  朱天华
+@usage
+本文件为多网卡驱动模块 ,核心业务逻辑为:
+1、调用exnetif.set_priority_order配置多网卡的控制参数以及优先级;
+
+
+通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡:
+Air8101核心板和AirPHY_1000配件板的硬件接线方式为:
+Air8101核心板通过TYPE-C USB口供电(核心板背面的功耗测试开关拨到OFF一端);
+如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的VIN管脚进行5V供电;
+| Air8101核心板 | AirPHY_1000配件板  |
+| ------------ | ------------------ |
+|    59/3V3    |         3.3v       |
+|     gnd      |         gnd        |
+|     5/D2     |         RX1        |
+|    72/D1     |         RX0        |
+|    71/D3     |         CRS        |
+|     4/D0     |         MDIO       |
+|     6/D4     |         TX0        |
+|    74/PCK    |         MDC        |
+|    70/D5     |         TX1        |
+|     7/D6     |         TXEN       |
+|     不接     |          NC        |
+|    69/D7     |         CLK        |
+
+
+通过SPI外挂CH390H芯片的以太网卡(此网卡和4G网卡硬件连接有冲突,如果使用以太网,可以优先使用rmii接口的以太网卡,如果必须使用spi以太网卡,注意更换以太网或者4G网卡使用的spi,不要冲突):
+Air8101核心板和AirETH_1000配件板的硬件接线方式为:
+Air8101核心板通过TYPE-C USB口供电(核心板背面的功耗测试开关拨到OFF一端);
+如果测试发现软件重启,并且日志中出现  poweron reason 0,表示供电不足,此时再通过直流稳压电源对核心板的VIN管脚进行5V供电;
+| Air8101核心板   |  AirETH_1000配件板 |
+| --------------- | ----------------- |
+| 59/3V3          | 3.3v              |
+| gnd             | gnd               |
+| 28/DCLK         | SCK               |
+| 54/DISP         | CSS               |
+| 55/HSYN         | SDO               |
+| 57/DE           | SDI               |
+| 14/GPIO8        | INT               |
+
+
+通过SPI接口外挂4G模组(Air780EHM/Air780EHV/Air780EGH/Air780EPM)的4G网卡:
+Air8101核心板和Air780EHM/Air780EHV/Air780EGH/Air780EPM核心板或者开发板的硬件接线方式,参考netdrv_4g.lua的文件头注释;
+
+
+本文件没有对外接口,直接在其他功能模块中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(
+        {
+            -- “通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)”的以太网卡,可以使用Air8101核心板+AirPHY_1000配件板验证
+            {
+                ETHERNET = {
+                    -- 供电使能GPIO,此demo使用的59脚3V3供电,受GPIO13控制
+                    pwrpin = 13,
+                    -- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
+                    -- 如果没有传入此参数,exnetif会使用默认值10秒
+                    ping_time = 3000,
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,exnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+                }
+            },
+
+            -- “通过SPI外挂CH390H芯片”的以太网卡,可以使用Air8101核心板+AirETH_1000配件板验证
+            -- {
+            --     ETHUSER1 = {
+            --         -- 供电使能GPIO,此demo使用的59脚3V3供电,受GPIO13控制
+            --         pwrpin = 13,
+            --         -- 设置的多个“已经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=15}
+            --     }
+            -- },
+
+            -- WIFI STA网卡
+            {
+                WIFI = {
+                    -- 要连接的WIFI路由器名称
+                    ssid = "茶室-降功耗,找合宙!",
+                    -- 要连接的WIFI路由器密码
+                    password = "Air123456", 
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,exnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+                }
+            }
+        }
+    )    
+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)

+ 55 - 0
module/Air8101/demo/fota2/self_server/netdrv/netdrv_wifi.lua

@@ -0,0 +1,55 @@
+--[[
+@module  netdrv_wifi
+@summary “WIFI STA网卡”驱动模块
+@version 1.0
+@date    2025.07.01
+@author  朱天华
+@usage
+本文件为WIFI STA网卡驱动模块,核心业务逻辑为:
+1、初始化WIFI网络;
+2、连接WIFI路由器;
+3、和WIFI路由器之间的连接状态发生变化时,在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_wifi"就可以加载运行;
+]]
+
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_STA then
+        log.info("netdrv_wifi.ip_ready_func", "IP_READY", json.encode(wlan.getInfo()))
+    end
+end
+
+local function ip_lose_func(adapter)
+    if adapter == socket.LWIP_STA then
+        log.warn("netdrv_wifi.ip_lose_func", "IP_LOSE")
+    end
+end
+
+
+-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
+-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察WIFI的连接状态
+-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 设置默认网卡为socket.LWIP_STA
+-- 在Air8101上,内核固件运行起来之后,默认网卡就是socket.LWIP_STA
+-- 在单socket.LWIP_STA网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
+socket.dft(socket.LWIP_STA)
+
+
+wlan.init()
+-- 连接WIFI热点,连接结果会通过"IP_READY"或者"IP_LOSE"消息通知
+-- Air8101仅支持2.4G的WIFI,不支持5G的WIFI
+-- 此处前两个参数表示WIFI热点名称以及密码,更换为自己测试时的真实参数即可
+-- 第三个参数1表示WIFI连接异常时,内核固件会自动重连
+wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1)
+
+-- WIFI联网成功(做为STATION成功连接AP,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
+-- 各个功能模块可以订阅"IP_READY"消息实时处理WIFI联网成功的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功
+
+-- WIFI断网后,内核固件会产生一个"IP_LOSE"消息
+-- 各个功能模块可以订阅"IP_LOSE"消息实时处理WIFI断网的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功

+ 37 - 0
module/Air8101/demo/fota2/self_server/netdrv_device.lua

@@ -0,0 +1,37 @@
+--[[
+@module  netdrv_device
+@summary 网络驱动设备功能模块
+@version 1.0
+@date    2025.07.24
+@author  朱天华
+@usage
+本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
+1、netdrv_wifi:socket.LWIP_STA,WIFI STA网卡;
+2、netdrv_ethernet_rmii:socket.LWIP_ETH,通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡;
+3、netdrv_ethernet_spi:socket.LWIP_USER1,通过SPI外挂CH390H芯片的以太网卡;
+4、netdrv_4g:socket.LWIP_USER0,通过SPI外挂4G模组的4G网卡;
+5、netdrv_multi_network:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
+
+根据自己的项目需求,只需要require以上五种中的一种即可;
+
+
+本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
+]]
+
+
+-- 根据自己的项目需求,只需要require以下五种中的一种即可;
+
+-- 加载“WIFI STA网卡”驱动模块
+require "netdrv_wifi"
+
+-- 加载“通过MAC层的rmii接口外挂PHY芯片(LAN8720Ai)的以太网卡”驱动模块
+-- require "netdrv_eth_rmii"
+
+-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
+-- require "netdrv_eth_spi"
+
+-- 加载“通过SPI外挂4G模组的4G网卡”驱动模块
+-- require "netdrv_4G"
+
+-- 加载“可以配置优先级的多种网卡”驱动模块
+-- require "netdrv_multiple"

Неке датотеке нису приказане због велике количине промена