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

- 来源: 合宙 LuatOS 官方仓库 air780epm 模块完整代码
- 路径: luatos/air780epm/module/Air780EPM/demo 含官方 demo(含 mqtt/mqtts/socket/uart 等)
- 后续: 基于 demo 开发 UART<->MQTT 数据上报功能
This commit is contained in:
wangfq
2026-08-31 08:53:49 +08:00
commit 173dd6874f
813 changed files with 125484 additions and 0 deletions
@@ -0,0 +1,77 @@
--[[
@module httpsrv_start
@summary Air780EPM HTTP服务器应用模块
@version 1.0
@date 2025.10.24
@author 拓毅恒
@usage
本文件为Air780EPM开发板演示 HTTP 服务器功能的应用模块,核心业务逻辑为:
1. 初始化HTTP服务器
2. 配置HTTP服务器参数
3. 启动HTTP服务器服务
4. 处理HTTP请求和响应
]]
local function handle_http_request(fd, method, uri, headers, body)
log.info("httpsrv", method, uri, body or "")
if uri == "/send/text" then
log.info("Text Request", "Method:", method, "Body Length:", body and #body or 0)
if method == "POST" and body and #body > 0 then
-- 直接打印接收到的文本内容
log.info("Received Text:", body)
return 200, {}, "ok"
end
return 400, {}, "Invalid request"
elseif uri == "/scan/go" then
wlan.scan()
return 200, {}, "ok"
elseif uri == "/scan/list" then
return 200, {["Content-Type"]="application/json"}, (json.encode({data=scan_result, ok=true}))
elseif uri == "/connect" then
if method == "POST" and body and #body > 2 then
local jdata = json.decode(body)
if jdata and jdata.ssid then
sys.timerStart(wlan.connect, 500, jdata.ssid, jdata.passwd)
return 200, {}, "ok"
end
end
return 400, {}, "ok"
elseif uri == "/connok" then
log.info("connok", json.encode({ip=socket.localIP(2)}))
return 200, {["Content-Type"]="application/json"}, json.encode({ip=socket.localIP(2)})
end
return 404, {}, "Not Found" .. uri
end
local function start_http_server()
-- 等待网络就绪事件
sys.waitUntil("CREATE_OK")
local local_ip = socket.localIP(socket.LWIP_ETH)
-- 启动HTTP服务器
httpsrv.start(80, handle_http_request, socket.LWIP_ETH)
log.info("HTTP", "文件服务器已启动,使用AirETH_1000-以太网模式")
log.info("HTTP", "访问地址: http://" .. (local_ip or "192.168.4.1"))
end
local function scan_done_handle()
local result = wlan.scanResult()
scan_result = {}
for k, v in pairs(result) do
log.info("scan", (v["ssid"] and #v["ssid"] > 0) and v["ssid"] or "[隐藏SSID]", v["rssi"], (v["bssid"]:toHex()))
if v["ssid"] and #v["ssid"] > 0 then
table.insert(scan_result, {
ssid = v["ssid"],
rssi = v["rssi"]
})
end
end
log.info("scan", "aplist count:", #scan_result)
end
-- 订阅WiFi扫描完成事件
sys.subscribe("WLAN_SCAN_DONE", scan_done_handle)
sys.taskInit(start_http_server)
@@ -0,0 +1,228 @@
<!DOCTYPE html>
<html lang="zh">
<header>
<meta charset="utf-8" />
<title>Air780EPM 控制中心</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 20px;
color: #333;
}
h2, h4 {
color: #2c3e50;
}
.container {
max-width: 600px;
margin: auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 5px 0;
transition: background-color 0.3s;
}
button:hover {
background-color: #2980b9;
}
input[type="text"] {
width: 100%;
padding: 10px;
margin: 5px 0 15px;
border: 1px solid #ccc;
border-radius: 5px;
box-sizing: border-box;
}
.control-panel {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #eee;
border-radius: 8px;
background-color: #fafafa;
}
.status {
font-weight: bold;
margin-top: 10px;
color: #27ae60;
}
#sendBtn {
background-color: #27ae60;
}
#sendBtn:hover {
background-color: #229954;
}
#scanBtn {
background-color: #9b59b6;
}
#scanBtn:hover {
background-color: #8e44ad;
}
#wifiResults {
margin-top: 15px;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
background-color: #f9f9f9;
}
#wifiResults ul {
list-style: none;
padding: 0;
margin: 0;
}
#wifiResults li {
padding: 8px;
margin: 2px 0;
background-color: white;
border-radius: 4px;
border: 1px solid #eee;
}
.ssid {
font-weight: bold;
margin-right: 10px;
}
.rssi-strong {
color: #27ae60;
}
.rssi-medium {
color: #f39c12;
}
.rssi-weak {
color: #e74c3c;
}
</style>
<script type="text/javascript">
function sendText() {
var text = document.getElementById("inputText").value;
if (text.trim() === "") {
alert("请输入文本内容");
return;
}
// 添加调试日志
console.log("发送文本:", text);
fetch("/send/text", {
method: "POST",
headers: {
"Content-Type": "text/plain"
},
body: text
}).then(function(resp) {
console.log("响应状态:", resp.status);
if (resp.status == 200) {
document.getElementById("status").textContent = "文本已发送至设备日志";
document.getElementById("inputText").value = "";
} else {
document.getElementById("status").textContent = "发送失败: " + resp.status;
}
}).catch(function(error) {
console.error("发送错误:", error);
document.getElementById("status").textContent = "发送出错: " + error.message;
});
}
// 按下回车键时发送文本
function handleKeyPress(event) {
if (event.key === "Enter") {
sendText();
}
}
// WiFi扫描功能
function scanWifi() {
document.getElementById("status").textContent = "正在扫描WiFi...";
document.getElementById("wifiResults").innerHTML = "";
fetch("/scan/go")
.then(function(resp) {
if (resp.status == 200) {
document.getElementById("status").textContent = "扫描已开始,正在获取结果...";
// 等待1秒后获取扫描结果
setTimeout(getWifiResults, 1000);
} else {
document.getElementById("status").textContent = "扫描失败: " + resp.status;
}
})
.catch(function(error) {
console.error("扫描错误:", error);
document.getElementById("status").textContent = "扫描出错: " + error.message;
});
}
// 获取WiFi扫描结果
function getWifiResults() {
fetch("/scan/list")
.then(function(resp) {
if (resp.status == 200) {
return resp.json();
} else {
throw new Error("获取结果失败: " + resp.status);
}
})
.then(function(data) {
const resultsDiv = document.getElementById("wifiResults");
if (data && data.data && data.data.length > 0) {
let html = "<ul>";
data.data.forEach(function(ap) {
let rssiClass = "rssi-weak";
if (ap.rssi > -70) rssiClass = "rssi-strong";
else if (ap.rssi > -85) rssiClass = "rssi-medium";
html += `<li><span class="ssid">${ap.ssid}</span> <span class="${rssiClass}">信号: ${ap.rssi} dBm</span></li>`;
});
html += "</ul>";
resultsDiv.innerHTML = html;
document.getElementById("status").textContent = "扫描完成,共发现 " + data.data.length + " 个WiFi网络";
} else {
resultsDiv.innerHTML = "未发现WiFi网络";
document.getElementById("status").textContent = "扫描完成,未发现WiFi网络";
}
})
.catch(function(error) {
console.error("获取结果错误:", error);
document.getElementById("status").textContent = "获取结果出错: " + error.message;
});
}
</script>
</header>
<body>
<div class="container">
<h2>Air780EPM 控制中心</h2>
<!-- 文本发送功能 -->
<div class="control-panel">
<h4>文本发送</h4>
<label for="inputText">输入文本:</label>
<input type="text" id="inputText" placeholder="请输入要发送到设备日志的文本" onkeypress="handleKeyPress(event)">
<button id="sendBtn" onclick="sendText()">发送文本</button>
</div>
<!-- WiFi扫描功能 -->
<div class="control-panel">
<h4>WiFi扫描</h4>
<button id="scanBtn" onclick="scanWifi()">扫描WiFi网络</button>
<div id="wifiResults">请点击上方按钮开始扫描</div>
</div>
<!-- 状态显示 -->
<div class="status" id="status">就绪</div>
</div>
</body>
</html>
@@ -0,0 +1,61 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.10.24
@author 拓毅恒
@usage
本demo演示的核心功能为:
HTTP服务器应用功能,通过加载httpsrv_start模块来启动和配置HTTP服务器,处理HTTP请求和响应。
netdrv_eth_spi:配置连接外网使用的网卡,通过SPI外挂CH390H芯片的以太网卡。
更多说明参考本目录下的readme.md文件
]]
--[[
必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
PROJECT:项目名,ascii string类型
可以随便定义,只要不使用,就行
VERSION:项目版本号,ascii string类型
如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为999
如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
]]
PROJECT = "httpsrv_testdemo"
VERSION = "001.999.000"
log.info("main", "project name is ", PROJECT, "version is ", VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 加载网络驱动设备功能模块
require "netdrv_eth_spi"
-- 加载 httpsrv_start 功能模块
require "httpsrv_start"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
@@ -0,0 +1,78 @@
--[[
@module netdrv_eth_spi
@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
@version 1.0
@date 2025.07.24
@author 马梦阳
@usage
本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块,核心业务逻辑为:
1、打开CH390H芯片供电开关;
2、初始化spi0,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
3、以太网卡的连接状态发生变化时,在日志中进行打印;
直接使用Air780EPM V1.3版本开发板硬件测试即可;
本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
]]
local exnetif = require "exnetif"
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_ETH then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
sys.publish("CREATE_OK")
end
end
local function ip_lose_func(adapter)
if adapter == socket.LWIP_ETH then
log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
end
end
-- 以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
-- 各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
-- 以太网断网后,内核固件会产生一个"IP_LOSE"消息
-- 各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
--此处订阅"IP_READY"和"IP_LOSE"两种消息
--在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI外挂CH390H芯片的以太网卡”的连接状态
--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
local function netdrv_eth_spi_task_func()
-- 配置SPI外接以太网芯片CH390H的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_ETH
-- 本demo使用Air780EPM V1.3版本开发板测试,开发板上的硬件配置为:
-- GPIO20为CH390H以太网芯片的供电使能控制引脚
-- 使用spi0,片选引脚使用GPIO8,中断引脚使用GPIO22
-- 如果使用的硬件和以上描述的环境不同,根据自己的硬件配置修改以下参数
exnetif.set_priority_order({
{
ETHERNET = {
pwrpin = 20,
tp = netdrv.CH390,
opts = {spi = 0, cs = 8, irq = 22}
}
}
})
end
-- 启动一个tasktask的处理函数为netdrv_eth_spi_task_func
-- 在处理函数中调用exnetif.set_priority_order设置网卡优先级
-- 因为exnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
sys.taskInit(netdrv_eth_spi_task_func)
@@ -0,0 +1,94 @@
## 功能模块介绍
1、main.lua:主程序入口;
2、httpsrv_start.luaHTTP服务器实现模块,包含服务器初始化、路由处理、文本发送和WiFi扫描功能;
3、index.htmlWeb控制界面,提供LED控制按钮、文本发送输入框和WiFi扫描功能;
4、netdrv_eth_spi.lua:网络驱动设备功能模块,通过以太网SPI网卡驱动,SPI接口连接CH390H芯片实现有线网络连接;
## 演示功能概述
1、HTTP服务器:创建Web服务器,提供Web控制界面
- 支持以太网SPI模式
- HTTP服务器监听80端口,通过网线连接网络,IP地址由路由器DHCP分配
- 支持访问Web控制界面
2、文本发送功能:通过Web界面发送文本数据
- 提供文本发送(/send/text)接口
- 支持在Web界面的输入框中输入文本并发送
- 发送的文本会在设备日志中显示
3、WiFi扫描功能:搜索周围可用的WiFi热点
- 提供开始扫描(/scan/go)接口
- 提供获取扫描结果(/scan/list)接口
- Web界面上有扫描按钮,点击后显示周围WiFi热点列表
- 显示WiFi的SSID和信号强度信息
## 演示硬件环境
1、Air780EPM开发板一块
2、TYPE-C USB数据线一根,Air780EPM开发板和数据线的硬件接线方式为:
- Air780EPM开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
- TYPE-C USB数据线直接插到开发板的TYPE-C USB座子,另外一端连接电脑USB口;
## 演示软件环境
1、Luatools下载调试工具
2、Air780EPM固件[Air780EPM 版本固件](https://docs.openluat.com/air780epm/luatos/firmware/version/#air780epmluatos)
## 演示核心步骤
### 以太网SPI模式
1、确保CH390H以太网模块正确连接到Air780EPM开发板
3、使用网线将以太网模块连接到路由器或网络交换机
4、使用Luatools烧录内核固件和demo脚本代码
5、烧录成功后,设备自动开机运行并尝试通过以太网连接到网络
6、通过Luatools日志查看设备获取的IP地址(例如:192.168.1.101
7、确保你的电脑连接到同一路由器或网络
8、在浏览器中输入设备的IP地址(如http://192.168.1.101),访问Web控制界面
## Web控制界面功能
在浏览器访问Web控制界面后,你可以使用以下功能:
- 发送文本消息(会显示在设备日志中)
- 点击WiFi扫描按钮,查看周围可用的WiFi热点列表
```lua
[2025-11-28 13:31:17.997][000000000.201] I/user.main project name is httpsrv_testdemo version is 001.000.000
[2025-11-28 13:31:17.999][000000000.207] change from 1 to 4
[2025-11-28 13:31:18.000][000000000.207] SPI_HWInit 552:spi0 speed 25600000,25600000,12
[2025-11-28 13:31:18.002][000000000.208] I/user.netdrv_eth_spi spi open result 0
[2025-11-28 13:31:18.003][000000000.208] D/ch390h CH390H设备(4) SPI id 0 cs 8 irq 255
[2025-11-28 13:31:18.006][000000000.209] D/ch390h adapter 4 netif init ok
[2025-11-28 13:31:25.657][000000008.223] D/DHCP DHCP get ip ready
[2025-11-28 13:31:25.659][000000008.223] D/ulwip adapter 4 ip 192.168.1.2
[2025-11-28 13:31:25.661][000000008.223] D/ulwip adapter 4 mask 255.255.255.0
[2025-11-28 13:31:25.662][000000008.223] D/ulwip adapter 4 gateway 192.168.1.1
[2025-11-28 13:31:25.663][000000008.223] D/ulwip adapter 4 lease_time 86400s
[2025-11-28 13:31:25.665][000000008.224] D/ulwip adapter 4 DNS1:192.168.1.1
[2025-11-28 13:31:25.667][000000008.224] D/net network ready 4, setup dns server
[2025-11-28 13:31:25.668][000000008.225] D/netdrv IP_READY 4 192.168.1.2
[2025-11-28 13:31:25.670][000000008.226] I/user.netdrv_eth_spi.ip_ready_func IP_READY 192.168.1.2 255.255.255.0 192.168.1.1 nil
[2025-11-28 13:31:25.671][000000008.227] I/httpsrv http listen at 192.168.1.2:80
[2025-11-28 13:31:25.674][000000008.227] I/user.HTTP 使AirETH_1000-
[2025-11-28 13:31:25.676][000000008.228] I/user.HTTP 访: http://192.168.1.2
```
通过Luatools工具,可以查看设备的运行日志,包括收到的文本消息和WiFi扫描结果