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,70 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.11.09
@author 陈取德
@usage
本demo主要使用AirCAMERA_1040 SPI摄像头完成一次拍照上传任务
]] --
--[[
必须定义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 = "AirCAMERA_1040_Demo"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 导入netdrv_4g联网状态模块
require "netdrv_4g"
-- 导入gc032a配置表
require "gc032a"
-- 拍照和扫描模式需要在初始化时确认,导入对应模式的DEMO后注释另一个DEMO,拍照和扫描不可同时使用
--[[ 导入photo_to_aircloud拍照上传应用DEMO,该DEMO上传照片至aircloud平台,需要先配置photo_to_aircloud.lua中32行的 project_auth_key ,否则会报nil
project_auth_keyaircloud应用的认证密钥,用于验证上传请求的合法性
获取方式为:登录iot.openluat.com,点击”我的项目“,找到您所使用的模块对应的项目,即可复制项目KEY
当您的模块没有在您的账号下时,请参考https://docs.openluat.com/air780epm/product/attributioniot/ 处理办法,获取项目KEY]] --
require "photo_to_aircloud"
-- 导入take_photo_http_post拍照上传应用DEMOair32.cn为虚拟服务器,仅用于演示,实际使用时请替换为自己的服务器。
-- 与photo_to_aircloud.lua二选一执行即可,不可同时导入
-- require "take_photo_http_post"
-- 导入scan_code扫描二维码应用DEMO
-- require "scan_code"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
@@ -0,0 +1,37 @@
--[[
@module netdrv_4g
@summary “4G网卡”驱动模块
@version 1.0
@date 2025.07.01
@author 朱天华
@usage
本文件为4G网卡驱动模块,核心业务逻辑为:
1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
]]
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_GP then
log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
end
end
local function ip_lose_func(adapter)
if adapter == socket.LWIP_GP then
log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
end
end
-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
-- 设置默认网卡为socket.LWIP_GP
-- 在Air8000上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
-- 在单4G网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
socket.dft(socket.LWIP_GP)
@@ -0,0 +1,293 @@
--[[
@module photo_to_aircloud
@summary AirCAMERA_1040 gc032a摄像头拍照上传应用模块
@version 1.0
@date 2026.4.30
@author 陈取德
@usage
本demo主要使用AirCAMERA_1040 gc032a摄像头完成拍照上传任务
Air780EPM的sys ram内存资源只有2.3M,在使用camera功能时,需要非常谨慎,避免内存泄漏导致系统崩溃,需要注意以下情况:
1、exmcamera业务执行时,必须要确保有连续的内存空间,否则会导致excamera.open()初始化失败,需要的空间计算方式为:
( 摄像头像素高 X 摄像头像素宽 X 2 ),用“GC032A”举例,内存空间为:640 * 480 * 2 = 614400 (B) ≈ 620KB
2、当使用ZBUFF方式存储照片,或者将照片放到 /ram/ 路径下保存时,必须确保有更多的内存连续空间,需要的总空间大小计算方式为:
( 摄像头像素高 X 摄像头像素宽 X 3.5 ),用“GC032A”举例,内存空间为:640 * 480 * 3.5 = 1075200 (B) ≈ 1MB
3、本DEMO为常规模式下,摄像头稳定供电使用,DEMO逻辑为excamera.open()初始化完摄像头后,只需要重复调用excamera.photo()拍照即可,不需要反复的初始化关闭摄像头;
4、在实际应用中,务必将摄像头初始化的优先级排前,因为摄像头业务占用内存过大,尽早的初始化可以确保摄像头初始化时拿到足够的连续内存,确保后续拍照时能够正常执行;
5、Air780EPM的内存非常有限,在摄像头需求高的产品设计中,请选择Air780EHM/EGH/EHV、Air8000、Air700ECH这类 8MB + 8MB 的SOC,更大的内存才是摄像头业务稳定运行的最优解;
6、低功耗模式下使用excamera请参考lowpower DEMO,链接:https://docs.openluat.com/air780epm/luatos/app/lowpower/sleep/#_1
]] --
-- 摄像头拍照模块
-- 功能:提供摄像头初始化、拍照和资源管理功能
-- 引入excamera扩展库模块
local excamera = require "excamera"
-- 引入httpplus扩展库模块
local httpplus = require "httpplus"
-- 引入exmux扩展库模块
local exmux = require "exmux"
-- 导入excloud库
local excloud = require "excloud"
-- pm.ioVol(pm.IOVOL_ALL_GPIO, 2800)
-- 配置excloud参数
local project_auth_key = "123456"
-- 硬件I2C/SPI配置,当您使用合宙开发板时,请根据具体的开发板版本选择对应的变量,
-- exmux库将会自动处理开发板上的I2C/SPI外设,确保总线通讯正常
-- 当您使用自己的制作的板子,请参考exmux库的文档,配置对应的变量:https://docs.openluat.com/osapi/ext/exmux/
-- local HARDWARE_ENV = "DEV_BOARD_8000_V2.0"
-- local HARDWARE_ENV = "DEV_BOARD_780_V1.2"
local HARDWARE_ENV = "DEV_BOARD_780_V1.3"
-- 定义照片保存方式,有三种类型:
-- 1、ZBUFF保存,输入"ZBUFF"即可,excamera库会自动处理ZBUFF
-- 2、保存到内存文件系统中,路径名需指向/ram/文件夹
-- 3、保存到内置FLASH文件系统中
-- 选择其中一个即可,注释另两个路径变量
local save_method = "ZBUFF"
-- local save_method = "/ram/test.jpg"
-- local save_method = "/test.jpg"
--[[
excloud事件回调函数
参数:
event: 事件类型字符串
data: 事件数据,根据事件类型不同而不同
事件类型说明:
connect_result: 连接结果
auth_result: 认证结果
disconnect: 断开连接
reconnect_failed: 重连失败
]]
function on_excloud_event(event, data)
-- 打印事件信息
log.info("用户回调函数", event, json.encode(data))
-- 处理连接结果事件
if event == "connect_result" then
if data.success then
log.info("连接成功")
-- 发布连接成功消息,通知其他任务
sys.publish("aircloud_connected")
else
log.info("连接失败: " .. (data.error or "未知错误"))
end
-- 处理认证结果事件
elseif event == "auth_result" then
if data.success then
log.info("认证成功")
else
log.info("认证失败: " .. data.message)
end
-- 处理断开连接事件
elseif event == "disconnect" then
log.warn("与服务器断开连接")
-- 处理重连失败事件
elseif event == "reconnect_failed" then
log.info("重连失败,已尝试 " .. data.count .. "")
end
end
-- 注册excloud事件回调函数
excloud.on(on_excloud_event)
--[[
excloud任务函数
功能:
1. 等待网络连接就绪
2. 配置excloud参数
3. 初始化并开启excloud服务
4. 启动自动心跳
]]
local function excloud_task_func()
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("excloud_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 检查是否使用默认的示例key
if project_auth_key == "123" or project_auth_key == "123456" then
log.warn("photo_to_aircloud",
"请改为自己的key,如不知道对应key的可以查看main.lua 54-57行的指导进行添加key的操作")
end
local ok, err_msg = excloud.setup({
use_getip = true, -- 使用getip服务
device_type = 1, -- 4G设备
auth_key = project_auth_key, -- 认证密钥
transport = "tcp", -- 使用TCP传输
auto_reconnect = true, -- 自动重连
reconnect_interval = 10, -- 重连间隔(秒)
max_reconnect = 5, -- 最大重连次数
mtn_log_enabled = true, -- 启用运维日志
mtn_log_blocks = 2, -- 日志文件块数
mtn_log_write_way = excloud.MTN_LOG_CACHE_WRITE -- 缓存写入方式
})
-- 检查初始化是否成功
if not ok then
log.info("初始化失败: " .. err_msg)
return
end
log.info("excloud初始化成功")
-- 开启excloud服务
local ok, err_msg = excloud.open()
if not ok then
log.info("开启excloud服务失败: " .. err_msg)
return
end
log.info("excloud服务已开启")
-- 启动自动心跳,默认5分钟一次的心跳
excloud.start_heartbeat()
log.info("自动心跳已启动")
end
--[[
照片上传任务函数
功能:
1. 等待excloud连接建立
2. 等待图片数据
3. 上传图片到云端
4. 处理上传结果
]]
function upload_image_fun(image)
if excloud.status().is_connected then
log.info("开始上传图片")
if image then
local ok, err = excloud.upload_image(image, "test.jpg")
if ok then
log.info("图片上传成功")
return true
else
log.error("图片上传失败:", err)
return false
end
else
log.warn("图片数据为空")
return false
end
end
log.info("excloud连接已断开,等待重连")
return false
end
-- 拍照功能函数
-- 作用:循环监听拍照事件,执行摄像头初始化、拍照和资源释放
local function capture_func()
-- 定义变量用于存储操作结果和数据
local result, data, err
-- 增加重试次数,最多重试5次,避免日志量过大
local retry_count = 0
-- 初始化开发板
exmux.setup(HARDWARE_ENV)
-- 出现异常后重新初始化,最多重试5次
while retry_count < 5 do
-- 配置gc032a摄像头参数表
local spi_camera_param = {
id = "gc032a", -- SPI摄像头仅支持"gc032a"、"gc0310"、"bf30a2",请带引号填写
i2c_id = 1, -- 模块上使用的I2C编号
work_mode = 0, -- 工作模式,0为拍照模式,1为扫描模式
save_path = save_method, -- 拍照结果存储路径,可用"ZBUFF"交由excamera库内部管理
camera_pwr = 2, -- 摄像头使能管脚,填写GPIO号即可,无则填nil
camera_pwdn = 5, -- 摄像头pwdn开关脚,填写GPIO号即可,无则填nil
camera_light = nil -- 摄像头补光灯控制管脚,填写GPIO号即可,无则填nil
}
-- 打开外设分组
exmux.open("i2c1")
-- 初始化摄像头,传入配置参数
result = excamera.open(spi_camera_param)
-- 记录摄像头初始化状态
log.info("初始化状态", result)
-- 判断摄像头初始化是否成功,不成功则直接关闭,成功则启动拍照
-- 无限循环,持续等待拍照事件
while result do
-- 等待外部触发拍照事件(ONCE_CAPTURE)
sys.waitUntil("ONCE_CAPTURE")
-- 执行拍照操作
result, data = excamera.photo()
-- 拍照执行完成则上传,否则关闭摄像头
if result then
-- 通过网卡状态判断WIFI是否连接成功,WIFI连接成功后再运行照片上传任务。
while not socket.adapter(socket.dft()) do
-- 在此处阻塞等待WIFI连接成功的消息"IP_READY",避免联网过快,丢失了"IP_READY"信息而导致一直被卡住。
-- 或者等待30秒超时退出阻塞等待状态
log.warn("tcp_client_main_task_func", "wait IP_READY")
sys.waitUntil("IP_READY", 30000)
end
upload_image_fun(data)
end
-- 判断是否ZBUFF存储方式,如果是文件系统保存则删除本地文件
if save_method ~= "ZBUFF" then
os.remove(spi_camera_param.save_path)
end
end
-- 关闭摄像头,释放资源
-- 使用ZBUFF存储方式时,close传入true后,excamera内部创建的ZBUFF会缩减至0字节,放出内存但是不释放ZBUFF,便于下次拍照时调用;
-- 重复申请和释放ZBUFF会导致垃圾内存堆积,影响系统内存;
excamera.close(true)
-- 关闭外设分组
exmux.close("i2c1")
-- 拍照出错,等待5秒,重试摄像头初始化
sys.wait(5000)
-- 重试次数增加
retry_count = retry_count + 1
log.info("retry_count", retry_count)
end
-- 重试5次后,提示用户检查摄像头连接或重启设备
log.info("camera init failed, please check the camera connection or reboot, retry_count:", retry_count)
end
-- 内存检查函数
-- 作用:定期监控系统内存使用情况
local function memory_check()
-- 无限循环,定期检查内存
while true do
-- 等待3秒
sys.wait(3000)
-- 打印系统内存使用信息
log.info("sys ram", rtos.meminfo("sys"))
-- 打印Lua虚拟机内存使用信息
log.info("lua ram", rtos.meminfo("lua"))
end
end
-- AirCAMERA_1040 DEMO应用触发函数,每30S触发一次拍照
local function AirCAMERA_1040_func()
while true do
sys.wait(30000)
sys.publish("ONCE_CAPTURE")
end
end
-- 按键触发函数,用于手动拍照拍照
local function press_key()
log.info("boot press")
sys.publish("ONCE_CAPTURE")
end
-- 配置BOOT按键引脚为输入模式,下拉电阻,上升沿触发
gpio.setup(0, press_key, gpio.PULLDOWN, gpio.RISING)
gpio.debounce(0, 100, 1)
-- 启动excloud连接任务
sys.taskInit(excloud_task_func)
-- 创建拍照功能任务
-- 作用:在单独的任务中运行拍照逻辑
sys.taskInit(capture_func)
-- 创建内存监控任务
-- 作用:在单独的任务中运行内存监控逻辑
sys.taskInit(memory_check)
-- 创建拍照触发任务
-- 作用:每30秒触发一次拍照上传业务
sys.taskInit(AirCAMERA_1040_func)
@@ -0,0 +1,251 @@
# AirCAMERA_1040 DEMO
## 演示功能概述
本示例主要是展示 AirCAMERA_1040 的使用,本地拍摄照片后通过 httpplus 扩展库将图片上传至 air32.com
1、main.lua:主程序入口
2、take_photo_http_post.lua:执行拍照后使用推送照片上传至air32.cn服务器,该服务器为虚拟服务器,仅用于演示,实际使用时请替换为自己的服务器。
3、scan_code.lua:扫描二维码应用DEMO模块
4、netdrv_4g.lua:联网状态检测模块
5、photo_to_aircloud.lua:执行拍照后使用推送照片上传至aircloud平台
注意事项:
- 拍照或者扫描模式需要在摄像头初始化时确定
- 如使用拍照模式就无法使用扫描模式,扫描模式同理
- 需要拍照后执行扫描的话需要重新初始化
- 所以拍照和扫描不可同时使用,如需切换模式需重新初始化
- 因为Air32.com平台已经不开放使用了,所以该DEMO仅作上传照片至服务器的演示作用,使用时请将上传URL修改为您自己的服务器地址,或者通过excloud扩展库上传到合宙IOT平台uploadtest.luatos.com
## 演示功能概述
### 1、主程序入口模块(main.lua
- 初始化项目信息和版本号
- 初始化看门狗,并定时喂狗
- 启动一个循环定时器,每隔 3 秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况方便分析内存使用是否有异常
- 加载 netdrv_4g 4G联网状态检测模块
- 加载 photo_to_aircloud 拍照上传至aircloud平台模块,务必先配置auth_key,否则会报错
- 加载 take_photo_http_post 模块
### 2、4G联网状态检测模块(netdrv_4g.lua
- 订阅"IP_READY"消息,收到消息后打印联网成功日志
- 订阅"IP_LOSE"消息,收到消息后打印联网失败日志
### 3、拍照上传服务器演示模块(take_photo_http_post.lua
- 每 30 秒触发一次拍照:AirCAMERA_1040_func()
- 每 3 秒打印一次系统和 LUA 的内存信息:memory_check()
- 配置摄像头信息表:spi_camera_param
- 初始化摄像头:excamera.open()
- 执行拍照:excamera.photo()
- 上传照片:httpplus.request()
- 关闭摄像头:excamera.close()
### 4、扫描二维码应用DEMOscan_code.lua
- 每 30 秒触发一次拍照:AirCAMERA_1040_func()
- 每 3 秒打印一次系统和 LUA 的内存信息:memory_check()
- 配置摄像头信息表:spi_camera_param
- 初始化摄像头:excamera.open()
- 执行扫描:excamera.scan()
- 关闭摄像头:excamera.close()
### 5、拍照上传至aircloud平台业务模块(photo_to_aircloud.lua
- 配置excloud链接,启动心跳包
- 开启自动重连机制,断网后自动重连
- 订阅excloud_post_image_done消息,收到消息后打印上传结果:log.info("excloud_post_image_func", err)
- 每 30 秒触发一次拍照:AirCAMERA_1040_func()
- 每 3 秒打印一次系统和 LUA 的内存信息:memory_check()
- 注册excloud回调:on_excloud_event()
- 配置excloud链接,启动心跳包:excloud_task_func()
- 照片上传任务函数:upload_image_fun()
- 配置摄像头信息表:spi_camera_param
- 初始化摄像头:excamera.open()
- 执行拍照:excamera.photo()
- 上传照片:httpplus.request()
- 关闭摄像头:excamera.close()
## 演示硬件环境
1、Air780EPM 开发板一块
2、TYPE-C USB 数据线一根
3、合宙标准配件 AirCAMERA_1040 一个
4、Air780EPM 开发板和合宙标准配件 AirCAMERA_1040 的硬件接线方式为
Air780EPM 开发板通过 TYPE-C USB 口供电;(侧面拨码拨至USB供电)
TYPE-C USB 数据线直接插到开发板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;
AirCAMERA_1040 配件板插入Air780EPM 开发板的SPI摄像头座子中
## 演示软件环境
1、Luatools 下载调试工具:[https://docs.openluat.com/air780epm/common/Luatools/](https://docs.openluat.com/air780epm/common/Luatools/)
2、固件版本:LuatOS-SoC_V2018_Air780EPM_1,固件地址,如有最新固件请用最新 https://docs.openluat.com/air780epm/luatos/firmware/version/
## 演示核心步骤
1、搭建硬件环境;
2、烧录 DEMO 代码;
3、等待自动拍照完成后上传平台,LUATOOLS会有如下打印;
```lua
[2026-04-08 17:36:12.996][000000000.205] I/user.main AirCAMERA_1040_Demo 001.999.000
[2026-04-08 17:36:12.999][000000000.325] W/user.excloud_task_func wait IP_READY 1 1
[2026-04-08 17:36:13.002][000000000.326] I/user.exmux DEV_BOARD_780_V1.2
[2026-04-08 17:36:13.504][000000001.326] W/user.excloud_task_func wait IP_READY 1 1
[2026-04-08 17:36:14.408][000000002.166] I/mobile sim0 sms ready
[2026-04-08 17:36:14.412][000000002.167] D/mobile cid1, state0
[2026-04-08 17:36:14.423][000000002.168] D/mobile bearer act 0, result 0
[2026-04-08 17:36:14.430][000000002.168] D/mobile NETIF_LINK_ON -> IP_READY
[2026-04-08 17:36:14.438][000000002.169] I/user.netdrv_4g.ip_ready_func IP_READY 10.122.36.125 255.255.255.255 0.0.0.0 nil
[2026-04-08 17:36:14.445][000000002.173] I/user.[excloud]4G设备 IMEI: 867920071472378 MUID: 20250604131416A755490A2989432885
[2026-04-08 17:36:14.451][000000002.189] I/user.exmtn 1
[2026-04-08 17:36:14.457][000000002.189] I/user.exmtn 2
[2026-04-08 17:36:14.464][000000002.190] I/user.exmtn 0
[2026-04-08 17:36:14.468][000000002.190] I/user.exmtn false
[2026-04-08 17:36:14.473][000000002.193] I/user.exmtn
[2026-04-08 17:36:14.479][000000002.197] I/user.exmtn : 8.00 KB (2 × 4096 ), 32.00 KB (4 )
[2026-04-08 17:36:14.484][000000002.197] I/user.[excloud]
[2026-04-08 17:36:14.488][000000002.198] I/user.[excloud]excloud.setup ID: 867920071472378
[2026-04-08 17:36:14.494][000000002.198] I/user.excloud初始化成功
[2026-04-08 17:36:14.499][000000002.198] I/user.[excloud]...
[2026-04-08 17:36:14.505][000000002.199] I/user.[excloud]excloud.getip : 3 key: sh5g0OTP7ThOSlGKmE5jiEMbOBqQWyw9-867920071472378
[2026-04-08 17:36:14.512][000000002.205] D/socket connect to gps.openluat.com,443
[2026-04-08 17:36:14.516][000000002.209] dns_run 676:gps.openluat.com state 0 id 1 ipv6 0 use dns server2, try 0
[2026-04-08 17:36:14.520][000000002.231] D/mobile TIME_SYNC 0 tm 1775640975
[2026-04-08 17:36:14.526][000000002.273] dns_run 693:dns all done ,now stop
[2026-04-08 17:36:15.179][000000003.008] I/user.httpplus ,
[2026-04-08 17:36:15.211][000000003.037] I/user.[excloud]excloud.getip响应 HTTP Code: 200 Body:
[2026-04-08 17:36:15.218][000000003.037] {"msg":"ok","conninfo":{"ipv4":"124.71.128.165","port":9108},"imginfo":{"url":"https://gps.openluat.com/aircloud/air_up/image","data_key":"f","data_param":{"key":"8bcWhwHK4pZR7gTbvPUdxL1TAbW5H3C65MFcC","tip":""}},"audinfo":{"url":"https://gps.openluat.com/aircloud/air_up/audio","data_key":"f","data_param":{"key":"8bcWhwHK4pZR7gTbvPUdxL1TAbW5H3C65MFcC","tip":""}},"mtninfo":{"url":"https://gps.openluat.com/aircloud/air_up/file","data_key":"f","data_param":{"key":"8bcWhwHK4pZR7gTbvPUdxL1TAbW5H3C65MFcC","tip":""}},"qrinfo":{"url":"https://gps.openluat.com/#/redirect?key=_5oruyMsMk5Ny5ZajRyBqaZEqqaPjVSb93qH"}}
[2026-04-08 17:36:15.225][000000003.038] I/user.[excloud]excloud.getip响应 HTTP Code: 200 Body:
[2026-04-08 17:36:15.231][000000003.038] {"msg":"ok","conninfo":{"ipv4":"124.71.128.165","port":9108},"imginfo":{"url":"https://gps.openluat.com/aircloud/air_up/image","data_key":"f","data_param":{"key":"8bcWhwHK4pZR7gTbvPUdxL1TAbW5H3C65MFcC","tip":""}},"audinfo":{"url":"https://gps.openluat.com/aircloud/air_up/audio","data_key":"f","data_param":{"key":"8bcWhwHK4pZR7gTbvPUdxL1TAbW5H3C65MFcC","tip":""}},"mtninfo":{"url":"https://gps.openluat.com/aircloud/air_up/file","data_key":"f","data_param":{"key":"8bcWhwHK4pZR7gTbvPUdxL1TAbW5H3C65MFcC","tip":""}},"qrinfo":{"url":"https://gps.openluat.com/#/redirect?key=_5oruyMsMk5Ny5ZajRyBqaZEqqaPjVSb93qH"}}
[2026-04-08 17:36:15.237][000000003.039] I/user.[excloud]TCP/UDP连接信息 host: 124.71.128.165 port: 9108 key: nil
[2026-04-08 17:36:15.242][000000003.039] I/user.[excloud]
[2026-04-08 17:36:15.247][000000003.040] I/user.[excloud]
[2026-04-08 17:36:15.255][000000003.040] I/user.[excloud]
[2026-04-08 17:36:15.262][000000003.040] I/user.[excloud]
[2026-04-08 17:36:15.266][000000003.041] I/user.[excloud]excloud.getip : 124.71.128.165 9108
[2026-04-08 17:36:15.271][000000003.041] I/user.[excloud]excloud.getip : true : {"ipv4":"124.71.128.165","port":9108}
[2026-04-08 17:36:15.276][000000003.042] I/user.[excloud] host: 124.71.128.165 port: 9108 transport: tcp
[2026-04-08 17:36:15.280][000000003.042] I/user.[excloud]TCP连接
[2026-04-08 17:36:15.286][000000003.043] D/socket connect to 124.71.128.165,9108
[2026-04-08 17:36:15.291][000000003.044] network_socket_connect 1610:network 1-0 local port auto select 51002
[2026-04-08 17:36:15.295][000000003.045] I/user.[excloud]TCP连接结果 true false
[2026-04-08 17:36:15.301][000000003.046] I/user.[excloud]excloud service started
[2026-04-08 17:36:15.306][000000003.046] I/user.excloud服务已开启
[2026-04-08 17:36:15.311][000000003.047] I/user.[excloud]excloud 300
[2026-04-08 17:36:15.315][000000003.047] I/user.
[2026-04-08 17:36:15.323][000000003.127] network_default_socket_callback 1123:before process socket 1,event:0xf2000009(),state:3(),wait:2()
[2026-04-08 17:36:15.328][000000003.127] network_default_socket_callback 1127:after process socket 1,state:5(线),wait:0()
[2026-04-08 17:36:15.332][000000003.128] I/user.[excloud]socket cb userdata: 0C1843F0 33554449 0
[2026-04-08 17:36:15.337][000000003.129] I/user.[excloud]socket TCP连接成功
[2026-04-08 17:36:15.342][000000003.129] I/user. connect_result {"success":true}
[2026-04-08 17:36:15.346][000000003.129] I/user.
[2026-04-08 17:36:15.352][000000003.133] I/user.[excloud] 16 3 sh5g0OTP7ThOSlGKmE5jiEMbOBqQWyw9-867920071472378-20250604131416A755490A2989432885
[2026-04-08 17:36:15.357][000000003.134] I/user.[excloud]tlv发送数据长度4 85
[2026-04-08 17:36:15.362][000000003.135] I/user.[excloud] y r7
[2026-04-08 17:36:15.371][000000003.137] I/user. send_result {"sequence_num":1,"success":true,"error_msg":"Send successful"}
[2026-04-08 17:36:15.376][000000003.143] I/user.[excloud] 101
[2026-04-08 17:36:15.385][000000003.207] network_default_socket_callback 1123:before process socket 1,event:0xf2000004(),state:5(线),wait:3()
[2026-04-08 17:36:15.390][000000003.207] network_default_socket_callback 1127:after process socket 1,state:5(线),wait:0()
[2026-04-08 17:36:15.395][000000003.208] I/user.[excloud]socket cb userdata: 0C1843F0 33554450 0
[2026-04-08 17:36:15.403][000000003.208] I/user.[excloud]socket
[2026-04-08 17:36:15.491][000000003.327] I/user.sys ram 2375920 80156 121112
[2026-04-08 17:36:15.496][000000003.328] I/user.lua ram 1048568 174392 190984
[2026-04-08 17:36:18.498][000000006.328] I/user.sys ram 2375920 80156 121112
[2026-04-08 17:36:18.531][000000006.329] I/user.lua ram 1048568 174712 190984
[2026-04-08 17:36:21.500][000000009.329] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:21.518][000000009.330] I/user.lua ram 1048568 175240 190984
[2026-04-08 17:36:24.496][000000012.330] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:24.501][000000012.331] I/user.lua ram 1048568 175440 190984
[2026-04-08 17:36:27.508][000000015.331] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:27.512][000000015.332] I/user.lua ram 1048568 175632 190984
[2026-04-08 17:36:30.505][000000018.332] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:30.511][000000018.333] I/user.lua ram 1048568 175832 190984
[2026-04-08 17:36:33.503][000000021.333] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:33.510][000000021.334] I/user.lua ram 1048568 176024 190984
[2026-04-08 17:36:36.506][000000024.334] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:36.510][000000024.335] I/user.lua ram 1048568 176216 190984
[2026-04-08 17:36:39.512][000000027.335] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:39.524][000000027.336] I/user.lua ram 1048568 176416 190984
[2026-04-08 17:36:42.504][000000030.327] I/user.exmux pwr2 (23)
[2026-04-08 17:36:42.510][000000030.328] I/user.exmux pwr1 (2)
[2026-04-08 17:36:42.513][000000030.328] I/user.exmux i2c1
[2026-04-08 17:36:42.517][000000030.329] I2C_MasterSetup 426:I2C1, Total 65 HCNT 22 LCNT 40
[2026-04-08 17:36:42.522][000000030.329] CSPI_Setup 1924:APB MP 102400000
[2026-04-08 17:36:42.551][000000030.388] I/user. true
[2026-04-08 17:36:42.558][000000030.388] CSPI_Rx 2022:block len 7680, total block 80
[2026-04-08 17:36:42.561][000000030.389] I/user. /ram/test.jpg
[2026-04-08 17:36:42.564][000000030.389] luat_camera_capture_config 700:0,0,0,0
[2026-04-08 17:36:42.568][000000030.389] luat_camera_capture 676:save file in /ram/test.jpg
[2026-04-08 17:36:42.572][000000030.391] I/user.sys ram 2375920 696088 696224
[2026-04-08 17:36:42.575][000000030.391] I/user.lua ram 1048568 178544 190984
[2026-04-08 17:36:43.126][000000030.955] I/user. 58612
[2026-04-08 17:36:43.138][000000030.956] I/user.
[2026-04-08 17:36:43.144][000000030.956] I/user.
[2026-04-08 17:36:43.149][000000030.957] I/user.[excloud] : 1 : /ram/test.jpg : 58612
[2026-04-08 17:36:43.155][000000030.957] I/user.[excloud] : 1 : test.jpg : 58612
[2026-04-08 17:36:43.160][000000030.958] I/user.[excloud] 23 0 0
[2026-04-08 17:36:43.164][000000030.959] I/user.[excloud] 784 0 1
[2026-04-08 17:36:43.169][000000030.960] I/user.[excloud] 785 3 test.jpg
[2026-04-08 17:36:43.172][000000030.961] I/user.[excloud] 786 0 58612
[2026-04-08 17:36:43.177][000000030.963] I/user.[excloud]tlv发送数据长度4 36
[2026-04-08 17:36:43.182][000000030.964] I/user.[excloud] y r7
[2026-04-08 17:36:43.185][000000030.968] I/user. send_result {"sequence_num":2,"success":true,"error_msg":"Send successful"}
[2026-04-08 17:36:43.189][000000030.973] I/user.[excloud] 52
[2026-04-08 17:36:43.195][000000030.985] I/user.[excloud]HTTP请求 URL: https://gps.openluat.com/aircloud/air_up/image
[2026-04-08 17:36:43.199][000000030.989] dns_run 676:gps.openluat.com state 0 id 2 ipv6 0 use dns server2, try 0
[2026-04-08 17:36:43.298][000000031.126] dns_run 693:dns all done ,now stop
[2026-04-08 17:36:43.304][000000031.135] network_default_socket_callback 1123:before process socket 1,event:0xf2000004(),state:5(线),wait:3()
[2026-04-08 17:36:43.308][000000031.135] network_default_socket_callback 1127:after process socket 1,state:5(线),wait:0()
[2026-04-08 17:36:43.312][000000031.136] I/user.[excloud]socket cb userdata: 0C1843F0 33554450 0
[2026-04-08 17:36:43.318][000000031.137] I/user.[excloud]socket
[2026-04-08 17:36:45.565][000000033.392] I/user.sys ram 2375920 876224 1064884
[2026-04-08 17:36:45.570][000000033.393] I/user.lua ram 1048568 353216 469512
[2026-04-08 17:36:46.436][000000034.268] I/http http close c267f50
[2026-04-08 17:36:46.441][000000034.270] I/user.[excloud]excloud.getip文件上传响应 HTTP Code: 200 Body: {"info":"aircloud./aircloud/air_up/image","code":0,"trace":"code:aircloud./aircloud/air_up/image, trcace:","log":"^^^","value":{"uri":"/vsa/aircloud_image/FFZDbqsuzrLpbgPfzVd7De/2026-04/867920071472378/20260408173647_test.jpg","size":"57.00KB","thumb":"/vsa/aircloud_image/FFZDbqsuzrLpbgPfzVd7De/2026-04/867920071472378/20260408173647_testt.jpg"}}
[2026-04-08 17:36:46.446][000000034.271] I/user.[excloud] URL: /vsa/aircloud_image/FFZDbqsuzrLpbgPfzVd7De/2026-04/867920071472378/20260408173647_test.jpg
[2026-04-08 17:36:46.450][000000034.271] I/user.[excloud] 24 0 0
[2026-04-08 17:36:46.457][000000034.274] I/user.[excloud] 784 0 1
[2026-04-08 17:36:46.461][000000034.276] I/user.[excloud] 785 3 test.jpg
[2026-04-08 17:36:46.466][000000034.277] I/user.[excloud] 787 0 0
[2026-04-08 17:36:46.470][000000034.278] I/user.[excloud]tlv发送数据长度4 36
[2026-04-08 17:36:46.475][000000034.279] I/user.[excloud] y r7
[2026-04-08 17:36:46.479][000000034.282] I/user. send_result {"sequence_num":3,"success":true,"error_msg":"Send successful"}
[2026-04-08 17:36:46.486][000000034.287] I/user.[excloud] 52
[2026-04-08 17:36:46.491][000000034.287] I/user.
[2026-04-08 17:36:46.498][000000034.288] I/user.exmux pwr2 (23)
[2026-04-08 17:36:46.502][000000034.289] I/user.exmux pwr1 (2)
[2026-04-08 17:36:46.507][000000034.289] I/user.exmux i2c1
[2026-04-08 17:36:46.512][000000034.347] network_default_socket_callback 1123:before process socket 1,event:0xf2000004(),state:5(线),wait:3()
[2026-04-08 17:36:46.519][000000034.347] network_default_socket_callback 1127:after process socket 1,state:5(线),wait:0()
[2026-04-08 17:36:46.524][000000034.348] I/user.[excloud]socket cb userdata: 0C1843F0 33554450 0
[2026-04-08 17:36:46.529][000000034.349] I/user.[excloud]socket
```
4、等待自动扫描任务完成后,LUATOOLS会有如下打印;
```lua
[2025-11-21 15:19:17.310][000000000.269] I2C_MasterSetup 426:I2C1, Total 65 HCNT 22 LCNT 40
[2025-11-21 15:19:17.313][000000000.332] I/user. true
[2025-11-21 15:19:17.316][000000000.332] CSPI_Rx 2000:block len 7680, total block 40
[2025-11-21 15:19:17.319][000000000.541] I/user. Air780EPM
[2025-11-21 15:19:17.323][000000000.542] I/user. Air780EPM
[2025-11-21 15:19:17.327][000000000.542] I/user.Scan result : Air780EPM
```
5、登录 https://iot.luatos.com/ 查看拍摄的照片;
@@ -0,0 +1,99 @@
--[[
@module scan_code
@summary AirCAMERA_1040 gc032a摄像头扫描二维码应用模块
@version 1.0
@date 2025.11.09
@author 陈取德
@usage
本demo主要使用AirCAMERA_1040 gc032a摄像头完成一次扫描二维码任务
]] --
-- 摄像头扩展库模块
-- 功能:提供摄像头初始化、扫描和资源管理功能
-- 引入excamera扩展库模块
local excamera = require "excamera"
-- 引入exmux扩展库模块
local exmux = require "exmux"
-- 硬件I2C/SPI配置,当您使用合宙开发板时,请根据具体的开发板版本选择对应的变量,
-- exmux库将会自动处理开发板上的I2C/SPI外设,确保总线通讯正常
-- 当您使用自己的制作的板子,请参考exmux库的文档,配置对应的变量:https://docs.openluat.com/osapi/ext/exmux/
-- local HARDWARE_ENV = "DEV_BOARD_8000_V2.0"
local HARDWARE_ENV = "DEV_BOARD_780_V1.2"
-- local HARDWARE_ENV = "DEV_BOARD_780_V1.3"
-- 扫描功能函数
-- 作用:循环监听扫描事件,执行摄像头初始化、扫描和资源释放
local function scan_code_func()
-- 定义变量用于存储操作结果和数据
local result, data
-- 初始化外设分组开关状态
exmux.setup(HARDWARE_ENV)
-- 无限循环,持续等待扫描事件
while true do
-- 配置gc032a摄像头参数表
local spi_camera_param = {
id = "gc032a", -- SPI摄像头仅支持"gc032a"、"gc0310"、"bf30a2",请带引号填写
i2c_id = 1, -- 模块上使用的I2C编号
work_mode = 1, -- 工作模式,0为拍照模式,1为扫描模式
save_path = nil, -- 扫描结果为字符串返回,使用变量赋值既可
camera_pwr = 2, -- 摄像头使能管脚,填写GPIO号即可,无则填nil
camera_pwdn = 5, -- 摄像头pwdn开关脚,填写GPIO号即可,无则填nil
camera_light = nil -- 摄像头补光灯控制管脚,填写GPIO号即可,无则填nil
}
-- 等待外部触发扫描事件(SCAN_CODE)
sys.waitUntil("SCAN_CODE")
-- 打开外设分组
exmux.open("i2c1")
-- 初始化摄像头,传入配置参数
result = excamera.open(spi_camera_param)
-- 记录摄像头初始化状态
log.info("初始化状态", result)
-- 判断摄像头初始化是否成功,不成功则直接关闭,成功则启动扫描
if result then
-- 执行扫描操作,5秒超时
result, data = excamera.scan(5000)
-- 扫描执行完成则上传,否则关闭摄像头
if result then
log.info("Scan result :", data )
end
end
-- 关闭摄像头,释放资源
excamera.close()
-- 关闭外设分组
exmux.close("i2c1")
end
end
-- 内存检查函数
-- 作用:定期监控系统内存使用情况
local function memory_check()
-- 无限循环,定期检查内存
while true do
-- 等待3秒
sys.wait(3000)
-- 打印系统内存使用信息
log.info("sys ram", rtos.meminfo("sys"))
-- 打印Lua虚拟机内存使用信息
log.info("lua ram", rtos.meminfo("lua"))
end
end
-- AirCAMERA_1040 DEMO应用触发函数,每30S触发一次扫描
local function AirCAMERA_1040_func()
while true do
sys.publish("SCAN_CODE")
sys.wait(30000)
end
end
-- 创建扫描功能任务
-- 作用:在单独的任务中运行扫描逻辑
sys.taskInit(scan_code_func)
-- 创建内存监控任务
-- 作用:在单独的任务中运行内存监控逻辑
sys.taskInit(memory_check)
-- 创建扫描触发任务
-- 作用:每30秒触发一次扫描二维码业务
sys.taskInit(AirCAMERA_1040_func)
@@ -0,0 +1,142 @@
--[[
@module take_photo_http_post
@summary AirCAMERA_1040 gc032a摄像头拍照上传应用模块
@version 1.0
@date 2025.11.09
@author 陈取德
@usage
本demo主要使用AirCAMERA_1040 gc032a摄像头完成一次拍照上传任务
]] -- 摄像头拍照模块
-- 功能:提供摄像头初始化、拍照和资源管理功能
-- 引入excamera扩展库模块
local excamera = require "excamera"
-- 引入httpplus扩展库模块
local httpplus = require "httpplus"
-- 引入exmux扩展库模块
local exmux = require "exmux"
-- 硬件I2C/SPI配置,当您使用合宙开发板时,请根据具体的开发板版本选择对应的变量,
-- exmux库将会自动处理开发板上的I2C/SPI外设,确保总线通讯正常
-- 当您使用自己的制作的板子,请参考exmux库的文档,配置对应的变量:https://docs.openluat.com/osapi/ext/exmux/
-- local HARDWARE_ENV = "DEV_BOARD_8000_V2.0"
local HARDWARE_ENV = "DEV_BOARD_780_V1.2"
-- local HARDWARE_ENV = "DEV_BOARD_780_V1.3"
-- 定义照片保存方式,有三种类型:
-- 1、ZBUFF保存,输入"ZBUFF"即可,excamera库会自动处理ZBUFF
-- 2、保存到内存文件系统中,路径名需指向/ram/文件夹
-- 3、保存到内置FLASH文件系统中
-- 选择其中一个即可,注释另两个路径变量
local save_method = "ZBUFF"
-- local save_method = "/ram/test.jpg"
-- local save_method = "/test.jpg"
-- 拍照功能函数
-- 作用:循环监听拍照事件,执行摄像头初始化、拍照和资源释放
local function capture_func()
-- 定义变量用于存储操作结果和数据
local result, data
-- 初始化外设分组开关状态
exmux.setup(HARDWARE_ENV)
-- 无限循环,持续等待拍照事件
while true do
-- 配置gc032a摄像头参数表
local spi_camera_param = {
id = "gc032a", -- SPI摄像头仅支持"gc032a"、"gc0310"、"bf30a2",请带引号填写
i2c_id = 1, -- 模块上使用的I2C编号
work_mode = 0, -- 工作模式,0为拍照模式,1为扫描模式
save_path = save_method, -- 拍照结果存储路径,可用"ZBUFF"交由excamera库内部管理
camera_pwr = 2, -- 摄像头使能管脚,填写GPIO号即可,无则填nil
camera_pwdn = 5, -- 摄像头pwdn开关脚,填写GPIO号即可,无则填nil
camera_light = nil -- 摄像头补光灯控制管脚,填写GPIO号即可,无则填nil
}
-- 等待外部触发拍照事件(ONCE_CAPTURE)
sys.waitUntil("ONCE_CAPTURE")
-- 打开外设分组
exmux.open("i2c1")
-- 初始化摄像头,传入配置参数
result = excamera.open(spi_camera_param)
-- 记录摄像头初始化状态
log.info("初始化状态", result)
-- 判断摄像头初始化是否成功,不成功则直接关闭,成功则启动拍照
if result then
-- 执行拍照操作
result, data = excamera.photo()
-- 拍照执行完成则上传,否则关闭摄像头
if result then
-- 通过网卡状态判断WIFI是否连接成功,WIFI连接成功后再运行照片上传任务。
while not socket.adapter(socket.dft()) do
-- 在此处阻塞等待WIFI连接成功的消息"IP_READY",避免联网过快,丢失了"IP_READY"信息而导致一直被卡住。
-- 或者等待30秒超时退出阻塞等待状态
log.warn("tcp_client_main_task_func", "wait IP_READY")
sys.waitUntil("IP_READY", 30000)
end
if type(data) == "userdata" then
data = data:query()
else
data = io.readFile(data)
end
-- 通过网卡(本demo使用的是socket.LWIP_STA网卡)将拍摄到的照片数据result上传到服务器air32.cn
-- 如果上传成功,电脑上浏览器打开https://uploadtest.luatos.com/upload/jpg/,打开对应的测试日期目录,点击具体的测试时间照片,可以查看摄像头拍照上传的照片
-- 执行httpplus.request后,等待服务器的http应答,此处会阻塞当前task,等待整个过程成功结束或者出现错误异常结束
-- code表示结果,number类型,详细说明参考API手册,一般来说:
-- 200表示成功
-- 小于0的值表示出错,例如-8表示超时错误
-- 其余结果值参考API手册
-- 因为Air32.com平台已经不开放使用了,所以该DEMO仅作上传照片至服务器的演示作用,使用时请将上传URL修改为您自己的服务器地址
-- 或者通过excloud扩展库上传到合宙IOT平台uploadtest.luatos.com,详见:photo_to_aircloud.lua,详见:photo_to_aircloud.lua
local code = httpplus.request({
url = "http://uploadtest.luatos.com/api/upload/jpg",
method = "POST",
body = data
})
-- 打印http传输状态
log.info("http_upload_photo_task_func", "httpplus.request", code)
end
end
-- 判断是否ZBUFF存储方式,如果是文件系统保存则删除本地文件
if save_method ~= "ZBUFF" then
os.remove(spi_camera_param.save_path)
end
-- 关闭摄像头,释放资源
-- 使用ZBUFF存储方式时,close传入true后,excamera内部创建的ZBUFF会缩减至0字节,放出内存但是不释放ZBUFF,便于下次拍照时调用;
-- 重复申请和释放ZBUFF会导致垃圾内存堆积,影响系统内存;
excamera.close(true)
-- 关闭外设分组
exmux.close("i2c1")
end
end
-- 内存检查函数
-- 作用:定期监控系统内存使用情况
local function memory_check()
-- 无限循环,定期检查内存
while true do
-- 等待3秒
sys.wait(3000)
-- 打印系统内存使用信息
log.info("sys ram", rtos.meminfo("sys"))
-- 打印Lua虚拟机内存使用信息
log.info("lua ram", rtos.meminfo("lua"))
end
end
-- AirCAMERA_1040 DEMO应用触发函数,每30S触发一次拍照
local function AirCAMERA_1040_func()
while true do
sys.publish("ONCE_CAPTURE")
sys.wait(30000)
end
end
-- 创建拍照功能任务
-- 作用:在单独的任务中运行拍照逻辑
sys.taskInit(capture_func)
-- 创建内存监控任务
-- 作用:在单独的任务中运行内存监控逻辑
sys.taskInit(memory_check)
-- 创建拍照触发任务
-- 作用:每30秒触发一次拍照上传业务
sys.taskInit(AirCAMERA_1040_func)
@@ -0,0 +1,76 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.11.09
@author 陈取德
@usage
本demo主要使用AirCAMERA_1050 SPI摄像头完成一次拍照上传任务
]]
--[[
必须定义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 = "AirCAMERA_1050_Demo"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 导入netdrv_4g联网状态模块
require "netdrv_4g"
-- 导入gc0310配置表
require "gc0310"
-- 拍照和扫描模式需要在初始化时确认,导入对应模式的DEMO后注释另一个DEMO,拍照和扫描不可同时使用
--[[ 导入photo_to_aircloud拍照上传应用DEMO,该DEMO上传照片至aircloud平台,需要先配置photo_to_aircloud.lua中105行的 auth_key ,否则会报错
auth_keyaircloud应用的认证密钥,用于验证上传请求的合法性
获取方式为:登录iot.openluat.com,点击”我的项目“,找到您所使用的模块对应的项目,即可复制项目KEY
当您的模块没有在您的账号下时,请参考https://docs.openluat.com/air780epm/product/attributioniot/ 处理办法,获取项目KEY]] --
require "photo_to_aircloud"
-- 导入take_photo_http_post拍照上传应用DEMOair32.cn为虚拟服务器,仅用于演示,实际使用时请替换为自己的服务器。
-- 与photo_to_aircloud.lua二选一执行即可,不可同时导入
-- require "take_photo_http_post"
-- 导入scan_code扫描二维码应用DEMO
-- require "scan_code"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
@@ -0,0 +1,37 @@
--[[
@module netdrv_4g
@summary “4G网卡”驱动模块
@version 1.0
@date 2025.07.01
@author 朱天华
@usage
本文件为4G网卡驱动模块,核心业务逻辑为:
1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
]]
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_GP then
log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
end
end
local function ip_lose_func(adapter)
if adapter == socket.LWIP_GP then
log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
end
end
-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
-- 设置默认网卡为socket.LWIP_GP
-- 在Air8000上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
-- 在单4G网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
socket.dft(socket.LWIP_GP)
@@ -0,0 +1,287 @@
--[[
@module photo_to_aircloud
@summary AirCAMERA_1050 gc0310摄像头拍照上传应用模块
@version 1.0
@date 2026.4.30
@author 陈取德
@usage
本demo主要使用AirCAMERA_1050 gc0310摄像头完成一次拍照上传任务
Air780EPM的sys ram内存资源只有2.3M,在使用camera功能时,需要非常谨慎,避免内存泄漏导致系统崩溃,需要注意以下情况:
1、exmcamera业务执行时,必须要确保有连续的内存空间,否则会导致excamera.open()初始化失败,需要的空间计算方式为:
( 摄像头像素高 X 摄像头像素宽 X 2 ),用“GC032A”举例,内存空间为:640 * 480 * 2 = 614400 (B) ≈ 620KB
2、当使用ZBUFF方式存储照片,或者将照片放到 /ram/ 路径下保存时,必须确保有更多的内存连续空间,需要的总空间大小计算方式为:
( 摄像头像素高 X 摄像头像素宽 X 3.5 ),用“GC032A”举例,内存空间为:640 * 480 * 3.5 = 1075200 (B) ≈ 1MB
3、本DEMO为常规模式下,摄像头稳定供电使用,DEMO逻辑为excamera.open()初始化完摄像头后,只需要重复调用excamera.photo()拍照即可,不需要反复的初始化关闭摄像头;
4、在实际应用中,务必将摄像头初始化的优先级排前,因为摄像头业务占用内存过大,尽早的初始化可以确保摄像头初始化时拿到足够的连续内存,确保后续拍照时能够正常执行;
5、Air780EPM的内存非常有限,在摄像头需求高的产品设计中,请选择Air780EHM/EGH/EHV、Air8000、Air700ECH这类 8MB + 8MB 的SOC,更大的内存才是摄像头业务稳定运行的最优解;
6、低功耗模式下使用excamera请参考lowpower DEMO,链接:https://docs.openluat.com/air780epm/luatos/app/lowpower/sleep/#_1
]] --
-- 摄像头拍照模块
-- 功能:提供摄像头初始化、拍照和资源管理功能
-- 引入excamera扩展库模块
local excamera = require "excamera"
-- 引入httpplus扩展库模块
local httpplus = require "httpplus"
-- 引入exmux扩展库模块
local exmux = require "exmux"
-- 导入excloud库
local excloud = require "excloud"
-- pm.ioVol(pm.IOVOL_ALL_GPIO, 2800)
-- 硬件I2C/SPI配置,当您使用合宙开发板时,请根据具体的开发板版本选择对应的变量,
-- exmux库将会自动处理开发板上的I2C/SPI外设,确保总线通讯正常
-- 当您使用自己的制作的板子,请参考exmux库的文档,配置对应的变量:https://docs.openluat.com/osapi/ext/exmux/
-- local HARDWARE_ENV = "DEV_BOARD_8000_V2.0"
local HARDWARE_ENV = "DEV_BOARD_780_V1.2"
-- local HARDWARE_ENV = "DEV_BOARD_780_V1.3"
-- 定义照片保存方式,有三种类型:
-- 1、ZBUFF保存,输入"ZBUFF"即可,excamera库会自动处理ZBUFF
-- 2、保存到内存文件系统中,路径名需指向/ram/文件夹
-- 3、保存到内置FLASH文件系统中
-- 选择其中一个即可,注释另两个路径变量
-- local save_method = "ZBUFF"
local save_method = "/ram/test.jpg"
-- local save_method = "/test.jpg"
--[[
excloud事件回调函数
参数:
event: 事件类型字符串
data: 事件数据,根据事件类型不同而不同
事件类型说明:
connect_result: 连接结果
auth_result: 认证结果
disconnect: 断开连接
reconnect_failed: 重连失败
]]
function on_excloud_event(event, data)
-- 打印事件信息
log.info("用户回调函数", event, json.encode(data))
-- 处理连接结果事件
if event == "connect_result" then
if data.success then
log.info("连接成功")
-- 发布连接成功消息,通知其他任务
sys.publish("aircloud_connected")
else
log.info("连接失败: " .. (data.error or "未知错误"))
end
-- 处理认证结果事件
elseif event == "auth_result" then
if data.success then
log.info("认证成功")
else
log.info("认证失败: " .. data.message)
end
-- 处理断开连接事件
elseif event == "disconnect" then
log.warn("与服务器断开连接")
-- 处理重连失败事件
elseif event == "reconnect_failed" then
log.info("重连失败,已尝试 " .. data.count .. "")
end
end
-- 注册excloud事件回调函数
excloud.on(on_excloud_event)
--[[
excloud任务函数
功能:
1. 等待网络连接就绪
2. 配置excloud参数
3. 初始化并开启excloud服务
4. 启动自动心跳
]]
local function excloud_task_func()
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("excloud_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 配置excloud参数
local ok, err_msg = excloud.setup({
use_getip = true, -- 使用getip服务
device_type = 1, -- 4G设备
auth_key = "sh5g0OTP7ThOSlGKmE5jiEMbOBqQWyw9", -- 认证密钥
transport = "tcp", -- 使用TCP传输
auto_reconnect = true, -- 自动重连
reconnect_interval = 10, -- 重连间隔(秒)
max_reconnect = 5, -- 最大重连次数
mtn_log_enabled = true, -- 启用运维日志
mtn_log_blocks = 2, -- 日志文件块数
mtn_log_write_way = excloud.MTN_LOG_CACHE_WRITE -- 缓存写入方式
})
-- 检查初始化是否成功
if not ok then
log.info("初始化失败: " .. err_msg)
return
end
log.info("excloud初始化成功")
-- 开启excloud服务
local ok, err_msg = excloud.open()
if not ok then
log.info("开启excloud服务失败: " .. err_msg)
return
end
log.info("excloud服务已开启")
-- 启动自动心跳,默认5分钟一次的心跳
excloud.start_heartbeat()
log.info("自动心跳已启动")
end
--[[
照片上传任务函数
功能:
1. 等待excloud连接建立
2. 等待图片数据
3. 上传图片到云端
4. 处理上传结果
]]
function upload_image_fun(image)
-- 连接成功,等待图片数据
if excloud.status().is_connected then
log.info("开始上传图片")
if image then
local ok, err = excloud.upload_image(image, "test.jpg")
if ok then
log.info("图片上传成功")
return true
else
log.error("图片上传失败:", err)
return false
end
else
log.warn("测试图片文件不存在")
return false
end
end
-- 连接断开,等待重连
log.info("excloud连接已断开,等待重连")
return false
end
-- 拍照功能函数
-- 作用:循环监听拍照事件,执行摄像头初始化、拍照和资源释放
local function capture_func()
-- 定义变量用于存储操作结果和数据
local result, data, err
-- 增加重试次数,最多重试5次,避免日志量过大
local retry_count = 0
-- 初始化开发板
exmux.setup(HARDWARE_ENV)
-- 出现异常后重新初始化,最多重试5次
while retry_count < 5 do
-- 配置gc0310摄像头参数表
local spi_camera_param = {
id = "gc0310", -- SPI摄像头仅支持"gc032a"、"gc0310"、"bf30a2",请带引号填写
i2c_id = 1, -- 模块上使用的I2C编号
work_mode = 0, -- 工作模式,0为拍照模式,1为扫描模式
save_path = save_method, -- 拍照结果存储路径,可用"ZBUFF"交由excamera库内部管理
camera_pwr = 2, -- 摄像头使能管脚,填写GPIO号即可,无则填nil
camera_pwdn = 5, -- 摄像头pwdn开关脚,填写GPIO号即可,无则填nil
camera_light = nil -- 摄像头补光灯控制管脚,填写GPIO号即可,无则填nil
}
-- 打开外设分组
exmux.open("i2c1")
-- 初始化摄像头,传入配置参数
result = excamera.open(spi_camera_param)
-- 记录摄像头初始化状态
log.info("初始化状态", result)
-- 判断摄像头初始化是否成功,不成功则直接关闭,成功则启动拍照
-- 无限循环,持续等待拍照事件
while result do
-- 等待外部触发拍照事件(ONCE_CAPTURE)
sys.waitUntil("ONCE_CAPTURE")
-- 执行拍照操作
result, data = excamera.photo()
-- 拍照执行完成则上传,否则关闭摄像头
if result then
-- 通过网卡状态判断WIFI是否连接成功,WIFI连接成功后再运行照片上传任务。
while not socket.adapter(socket.dft()) do
-- 在此处阻塞等待WIFI连接成功的消息"IP_READY",避免联网过快,丢失了"IP_READY"信息而导致一直被卡住。
-- 或者等待30秒超时退出阻塞等待状态
log.warn("tcp_client_main_task_func", "wait IP_READY")
sys.waitUntil("IP_READY", 30000)
end
upload_image_fun(data)
end
-- 判断是否ZBUFF存储方式,如果是文件系统保存则删除本地文件
if save_method ~= "ZBUFF" then
os.remove(spi_camera_param.save_path)
end
end
-- 关闭摄像头,释放资源
-- 使用ZBUFF存储方式时,close传入true后,excamera内部创建的ZBUFF会缩减至0字节,放出内存但是不释放ZBUFF,便于下次拍照时调用;
-- 重复申请和释放ZBUFF会导致垃圾内存堆积,影响系统内存;
excamera.close(true)
-- 关闭外设分组
exmux.close("i2c1")
-- 拍照出错,等待5秒,重试摄像头初始化
sys.wait(5000)
-- 重试次数增加
retry_count = retry_count + 1
log.info("retry_count", retry_count)
end
-- 重试5次后,提示用户检查摄像头连接或重启设备
log.info("camera init failed, please check the camera connection or reboot, retry_count:", retry_count)
end
-- 内存检查函数
-- 作用:定期监控系统内存使用情况
local function memory_check()
-- 无限循环,定期检查内存
while true do
-- 等待3秒
sys.wait(3000)
-- 打印系统内存使用信息
log.info("sys ram", rtos.meminfo("sys"))
-- 打印Lua虚拟机内存使用信息
log.info("lua ram", rtos.meminfo("lua"))
end
end
-- AirCAMERA_1050 DEMO应用触发函数,每30S触发一次拍照
local function AirCAMERA_1050_func()
while true do
sys.wait(30000)
sys.publish("ONCE_CAPTURE")
end
end
-- 按键触发函数,用于手动拍照拍照
local function press_key()
log.info("boot press")
sys.publish("ONCE_CAPTURE")
end
-- 配置BOOT按键引脚为输入模式,下拉电阻,上升沿触发
gpio.setup(0, press_key, gpio.PULLDOWN, gpio.RISING)
gpio.debounce(0, 100, 1)
-- 启动excloud连接任务
sys.taskInit(excloud_task_func)
-- 创建拍照功能任务
-- 作用:在单独的任务中运行拍照逻辑
sys.taskInit(capture_func)
-- 创建内存监控任务
-- 作用:在单独的任务中运行内存监控逻辑
sys.taskInit(memory_check)
-- 创建拍照触发任务
-- 作用:每30秒触发一次拍照上传业务
sys.taskInit(AirCAMERA_1050_func)
@@ -0,0 +1,251 @@
# AirCAMERA_1050 DEMO
## 演示功能概述
本示例主要是展示 AirCAMERA_1050 的使用,本地拍摄照片后通过 httpplus 扩展库将图片上传至 air32.com
1、main.lua:主程序入口
2、take_photo_http_post.lua:执行拍照后使用推送照片上传至air32.cn服务器,该服务器为虚拟服务器,仅用于演示,实际使用时请替换为自己的服务器。
3、scan_code.lua:扫描二维码应用DEMO模块
4、netdrv_4g.lua:联网状态检测模块
5、photo_to_aircloud.lua:执行拍照后使用推送照片上传至aircloud平台
注意事项:
- 拍照或者扫描模式需要在摄像头初始化时确定
- 如使用拍照模式就无法使用扫描模式,扫描模式同理
- 需要拍照后执行扫描的话需要重新初始化
- 所以拍照和扫描不可同时使用,如需切换模式需重新初始化
- 因为Air32.com平台已经不开放使用了,所以该DEMO仅作上传照片至服务器的演示作用,使用时请将上传URL修改为您自己的服务器地址,或者通过excloud扩展库上传到合宙IOT平台uploadtest.luatos.com
## 演示功能概述
### 1、主程序入口模块(main.lua
- 初始化项目信息和版本号
- 初始化看门狗,并定时喂狗
- 启动一个循环定时器,每隔 3 秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况方便分析内存使用是否有异常
- 加载 netdrv_4g 4G联网状态检测模块
- 加载 photo_to_aircloud 拍照上传至aircloud平台模块,务必先配置auth_key,否则会报错
- 加载 take_photo_http_post 模块
### 2、4G联网状态检测模块(netdrv_4g.lua
- 订阅"IP_READY"消息,收到消息后打印联网成功日志
- 订阅"IP_LOSE"消息,收到消息后打印联网失败日志
### 3、拍照上传服务器演示模块(take_photo_http_post.lua
- 每 30 秒触发一次拍照:AirCAMERA_1050_func()
- 每 3 秒打印一次系统和 LUA 的内存信息:memory_check()
- 配置摄像头信息表:spi_camera_param
- 初始化摄像头:excamera.open()
- 执行拍照:excamera.photo()
- 上传照片:httpplus.request()
- 关闭摄像头:excamera.close()
### 4、扫描二维码应用DEMOscan_code.lua
- 每 30 秒触发一次拍照:AirCAMERA_1050_func()
- 每 3 秒打印一次系统和 LUA 的内存信息:memory_check()
- 配置摄像头信息表:spi_camera_param
- 初始化摄像头:excamera.open()
- 执行扫描:excamera.scan()
- 关闭摄像头:excamera.close()
### 5、拍照上传至aircloud平台业务模块(photo_to_aircloud.lua
- 配置excloud链接,启动心跳包
- 开启自动重连机制,断网后自动重连
- 订阅excloud_post_image_done消息,收到消息后打印上传结果:log.info("excloud_post_image_func", err)
- 每 30 秒触发一次拍照:AirCAMERA_1050_func()
- 每 3 秒打印一次系统和 LUA 的内存信息:memory_check()
- 注册excloud回调:on_excloud_event()
- 配置excloud链接,启动心跳包:excloud_task_func()
- 照片上传任务函数:upload_image_fun()
- 配置摄像头信息表:spi_camera_param
- 初始化摄像头:excamera.open()
- 执行拍照:excamera.photo()
- 上传照片:httpplus.request()
- 关闭摄像头:excamera.close()
## 演示硬件环境
1、Air780EPM 开发板一块
2、TYPE-C USB 数据线一根
3、合宙标准配件 AirCAMERA_1050 一个
4、Air780EPM 开发板和合宙标准配件 AirCAMERA_1050 的硬件接线方式为
Air780EPM 开发板通过 TYPE-C USB 口供电;(侧面拨码拨至USB供电)
TYPE-C USB 数据线直接插到开发板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;
AirCAMERA_1050 配件板插入Air780EPM 开发板的SPI摄像头座子中
## 演示软件环境
1、Luatools 下载调试工具:[https://docs.openluat.com/air780epm/common/Luatools/](https://docs.openluat.com/air780epm/common/Luatools/)
2、固件版本:LuatOS-SoC_V2018_Air780EPM_1,固件地址,如有最新固件请用最新 https://docs.openluat.com/air780epm/luatos/firmware/version/
## 演示核心步骤
1、搭建硬件环境;
2、烧录 DEMO 代码;
3、等待自动拍照完成后上传平台,LUATOOLS会有如下打印;
```lua
[2026-04-08 17:36:12.996][000000000.205] I/user.main AirCAMERA_1040_Demo 001.999.000
[2026-04-08 17:36:12.999][000000000.325] W/user.excloud_task_func wait IP_READY 1 1
[2026-04-08 17:36:13.002][000000000.326] I/user.exmux DEV_BOARD_780_V1.2
[2026-04-08 17:36:13.504][000000001.326] W/user.excloud_task_func wait IP_READY 1 1
[2026-04-08 17:36:14.408][000000002.166] I/mobile sim0 sms ready
[2026-04-08 17:36:14.412][000000002.167] D/mobile cid1, state0
[2026-04-08 17:36:14.423][000000002.168] D/mobile bearer act 0, result 0
[2026-04-08 17:36:14.430][000000002.168] D/mobile NETIF_LINK_ON -> IP_READY
[2026-04-08 17:36:14.438][000000002.169] I/user.netdrv_4g.ip_ready_func IP_READY 10.122.36.125 255.255.255.255 0.0.0.0 nil
[2026-04-08 17:36:14.445][000000002.173] I/user.[excloud]4G设备 IMEI: 867920071472378 MUID: 20250604131416A755490A2989432885
[2026-04-08 17:36:14.451][000000002.189] I/user.exmtn 1
[2026-04-08 17:36:14.457][000000002.189] I/user.exmtn 2
[2026-04-08 17:36:14.464][000000002.190] I/user.exmtn 0
[2026-04-08 17:36:14.468][000000002.190] I/user.exmtn false
[2026-04-08 17:36:14.473][000000002.193] I/user.exmtn
[2026-04-08 17:36:14.479][000000002.197] I/user.exmtn : 8.00 KB (2 × 4096 ), 32.00 KB (4 )
[2026-04-08 17:36:14.484][000000002.197] I/user.[excloud]
[2026-04-08 17:36:14.488][000000002.198] I/user.[excloud]excloud.setup ID: 867920071472378
[2026-04-08 17:36:14.494][000000002.198] I/user.excloud初始化成功
[2026-04-08 17:36:14.499][000000002.198] I/user.[excloud]...
[2026-04-08 17:36:14.505][000000002.199] I/user.[excloud]excloud.getip : 3 key: sh5g0OTP7ThOSlGKmE5jiEMbOBqQWyw9-867920071472378
[2026-04-08 17:36:14.512][000000002.205] D/socket connect to gps.openluat.com,443
[2026-04-08 17:36:14.516][000000002.209] dns_run 676:gps.openluat.com state 0 id 1 ipv6 0 use dns server2, try 0
[2026-04-08 17:36:14.520][000000002.231] D/mobile TIME_SYNC 0 tm 1775640975
[2026-04-08 17:36:14.526][000000002.273] dns_run 693:dns all done ,now stop
[2026-04-08 17:36:15.179][000000003.008] I/user.httpplus ,
[2026-04-08 17:36:15.211][000000003.037] I/user.[excloud]excloud.getip响应 HTTP Code: 200 Body:
[2026-04-08 17:36:15.218][000000003.037] {"msg":"ok","conninfo":{"ipv4":"124.71.128.165","port":9108},"imginfo":{"url":"https://gps.openluat.com/aircloud/air_up/image","data_key":"f","data_param":{"key":"8bcWhwHK4pZR7gTbvPUdxL1TAbW5H3C65MFcC","tip":""}},"audinfo":{"url":"https://gps.openluat.com/aircloud/air_up/audio","data_key":"f","data_param":{"key":"8bcWhwHK4pZR7gTbvPUdxL1TAbW5H3C65MFcC","tip":""}},"mtninfo":{"url":"https://gps.openluat.com/aircloud/air_up/file","data_key":"f","data_param":{"key":"8bcWhwHK4pZR7gTbvPUdxL1TAbW5H3C65MFcC","tip":""}},"qrinfo":{"url":"https://gps.openluat.com/#/redirect?key=_5oruyMsMk5Ny5ZajRyBqaZEqqaPjVSb93qH"}}
[2026-04-08 17:36:15.225][000000003.038] I/user.[excloud]excloud.getip响应 HTTP Code: 200 Body:
[2026-04-08 17:36:15.231][000000003.038] {"msg":"ok","conninfo":{"ipv4":"124.71.128.165","port":9108},"imginfo":{"url":"https://gps.openluat.com/aircloud/air_up/image","data_key":"f","data_param":{"key":"8bcWhwHK4pZR7gTbvPUdxL1TAbW5H3C65MFcC","tip":""}},"audinfo":{"url":"https://gps.openluat.com/aircloud/air_up/audio","data_key":"f","data_param":{"key":"8bcWhwHK4pZR7gTbvPUdxL1TAbW5H3C65MFcC","tip":""}},"mtninfo":{"url":"https://gps.openluat.com/aircloud/air_up/file","data_key":"f","data_param":{"key":"8bcWhwHK4pZR7gTbvPUdxL1TAbW5H3C65MFcC","tip":""}},"qrinfo":{"url":"https://gps.openluat.com/#/redirect?key=_5oruyMsMk5Ny5ZajRyBqaZEqqaPjVSb93qH"}}
[2026-04-08 17:36:15.237][000000003.039] I/user.[excloud]TCP/UDP连接信息 host: 124.71.128.165 port: 9108 key: nil
[2026-04-08 17:36:15.242][000000003.039] I/user.[excloud]
[2026-04-08 17:36:15.247][000000003.040] I/user.[excloud]
[2026-04-08 17:36:15.255][000000003.040] I/user.[excloud]
[2026-04-08 17:36:15.262][000000003.040] I/user.[excloud]
[2026-04-08 17:36:15.266][000000003.041] I/user.[excloud]excloud.getip : 124.71.128.165 9108
[2026-04-08 17:36:15.271][000000003.041] I/user.[excloud]excloud.getip : true : {"ipv4":"124.71.128.165","port":9108}
[2026-04-08 17:36:15.276][000000003.042] I/user.[excloud] host: 124.71.128.165 port: 9108 transport: tcp
[2026-04-08 17:36:15.280][000000003.042] I/user.[excloud]TCP连接
[2026-04-08 17:36:15.286][000000003.043] D/socket connect to 124.71.128.165,9108
[2026-04-08 17:36:15.291][000000003.044] network_socket_connect 1610:network 1-0 local port auto select 51002
[2026-04-08 17:36:15.295][000000003.045] I/user.[excloud]TCP连接结果 true false
[2026-04-08 17:36:15.301][000000003.046] I/user.[excloud]excloud service started
[2026-04-08 17:36:15.306][000000003.046] I/user.excloud服务已开启
[2026-04-08 17:36:15.311][000000003.047] I/user.[excloud]excloud 300
[2026-04-08 17:36:15.315][000000003.047] I/user.
[2026-04-08 17:36:15.323][000000003.127] network_default_socket_callback 1123:before process socket 1,event:0xf2000009(),state:3(),wait:2()
[2026-04-08 17:36:15.328][000000003.127] network_default_socket_callback 1127:after process socket 1,state:5(线),wait:0()
[2026-04-08 17:36:15.332][000000003.128] I/user.[excloud]socket cb userdata: 0C1843F0 33554449 0
[2026-04-08 17:36:15.337][000000003.129] I/user.[excloud]socket TCP连接成功
[2026-04-08 17:36:15.342][000000003.129] I/user. connect_result {"success":true}
[2026-04-08 17:36:15.346][000000003.129] I/user.
[2026-04-08 17:36:15.352][000000003.133] I/user.[excloud] 16 3 sh5g0OTP7ThOSlGKmE5jiEMbOBqQWyw9-867920071472378-20250604131416A755490A2989432885
[2026-04-08 17:36:15.357][000000003.134] I/user.[excloud]tlv发送数据长度4 85
[2026-04-08 17:36:15.362][000000003.135] I/user.[excloud] y r7
[2026-04-08 17:36:15.371][000000003.137] I/user. send_result {"sequence_num":1,"success":true,"error_msg":"Send successful"}
[2026-04-08 17:36:15.376][000000003.143] I/user.[excloud] 101
[2026-04-08 17:36:15.385][000000003.207] network_default_socket_callback 1123:before process socket 1,event:0xf2000004(),state:5(线),wait:3()
[2026-04-08 17:36:15.390][000000003.207] network_default_socket_callback 1127:after process socket 1,state:5(线),wait:0()
[2026-04-08 17:36:15.395][000000003.208] I/user.[excloud]socket cb userdata: 0C1843F0 33554450 0
[2026-04-08 17:36:15.403][000000003.208] I/user.[excloud]socket
[2026-04-08 17:36:15.491][000000003.327] I/user.sys ram 2375920 80156 121112
[2026-04-08 17:36:15.496][000000003.328] I/user.lua ram 1048568 174392 190984
[2026-04-08 17:36:18.498][000000006.328] I/user.sys ram 2375920 80156 121112
[2026-04-08 17:36:18.531][000000006.329] I/user.lua ram 1048568 174712 190984
[2026-04-08 17:36:21.500][000000009.329] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:21.518][000000009.330] I/user.lua ram 1048568 175240 190984
[2026-04-08 17:36:24.496][000000012.330] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:24.501][000000012.331] I/user.lua ram 1048568 175440 190984
[2026-04-08 17:36:27.508][000000015.331] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:27.512][000000015.332] I/user.lua ram 1048568 175632 190984
[2026-04-08 17:36:30.505][000000018.332] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:30.511][000000018.333] I/user.lua ram 1048568 175832 190984
[2026-04-08 17:36:33.503][000000021.333] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:33.510][000000021.334] I/user.lua ram 1048568 176024 190984
[2026-04-08 17:36:36.506][000000024.334] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:36.510][000000024.335] I/user.lua ram 1048568 176216 190984
[2026-04-08 17:36:39.512][000000027.335] I/user.sys ram 2375920 71760 121112
[2026-04-08 17:36:39.524][000000027.336] I/user.lua ram 1048568 176416 190984
[2026-04-08 17:36:42.504][000000030.327] I/user.exmux pwr2 (23)
[2026-04-08 17:36:42.510][000000030.328] I/user.exmux pwr1 (2)
[2026-04-08 17:36:42.513][000000030.328] I/user.exmux i2c1
[2026-04-08 17:36:42.517][000000030.329] I2C_MasterSetup 426:I2C1, Total 65 HCNT 22 LCNT 40
[2026-04-08 17:36:42.522][000000030.329] CSPI_Setup 1924:APB MP 102400000
[2026-04-08 17:36:42.551][000000030.388] I/user. true
[2026-04-08 17:36:42.558][000000030.388] CSPI_Rx 2022:block len 7680, total block 80
[2026-04-08 17:36:42.561][000000030.389] I/user. /ram/test.jpg
[2026-04-08 17:36:42.564][000000030.389] luat_camera_capture_config 700:0,0,0,0
[2026-04-08 17:36:42.568][000000030.389] luat_camera_capture 676:save file in /ram/test.jpg
[2026-04-08 17:36:42.572][000000030.391] I/user.sys ram 2375920 696088 696224
[2026-04-08 17:36:42.575][000000030.391] I/user.lua ram 1048568 178544 190984
[2026-04-08 17:36:43.126][000000030.955] I/user. 58612
[2026-04-08 17:36:43.138][000000030.956] I/user.
[2026-04-08 17:36:43.144][000000030.956] I/user.
[2026-04-08 17:36:43.149][000000030.957] I/user.[excloud] : 1 : /ram/test.jpg : 58612
[2026-04-08 17:36:43.155][000000030.957] I/user.[excloud] : 1 : test.jpg : 58612
[2026-04-08 17:36:43.160][000000030.958] I/user.[excloud] 23 0 0
[2026-04-08 17:36:43.164][000000030.959] I/user.[excloud] 784 0 1
[2026-04-08 17:36:43.169][000000030.960] I/user.[excloud] 785 3 test.jpg
[2026-04-08 17:36:43.172][000000030.961] I/user.[excloud] 786 0 58612
[2026-04-08 17:36:43.177][000000030.963] I/user.[excloud]tlv发送数据长度4 36
[2026-04-08 17:36:43.182][000000030.964] I/user.[excloud] y r7
[2026-04-08 17:36:43.185][000000030.968] I/user. send_result {"sequence_num":2,"success":true,"error_msg":"Send successful"}
[2026-04-08 17:36:43.189][000000030.973] I/user.[excloud] 52
[2026-04-08 17:36:43.195][000000030.985] I/user.[excloud]HTTP请求 URL: https://gps.openluat.com/aircloud/air_up/image
[2026-04-08 17:36:43.199][000000030.989] dns_run 676:gps.openluat.com state 0 id 2 ipv6 0 use dns server2, try 0
[2026-04-08 17:36:43.298][000000031.126] dns_run 693:dns all done ,now stop
[2026-04-08 17:36:43.304][000000031.135] network_default_socket_callback 1123:before process socket 1,event:0xf2000004(),state:5(线),wait:3()
[2026-04-08 17:36:43.308][000000031.135] network_default_socket_callback 1127:after process socket 1,state:5(线),wait:0()
[2026-04-08 17:36:43.312][000000031.136] I/user.[excloud]socket cb userdata: 0C1843F0 33554450 0
[2026-04-08 17:36:43.318][000000031.137] I/user.[excloud]socket
[2026-04-08 17:36:45.565][000000033.392] I/user.sys ram 2375920 876224 1064884
[2026-04-08 17:36:45.570][000000033.393] I/user.lua ram 1048568 353216 469512
[2026-04-08 17:36:46.436][000000034.268] I/http http close c267f50
[2026-04-08 17:36:46.441][000000034.270] I/user.[excloud]excloud.getip文件上传响应 HTTP Code: 200 Body: {"info":"aircloud./aircloud/air_up/image","code":0,"trace":"code:aircloud./aircloud/air_up/image, trcace:","log":"^^^","value":{"uri":"/vsa/aircloud_image/FFZDbqsuzrLpbgPfzVd7De/2026-04/867920071472378/20260408173647_test.jpg","size":"57.00KB","thumb":"/vsa/aircloud_image/FFZDbqsuzrLpbgPfzVd7De/2026-04/867920071472378/20260408173647_testt.jpg"}}
[2026-04-08 17:36:46.446][000000034.271] I/user.[excloud] URL: /vsa/aircloud_image/FFZDbqsuzrLpbgPfzVd7De/2026-04/867920071472378/20260408173647_test.jpg
[2026-04-08 17:36:46.450][000000034.271] I/user.[excloud] 24 0 0
[2026-04-08 17:36:46.457][000000034.274] I/user.[excloud] 784 0 1
[2026-04-08 17:36:46.461][000000034.276] I/user.[excloud] 785 3 test.jpg
[2026-04-08 17:36:46.466][000000034.277] I/user.[excloud] 787 0 0
[2026-04-08 17:36:46.470][000000034.278] I/user.[excloud]tlv发送数据长度4 36
[2026-04-08 17:36:46.475][000000034.279] I/user.[excloud] y r7
[2026-04-08 17:36:46.479][000000034.282] I/user. send_result {"sequence_num":3,"success":true,"error_msg":"Send successful"}
[2026-04-08 17:36:46.486][000000034.287] I/user.[excloud] 52
[2026-04-08 17:36:46.491][000000034.287] I/user.
[2026-04-08 17:36:46.498][000000034.288] I/user.exmux pwr2 (23)
[2026-04-08 17:36:46.502][000000034.289] I/user.exmux pwr1 (2)
[2026-04-08 17:36:46.507][000000034.289] I/user.exmux i2c1
[2026-04-08 17:36:46.512][000000034.347] network_default_socket_callback 1123:before process socket 1,event:0xf2000004(),state:5(线),wait:3()
[2026-04-08 17:36:46.519][000000034.347] network_default_socket_callback 1127:after process socket 1,state:5(线),wait:0()
[2026-04-08 17:36:46.524][000000034.348] I/user.[excloud]socket cb userdata: 0C1843F0 33554450 0
[2026-04-08 17:36:46.529][000000034.349] I/user.[excloud]socket
```
4、等待自动扫描任务完成后,LUATOOLS会有如下打印;
```lua
[2025-11-21 15:19:17.310][000000000.269] I2C_MasterSetup 426:I2C1, Total 65 HCNT 22 LCNT 40
[2025-11-21 15:19:17.313][000000000.332] I/user. true
[2025-11-21 15:19:17.316][000000000.332] CSPI_Rx 2000:block len 7680, total block 40
[2025-11-21 15:19:17.319][000000000.541] I/user. Air780EPM
[2025-11-21 15:19:17.323][000000000.542] I/user. Air780EPM
[2025-11-21 15:19:17.327][000000000.542] I/user.Scan result : Air780EPM
```
5、登录 https://iot.luatos.com/ 查看拍摄的照片;
@@ -0,0 +1,99 @@
--[[
@module scan_code
@summary AirCAMERA_1050 gc0310摄像头扫描二维码应用模块
@version 1.0
@date 2025.11.09
@author 陈取德
@usage
本demo主要使用AirCAMERA_1050 gc0310摄像头完成一次扫描二维码任务
]] --
-- 摄像头扩展库模块
-- 功能:提供摄像头初始化、扫描和资源管理功能
-- 引入excamera扩展库模块
local excamera = require "excamera"
-- 引入exmux扩展库模块
local exmux = require "exmux"
-- 硬件I2C/SPI配置,当您使用合宙开发板时,请根据具体的开发板版本选择对应的变量,
-- exmux库将会自动处理开发板上的I2C/SPI外设,确保总线通讯正常
-- 当您使用自己的制作的板子,请参考exmux库的文档,配置对应的变量:https://docs.openluat.com/osapi/ext/exmux/
-- local HARDWARE_ENV = "DEV_BOARD_8000_V2.0"
local HARDWARE_ENV = "DEV_BOARD_780_V1.2"
-- local HARDWARE_ENV = "DEV_BOARD_780_V1.3"
-- 扫描功能函数
-- 作用:循环监听扫描事件,执行摄像头初始化、扫描和资源释放
local function scan_code_func()
-- 定义变量用于存储操作结果和数据
local result, data
-- 初始化外设分组开关状态
exmux.setup(HARDWARE_ENV)
-- 无限循环,持续等待扫描事件
while true do
-- 配置gc0310摄像头参数表
local spi_camera_param = {
id = "gc0310", -- SPI摄像头仅支持"gc032a"、"gc0310"、"bf30a2",请带引号填写
i2c_id = 1, -- 模块上使用的I2C编号
work_mode = 1, -- 工作模式,0为拍照模式,1为扫描模式
save_path = nil, -- 扫描结果为字符串返回,使用变量赋值既可
camera_pwr = 2, -- 摄像头使能管脚,填写GPIO号即可,无则填nil
camera_pwdn = 5, -- 摄像头pwdn开关脚,填写GPIO号即可,无则填nil
camera_light = nil -- 摄像头补光灯控制管脚,填写GPIO号即可,无则填nil
}
-- 等待外部触发扫描事件(SCAN_CODE)
sys.waitUntil("SCAN_CODE")
-- 打开外设分组
exmux.open("i2c1")
-- 初始化摄像头,传入配置参数
result = excamera.open(spi_camera_param)
-- 记录摄像头初始化状态
log.info("初始化状态", result)
-- 判断摄像头初始化是否成功,不成功则直接关闭,成功则启动扫描
if result then
-- 执行扫描操作,5秒超时
result, data = excamera.scan(5000)
-- 扫描执行完成则上传,否则关闭摄像头
if result then
log.info("Scan result :", data )
end
end
-- 关闭摄像头,释放资源
excamera.close()
-- 关闭外设分组
exmux.close("i2c1")
end
end
-- 内存检查函数
-- 作用:定期监控系统内存使用情况
local function memory_check()
-- 无限循环,定期检查内存
while true do
-- 等待3秒
sys.wait(3000)
-- 打印系统内存使用信息
log.info("sys ram", rtos.meminfo("sys"))
-- 打印Lua虚拟机内存使用信息
log.info("lua ram", rtos.meminfo("lua"))
end
end
-- AirCAMERA_1050 DEMO应用触发函数,每30S触发一次扫描
local function AirCAMERA_1050_func()
while true do
sys.publish("SCAN_CODE")
sys.wait(30000)
end
end
-- 创建扫描功能任务
-- 作用:在单独的任务中运行扫描逻辑
sys.taskInit(scan_code_func)
-- 创建内存监控任务
-- 作用:在单独的任务中运行内存监控逻辑
sys.taskInit(memory_check)
-- 创建扫描触发任务
-- 作用:每30秒触发一次扫描二维码业务
sys.taskInit(AirCAMERA_1050_func)
@@ -0,0 +1,142 @@
--[[
@module take_photo_http_post
@summary AirCAMERA_1050 gc0310摄像头拍照上传应用模块
@version 1.0
@date 2025.11.09
@author 陈取德
@usage
本demo主要使用AirCAMERA_1050 gc0310摄像头完成一次拍照上传任务
]] -- 摄像头拍照模块
-- 功能:提供摄像头初始化、拍照和资源管理功能
-- 引入excamera扩展库模块
local excamera = require "excamera"
-- 引入httpplus扩展库模块
local httpplus = require "httpplus"
-- 引入exmux扩展库模块
local exmux = require "exmux"
-- 硬件I2C/SPI配置,当您使用合宙开发板时,请根据具体的开发板版本选择对应的变量,
-- exmux库将会自动处理开发板上的I2C/SPI外设,确保总线通讯正常
-- 当您使用自己的制作的板子,请参考exmux库的文档,配置对应的变量:https://docs.openluat.com/osapi/ext/exmux/
-- local HARDWARE_ENV = "DEV_BOARD_8000_V2.0"
local HARDWARE_ENV = "DEV_BOARD_780_V1.2"
-- local HARDWARE_ENV = "DEV_BOARD_780_V1.3"
-- 定义照片保存方式,有三种类型:
-- 1、ZBUFF保存,输入"ZBUFF"即可,excamera库会自动处理ZBUFF
-- 2、保存到内存文件系统中,路径名需指向/ram/文件夹
-- 3、保存到内置FLASH文件系统中
-- 选择其中一个即可,注释另两个路径变量
local save_method = "ZBUFF"
-- local save_method = "/ram/test.jpg"
-- local save_method = "/test.jpg"
-- 拍照功能函数
-- 作用:循环监听拍照事件,执行摄像头初始化、拍照和资源释放
local function capture_func()
-- 定义变量用于存储操作结果和数据
local result, data
-- 初始化外设分组开关状态
exmux.setup(HARDWARE_ENV)
-- 无限循环,持续等待拍照事件
while true do
-- 配置gc0310摄像头参数表
local spi_camera_param = {
id = "gc0310", -- SPI摄像头仅支持"gc032a"、"gc0310"、"bf30a2",请带引号填写
i2c_id = 1, -- 模块上使用的I2C编号
work_mode = 0, -- 工作模式,0为拍照模式,1为扫描模式
save_path = save_method, -- 拍照结果存储路径,可用"ZBUFF"交由excamera库内部管理
camera_pwr = 2, -- 摄像头使能管脚,填写GPIO号即可,无则填nil
camera_pwdn = 5, -- 摄像头pwdn开关脚,填写GPIO号即可,无则填nil
camera_light = nil -- 摄像头补光灯控制管脚,填写GPIO号即可,无则填nil
}
-- 等待外部触发拍照事件(ONCE_CAPTURE)
sys.waitUntil("ONCE_CAPTURE")
-- 打开外设分组
exmux.open("i2c1")
-- 初始化摄像头,传入配置参数
result = excamera.open(spi_camera_param)
-- 记录摄像头初始化状态
log.info("初始化状态", result)
-- 判断摄像头初始化是否成功,不成功则直接关闭,成功则启动拍照
if result then
-- 执行拍照操作
result, data = excamera.photo()
-- 拍照执行完成则上传,否则关闭摄像头
if result then
-- 通过网卡状态判断WIFI是否连接成功,WIFI连接成功后再运行照片上传任务。
while not socket.adapter(socket.dft()) do
-- 在此处阻塞等待WIFI连接成功的消息"IP_READY",避免联网过快,丢失了"IP_READY"信息而导致一直被卡住。
-- 或者等待30秒超时退出阻塞等待状态
log.warn("tcp_client_main_task_func", "wait IP_READY")
sys.waitUntil("IP_READY", 30000)
end
if type(data) == "userdata" then
data = data:query()
else
data = io.readFile(data)
end
-- 通过网卡(本demo使用的是socket.LWIP_STA网卡)将拍摄到的照片数据result上传到服务器air32.cn
-- 如果上传成功,电脑上浏览器打开https://uploadtest.luatos.com/upload/jpg/,打开对应的测试日期目录,点击具体的测试时间照片,可以查看摄像头拍照上传的照片
-- 执行httpplus.request后,等待服务器的http应答,此处会阻塞当前task,等待整个过程成功结束或者出现错误异常结束
-- code表示结果,number类型,详细说明参考API手册,一般来说:
-- 200表示成功
-- 小于0的值表示出错,例如-8表示超时错误
-- 其余结果值参考API手册
-- 因为Air32.com平台已经不开放使用了,所以该DEMO仅作上传照片至服务器的演示作用,使用时请将上传URL修改为您自己的服务器地址
-- 或者通过excloud扩展库上传到合宙IOT平台uploadtest.luatos.com,详见:photo_to_aircloud.lua
local code = httpplus.request({
url = "http://uploadtest.luatos.com/api/upload/jpg",
method = "POST",
body = data
})
-- 打印http传输状态
log.info("http_upload_photo_task_func", "httpplus.request", code)
end
end
-- 判断是否ZBUFF存储方式,如果是文件系统保存则删除本地文件
if save_method ~= "ZBUFF" then
os.remove(spi_camera_param.save_path)
end
-- 关闭摄像头,释放资源
-- 使用ZBUFF存储方式时,close传入true后,excamera内部创建的ZBUFF会缩减至0字节,放出内存但是不释放ZBUFF,便于下次拍照时调用;
-- 重复申请和释放ZBUFF会导致垃圾内存堆积,影响系统内存;
excamera.close(true)
-- 关闭外设分组
exmux.close("i2c1")
end
end
-- 内存检查函数
-- 作用:定期监控系统内存使用情况
local function memory_check()
-- 无限循环,定期检查内存
while true do
-- 等待3秒
sys.wait(3000)
-- 打印系统内存使用信息
log.info("sys ram", rtos.meminfo("sys"))
-- 打印Lua虚拟机内存使用信息
log.info("lua ram", rtos.meminfo("lua"))
end
end
-- AirCAMERA_1050 DEMO应用触发函数,每30S触发一次拍照
local function AirCAMERA_1050_func()
while true do
sys.publish("ONCE_CAPTURE")
sys.wait(30000)
end
end
-- 创建拍照功能任务
-- 作用:在单独的任务中运行拍照逻辑
sys.taskInit(capture_func)
-- 创建内存监控任务
-- 作用:在单独的任务中运行内存监控逻辑
sys.taskInit(memory_check)
-- 创建拍照触发任务
-- 作用:每30秒触发一次拍照上传业务
sys.taskInit(AirCAMERA_1050_func)
@@ -0,0 +1,79 @@
--[[
@module http_app
@summary http应用功能模块
@version 1.0
@date 2025.09.17
@author 王城钧
@usage
本文件为http应用功能模块,核心业务逻辑为:基于不同的应用场景,演示http核心库的使用方式;
本文件没有对外接口,直接在main.lua中require "http_app"就可以加载运行;
]]
-- 普通的http get请求功能演示
-- 请求的body数据保存到内存变量中,在内存够用的情况下,最大支持32KB的数据存储到内存中
-- timeout可以设置超时时间
-- callback可以设置回调函数,可用于实时检测body数据的下载进度
local function http_app_get()
-- https get请求https://httpbin.luatos.com/get网页内容
-- 如果请求成功,请求的数据保存到body中
local code, headers, body = http.request("GET", "https://httpbin.luatos.com/get").wait()
log.info("http_app_get1",
code == 200 and "success" or "error",
code,
json.encode(headers or {}),
body and (body:len() > 512 and body:len() or body) or "nil")
-- https get请求https://www.luatos.com/网页内容,超时时间为10秒
-- 请求超时时间为10秒,用户自己写代码时,不要照抄10秒,根据自己业务逻辑的需要设置合适的超时时间
-- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get2"
-- 如果请求成功,请求的数据保存到body中
code, headers, body = http.request("GET", "https://www.luatos.com/", nil, nil,
{ timeout = 10000, userdata = "http_app_get2", callback = http_cbfunc }).wait()
log.info("http_app_get2",
code == 200 and "success" or "error",
code,
json.encode(headers or {}),
body and (body:len() > 512 and body:len() or body) or "nil")
-- http get请求http://httpbin.luatos.com/get网页内容,超时时间为3秒
-- 请求超时时间为3秒,用户自己写代码时,不要照抄3秒,根据自己业务逻辑的需要设置合适的超时时间
-- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get3"
-- 如果请求成功,请求的数据保存到body中
code, headers, body = http.request("GET", "http://httpbin.luatos.com/get", nil, nil,
{ timeout = 3000, userdata = "http_app_get3", callback = http_cbfunc }).wait()
log.info("http_app_get3",
code == 200 and "success" or "error",
code,
json.encode(headers or {}),
body and (body:len() > 512 and body:len() or body) or "nil")
end
-- http app task 的任务处理函数
local function http_app_task_func()
while true do
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("http_app_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 检测到了IP_READY消息
log.info("http_app_task_func", "recv IP_READY", socket.dft())
-- 普通的http get请求功能演示
http_app_get()
-- 50秒之后,循环测试
sys.wait(50000)
end
end
--创建并且启动一个task
--运行这个task的处理函数http_app_task_func
sys.taskInit(http_app_task_func)
@@ -0,0 +1,72 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.09.17
@author 王城钧
@usage
本demo演示的核心功能为:
1、http_app: 普通的http get请求功能演示;
2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
(1) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
(2)netdrv_4gsocket.LWIP_GP4G网卡;
(3)netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
(4)netdrv_pcsocket.LWIP_PCPC模拟器网卡;
更多说明参考本目录下的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 = "HTTP"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 加载网络驱动设备功能模块
require "netdrv_device"
-- 加载http应用功能模块
require "http_app"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,43 @@
--[[
@module netdrv_4g
@summary “4G网卡”驱动模块
@version 1.0
@date 2025.07.01
@author 马梦阳
@usage
本文件为4G网卡驱动模块,核心业务逻辑为:
1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
]]
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_GP then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
end
end
local function ip_lose_func(adapter)
if adapter == socket.LWIP_GP then
log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
end
end
-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
-- 在Air780EPM上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
@@ -0,0 +1,77 @@
--[[
@module netdrv_eth_spi
@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
@version 1.0
@date 2025.07.24
@author 马梦阳
@usage
本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块,核心业务逻辑为:
1、打开CH390H芯片供电开关;
2、初始化spi0,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
3、以太网卡的连接状态发生变化时,在日志中进行打印;
直接使用Air780EPM V1.3版本开发板硬件测试即可;
本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
]]
local exnetif = require "exnetif"
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_ETH then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
end
end
local function ip_lose_func(adapter)
if adapter == socket.LWIP_ETH then
log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
end
end
-- 以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
-- 各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
-- 以太网断网后,内核固件会产生一个"IP_LOSE"消息
-- 各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
--此处订阅"IP_READY"和"IP_LOSE"两种消息
--在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI外挂CH390H芯片的以太网卡”的连接状态
--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
local function netdrv_eth_spi_task_func()
-- 配置SPI外接以太网芯片CH390H的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_ETH
-- 本demo使用Air780EPM V1.3版本开发板测试,开发板上的硬件配置为:
-- GPIO20为CH390H以太网芯片的供电使能控制引脚
-- 使用spi0,片选引脚使用GPIO8,中断引脚使用GPIO1
-- 如果使用的硬件和以上描述的环境不同,根据自己的硬件配置修改以下参数
exnetif.set_priority_order({
{
ETHERNET = {
pwrpin = 20,
tp = netdrv.CH390,
opts = {spi = 0, cs = 8, irq = 1}
}
}
})
end
-- 启动一个tasktask的处理函数为netdrv_eth_spi_task_func
-- 在处理函数中调用exnetif.set_priority_order设置网卡优先级
-- 因为exnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
sys.taskInit(netdrv_eth_spi_task_func)
@@ -0,0 +1,88 @@
--[[
@module netdrv_multiple
@summary 多网卡(4G网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块
@version 1.0
@date 2025.07.24
@author 马梦阳
@usage
本文件为多网卡驱动模块,核心业务逻辑为:
1、调用exnetif.set_priority_order配置多网卡的控制参数以及优先级;
直接使用Air780EPM V1.3版本开发板硬件测试即可;
本文件没有对外接口,直接在其他功能模块中require "netdrv_multiple"就可以加载运行;
]]
local exnetif = require "exnetif"
-- 网卡状态变化通知回调函数
-- 当exnetif中检测到网卡切换或者所有网卡都断网时,会触发调用此回调函数
-- 当网卡切换切换时:
-- net_typestring类型,表示当前使用的网卡字符串
-- adapternumber类型,表示当前使用的网卡id
-- 当所有网卡断网时:
-- net_type:为nil
-- adapternumber类型,为-1
local function netdrv_multiple_notify_cbfunc(net_type,adapter)
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
if type(net_type)=="string" then
log.info("netdrv_multiple_notify_cbfunc", "use new adapter", net_type, adapter)
elseif type(net_type)=="nil" then
log.warn("netdrv_multiple_notify_cbfunc", "no available adapter", net_type, adapter)
else
log.warn("netdrv_multiple_notify_cbfunc", "unknown status", net_type, adapter)
end
end
local function netdrv_multiple_task_func()
--设置网卡优先级
exnetif.set_priority_order(
{
-- “通过SPI外挂CH390H芯片”的以太网卡,使用Air780EPM V1.3版本开发板验证
{
ETHERNET = {
-- 供电使能GPIO
pwrpin = 20,
-- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
-- 如果没有传入此参数,exnetif会使用默认值10秒
ping_time = 3000,
-- 连通性检测ip(选填参数)
-- 如果没有传入ip地址,exnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
-- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
-- ping_ip = "填入可靠的并且可以ping通的ip地址",
-- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
tp = netdrv.CH390,
opts = {spi=0, cs=8, irq=1}
}
},
-- 4G网卡
{
LWIP_GP = true
}
}
)
end
-- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc
exnetif.notify_status(netdrv_multiple_notify_cbfunc)
-- 如果存在udp网络应用,并且udp网络应用中,根据应用层的心跳能够判断出来udp数据通信出现了异常;
-- 可以在判断出现异常的位置,调用一次exnetif.check_network_status()接口,强制对当前正式使用的网卡进行一次连通性检测;
-- 如果存在tcp网络应用,不需要用户调用exnetif.check_network_status()接口去控制,exnetif会在tcp网络应用通信异常时自动对当前使用的网卡进行连通性检测。
-- 启动一个tasktask的处理函数为netdrv_multiple_task_func
-- 在处理函数中调用exnetif.set_priority_order设置网卡优先级
-- 因为exnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
sys.taskInit(netdrv_multiple_task_func)
@@ -0,0 +1,45 @@
--[[
@module netdrv_pc
@summary “pc模拟器网卡”驱动模块
@version 1.0
@date 2025.07.01
@author 王城钧
@usage
本文件为pc模拟器网卡驱动模块,核心业务逻辑为:
1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
本文件没有对外接口,直接在其他功能模块中require "netdrv_pc"就可以加载运行;
]]
local function ip_ready_func(ip, adapter)
if adapter == socket.ETH0 then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
log.info("netdrv_pc.ip_ready_func", "IP_READY", socket.localIP(socket.ETH0))
end
end
local function ip_lose_func(adapter)
if adapter == socket.ETH0 then
log.warn("netdrv_pc.ip_lose_func", "IP_LOSE")
end
end
--此处订阅"IP_READY"和"IP_LOSE"两种消息
--在消息的处理函数中,仅仅打印了一些信息,便于实时观察pc模拟器网络的连接状态
--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
-- 设置默认网卡为socket.ETH0
-- pc模拟器上的默认网卡仍然需要使用接口(socket.ETH0)来设置,因为exnetif扩展库当前还不支持模拟器
socket.dft(socket.ETH0)
@@ -0,0 +1,32 @@
--[[
@module netdrv_device
@summary 网络驱动设备功能模块
@version 1.0
@date 2025.07.24
@author 王城钧
@usage
本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
1、netdrv_4gsocket.LWIP_GP4G网卡;
2、netdrv_eth_spisocket.LWIP_ETH,通过SPI外挂CH390H芯片的以太网卡;
3、netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
4、netdrv_pcsocket.LWIP_PCPC模拟器网卡;
根据自己的项目需求,只需要require以上其中的一种即可;
本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
]]
-- 根据自己的项目需求,只需要require以下其中的一种即可;
-- 加载“4G网卡”驱动模块
-- require "netdrv_4g"
-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
require "netdrv_eth_spi"
-- 加载“可以配置优先级的多种网卡”驱动模块
-- require "netdrv_multiple"
-- 加载“pc模拟器网卡”驱动模块
-- require "netdrv_pc"
@@ -0,0 +1,75 @@
## 功能模块介绍:
1、main.lua:主程序入口;
2、netdrv_device.lua:加载网络驱动设备功能模块;
3、http_lua:加载http应用模块;
## 演示功能概述
1、以太网给模组供网,通过连接http测试连通。
## 演示硬件环境
1、Air780EPM核心板一块+可上网的sim卡一张+网线一根+AirETH_1000板子一个;
[](https://docs.openLuat.com/cdn/image/780EPM_AirETH1000.jpg)
<img title="" src="https://docs.openLuat.com/cdn/image/780EPM_AirETH1000.jpg" alt="lan" style="zoom:25%;">
2、TYPE-C USB数据线一根 + 杜邦线若干;
* Air780EPM核心板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
* TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
* AirETH_1000板子网口与路由器网口通过网线连接;
3、Air780EPM核心板和AirETH_1000配件板的硬件接线方式为:
| Air780EPM核心板 | AirETH_1000配件板 |
| --------------- | ----------------- |
| 3.3V | 3.3v |
| gnd | gnd |
| spi0_sclk | SCK |
| spi0_cs | CSS |
| spi0_miso | SDO |
| spi0_mosi | SDI |
| gpio1 | INT |
演示软件环境
------
1、Luatools下载调试工具
2、[Air780EPM V2014版本固件](https://docs.openluat.com/air780EPM/luatos/firmware/)(理论上,2025年9月12日之后发布的固件都可以)
## 演示核心步骤
1、搭建好硬件环境,按接线图连接硬件。
2、烧录内核固件和本项目的Lua脚本:main.lua:主程序入口(需要在main.lua文件中打开require"netdrv_device"和require"http_app")
3、启动设备,观察日志输出:
出现类似如下打印,就表示成功。
```
[2025-09-18 11:31:29.931][000000003.367] I/user.netdrv_eth_spi.ip_ready_func IP_READY 192.168.0.52 255.255.255.0 192.168.0.1 nil
[2025-09-18 11:31:29.934][000000003.368] I/user.http_app_task_func recv IP_READY 4
[2025-09-18 11:31:29.938][000000003.372] dns_run 676:www.air32.cn state 0 id 1 ipv6 0 use dns server0, try
[2025-09-18 11:31:29.942][000000003.372] D/net adatper 4 dns server 192.168.0.1
[2025-09-18 11:31:29.944][000000003.372] D/net dns udp sendto 192.168.0.1:53 from 192.168.0.52
[2025-09-18 11:31:29.948][000000003.384] dns_run 693:dns all done ,now stop
[2025-09-18 11:31:29.950][000000003.385] D/net connect 49.232.89.122:443 TCP
[2025-09-18 11:31:30.641][000000004.123] I/user.http_app_get1 success 200 {"Transfer-Encoding":"chunked","Date":"Thu, 18 Sep 2025 03:31:33 GMT","Connection":"keep-alive","Server":"openresty\/1.27.1.2","Content-Type":"text\/html"} 2416
```
@@ -0,0 +1,63 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.09.22
@author 王城钧
@usage
本demo演示的核心功能为:
1.设置多网融合功能,4G提供网络供以太网设备上网
更多说明参考本目录下的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 = "4g_out_ethernet_in"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 开启多网融合功能
require "netif_app"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
@@ -0,0 +1,48 @@
--[[
@module netif_app
@summary netif_app 网络管理模块,开启多网融合功能,4G提供网络供以太网设备上网
@version 1.0
@date 2025.09.22
@author 王城钧
@usage
本文件为网络管理模块,核心业务逻辑为:
1、设置多网融合功能,4G提供网络供以太网设备上网
2、http测试4G网络
本文件没有对外接口,直接在main.lua中require "netif_app"就可以加载运行;
]]
exnetif = require "exnetif"
function netif_app_task_func()
local res
-- 等待4G网络连接成功
while not socket.adapter() do
-- 在此处阻塞等待4G网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
sys.waitUntil("IP_READY", 1000)
end
-- 设置多网融合功能,4G提供网络供以太网设备上网
res = exnetif.setproxy(socket.LWIP_ETH, socket.LWIP_GP, {
ethpower_en = 20, -- 以太网模块的pwrpin引脚(gpio编号)
tp = netdrv.CH390, -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
opts = {spi = 0, cs = 8, irq = 1}, -- 外挂方式,需要额外的参数(选填参数),仅spi方式外挂以太网时需要填写。
adapter_addr = "192.168.2.1", -- 自定义LWIP_ETH网卡的ip地址(选填),需要自定义ip和网关ip时填写
adapter_gw = {192, 168, 2, 1} -- 自定义LWIP_ETH网卡的网关地址(选填),需要自定义ip和网关ip时填写
})
if res then
log.info("exnetif", "setproxy success")
else
log.info("开启失败,请检查配置项是否正确,日志中是否打印了错误信息")
end
-- 每5秒进行HTTPS连接测试,实时监测4G网络连接状态, 仅供测试需要,量产不需要,用来判断当前网络是否可用,需要的话可以打开注释
-- while 1 do
-- local code, headers, body = http.request("GET", "https://httpbin.luatos.com/bytes/2048", nil, nil,
-- { adapter = socket.LWIP_GP, timeout = 5000, debug = false }).wait()
-- log.info("http执行结果", code, headers, body and #body)
-- sys.wait(5000)
-- end
end
sys.taskInit(netif_app_task_func)
@@ -0,0 +1,60 @@
## 功能模块介绍
1、main.lua:主程序入口;
2、netif_app: 网络管理模块,开启多网融合功能,4G提供网络供以太网设备上网;
## 演示功能概述
1、开启多网融合模式,4G连接外部网络,以太网lan模式为其他以太网设备提供接入
2、​网络监控​,每5秒进行HTTPS连接测试,实时监测4G网络的连接状态
## 演示硬件环境
![](https://docs.openLuat.com/cdn/image/780EPM_AirETH1000.jpg)
1、Air780EPM 核心板一块+可上网的sim卡一张+网线一根:
- sim卡插入核心板的sim卡槽
- 网线一端插入核心板外接的AirETH_1000小板上,另外一端连接需接入以太网的设备
2、TYPE-C USB数据线一根 + 网线一根,Air780EPM核心板和数据线的硬件接线方式为:
- Air780EPM 本核心板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
- 核心板与AirETH_1000的接线方式如下:
| Air780EPM核心板 | AirETH_1000配件板 |
| --------------- | ----------------- |
| vdd | 3.3v |
| gnd | gnd |
| spi0_sclk | SCK |
| spi0_cs | CSS |
| spi0_miso | SDO |
| spi0_mosi | SDI |
| gpio1 | INT |
## 演示软件环境
1、Luatools下载调试工具
2、[Air780EPM V2014版本固件](https://docs.openluat.com/air780epm/luatos/firmware/version/)(理论上,2025年8月10日之后发布的固件都可以)
## 演示核心步骤
1、搭建好硬件环境,按接线图连接硬件,
2、烧录内核固件和本项目的Lua脚本:main.lua:主程序入口,netif_app.lua:网络管理模块
3、启动设备,观察日志输出:
```lua
[INFO] exnetif setproxy success
[INFO] http执行结果 200 ...
```
4、其他设备通过以太网接入780EPM,其他设备都能正常上网,则表示验证成功。
@@ -0,0 +1,9 @@
本文件下共有两个示例demo
1、http:通过外挂AirETH_1000小板使用WAN功能或者多网切换模式连接http测试网络连通性。
2、network_routing
(1)4g_out_ethernet_in
使用网络路由功能,4G提供网络供以太网设备上网
@@ -0,0 +1,472 @@
--[[
@module AirGPIO_1000
@summary AirGPIO_1000应用功能模块
@version 1.0
@date 2025.10.21
@author 沈园园
@usage
本文件为AirGPIO_1000驱动配置文件,核心业务逻辑为:
1、配置主机和AirGPIO_1000之间的通信参数;
2、配置AirGPIO_1000上的扩展GPIO管脚功能;支持配置为输出,输入和中断三种模式;
本文件没有对外接口,直接require "AirGPIO_1000"就可以加载运行;
]]
--本文件中的主机是指I2C主机,具体指Air780epm
--本文件中的从机是指I2C从机,具体指AirGPIO_1000配件板上的IO扩展芯片
local AirGPIO_1000 =
{
-- i2c_id:主机的i2c id
-- gpio_int_id:主机的GPIO中断引脚id
-- slave_address:从机地址;
-- ints = --从机各个扩展IO配置为中断时的处理函数以及上一次的输入电平
-- {
-- [0x00] = {cb_func=, old_level=},
-- [0x01] = {cb_func=, old_level=},
-- [0x02] = {cb_func=, old_level=},
-- [0x03] = {cb_func=, old_level=},
-- [0x04] = {cb_func=, old_level=},
-- [0x05] = {cb_func=, old_level=},
-- [0x06] = {cb_func=, old_level=},
-- [0x07] = {cb_func=, old_level=},
-- [0x10] = {cb_func=, old_level=},
-- [0x11] = {cb_func=, old_level=},
-- [0x12] = {cb_func=, old_level=},
-- [0x13] = {cb_func=, old_level=},
-- [0x14] = {cb_func=, old_level=},
-- [0x15] = {cb_func=, old_level=},
-- [0x16] = {cb_func=, old_level=},
-- [0x17] = {cb_func=, old_level=},
-- }
}
-- 从机硬件上有三个引脚A0 A1 A2可以配置I2C从设备的地址
-- 当A0 A1 A2都接地时的从设备基地址为(0x40 >> 1)
-- A0 A1 A2有0到7一共八种排序组合,基地址+0/1/2/3/4/5/6/7即为八种从设备的地址
-- AirGPIO_1000默认A0 A1 A2都接地,所以AirGPIO_1000的从设备地址默认也为(0x40 >> 1)
local SALVE_ADDRESS_HIGH_4BIT = (0x40 >> 1)
-- 寄存器地址
local REG_INPUT_PORT_0 = 0x00 -- 输入端口0
local REG_INPUT_PORT_1 = 0x01 -- 输入端口1
local REG_OUTPUT_PORT_0 = 0x02 -- 输出端口0
local REG_OUTPUT_PORT_1 = 0x03 -- 输出端口1
local REG_POL_INV_0 = 0x04 -- 极性反转端口0
local REG_POL_INV_1 = 0x05 -- 极性反转端口1
local REG_CONFIG_0 = 0x06 -- 配置端口0
local REG_CONFIG_1 = 0x07 -- 配置端口1
-- 写入AirGPIO_1000的寄存器
--regnumber类型;
-- 表示AirGPIO_1000上的寄存器地址;
-- 取值范围:0x00到0x07,参考本文件上方的寄存器地址列表;
-- 必须传入,不允许为空;
--valuenumber类型;
-- 表示要写入到AirGPIO_1000寄存器中的数据;
-- 取值范围:0x00到0xFF,1个字节的长度;
-- 必须传入,不允许为空;
--返回值:成功返回true,失败返回false
local function write_register(reg, value)
local data = {reg, value}
local result = i2c.send(AirGPIO_1000.i2c_id, AirGPIO_1000.slave_address, data)
return result
end
-- 读取AirGPIO_1000的寄存器
--regnumber类型;
-- 表示AirGPIO_1000上的寄存器地址;
-- 取值范围:0x00到0x07,参考本文件上方的寄存器地址列表;
-- 必须传入,不允许为空;
--返回值:成功返回1个字节的number类型,失败返回nil
local function read_register(reg)
i2c.send(AirGPIO_1000.i2c_id, AirGPIO_1000.slave_address, reg)
local data = i2c.recv(AirGPIO_1000.i2c_id, AirGPIO_1000.slave_address, 1)
if data and #data == 1 then
return string.byte(data, 1)
end
return nil
end
--主机上的中断引脚处理函数
local function gpio_int_callback()
log.info("gpio_int_callback")
--在中断处理函数中不能直接执行耗时较长的动作
--所以在此处publish一个"AirGPIO_1000_INT"消息
--在其他位置订阅这个消息,进行异步处理
--异步处理这个消息的函数可以直接执行耗时较长的动作
sys.publish("AirGPIO_1000_INT")
end
--遍历用户扩展GPIO中断函数表,进行处理
local function user_gpio_int_callback()
if AirGPIO_1000.ints then
--遍历用户扩展GPIO中断函数表
for k,v in pairs(AirGPIO_1000.ints) do
if v then
--读取扩展GPIO的输入电平
local cur_level = AirGPIO_1000.get(k)
--如果输入电平和上一次输入电平不一致
--则执行用户扩展GPIO中断函数
if v.old_level~=cur_level then
v.old_level = cur_level
if v.cb_func then v.cb_func(k, cur_level) end
end
end
end
end
end
--订阅"AirGPIO_1000_INT"消息的处理函数user_gpio_int_callback
--当其他位置publish "AirGPIO_1000_INT"消息时,会执行user_gpio_int_callback
sys.subscribe("AirGPIO_1000_INT", user_gpio_int_callback)
--检查AirGPIO_1000上的扩展GPIO ID是否有效
--gpio_idnumber类型;
-- 表示AirGPIO_1000上的扩展GPIO ID
-- 取值范围:0x00到0x070x10到0x17,一共16种,分别对应16个扩展GPIO引脚;
-- 必须传入,不允许为空;
--返回值:有效返回true,无效返回false
local function check_gpio_id_valid(gpio_id)
return (gpio_id>=0x00 and gpio_id<=0x07 or gpio_id>=0x10 and gpio_id<=0x17)
end
--配置主机和AirGPIO_1000之间的通信参数;
--i2c_idnumber类型;
-- 主机使用的I2C ID,用来控制AirGPIO_1000
-- 取值范围:仅支持0和1;
-- 如果没有传入此参数,则默认为0;
--int_idnumber类型;
-- 主机使用的中断引脚GPIO ID,和AirGPIO_1000上的INT引脚相连;
-- AirGPIO_1000可以扩展出来16个GPIO,这些GPIO支持配置为输入;
-- AirGPIO_1000上的任意一个输入GPIO的状态发生上升沿或者下降沿变化时,会通过INT引脚通知到主机的int_id中断引脚;
-- 此时主机可以通过I2C接口立即读取AirGPIO_1000上配置为输入模式的扩展GPIO的电平状态,从而判断是哪些扩展GPIO的输入电平发生了变化;
-- 如果没有传入此参数,则默认为空,表示不使用中断通知功能;
--返回值:成功返回true,失败返回false
function AirGPIO_1000.init(i2c_id, gpio_int_id)
--检查参数的合法性
if not (i2c_id == 0 or i2c_id == 1) then
log.error("AirGPIO_1000.init", "invalid i2c_id", i2c_id)
return false
end
if not (gpio_int_id==nil or gpio_int_id>=0 and gpio_int_id<=9 or gpio_int_id>=12 and gpio_int_id<=55) then
log.error("AirGPIO_1000.init", "invalid gpio_int_id", gpio_int_id)
return false
end
AirGPIO_1000.i2c_id = i2c_id
AirGPIO_1000.gpio_int_id = gpio_int_id
--初始化I2C
if i2c.setup(i2c_id, i2c.FAST) ~= 1 then
log.error("AirGPIO_1000.init", "i2c.setup error", i2c_id)
return false
end
--自动识别从设备地址
--AirGPIO_1000上使用的TCA9555芯片有三个引脚,A2 A1 A0,可以配置三个bit的I2C从设备地址
--从 0 0 0 到 1 1 1,也就是十进制的0到7,一共可以配置8种;
--依次读取这8个从设备地址上的一个寄存器地址数据
--如果返回应答数据,则从设备地址自动识别成功
for i=0,7 do
i2c.send(i2c_id, SALVE_ADDRESS_HIGH_4BIT+i, REG_INPUT_PORT_0)
local data = i2c.recv(i2c_id, SALVE_ADDRESS_HIGH_4BIT+i, 1)
if data~=nil then
AirGPIO_1000.slave_address = SALVE_ADDRESS_HIGH_4BIT+i
log.error("AirGPIO_1000.init", "slave_address", SALVE_ADDRESS_HIGH_4BIT+i, data:byte())
break
end
end
--自动识别从设备地址失败
if not AirGPIO_1000.slave_address then
log.error("AirGPIO_1000.init", "slave_address unknown")
i2c.close(i2c_id)
return false
end
--配置主机上的中断GPIO,用来实时检测从机上扩展GPIO的输入电平变化
if gpio_int_id then
gpio.setup(gpio_int_id, gpio_int_callback, gpio.PULLUP, gpio.FALLING)
end
return true
end
--关闭主机和AirGPIO_1000之间的通信;
--返回值:成功返回true,失败返回false
function AirGPIO_1000.deinit()
--关闭主机I2C
if AirGPIO_1000.i2c_id then
i2c.close(AirGPIO_1000.i2c_id)
AirGPIO_1000.i2c_id = nil
AirGPIO_1000.slave_address = nil
end
--关闭主机中断GPIO
if AirGPIO_1000.gpio_int_id then
gpio.close(AirGPIO_1000.gpio_int_id)
AirGPIO_1000.gpio_int_id = nil
end
--清空用户注册的扩展GPIO中断处理表
if type(AirGPIO_1000.ints)=="table" then
for k,v in pairs(AirGPIO_1000.ints) do
AirGPIO_1000.ints[k] = nil
end
AirGPIO_1000.ints = nil
end
end
--[[
配置AirGPIO_1000上的扩展GPIO管脚功能;
支持配置为输出,输入和中断三种模式;
@api AirGPIO_1000.setup(gpio_id, gpio_mode)
@number
gpio_id
表示AirGPIO_1000上的扩展GPIO ID
取值范围:0x00到0x070x10到0x17,一共16种,分别对应16个扩展GPIO引脚;
必须传入,不允许为空或者nil
@number or function or nil or 空
gpio_mode
number类型时,表示输出模式,取值范围为0和1,0表示默认输出低电平,1表示默认输出高电平;
nil或者空类型时,表示输入模式;
function类型时,表示中断模式,此function为中断回调函数,函数的定义格式如下:
function cb_func(id, level)
--id:表示触发中断的AirGPIO_1000上的扩展GPIO ID,取值范围为0x00到0x070x10到0x17,一共16种,分别对应16个扩展GPIO引脚;
--level:触发中断后,某一时刻,扩展GPIO输入的电平状态,高电平为1, 低电平为0;并不是指触发中断的电平状态;
end
@return bool
成功返回true,失败返回false
@usage
-- GPIO ID 0x00配置为输出模式,默认输出低电平
AirGPIO_1000.setup(0x00, 0)
-- GPIO ID 0x11配置为输入模式
AirGPIO_1000.setup(0x11)
--P04引脚中断处理函数
--id0x04
--level:触发中断后,某一时刻,扩展GPIO输入的电平状态,高电平为1, 低电平为0
local function P04_int_cbfunc(id, level)
log.info("P04_int_cbfunc", id, level)
end
-- GPIO ID 0x04配置为中断模式,中断处理函数为P04_int_cbfunc
AirGPIO_1000.setup(0x04, P04_int_cbfunc)
]]
function AirGPIO_1000.setup(gpio_id, gpio_mode)
--检查参数的合法性
if not check_gpio_id_valid(gpio_id) then
log.error("AirGPIO_1000.setup", "invalid gpio_id", gpio_id)
return false
end
if not (gpio_mode==0 or gpio_mode==1 or gpio_mode==nil or type(gpio_mode)=="function") then
log.error("AirGPIO_1000.setup", "invalid gpio_mode", type(gpio_mode), gpio_mode)
return false
end
log.info("AirGPIO_1000.setup", "enter", gpio_id, type(gpio_mode), gpio_mode)
--根据扩展GPIO ID识别当前扩展GPIO使用的配置寄存器地址
--0x0x开头的ID为REG_CONFIG_00x01开头的ID为REG_CONFIG_1
local reg_addr = ((gpio_id>>4) == 0) and REG_CONFIG_0 or REG_CONFIG_1
--读取从机中输出寄存器当前的值
local reg_data = read_register(reg_addr)
if reg_data==nil then
log.error("AirGPIO_1000.setup", "read config register error", reg_addr)
return false
end
local mask = 1<<(gpio_id&0x0F)
local value
--GPIO配置为输出模式
if gpio_mode==0 or gpio_mode==1 then
value = reg_data & (~mask)
--GPIO配置为输入模式
elseif gpio_mode==nil or type(gpio_mode)=="function" then
value = reg_data | mask
end
--如果寄存器新值和旧值相比,发生变化
--写新值到从机的配置寄存器中
if reg_data~=value then
if not write_register(reg_addr, value) then
log.error("AirGPIO_1000.setup", "config write error", reg_addr, value)
return false
end
end
log.info("AirGPIO_1000.setup", "config", reg_addr, reg_data, value)
--如果是中断模式,并且用户注册了中断处理函数
if type(gpio_mode)=="function" then
if AirGPIO_1000.ints==nil then
AirGPIO_1000.ints = {}
end
if AirGPIO_1000.ints[gpio_id]==nil then
AirGPIO_1000.ints[gpio_id] = {}
end
--存储中断处理函数
AirGPIO_1000.ints[gpio_id].cb_func = gpio_mode
--读取当前时刻GPIO的输入电平状态
AirGPIO_1000.ints[gpio_id].old_level = AirGPIO_1000.get(gpio_id)
end
--如果配置的是输入模式或者中断模式,可以直接返回了
if gpio_mode~=0 and gpio_mode~=1 then return true end
--如果配置的输出模式,初始化输出的电平为gpio_mode
if not AirGPIO_1000.set(gpio_id, gpio_mode) then
log.error("AirGPIO_1000.setup", "output set error")
return false
end
log.info("AirGPIO_1000.setup", "output", reg_addr, reg_data, value)
return true
end
--设置AirGPIO_1000上配置为输出模式的扩展GPIO的输出电平
--gpio_idnumber类型;
-- 表示AirGPIO_1000上的扩展GPIO ID
-- 取值范围:0x00到0x070x10到0x17,一共16种,分别对应16个扩展GPIO引脚;
-- 必须传入,不允许为空;
--output_levelnumber类型;
-- 表示配置为输出模式的扩展GPIO对外输出的电平;
-- 取值范围:0和1,0表示输出低电平,1表示输出高电平;
-- 必须传入,不允许为空;
--返回值:成功返回true,失败返回false
function AirGPIO_1000.set(gpio_id, output_level)
--检查参数的合法性
if not check_gpio_id_valid(gpio_id) then
log.error("AirGPIO_1000.set", "invalid gpio_id", gpio_id)
return false
end
if not (output_level==0 or output_level==1) then
log.error("AirGPIO_1000.set", "invalid output_level", type(output_level), output_level)
return false
end
log.info("AirGPIO_1000.set", "enter", gpio_id, output_level)
--根据扩展GPIO ID识别当前扩展GPIO使用的输出寄存器地址
--0x0x开头的ID为REG_OUTPUT_PORT_00x01开头的ID为REG_OUTPUT_PORT_1
local reg_addr = ((gpio_id>>4) == 0) and REG_OUTPUT_PORT_0 or REG_OUTPUT_PORT_1
--读取从机中输出寄存器当前的值
local reg_data = read_register(reg_addr)
if reg_data==nil then
log.error("AirGPIO_1000.set", "read output register error", reg_addr)
return false
end
local mask = 1<<(gpio_id&0x0F)
local value
--输出低电平
if output_level==0 then
value = reg_data & (~mask)
--输出高电平
elseif output_level==1 then
value = reg_data | mask
end
--如果寄存器新值和旧值相比,发生变化
--写新值到从机的输出寄存器中
if reg_data~=value then
if not write_register(reg_addr, value) then
log.error("AirGPIO_1000.set", "output write error", reg_addr, value)
return false
end
end
log.info("AirGPIO_1000.set", "output", reg_addr, reg_data, value)
return true
end
--读取AirGPIO_1000上配置为输入或者中断模式的扩展GPIO的输入电平
--gpio_idnumber类型;
-- 表示AirGPIO_1000上的扩展GPIO ID
-- 取值范围:0x00到0x070x10到0x17,一共16种,分别对应16个扩展GPIO引脚;
-- 必须传入,不允许为空;
--返回值:number类型,表示输入的电平,0表示低电平,1表示高电平;如果读取失败,返回false
function AirGPIO_1000.get(gpio_id)
--检查参数的合法性
if not check_gpio_id_valid(gpio_id) then
log.error("AirGPIO_1000.get", "invalid gpio_id", gpio_id)
return false
end
--根据扩展GPIO ID识别当前扩展GPIO使用的输入寄存器地址
--0x0x开头的ID为REG_INPUT_PORT_00x01开头的ID为REG_INPUT_PORT_1
local reg_addr = ((gpio_id>>4) == 0) and REG_INPUT_PORT_0 or REG_INPUT_PORT_1
--读取从机中输入寄存器当前的值
local value = read_register(reg_addr)
if not value then
log.error("AirGPIO_1000.get", "read_register error", reg_addr)
return false
end
--返回输入寄存器的值和GPIO对应的bit位的值
return ((value>>(gpio_id&0x0F)) & 0x01)
end
--关闭AirGPIO_1000上的扩展GPIO功能
--实际上是恢复为默认状态(配置为输入)
--gpio_idnumber类型;
-- 表示AirGPIO_1000上的扩展GPIO ID
-- 取值范围:0x00到0x070x10到0x17,一共16种,分别对应16个扩展GPIO引脚;
-- 必须传入,不允许为空;
--返回值:成功返回true,失败返回false
function AirGPIO_1000.close(gpio_id)
local result = AirGPIO_1000.setup(gpio_id)
if not result then
log.error("AirGPIO_1000.close", "error", gpio_id)
end
return result
end
return AirGPIO_1000
@@ -0,0 +1,111 @@
--[[
@module gpio_app
@summary gpio_app应用功能模块
@version 1.0
@date 2025.10.21
@author 沈园园
@usage
本文件为gpio_app应用功能模块,核心业务逻辑为:
1、初始化Air780EPM和AirGPIO_1000之间的通信参数
2、GPIO输出测试,输入测试,GPIO中断测试
本文件没有对外接口,直接在main.lua中require "gpio_app"就可以加载运行;
]]
--加载AirGPIO_1000驱动文件
local air_gpio = require "AirGPIO_1000"
--AirGPIO_1000扩展GPIO输出测试
--P00每隔一秒切换输出一次高低电平,可以通过示波器或者万用表测量AirGPIO_1000上P00引脚电平
local function gpio_output_task_func()
air_gpio.setup(0x00, 0)
while true do
air_gpio.set(0x00, 0)
sys.wait(1000)
air_gpio.set(0x00, 1)
sys.wait(1000)
end
end
--AirGPIO_1000扩展GPIO输入测试
--P10配置为输出模式,每隔一秒切换输出一次高低电平
--P11配置为输入模式,每隔一秒调用get接口读取一次输入的电平
--将P10和P11两个引脚短接
local function gpio_input_task_func()
air_gpio.setup(0x10, 0)
air_gpio.setup(0x11)
while true do
air_gpio.set(0x10, 0)
sys.wait(1000)
log.info("air_gpio.get(0x11)", air_gpio.get(0x11))
air_gpio.set(0x10, 1)
sys.wait(1000)
log.info("air_gpio.get(0x11)", air_gpio.get(0x11))
end
end
--P04引脚中断处理函数
--id0x04
--level:触发中断后,某一时刻,扩展GPIO输入的电平状态,高电平为1, 低电平为0
local function P04_int_cbfunc(id, level)
log.info("P04_int_cbfunc", id, level)
end
--P14引脚中断处理函数
--id0x14
--level:触发中断后,某一时刻,扩展GPIO输入的电平状态,高电平为1, 低电平为0
local function P14_int_cbfunc(id, level)
log.info("P14_int_cbfunc", id, level)
end
--AirGPIO_1000扩展GPIO中断测试
--P03配置为输出模式,每隔一秒切换输出一次高低电平
--P04配置为中断模式,并且配置中断处理函数P04_int_cbfunc
--将P03和P04两个引脚短接
--P13配置为输出模式,每隔一秒切换输出一次高低电平
--P14配置为中断模式,并且配置中断处理函数P14_int_cbfunc
--将P13和P14两个引脚短接
local function gpio_int_task_func()
air_gpio.setup(0x03, 0)
air_gpio.setup(0x04, P04_int_cbfunc)
air_gpio.setup(0x13, 0)
air_gpio.setup(0x14, P14_int_cbfunc)
while true do
air_gpio.set(0x03, 0)
air_gpio.set(0x13, 0)
sys.wait(1000)
air_gpio.set(0x03, 1)
air_gpio.set(0x13, 1)
sys.wait(1000)
end
end
--初始化Air780EPM和AirGPIO_1000之间的通信参数
--使用Air780EPM的I2C0
--使用Air780EPM的GPIO2做为中断引脚
--Air780EPM核心板和AirGPIO_1000配件板的接线方式如下
--Air780EPM核心板 AirGPIO_1000配件板
--3V3-----------------3V3
-- GND-----------------GND
-- 66/I2C1SDA-----------------SDA
-- 67/I2C1SCL-----------------SCL
-- 23/GPIO2-----------------INT
air_gpio.init(1, 2)
--AirGPIO_1000的GPIO输出测试
sys.taskInit(gpio_output_task_func)
--AirGPIO_1000的GPIO输入测试
sys.taskInit(gpio_input_task_func)
--AirGPIO_1000的GPIO中断测试
sys.taskInit(gpio_int_task_func)
@@ -0,0 +1,65 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.10.24
@author 沈园园
@usage
AirGPIO_1000是合宙设计生产的一款I2C转16路扩展GPIO的配件板;
本demo演示的核心功能为:
Air780EPM核心板+AirGPIO_1000配件板,演示I2C扩展16路GPIO功能;
分输出、输入和中断三种应用场景来演示;
更多说明参考本目录下的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 = "AirGPIO_1000"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 加载gpio应用模块
require "gpio_app"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,131 @@
## 功能模块介绍
1、main.lua:主程序入口;
2、gpio_app.luaAirGPIO_1000扩展GPIO输出测试,输入测试,GPIO中断测试;
3、AirGPIO_1000.luaAirGPIO_1000驱动配置文件;
## 演示功能概述
AirGPIO_1000是合宙设计生产的一款I2C转16路扩展GPIO的配件板;
本demo演示的核心功能为:
Air780EPM核心板+AirGPIO_1000配件板,演示I2C扩展16路GPIO功能;
分输出、输入和中断三种应用场景来演示;
## 核心板+配件板资料
[Air780EPM核心板](https://docs.openluat.com/air780epm/product/shouce/)
[AirGPIO_1000配件板相关资料](https://docs.openluat.com/accessory/AirGPIO_1000/)
## 演示硬件环境
![](https://docs.openluat.com/accessory/AirGPIO_1000/image/connect_Air780EPM.png)
1、Air780EPM核心板
2、AirGPIO_1000配件板
3、母对母的杜邦线8根
4、Air780EPM核心板和AirGPIO_1000配件板的硬件接线方式为
| Air780EPM核心板 | AirGPIO_1000配件板 |
| ------------ | ------------------ |
| 3V3 | 3V3 |
| GND | GND |
| 66/I2C1SDA | SDA |
| 67/I2C1SCL | SCL |
| 23/GPIO2 | INT |
- 扩展GPIO输出演示时,无需接线;通过万用表或者示波器检测AirGPIO_1000配件板上的P00电平即可
- 扩展GPIO输入演示时,将AirGPIO_1000配件板上的P10和P11两个引脚通过杜邦线短接;软件上会将P10配置为输出(第一秒输出低电平,第二秒输出高电平,如此循环输出),将P11配置为输入,通过检测P11引脚输入电平的状态来演示
- 扩展GPIO中断演示时,将AirGPIO_1000配件板上的P03和P04两个引脚通过杜邦线短接,将AirGPIO_1000配件板上的P13和P14两个引脚通过杜邦线短接;软件上会将P03和P13配置为输出(第一秒输出低电平,第二秒输出高电平,如此循环输出),将P04和P14配置为中断,通过检测中断函数的触发状态来演示
## 演示软件环境
1、Luatools下载调试工具
2、[Air780EPM最新版本的内核固件](https://docs.openluat.com/air780epm/luatos/firmware/version/)
## 演示操作步骤
1、搭建好演示硬件环境
2、不需要修改demo脚本代码
3、Luatools烧录内核固件和demo脚本代码
4、烧录成功后,自动开机运行
(1) 通过万用表或者示波器检测AirGPIO_1000配件板上的P00电平,持续1秒输出0V的低电平,持续1秒输出3.3V的高电平,循环输出,表示GPIO输出测试正常;
(2) 通过观察Luatools的运行日志,首先打印 air_gpio.get(0x11) 0 再隔一秒打印 air_gpio.get(0x11) 1,再隔一秒打印 air_gpio.get(0x11) 0,如此循环输出,表示GPIO输入测试正常;
(3) 通过观察Luatools的运行日志,首先打印 P04_int_cbfunc 4 0 P14_int_cbfunc 20 0 再隔一秒打印 P04_int_cbfunc 4 1 P14_int_cbfunc 20 1,再隔一秒打印 P04_int_cbfunc 4 0 P14_int_cbfunc 20 0,如此循环输出,表示GPIO中断测试正常;
```
[2025-10-15 15:01:49.745][000000002.228] I/user.air_gpio.get(0x11) 1
[2025-10-15 15:01:49.752][000000002.229] I/user.AirGPIO_1000.set enter 16 0
[2025-10-15 15:01:49.757][000000002.230] I/user.AirGPIO_1000.set output 3 255 254
[2025-10-15 15:01:49.762][000000002.230] I/user.gpio_int_callback
[2025-10-15 15:01:49.767][000000002.239] I/user.AirGPIO_1000.set enter 3 0
[2025-10-15 15:01:49.771][000000002.240] I/user.AirGPIO_1000.set output 2 254 246
[2025-10-15 15:01:49.775][000000002.240] I/user.AirGPIO_1000.set enter 19 0
[2025-10-15 15:01:49.779][000000002.241] I/user.AirGPIO_1000.set output 3 254 246
[2025-10-15 15:01:49.784][000000002.242] I/user.gpio_int_callback
[2025-10-15 15:01:49.786][000000002.243] I/user.P04_int_cbfunc 4 0
[2025-10-15 15:01:49.789][000000002.244] I/user.P14_int_cbfunc 20 0
[2025-10-15 15:01:50.719][000000003.223] I/user.AirGPIO_1000.set enter 0 1
[2025-10-15 15:01:50.732][000000003.224] I/user.AirGPIO_1000.set output 2 246 247
[2025-10-15 15:01:50.744][000000003.230] I/user.air_gpio.get(0x11) 0
[2025-10-15 15:01:50.752][000000003.231] I/user.AirGPIO_1000.set enter 16 1
[2025-10-15 15:01:50.759][000000003.232] I/user.AirGPIO_1000.set output 3 246 247
[2025-10-15 15:01:50.767][000000003.232] I/user.gpio_int_callback
[2025-10-15 15:01:50.773][000000003.242] I/user.AirGPIO_1000.set enter 3 1
[2025-10-15 15:01:50.776][000000003.243] I/user.AirGPIO_1000.set output 2 247 255
[2025-10-15 15:01:50.780][000000003.243] I/user.AirGPIO_1000.set enter 19 1
[2025-10-15 15:01:50.785][000000003.244] I/user.AirGPIO_1000.set output 3 247 255
[2025-10-15 15:01:50.788][000000003.245] I/user.gpio_int_callback
[2025-10-15 15:01:50.790][000000003.246] I/user.P04_int_cbfunc 4 1
[2025-10-15 15:01:50.794][000000003.247] I/user.P14_int_cbfunc 20 1
[2025-10-15 15:01:51.727][000000004.224] I/user.AirGPIO_1000.set enter 0 0
[2025-10-15 15:01:51.736][000000004.225] I/user.AirGPIO_1000.set output 2 255 254
[2025-10-15 15:01:51.746][000000004.233] I/user.air_gpio.get(0x11) 1
[2025-10-15 15:01:51.756][000000004.233] I/user.AirGPIO_1000.set enter 16 0
[2025-10-15 15:01:51.765][000000004.234] I/user.AirGPIO_1000.set output 3 255 254
[2025-10-15 15:01:51.770][000000004.234] I/user.gpio_int_callback
[2025-10-15 15:01:51.776][000000004.244] I/user.AirGPIO_1000.set enter 3 0
[2025-10-15 15:01:51.782][000000004.245] I/user.AirGPIO_1000.set output 2 254 246
[2025-10-15 15:01:51.787][000000004.245] I/user.AirGPIO_1000.set enter 19 0
[2025-10-15 15:01:51.793][000000004.246] I/user.AirGPIO_1000.set output 3 254 246
[2025-10-15 15:01:51.796][000000004.247] I/user.gpio_int_callback
[2025-10-15 15:01:51.799][000000004.248] I/user.P04_int_cbfunc 4 0
[2025-10-15 15:01:51.803][000000004.249] I/user.P14_int_cbfunc 20 0
[2025-10-15 15:01:52.721][000000005.225] I/user.AirGPIO_1000.set enter 0 1
[2025-10-15 15:01:52.730][000000005.226] I/user.AirGPIO_1000.set output 2 246 247
[2025-10-15 15:01:52.742][000000005.234] I/user.air_gpio.get(0x11) 0
[2025-10-15 15:01:52.752][000000005.235] I/user.AirGPIO_1000.set enter 16 1
[2025-10-15 15:01:52.760][000000005.236] I/user.AirGPIO_1000.set output 3 246 247
[2025-10-15 15:01:52.770][000000005.236] I/user.gpio_int_callback
[2025-10-15 15:01:52.779][000000005.246] I/user.AirGPIO_1000.set enter 3 1
[2025-10-15 15:01:52.785][000000005.247] I/user.AirGPIO_1000.set output 2 247 255
[2025-10-15 15:01:52.791][000000005.247] I/user.AirGPIO_1000.set enter 19 1
[2025-10-15 15:01:52.798][000000005.248] I/user.AirGPIO_1000.set output 3 247 255
[2025-10-15 15:01:52.803][000000005.249] I/user.gpio_int_callback
[2025-10-15 15:01:52.808][000000005.250] I/user.P04_int_cbfunc 4 1
[2025-10-15 15:01:52.813][000000005.251] I/user.P14_int_cbfunc 20 1
[2025-10-15 15:01:53.721][000000006.226] I/user.AirGPIO_1000.set enter 0 0
[2025-10-15 15:01:53.724][000000006.227] I/user.AirGPIO_1000.set output 2 255 254
```
@@ -0,0 +1,63 @@
--[[
@module AirKEY_1000
@summary AirKEY_1000应用功能模块
@version 1.0
@date 2025.10.22
@author 沈园园
@usage
本文件为AirKEY_1000驱动配置文件,核心业务逻辑为:
1、配置主机和AirKEY_1000之间的控制参数;
本文件没有对外接口,直接require "AirKEY_1000"就可以加载运行;
]]
--本文件中的主机是指Air780EPM核心板
--AirKEY_1000是合宙设计生产的一款8路独立按键的配件板
local AirKEY_1000 = {}
--配置主机和AirKEY_1000之间的控制参数;
--key_idnumber类型;
-- AirKEY_1000的按键ID
-- 取值范围:1到8
-- 必须传入,不允许为空;
--gpio_idnumber类型;
-- 主机使用的中断引脚GPIO ID;
-- AirKEY_1000上的一个按键引脚,和主机上的一个GPIO中断引脚相连;
-- AirKEY_1000上的按键按下或者弹起,主机上的GPIO中断引脚输入电平发生变化,从而产生中断;
--int_modenumber类型;
-- 表示主机GPIO中断触发类型;
-- 取值范围:gpio.RISING表示上升沿触发,gpio.FALLING表示下降沿触发;
-- 如果没有传入此参数,则默认为gpio.FALLING
--int_cbfuncfunction类型;
-- 表示中断处理函数,函数的定义格式如下:
-- function cb_func(level, gpio_id)
-- --level:表示触发中断后,某一时刻引脚的电平,1为高电平,0为低电平,并不一定是触发中断时的电平
-- --gpio_id:表示触发中断的主机中断引脚的GPIO ID;
-- end
-- 中断函数中不要直接执行耗时较长的动作,例如写fskv,写文件,延时等,可以publish消息给其他协程或者给订阅消息的处理函数去执行耗时动作
-- 必须传入,不允许为空;
--返回值:成功返回true,失败返回false
function AirKEY_1000.setup(key_id, gpio_id, int_mode, int_cbfunc)
if not (int_mode==gpio.RISING or int_mode==gpio.FALLING) then
log.error("AirKEY_1000.setup error", "invalid int_mode", int_mode)
return false
end
if type(int_cbfunc)~="function" then
log.error("AirKEY_1000.setup error", "invalid int_cbfunc", type(int_cbfunc))
return false
end
-- gpio.setup(gpio_id, int_cbfunc, int_mode==gpio.RISING and gpio.PULLDOWN or gpio.PULLUP, int_mode)
gpio.setup(gpio_id, int_cbfunc, gpio.PULLUP, int_mode)
return true
end
return AirKEY_1000
@@ -0,0 +1,146 @@
--[[
@module key_app
@summary key_app应用功能模块
@version 1.0
@date 2025.10.22
@author 沈园园
@usage
本文件为key_app应用功能模块,核心业务逻辑为:
1、使用Air780EPM核心板的GPIO中断检测AirKEY_1000配件板上8个独立按键的按下或者弹起状态
2、AirKEY_1000是合宙设计生产的一款8路独立按键的配件板
本文件没有对外接口,直接在main.lua中require "key_app"就可以加载运行;
]]
--加载AirKEY_1000驱动文件
local air_key = require "AirKEY_1000"
--AirKEY_1000上8个按键对应的Air780EPM的GPIO ID
local KEY1_GPIO_ID = 26
local KEY2_GPIO_ID = 21
local KEY3_GPIO_ID = 24
local KEY4_GPIO_ID = 22
local KEY5_GPIO_ID = 23
local KEY6_GPIO_ID = 25
local KEY7_GPIO_ID = 28
local KEY8_GPIO_ID = 27
--按键1的中断处理函数
--int_levelnumber类型,表示触发中断后,某一时刻引脚的电平,1为高电平,0为低电平,并不一定是触发中断时的电平
--gpio_idnumber类型,air_key.setup函数配置按键1时,对应的Air780EPM上的GPIO ID
--在中断处理函数中,不要直接执行耗时较长的动作,例如写fskv,写文件,延时等
--可以publish消息给其他协程或者给订阅消息的处理函数去执行耗时动作
local function key1_int_cbfunc(int_level, gpio_id)
log.info("key1_int_cbfunc pressup", gpio_id, int_level)
--如果需要执行耗时较长的动作,不要在这里直接执行
--而是使用以下代码,publish一个消息出去,给其他协程或者给订阅消息的处理函数去执行耗时动作
sys.publish("KEY1_PRESSUP_IND")
end
--按键2的中断处理函数
--int_levelnumber类型,表示触发中断后,某一时刻引脚的电平,1为高电平,0为低电平,并不一定是触发中断时的电平
--gpio_idnumber类型,air_key.setup函数配置按键2时,对应的Air780EPM上的GPIO ID
--在中断处理函数中,不要直接执行耗时较长的动作,例如写fskv,写文件,延时等
--可以publish消息给其他协程或者给订阅消息的处理函数去执行耗时动作
local function key2_int_cbfunc(int_level, gpio_id)
log.info("key2_int_cbfunc pressup", gpio_id, int_level)
--如果需要执行耗时较长的动作,不要在这里直接执行
--而是使用以下代码,publish一个消息出去,给其他协程或者给订阅消息的处理函数去执行耗时动作
sys.publish("KEY2_PRESSUP_IND")
end
--按键3的中断处理函数
--int_levelnumber类型,表示触发中断后,某一时刻引脚的电平,1为高电平,0为低电平,并不一定是触发中断时的电平
--gpio_idnumber类型,air_key.setup函数配置按键3时,对应的Air780EPM上的GPIO ID
--在中断处理函数中,不要直接执行耗时较长的动作,例如写fskv,写文件,延时等
--可以publish消息给其他协程或者给订阅消息的处理函数去执行耗时动作
local function key3_int_cbfunc(int_level, gpio_id)
log.info("key3_int_cbfunc pressup", gpio_id, int_level)
--如果需要执行耗时较长的动作,不要在这里直接执行
--而是使用以下代码,publish一个消息出去,给其他协程或者给订阅消息的处理函数去执行耗时动作
sys.publish("KEY3_PRESSUP_IND")
end
--按键4的中断处理函数
--int_levelnumber类型,表示触发中断后,某一时刻引脚的电平,1为高电平,0为低电平,并不一定是触发中断时的电平
--gpio_idnumber类型,air_key.setup函数配置按键4时,对应的Air780EPM上的GPIO ID
--在中断处理函数中,不要直接执行耗时较长的动作,例如写fskv,写文件,延时等
--可以publish消息给其他协程或者给订阅消息的处理函数去执行耗时动作
local function key4_int_cbfunc(int_level, gpio_id)
log.info("key4_int_cbfunc pressup", gpio_id, int_level)
--如果需要执行耗时较长的动作,不要在这里直接执行
--而是使用以下代码,publish一个消息出去,给其他协程或者给订阅消息的处理函数去执行耗时动作
sys.publish("KEY4_PRESSUP_IND")
end
--按键5、6、7、8的中断处理函数
--int_levelnumber类型,表示触发中断后,某一时刻引脚的电平,1为高电平,0为低电平,并不一定是触发中断时的电平
--gpio_idnumber类型,air_key.setup函数配置按键5、6、7、8时,对应的Air780EPM上的GPIO ID
--在中断处理函数中,不要直接执行耗时较长的动作,例如写fskv,写文件,延时等
--可以publish消息给其他协程或者给订阅消息的处理函数去执行耗时动作
local function key5678_int_cbfunc(int_level, gpio_id)
log.info("key5678_int_cbfunc", gpio_id, int_level)
--如果需要执行耗时较长的动作,不要在这里直接执行
--而是使用以下代码,publish一个消息出去,给其他协程或者给订阅消息的处理函数去执行耗时动作
if gpio_id==KEY5_GPIO_ID then
log.info("key5 pressdown")
sys.publish("KEY5_PRESSDOWN_IND")
elseif gpio_id==KEY6_GPIO_ID then
log.info("key6 pressdown")
sys.publish("KEY6_PRESSDOWN_IND")
elseif gpio_id==KEY7_GPIO_ID then
log.info("key7 pressdown")
sys.publish("KEY7_PRESSDOWN_IND")
elseif gpio_id==KEY8_GPIO_ID then
log.info("key8 pressdown")
sys.publish("KEY8_PRESSDOWN_IND")
end
end
--本demo中,Air780EPM核心板和AirKEY_1000配件板的接线方式如下
--Air780EPM核心板 AirKEY_1000配件板
-- 25/GPIO26-----------------K1
-- 107/GPIO21-----------------K2
-- 20/GPIO24-----------------K3
-- 19/GPIO22-----------------K4
-- 99/GPIO23-----------------K5
-- 106/GPIO25-----------------K6
-- 78/GPIO28-----------------K7
-- 16/GPIO27-----------------K8
-- GND-----------------G
--AirKEY_1000上的1、2、3、4,四个按键的引脚
--分别和Air780EPM的KEY1_GPIO_ID、KEY2_GPIO_ID、KEY3_GPIO_ID、KEY4_GPIO_ID四个引脚相连
--GPIO配置为上升沿触发中断,可以实时检测到按键弹起的动作
--按键弹起时,会执行对应的中断处理函数key1_int_cbfunc、key2_int_cbfunc、key3_int_cbfunc、key4_int_cbfunc
--在中断处理函数中,不要直接执行耗时较长的动作,例如写fskv,写文件,延时等
--可以publish消息给其他协程或者给订阅消息的处理函数去执行耗时动作
air_key.setup(1, KEY1_GPIO_ID, gpio.RISING, key1_int_cbfunc)
air_key.setup(2, KEY2_GPIO_ID, gpio.RISING, key2_int_cbfunc)
air_key.setup(3, KEY3_GPIO_ID, gpio.RISING, key3_int_cbfunc)
air_key.setup(4, KEY4_GPIO_ID, gpio.RISING, key4_int_cbfunc)
--AirKEY_1000上的5、6、7、8,四个按键的引脚
--分别和Air780EPM的KEY5_GPIO_ID、KEY6_GPIO_ID、KEY7_GPIO_ID、KEY8_GPIO_ID四个引脚相连
--GPIO配置为下降沿触发中断,可以实时检测到按键按下的动作
--按键按下时,会执行对应的中断处理函数key5678_int_cbfunc
--这四个按键共用了同一个中断处理函数,可以通过函数传入的GPIO ID来区分是哪一个按键被按下
--在中断处理函数中,不要直接执行耗时较长的动作,例如写fskv,写文件,延时等
--可以publish消息给其他协程或者给订阅消息的处理函数去执行耗时动作
air_key.setup(5, KEY5_GPIO_ID, gpio.FALLING, key5678_int_cbfunc)
air_key.setup(6, KEY6_GPIO_ID, gpio.FALLING, key5678_int_cbfunc)
air_key.setup(7, KEY7_GPIO_ID, gpio.FALLING, key5678_int_cbfunc)
air_key.setup(8, KEY8_GPIO_ID, gpio.FALLING, key5678_int_cbfunc)
@@ -0,0 +1,64 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.10.24
@author 沈园园
@usage
AirKEY_1000是合宙设计生产的一款支持8个独立按键的配件板;
本demo演示的核心功能为:
Air780EPM核心板+AirKEY_1000配件板,使用Air780EPM核心板的GPIO中断检测AirKEY_1000配件板上8个独立按键的按下或者弹起状态;
更多说明参考本目录下的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 = "AirKEY_1000"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 加载KEY应用模块
require "key_app"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,120 @@
## 功能模块介绍
1、main.lua:主程序入口;
2、key_app.lua:使用核心板的GPIO中断检测AirKEY_1000配件板上8个独立按键的按下或者弹起状态;
3、AirKEY_1000.lua:配置主机和AirKEY_1000之间的控制参数;
## 用户消息介绍
1、"KEY1_PRESSUP_IND":按键消息,publish该消息给其他协程或者给订阅消息的处理函数去执行耗时动作;
2、"KEY2_PRESSUP_IND":按键消息,publish该消息给其他协程或者给订阅消息的处理函数去执行耗时动作;
3、"KEY3_PRESSUP_IND":按键消息,publish该消息给其他协程或者给订阅消息的处理函数去执行耗时动作;
4、"KEY4_PRESSUP_IND":按键消息,publish该消息给其他协程或者给订阅消息的处理函数去执行耗时动作;
5、"KEY5_PRESSUP_IND":按键消息,publish该消息给其他协程或者给订阅消息的处理函数去执行耗时动作;
6、"KEY5_PRESSUP_IND":按键消息,publish该消息给其他协程或者给订阅消息的处理函数去执行耗时动作;
7、"KEY7_PRESSUP_IND":按键消息,publish该消息给其他协程或者给订阅消息的处理函数去执行耗时动作;
8、"KEY8_PRESSUP_IND":按键消息,publish该消息给其他协程或者给订阅消息的处理函数去执行耗时动作;
## 演示功能概述
AirKEY_1000是合宙设计生产的一款支持8个独立按键的配件板;
本demo演示的核心功能为:
Air780EPM核心板+AirKEY_1000配件板,使用Air780EPM核心板的GPIO中断检测AirKEY_1000配件板上8个独立按键的按下或者弹起状态;
## 核心板+配件板资料
[Air780EPM核心板](https://docs.openluat.com/Air780EPM/product/shouce/)
[AirKEY_1000配件板相关资料](https://docs.openluat.com/accessory/AirKEY_1000/)
## 演示硬件环境
![](https://docs.openluat.com/accessory/AirKEY_1000/image/Air780EPM_connect.jpg)
1、Air780EPM核心板
2、AirKEY_1000配件板
3、母对母的杜邦线9根
4、Air780EPM核心板和AirKEY_1000配件板的硬件接线方式为
- Air780EPM核心板通过TYPE-C USB口连接TYPE-C USB 数据线,数据线的另外一端连接电脑的USB口;
- 核心板正面的 ON/OFF 拨动开关 拨到ON一端;
| Air780EPM核心板 | AirKEY_1000配件板 |
| ------------ | ------------------ |
| 25/GPIO26 | K1 |
| 107/GPIO21 | K2 |
| 20/GPIO24 | K3 |
| 19/GPIO22 | K4 |
| 99/GPIO23 | K5 |
| 106/GPIO25 | K6 |
| 78/GPIO28 | K7 |
| 16/GPIO27 | K8 |
| GND | G |
## 演示软件环境
1、[Luatools下载调试工具](https://docs.openluat.com/air780epm/common/Luatools/)
2、[Air780EPM 最新版本的内核固件](https://docs.openluat.com/air780epm/luatos/firmware/version/)
## 演示操作步骤
1、搭建好演示硬件环境
2、不需要修改demo脚本代码
3、Luatools烧录内核固件和demo脚本代码
4、烧录成功后,自动开机运行
(1) 按键1弹起时,Luatools的运行日志输出 key1_int_cbfunc pressup,表示按键1测试正常;
(2) 按键2弹起时,Luatools的运行日志输出 key2_int_cbfunc pressup,表示按键2测试正常;
(3) 按键3弹起时,Luatools的运行日志输出 key3_int_cbfunc pressup,表示按键3测试正常;
(4) 按键4弹起时,Luatools的运行日志输出 key4_int_cbfunc pressup,表示按键4测试正常;
(5) 按键5按下时,Luatools的运行日志输出 key5 pressdown,表示按键5测试正常;
(6) 按键6按下时,Luatools的运行日志输出 key6 pressdown,表示按键6测试正常;
(7) 按键7按下时,Luatools的运行日志输出 key7 pressdown,表示按键7测试正常;
(8) 按键8按下时,Luatools的运行日志输出 key8 pressdown,表示按键8测试正常;
```
[2025-10-17 17:11:50.721][000000017.690] I/user.key1_int_cbfunc pressup 26 1
[2025-10-17 17:11:51.867][000000018.832] I/user.key2_int_cbfunc pressup 21 1
[2025-10-17 17:11:53.408][000000020.378] I/user.key3_int_cbfunc pressup 24 1
[2025-10-17 17:11:54.785][000000021.744] I/user.key4_int_cbfunc pressup 22 1
[2025-10-17 17:11:56.077][000000023.043] I/user.key5678_int_cbfunc 23 0
[2025-10-17 17:11:56.082][000000023.043] I/user.key5 pressdown
[2025-10-17 17:11:56.244][000000023.213] I/user.key5678_int_cbfunc 23 0
[2025-10-17 17:11:56.249][000000023.213] I/user.key5 pressdown
[2025-10-17 17:11:58.053][000000025.013] I/user.key5678_int_cbfunc 25 0
[2025-10-17 17:11:58.053][000000025.013] I/user.key6 pressdown
[2025-10-17 17:11:59.426][000000026.394] I/user.key5678_int_cbfunc 28 0
[2025-10-17 17:11:59.428][000000026.394] I/user.key7 pressdown
[2025-10-17 17:12:00.847][000000027.816] I/user.key5678_int_cbfunc 27 0
[2025-10-17 17:12:00.854][000000027.816] I/user.key8 pressdown
```
@@ -0,0 +1 @@
-- hw_customer_font_drv是用户外部自定义外部点阵字体、lcd显示驱动配置和tp触摸驱动配置的驱动配置文件,目前exeasyui自定义字体功能正在开发中。
@@ -0,0 +1,60 @@
--[[
@module hw_default_font_drv
@summary 默认字体、lcd和tp驱动模块,使用内置12号英文点阵字体
@version 1.0
@date 2025.12.10
@author 江访
@usage
本文件为lcd和tp驱动模块,核心业务逻辑为:
1、根据配置的lcd和tp参数,初始化exEasyUI默认使用12号英文点阵字体、硬件显示和触摸;
2、提供无需外部硬件的字体显示能力;
本文件无对外接口,require加载后自动执行初始化;
@api ui.hw_init(config)
@summary 初始化exEasyUI硬件系统
@table config 硬件配置参数表,包含LCD配置和触摸配置
@field lcd_config table LCD显示配置参数
@field tp_config table 触摸屏配置参数
@return nil
@usage
]]
-- 使用默认12号英文点阵字体初始化exEasyUI硬件
ui.hw_init({
-- lcd_config参数填写可以参考合宙exlcd显示扩展库exlcd.init(param)接口说明:https://docs.openluat.com/osapi/ext/exlcd/#31-exlcdinitparam
lcd_config = {
lcd_model = "AirLCD_1000", -- LCD型号
-- pin_vcc = 24, -- 供电引脚,使用GPIO控制屏幕供电可配置
pin_rst = 36, -- 复位引脚
pin_pwr = 1, -- 背光控制引脚GPIO ID号
pin_pwm = 0, -- 背光控制引脚PWM ID号
port = lcd.HWID_0, -- 驱动端口
-- pin_dc = nil, -- lcd数据/命令选择引脚GPIO ID号,使用lcd 专用 SPI 接口 lcd.HWID_0不需要填此参数,使用通用SPI接口需要赋值
direction = 0, -- lcd屏幕方向 0:0° 1:90° 2:180° 3:270°,屏幕方向和分辨率保存一致
w = 320, -- lcd 水平分辨率
h = 480, -- lcd 竖直分辨率
xoffset = 0, -- x偏移(不同屏幕ic 不同屏幕方向会有差异)
yoffset = 0, -- y偏移(不同屏幕ic 不同屏幕方向会有差异)
sleepcmd = 0X10, -- 睡眠命令,默认0X10
wakecmd = 0X11, -- 唤醒命令,默认0X11
-- bus_speed = 50*1000*1000, -- SPI总线速度,不填默认50M,若速率要求更高需要进行设置
-- interface_mode = lcd.WIRE_4_BIT_8_INTERFACE_I, -- lcd模式,默认lcd.WIRE_4_BIT_8_INTERFACE_I
-- direction0 = {0x36,0x00}, -- 0°方向的命令,(不同屏幕ic会有差异)
-- direction90 = {0x36,0x60}, -- 90°方向的命令,(不同屏幕ic会有差异)
-- direction180 ={0x36,0xc0} , -- 180°方向的命令,(不同屏幕ic会有差异)
-- direction270 = {0x36,0xA0}, -- 270°方向的命令,(不同屏幕ic会有差异)
-- hbp = nil, -- 水平后廊
-- hspw = nil, -- 水平同步脉冲宽度
-- hfp = 0, -- 水平前廊
-- vbp = 0, -- 垂直后廊
-- vspw = 0, -- 垂直同步脉冲宽度
-- vfp = 0, -- 垂直前廊
-- initcmd = nil, -- 自定义屏幕初始化命令表
-- flush_rate = nil, -- 刷新率
-- spi_dev = nil, -- spi设备,当port = "device"时有效,当port ≠ "device"时可不填或者填nil
-- init_in_service = false, -- 允许初始化在lcd service里运行,在后台初始化LCD,默认是falseAir8000/G/W/T/A、Air780EHM/EGH/EHV 支持填true,可加快初始化速度,默认SPI总线速度80M
}
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,88 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.12.10
@author 江访
@usage
本demo演示的核心功能为:
1、加载exeasyui扩展库,初始化用户界面系统;
2、进行显示硬件以及字体的初始化,
3、初始化按键驱动系统,实现按键控制界面导航;
4、启动用户界面主循环,实现多页面切换和按键事件处理;
5、配置系统看门狗,确保系统稳定运行;
更多说明参考本目录下的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进行远程升级,根据自己项目的需求,自定义格式即可
]]
-- main.lua - 程序入口文件
-- 定义项目名称和版本号
PROJECT = "ui_demo" -- 项目名称
VERSION = "001.999.000" -- 版本号
-- 在日志中打印项目名和项目版本号
log.info("ui_demo", PROJECT, VERSION)
-- 设置日志输出风格为样式2(建议调试时开启)
-- log.style(2)
-- 如果内核固件支持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)
-- 必须加载才能启用exeasyui的功能
ui = require("exeasyui")
-- 加载lcd、tp和字库驱动管理功能模块:
-- 1、使用hw_default_font_drv,并按lcd显示驱动配置进行初始化
require("hw_default_font_drv")
-- 加载按键驱动模块
require("key_drv")
-- 加载exeassyui扩展库实现的用户界面功能模块
-- 实现多页面切换、触摸事件分发和界面渲染功能
-- 包含主页、组件演示页、默认字体演示页
require("ui_main")
-- 用户代码已结束
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,175 @@
# exEasyUI 演示
## 一、功能模块介绍
### 1.1 核心主程序模块
1. **main.lua** - 主程序入口,负责系统初始化和任务调度
2. **ui_main.lua** - exeasyui 主程序,负责页面管理和按键事件分发和执行exeasyui的任务调度
### 1.2 显示页面模块
1. **home_page.lua** - 主页模块,提供应用入口和导航功能
2. **component_page.lua** - UI 组件演示模块
3. **default_font_page.lua** - 默认12号英文点阵字体演示模块
### 1.3 硬件驱动模块
1. **hw_default_font_drv.lua** - LCD显示驱动配置驱动模块,使用内置 12 号英文点阵字体
2. **key_drv.lua** - 按键硬件驱动模块,负责GPIO初始化和按键事件发布
### 1.4 按键处理模块
1. **key_handler.lua** - 按键逻辑处理模块,负责光标管理和界面导航
- BOOT键 在可点击区域间循环切换
- 开机键 模拟点击当前中心区域
## 二、演示效果
<table>
<tr>
<td>主页<br/></td><td>组件演示页<br/></td><td>默认字体页<br/></td></tr>
<tr>
<td><img src="https://docs.openluat.com/cdn/image/Air780EPM_AirLCD_1000__exeasyui_homepage.png" width="80" /><br/></td><td><img src="https://docs.openluat.com/cdn/image/Air780EPM_AirLCD_1000__exeasyui_componentpage.png" width="80" /><br/></td><td><img src="https://docs.openluat.com/cdn/image/Air780EPM_AirLCD_1000__exeasyui_fontpage.png" width="80" /><br/></td></tr>
</table>
## 三、演示硬件环境
### 3.1 硬件清单
- Air780EPM 核心板 × 1
- AirLCD_1000 配件板 × 1
- 母对母杜邦线 × 8,杜邦线太长的话,会出现 spi 通信不稳定的现象;
- TYPE-C 数据线 × 1
- Air780EPM 核心板和 AirLCD_1000配件板的硬件接线方式为
- Air780EPM 核心板通过 TYPE-C USB 口供电(核心板正面开关拨到 ON 一端),此种供电方式下,VBAT 引脚为 3.3V,可以直接给 AirLCD_1000配件板供电;
- 为了演示方便,所以 Air780EPM 核心板上电后直接通过 VBAT 引脚给 AirLCD_1000配件板供电;
- 客户在设计实际项目时,一般来说,需要通过一个 GPIO 来控制 LDO 给配件板供电,这样可以灵活地控制配件板的供电,可以使项目的整体功耗降到最低;
### 3.2 接线配置
#### 3.2.1 LCD 显示屏接线
<table>
<tr>
<td>Air780EPM 核心板<br/></td><td>AirLCD_1000配件板<br/></td></tr>
<tr>
<td>53/LCD_CLK<br/></td><td>SCLK/CLK<br/></td></tr>
<tr>
<td>52/LCD_CS<br/></td><td>CS<br/></td></tr>
<tr>
<td>49/LCD_RST<br/></td><td>RES/RST<br/></td></tr>
<tr>
<td>50/LCD_SDA<br/></td><td>SDA/MOS<br/></td></tr>
<tr>
<td>51/LCD_RS<br/></td><td>DC/RS<br/></td></tr>
<tr>
<td>22/GPIO1<br/></td><td>BLK<br/></td></tr>
<tr>
<td>VBAT<br/></td><td>VCC<br/></td></tr>
<tr>
<td>GND<br/></td><td>GND<br/></td></tr>
</table>
#### 3.2.3 接线图
![](https://docs.openLuat.com/cdn/image/Air780EPM_AirLCD_1000接线图.jpg)
## 四、演示软件环境
### 4.1 开发工具
- [Luatools下载调试工具](https://docs.openluat.com/air780epm/luatos/common/download/) - 固件烧录和代码调试
### 4.2 内核固件
- [点击下载Air780EPM系列最新版本内核固件](https://docs.openluat.com/air780epm/luatos/firmware/version/)demo所使用的是LuatOS-SoC_V2018_Air780EPM 1号固件
## 五、演示核心步骤
### 5.1 硬件准备
1. 按照硬件接线表连接所有设备
2. 如使用 GTFont,需要连接 AirFONTS_1000 配件板
3. 通过 TYPE-C USB 口供电
4. 检查所有接线无误
### 5.2 软件配置
`main.lua` 中配置系统参数:
```lua
-- 必须加载才能启用exeasyui的功能
ui = require("exeasyui")
-- 加载lcd、tp和字库驱动管理功能模块:
-- 1、使用hw_default_font_drv,并按lcd显示驱动配置进行初始化
require("hw_default_font_drv")
-- 加载按键驱动模块
require("key_drv")
-- 加载exeassyui扩展库实现的用户界面功能模块
-- 实现多页面切换、触摸事件分发和界面渲染功能
-- 包含主页、组件演示页、默认字体演示页
require("ui_main")
```
### 5.3 驱动参数配置
在对应的驱动文件中根据实际硬件调整硬件参数:
1. **hw_default_font_drv.lua** - LCD显示驱动配置和默认字体驱动模块,使用内置 12 号英文点阵字体
2. **key_drv.lua** - 按键硬件驱动模块,负责GPIO初始化和按键事件发布
### 5.4 软件烧录
1. 使用 Luatools 烧录对应型号的最新内核固件
2. 下载并烧录本项目所有脚本文件
3. 将图片文件随脚本文件一起烧录到脚本分区
4. 设备自动重启后开始运行
5. [点击查看Luatools 下载和详细使用](https://docs.openluat.com/air780epm/common/Luatools/)
### 5.5 功能测试
#### 5.5.1 主页面操作
1. 设备启动后显示主页面,光标自动定位在第一个按钮
2. 按BOOT键在不同按钮间切换焦点
3. 按PWR键进入对应演示页面
4. 使用返回按钮返回主页面
#### 5.5.2 组件演示页面
1. 测试进度条组件的动态更新
2. 体验复选框的状态变化
3. 查看图片轮播效果(如有图片文件)
4. 使用按键在不同组件间切换焦点
#### 5.5.3 字体演示页面
1. **默认字体页**:查看固定 12 号字体的颜色和英文显示
2. 在各页面使用按键操作界面按钮
### 5.6 预期效果
- **系统启动**:正常初始化,显示主页面
- **字体显示**:各字体页面正常显示,动态调整功能正常
- **按键操作**:在各页面使用按键操作界面按钮
### 5.7 故障排除
1. **显示异常**:检查 LCD 接线,确认对应驱动文件中的硬件参数正确
2. **触摸无响应**:检查 I2C 接线,确认触摸芯片型号配置正确
3. **图片无法显示**:确认图片文件已正确烧录到指定路径
4. **系统卡顿**:调整 `ui_main.lua` 中的刷新率参数
## 六、扩展开发
本演示 demo 所有接口都在 [exeasyUI UI 扩展库](https://docs.openluat.com/osapi/ext/exeasyui/)内有详细说明,如需实现更丰富的自定义功能可按接口说明实现。
@@ -0,0 +1,70 @@
--[[
@module key_drv
@summary
@version 1.0
@date 2025.12.10
@author 访
@usage
1BOOT键和PWR键的GPIO
2
3
4
1
1key_drv.init()
]]
local key_drv = {}
-- 按键定义
local key_boot = 0 -- GPIO0按键(BOOT键)
local key_pwr = gpio.PWR_KEY -- 电源按键
-- 按键事件处理函数
local function handle_boot_key(val)
if val == 1 then
sys.publish("KEY_EVENT", "boot_down")
else
sys.publish("KEY_EVENT", "boot_up")
end
end
local function handle_pwr_key(val)
if val == 1 then
sys.publish("KEY_EVENT", "pwr_up")
else
sys.publish("KEY_EVENT", "pwr_down")
end
end
--[[
GPIO
BOOT键和PWR键的GPIO中断
@api key_drv.init()
@summary BOOT键和PWR键的GPIO中断
@return bool true
@usage
local result = key_drv.init()
if result then
log.info("按键驱动初始化成功")
end
]]
function key_drv.init()
gpio.setup(key_boot, handle_boot_key, gpio.PULLDOWN, gpio.BOTH)
gpio.debounce(key_boot, 50, 0) -- 防抖,防止频繁触发
gpio.setup(key_pwr, handle_pwr_key, gpio.PULLUP, gpio.BOTH)
gpio.debounce(key_pwr, 50, 0) -- 防抖,防止频繁触发
log.info("key_drv", "按键初始化完成")
return true
end
key_drv.init()
return key_drv
@@ -0,0 +1,214 @@
--[[
@module component_page
@summary exEasyUI组件演示页面模块
@version 1.0
@date 2025.12.10
@author 访
@usage
1
2UI组件
3
4
1
1component_page.create(ui)
]]
local component_page = {}
--[[
@api component_page.create(ui)
@summary
@table ui UI库对象
@return table
@usage
-- 在子页面工厂中调用创建组件演示页面
local component_page = require("component_page").create(ui)
]]
function component_page.create(ui)
local win = ui.window({
background_color = ui.COLOR_WHITE,
x = 0,
y = 0,
w = 320,
h = 480
})
-- 标题
local title = ui.label({
x = 120,
y = 25,
text = "Component Demo",
color = ui.COLOR_BLACK,
size = 12
})
-- 返回按钮
local btn_back = ui.button({
x = 20,
y = 20,
w = 60,
h = 30,
text = "Back",
on_click = function()
win:back()
end
})
-- ==================== 1. 进度条组件演示 ====================
local progress_label = ui.label({
x = 20,
y = 70,
text = "1. Progress Bar:",
color = ui.COLOR_BLACK,
size = 12
})
local progress_value = 0
local progress_bar = ui.progress_bar({
x = 20,
y = 100,
w = 180,
h = 26,
progress = progress_value
})
local btn_progress = ui.button({
x = 210,
y = 100,
w = 70,
h = 26,
text = "+10%",
on_click = function()
progress_value = progress_value + 10
if progress_value > 100 then
progress_value = 0
end
progress_bar:set_progress(progress_value)
progress_bar:set_text("Progress: " .. progress_value .. "%")
end
})
-- ==================== 2. 复选框组件演示 ====================
local checkbox_label = ui.label({
x = 20,
y = 140,
text = "2. Checkbox:",
color = ui.COLOR_BLACK,
size = 12
})
local checkbox1 = ui.check_box({
x = 20,
y = 170,
text = "Option A",
checked = false,
on_change = function(checked)
log.info("component_page", "Option A:", checked)
end
})
local checkbox2 = ui.check_box({
x = 120,
y = 170,
text = "Option B",
checked = true,
on_change = function(checked)
log.info("component_page", "Option B:", checked)
end
})
-- -- ==================== 3. 图片轮播组件演示 ====================
local picture_label = ui.label({
x = 20,
y = 220,
text = "3. Image Carousel:",
color = ui.COLOR_BLACK,
size = 12
})
local picture = ui.picture({
x = 20,
y = 250,
w = 128,
h = 128,
sources = { "/luadb/1.jpg", "/luadb/2.jpg", "/luadb/3.jpg" },
autoplay = true,
interval = 2000
})
-- ==================== 4. 按钮组件演示 ====================
local button_label = ui.label({
x = 20,
y = 400,
text = "4. Buttons:",
color = ui.COLOR_BLACK,
size = 12
})
-- 普通按钮
local normal_btn = ui.button({
x = 20,
y = 430,
w = 80,
h = 30,
text = "Normal",
on_click = function()
log.info("component_page", "Normal button clicked")
end
})
-- 带颜色的按钮
local colored_btn = ui.button({
x = 110,
y = 430,
w = 80,
h = 30,
text = "Blue",
bg_color = ui.COLOR_BLUE,
text_color = ui.COLOR_WHITE,
on_click = function()
log.info("component_page", "Blue button clicked")
end
})
-- 图片按钮
local image_btn = ui.button({
x = 200,
y = 413,
w = 64,
h = 64,
src = "/luadb/4.jpg",
src_toggled = "/luadb/5.jpg",
toggle = true,
on_click = function()
log.info("component_page", "Image button clicked")
end
})
-- 添加所有组件到窗口
win:add(title)
win:add(btn_back)
win:add(progress_label)
win:add(progress_bar)
win:add(btn_progress)
win:add(checkbox_label)
win:add(checkbox1)
win:add(checkbox2)
win:add(picture_label)
win:add(picture)
win:add(button_label)
win:add(normal_btn)
win:add(colored_btn)
win:add(image_btn)
return win
end
return component_page
@@ -0,0 +1,156 @@
--[[
@module default_font_page
@summary
@version 1.0
@date 2025.12.10
@author 访
@usage
112
2
3
4
1
1default_font_page.create(ui)
]]
local default_font_page = {}
--[[
@api default_font_page.create(ui)
@summary
@table ui UI库对象
@return table
@usage
-- 在子页面工厂中调用创建默认字体演示页面
local default_font_page = require("default_font_page").create(ui)
]]
function default_font_page.create(ui)
local win = ui.window({
background_color = ui.COLOR_WHITE,
x = 0,
y = 0,
w = 320,
h = 480
})
-- 标题
local title = ui.label({
x = 120,
y = 25,
text = "Default Font Demo",
color = ui.COLOR_BLACK,
size = 12
})
-- 返回按钮
local btn_back = ui.button({
x = 20,
y = 20,
w = 60,
h = 30,
text = "Back",
on_click = function()
win:back()
end
})
-- 字体演示标题
local demo_title = ui.label({
x = 20,
y = 70,
text = "Font Demo (Fixed 12-point):",
color = ui.COLOR_BLACK,
size = 12
})
-- 数字演示
local number_demo = ui.label({
x = 20,
y = 100,
text = "1. Numbers: 0123456789",
color = ui.COLOR_BLUE,
size = 12
})
-- 符号演示
local symbol_demo = ui.label({
x = 20,
y = 130,
text = "2. Symbols: !@#$%^&*()_+-=[]",
color = ui.COLOR_ORANGE,
size = 12
})
-- 中英文演示
local text_demo = ui.label({
x = 20,
y = 160,
text = "3. English: Hello World ABC",
color = ui.COLOR_RED,
size = 12
})
-- 默认字体特性说明
local feature_title = ui.label({
x = 20,
y = 200,
text = "Default Font Features:",
color = ui.COLOR_BLACK,
size = 12
})
local feature1 = ui.label({
x = 20,
y = 230,
text = "- Built-in 12-point English font",
color = ui.COLOR_GRAY,
size = 12
})
local feature2 = ui.label({
x = 20,
y = 260,
text = "- No external hardware required",
color = ui.COLOR_GRAY,
size = 12
})
local feature3 = ui.label({
x = 20,
y = 290,
text = "- Fast startup, low resource usage",
color = ui.COLOR_GRAY,
size = 12
})
local feature4 = ui.label({
x = 20,
y = 320,
text = "- Fixed font size 12-point",
color = ui.COLOR_GRAY,
size = 12
})
-- 添加所有组件到窗口
win:add(title)
win:add(btn_back)
win:add(demo_title)
win:add(number_demo)
win:add(symbol_demo)
win:add(text_demo)
win:add(feature_title)
win:add(feature1)
win:add(feature2)
win:add(feature3)
win:add(feature4)
return win
end
return default_font_page
@@ -0,0 +1,98 @@
--[[
@module home_page
@summary
@version 1.0
@date 2025.12.10
@author 访
@usage
1
2
3
4
1
1home_page.create()
]]
local home_page = {}
--[[
@api home_page.create()
@summary
@return nil
@usage
-- 在UI主程序中调用创建主页
home_page.create()
]]
function home_page.create()
-- 创建主页
local home = ui.window({ background_color = ui.COLOR_WHITE })
home.visible = true
-- 配置子页面工厂
home:configure_subpages({
component = function() return require("component_page").create(ui) end,
default_font = function() return require("default_font_page").create(ui) end,
})
-- 标题
local title = ui.label({
x = 80,
y = 30,
text = "exEasyUI v1.7.0 Demo System",
color = ui.COLOR_BLACK,
size = 12
})
local subtitle = ui.label({
x = 80,
y = 60,
text = "boot: Select pwr: Confirm",
color = ui.COLOR_GRAY,
size = 12
})
-- 组件演示按钮
local btn_component = ui.button({
x = 20,
y = 100,
w = 280,
h = 50,
text = "Component Demo",
bg_color = ui.COLOR_BLUE,
text_color = ui.COLOR_WHITE,
on_click = function()
home:show_subpage("component")
end
})
-- 默认字体演示按钮
local btn_default_font = ui.button({
x = 20,
y = 170,
w = 280,
h = 50,
text = "Default Font Demo",
bg_color = ui.COLOR_RED,
text_color = ui.COLOR_WHITE,
on_click = function()
home:show_subpage("default_font")
end
})
-- 添加所有组件到窗口
home:add(title)
home:add(subtitle)
home:add(btn_component)
home:add(btn_default_font)
ui.add(home)
end
return home_page
@@ -0,0 +1,333 @@
--[[
@module key_handler
@summary
@version 1.0
@date 2025.12.10
@author 访
@usage
1
2
3
4
5
8
1key_handler.register_page()
2key_handler.switch_to_page()
3key_handler.clear_areas()
4key_handler.add_area()
5key_handler.next_area()
6key_handler.simulate_click()
7key_handler.set_visible()/
8key_handler.get_current_area()
]]
local key_handler = {}
-- 模块内部状态
local clickable_areas = {}
local current_area_index = 0
local page_callbacks = {}
local config = {
cursor_thickness = 2,
cursor_color = 0xFF0000,
show_cursor = true,
last_switch_time = 0
}
local current_drawn_rect = nil
--[[
@api key_handler.register_page(page_name, callback)
@summary
@string
page_name
@function
callback
key_handler.clear_areas()key_handler.add_area()
@return nil
@usage
key_handler.register_page("home", function()
key_handler.clear_areas()
key_handler.add_area(20, 100, 280, 50) -- 主页按钮1
key_handler.add_area(20, 170, 280, 50) -- 主页按钮2
end)
]]
function key_handler.register_page(page_name, callback)
page_callbacks[page_name] = callback
end
--[[
@api key_handler.switch_to_page(page_name)
@summary
@string
page_name
key_handler.register_page()
@return boolean
truefalse
@usage
local result = key_handler.switch_to_page("home")
if result then
log.info("成功切换到主页")
end
]]
function key_handler.switch_to_page(page_name)
key_handler.clear_cursor()
if page_callbacks[page_name] then
page_callbacks[page_name]()
else
clickable_areas = {}
end
if #clickable_areas > 0 then
current_area_index = 1
config.last_switch_time = mcu.ticks()
key_handler.draw_cursor()
return true
else
current_area_index = 0
return false
end
end
-- 清空区域
function key_handler.clear_areas()
key_handler.clear_cursor()
clickable_areas = {}
current_area_index = 0
end
-- 添加区域
function key_handler.add_area(x, y, w, h)
table.insert(clickable_areas, { x = x, y = y, w = w, h = h })
if #clickable_areas == 1 and current_area_index == 0 then
current_area_index = 1
config.last_switch_time = mcu.ticks()
end
end
-- 获取当前区域矩形
local function get_current_area_rect()
if current_area_index < 1 or current_area_index > #clickable_areas then
return nil
end
local area = clickable_areas[current_area_index]
return {
x1 = area.x - 3,
y1 = area.y - 3,
x2 = area.x + area.w + 3,
y2 = area.y + area.h + 3
}
end
-- 获取当前区域中心
local function get_current_area_center()
if current_area_index < 1 or current_area_index > #clickable_areas then
return 160, 240
end
local area = clickable_areas[current_area_index]
local center_x = area.x + math.floor(area.w / 2)
local center_y = area.y + math.floor(area.h / 2)
return center_x, center_y
end
-- 清除光标
function key_handler.clear_cursor()
if current_drawn_rect then
lcd.setColor(0xFFFFFF, 0xFFFFFF)
local r = current_drawn_rect
for i = 1, config.cursor_thickness do
local offset = i - 1
lcd.drawRectangle(
r.x1 + offset,
r.y1 + offset,
r.x2 - offset,
r.y2 - offset
)
end
current_drawn_rect = nil
end
end
-- 绘制光标
function key_handler.draw_cursor()
if not config.show_cursor then
return
end
if current_area_index < 1 or current_area_index > #clickable_areas then
return
end
key_handler.clear_cursor()
local rect = get_current_area_rect()
if not rect then return end
current_drawn_rect = rect
lcd.setColor(0xFFFFFF, config.cursor_color)
local thickness = config.cursor_thickness
for i = 1, thickness do
local offset = i - 1
lcd.drawRectangle(
rect.x1 + offset,
rect.y1 + offset,
rect.x2 - offset,
rect.y2 - offset
)
end
config.last_switch_time = mcu.ticks()
end
--[[
@api key_handler.next_area()
@summary
@return boolean
truefalse
@usage
local result = key_handler.next_area()
if result then
log.info("已切换到下一个区域")
end
]]
function key_handler.next_area()
if #clickable_areas == 0 then
return false
end
local old_index = current_area_index
current_area_index = current_area_index + 1
if current_area_index > #clickable_areas then
current_area_index = 1
end
key_handler.draw_cursor()
return true
end
--[[
@api key_handler.simulate_click()
@summary
@return number, number
X, Y0, 0
@usage
local x, y = key_handler.simulate_click()
log.info("点击坐标:", x, ",", y)
]]
function key_handler.simulate_click()
if #clickable_areas == 0 or current_area_index == 0 then
return 0, 0
end
local center_x, center_y = get_current_area_center()
-- exeasyui必须先按下再单击
sys.publish("BASE_TOUCH_EVENT", "TOUCH_DOWN", center_x, center_y)
sys.publish("BASE_TOUCH_EVENT", "SINGLE_TAP", center_x, center_y)
return center_x, center_y
end
--[[
@api key_handler.set_visible(visible)
@summary
@boolean
visible
true表示显示光标false表示隐藏光标
@return nil
@usage
key_handler.set_visible(true) -- 显示光标
key_handler.set_visible(false) -- 隐藏光标
]]
function key_handler.set_visible(visible)
config.show_cursor = visible
if not visible then
key_handler.clear_cursor()
else
key_handler.draw_cursor()
end
end
--[[
@api key_handler.get_current_area()
@summary
@return table or nil
- index: number
- total: number
- x: number X坐标
- y: number Y坐标
- w: number
- h: number
- center_x: number X坐标
- center_y: number Y坐标
nil
@usage
local area = key_handler.get_current_area()
if area then
log.info("当前区域:", area.index, "/", area.total)
end
]]
function key_handler.get_current_area()
if current_area_index < 1 or current_area_index > #clickable_areas then
return nil
end
local area = clickable_areas[current_area_index]
local center_x, center_y = get_current_area_center()
return {
index = current_area_index,
total = #clickable_areas,
x = area.x,
y = area.y,
w = area.w,
h = area.h,
center_x = center_x,
center_y = center_y
}
end
return key_handler
@@ -0,0 +1,152 @@
--[[
@module ui_main
@summary exEasyUI主程序模块
@version 1.0
@date 2025.12.10
@author 访
@usage
exEasyUI主程序模块
1UI主题和窗口系统
2
3
4
5UI渲染主循环
require加载后自动执行初始化
]]
local home_page = require("home_page")
local key_handler = require("key_handler")
-- 页面名称常量
local PAGE_NAMES = {
HOME = "home",
COMPONENT = "component",
DEFAULT_FONT = "default_font",
}
-- 当前页面
local current_page = nil
-- 处理按键事件
local function handle_key_event(event_type)
if event_type == "boot_down" then
key_handler.next_area()
elseif event_type == "pwr_down" then
key_handler.simulate_click()
end
end
-- 主页区域回调函数
local function home_page_callback()
key_handler.clear_areas()
key_handler.add_area(20, 100, 280, 50) -- 组件按钮
key_handler.add_area(20, 170, 280, 50) -- 默认字体按钮
log.info("ui_main", "已注册主页区域,共4个区域")
end
-- 组件页面区域回调函数
local function component_page_callback()
key_handler.clear_areas()
key_handler.add_area(20, 20, 60, 30) -- 返回按钮
key_handler.add_area(210, 100, 70, 26) -- +10%按钮
key_handler.add_area(20, 170, 85, 30) -- 复选框A
key_handler.add_area(120, 170, 85, 30) -- 复选框B
key_handler.add_area(20, 430, 80, 30) -- 普通按钮
key_handler.add_area(110, 430, 80, 30) -- 蓝色按钮
key_handler.add_area(200, 430, 64, 30) -- 图片按钮
log.info("ui_main", "已注册组件页面区域,共7个区域")
end
-- 默认字体页面区域回调函数
local function default_font_page_callback()
key_handler.clear_areas()
key_handler.add_area(20, 20, 60, 30) -- 返回
log.info("ui_main", "已注册默认字体页面区域,共1个区域")
end
-- 注册所有页面的回调函数
local function register_all_pages()
key_handler.register_page(PAGE_NAMES.HOME, home_page_callback)
key_handler.register_page(PAGE_NAMES.COMPONENT, component_page_callback)
key_handler.register_page(PAGE_NAMES.DEFAULT_FONT, default_font_page_callback)
end
-- 页面切换处理
local function handle_page_change(page_name)
if not page_name then
page_name = PAGE_NAMES.HOME
end
if page_name ~= current_page then
current_page = page_name
key_handler.switch_to_page(page_name)
end
end
-- 页面切换任务函数
local function show_subpage_task(page_name)
handle_page_change(page_name)
end
local function back_task()
handle_page_change(PAGE_NAMES.HOME)
end
-- 包装的show_subpage方法
local function wrapped_show_subpage(self, page_name)
-- 调用原始方法
local original_show_subpage = getmetatable(self).original_show_subpage
original_show_subpage(self, page_name)
if page_name then
sys.taskInit(show_subpage_task, page_name)
end
end
-- 包装的back方法
local function wrapped_back(self)
-- 调用原始方法
local original_back = getmetatable(self).original_back
original_back(self)
sys.taskInit(back_task)
end
-- 包装窗口方法
local function wrap_window_methods()
local test_win = ui.window({})
local window_meta = getmetatable(test_win)
-- 保存原始方法
window_meta.original_show_subpage = window_meta.show_subpage
window_meta.original_back = window_meta.back
-- 替换为包装的方法
window_meta.show_subpage = wrapped_show_subpage
window_meta.back = wrapped_back
end
-- 启动UI主任务
local function ui_main()
-- 初始化UI主题
ui.sw_init({ theme = "light" })
-- 包装窗口方法
wrap_window_methods()
register_all_pages()
-- 创建主页
home_page.create()
-- 确保光标显示
key_handler.switch_to_page(PAGE_NAMES.HOME)
-- 订阅按键事件
sys.subscribe("KEY_EVENT", handle_key_event)
end
sys.taskInit(ui_main)
@@ -0,0 +1,16 @@
--[[
customer_font_drv是用户外部自定义外部点阵字体驱动功能模块,使
lcd核心库自定义字体制作说明https://docs.openluat.com/osapi/core/lcd/#_6
1 https://gitee.com/Dozingfiretruck/u8g2_font_tool
2
- .ttf和.otf格式字体加载到制作工具内
-
-
- lcd核心库可加载的.bin格式的字体文件
3使lcd.setFontFile(font)
- font为字体文件路径
- customer_font_24.bin luatools工具同代码一起烧录到脚本分区lcd.setFontFile("/luadb/customer_font_24.bin")
- customer_font_24.bin luatools工具同固件和代码一起烧录到文件系统lcd.setFontFile("/customer_font_24.bin")
4使lcd.drawStr(x,y,str,fg_color)
]]
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,50 @@
--[[
@module exlcd_drv
@summary LCD显示驱动模块exlcd扩展库
@version 1.0
@date 2025.12.3
@author 访
@usage
LCD显示驱动功能模块
1AirLCD_1000扩展LCD显示
2
1exlcd_drv.init()LCD显示驱动
exlcd扩展库中的exlcd.set_bl(level)
]]
local exlcd = require "exlcd"
--[[
LCD显示驱动
@api exlcd_drv.init()
@summary AirLCD_1000扩展LCD显示
@return boolean truefalse
@usage
-- 初始化扩展LCD显示
local result = exlcd_drv.init({})
if result then
log.info("扩展LCD初始化成功")
else
log.error("扩展LCD初始化失败")
end
]]
local function exlcd_drv_init()
local result = exlcd.init({lcd_model = "AirLCD_1000"})
log.info("exlcd.init", result)
if result then
-- 显示设置
lcd.setupBuff(nil, true)
lcd.autoFlush(false)
end
end
exlcd_drv_init()
@@ -0,0 +1,98 @@
--[[
@module lcd_drv
@summary LCD显示驱动模块lcd核心库
@version 1.0
@date 2025.12.3
@author 访
@usage
LCD显示驱动功能模块
1AirLCD_1000 IC ST7796
2LCD显示参数和显示缓冲区
3
1lcd_drv.init()LCD显示驱动
]]
--[[
LCD显示驱动
@api lcd_drv.init()
@summary ST7796 LCD控制器
@return boolean truefalse
@usage
-- 初始化LCD显示
local result = lcd_drv.init()
if result then
log.info("LCD初始化成功")
else
log.error("LCD初始化失败")
end
]]
local function lcd_drv_init()
-- 根据硬件环境配置LCD电源GPIO和背光控制引脚
-- HARDWARE_ENV参数说明:
-- "CORE_BOARD": 核心板直连,不需要控制GPIO供电,背光使用GPIO1控制
-- "DEV_BOARD_V1.2": V1.2开发板,使用GPIO28控制LCD电源,无需控制背光(未接背光)
-- "DEV_BOARD_V1.3": V1.3开发板,使用GPIO29控制LCD电源,背光使用GPIO30控制
local pin_pwr = nil -- 默认不通过lcd库控制背光
if HARDWARE_ENV == "DEV_BOARD_V1.2" then
gpio.setup(28, 1) -- GPIO28打开给LCD电源供电,无需控制背光
elseif HARDWARE_ENV == "DEV_BOARD_V1.3" then
gpio.setup(29, 1) -- GPIO29打开给LCD电源供电
gpio.set(29,1)
gpio.setup(30, 0) -- GPIO30拉低开启背光(拉高灭屏,拉低亮屏),不通过lcd库控制
gpio.set(30,0)
else
pin_pwr = 1 -- 核心板使用GPIO1控制背光,通过lcd库控制
end
local result = lcd.init("st7796",
{
pin_rst = 36, -- 复位引脚
pin_pwr = pin_pwr, -- 背光控制引脚GPIO ID号,根据硬件环境自适应
port = lcd.HWID_0, -- 驱动端口
-- pin_dc = 0xFF, -- lcd数据/命令选择引脚GPIO ID号,使用lcd 专用 SPI 接口 lcd.HWID_0不需要填此参数,使用通用SPI接口需要赋值
direction = 0, -- lcd屏幕方向 0:0° 1:90° 2:180° 3:270°,屏幕方向和分辨率保存一致
w = 320, -- lcd 水平分辨率
h = 480, -- lcd 竖直分辨率
xoffset = 0, -- x偏移(不同屏幕ic 不同屏幕方向会有差异)
yoffset = 0, -- y偏移(不同屏幕ic 不同屏幕方向会有差异)
sleepcmd = 0X10, -- 睡眠命令,默认0X10
wakecmd = 0X11, -- 唤醒命令,默认0X11
-- bus_speed = 50*1000*1000, -- SPI总线速度,不填默认50M,若速率要求更高需要进行设置
-- interface_mode = lcd.WIRE_4_BIT_8_INTERFACE_I, -- lcd模式,默认lcd.WIRE_4_BIT_8_INTERFACE_I
-- direction0 = {0x36,0x00}, -- 0°方向的命令,(不同屏幕ic会有差异)
-- direction90 = {0x36,0x60}, -- 90°方向的命令,(不同屏幕ic会有差异)
-- direction180 ={0x36,0xc0} , -- 180°方向的命令,(不同屏幕ic会有差异)
-- direction270 = {0x36,0xA0}, -- 270°方向的命令,(不同屏幕ic会有差异)
-- hbp = nil, -- 水平后廊
-- hspw = nil, -- 水平同步脉冲宽度
-- hfp = 0, -- 水平前廊
-- vbp = 0, -- 垂直后廊
-- vspw = 0, -- 垂直同步脉冲宽度
-- vfp = 0, -- 垂直前廊
-- initcmd = nil, -- 自定义屏幕初始化命令表
-- flush_rate = nil, -- 刷新率
-- spi_dev = nil, -- spi设备,当port = "device"时有效,当port ≠ "device"时可不填或者填nil
-- init_in_service = false, -- 允许初始化在lcd service里运行,在后台初始化LCD,默认是falseAir8000/G/W/T/A、Air780EHM/EGH/EHV 支持填true,可加快初始化速度,默认SPI总线速度80M
})
log.info("lcd.init", result)
if result then
-- 显示设置
lcd.setupBuff(nil, true)
lcd.autoFlush(false)
end
end
lcd_drv_init()
@@ -0,0 +1,110 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口
@version 1.0
@date 2025.12.3
@author 访
@usage
demo演示的核心功能为
1LCD显示屏驱动初始化lcd核心库和exlcd扩展库两种驱动方式
2BOOT键和PWR键的GPIO中断和防抖处理
3使
4LCD演示页和自定义字体演示页
5使
12GTFont矢量字体
readme.md文件
]]
--[[
PROJECT和VERSION变量Luatools工具会用到这两个变量
PROJECTascii string类型
便使,
VERSIONascii string类型
使iot.openluat.com进行远程升级"XXX.YYY.ZZZ"
XYZ各表示1位数字X表示的数字可以相同Y和三个Z表示的数字也是可以相同
YYY这三位数字必须存在999
使iot.openluat.com进行远程升级
]]
-- main.lua - 程序入口文件
-- 定义项目名称和版本号
PROJECT = "ui_demo" -- 项目名称
VERSION = "001.999.000" -- 版本号
-- 在日志中打印项目名和项目版本号
log.info("ui_demo", PROJECT, VERSION)
-- 设置日志输出风格为样式2(建议调试时开启)
-- log.style(2)
-- 硬件环境配置说明
-- 本Demo支持三种硬件环境,请根据实际使用情况修改HARDWARE_ENV变量:
-- "CORE_BOARD" - 核心板直连(默认)
-- LCD电源:通过核心板3.3V引脚直接给LCD供电,无需控制GPIO
-- LCD背光:使用GPIO1控制
-- "DEV_BOARD_V1.2" - V1.2开发板
-- LCD电源:使用GPIO28控制LCD电源供电
-- LCD背光:未接背光,无需控制
-- "DEV_BOARD_V1.3" - V1.3开发板
-- LCD电源:使用GPIO29控制LCD电源供电
-- LCD背光:使用GPIO30控制
-- 详细说明请参考本目录下的readme.md文件
HARDWARE_ENV = "DEV_BOARD_V1.3" -- 修改此值适配不同硬件环境:"CORE_BOARD"=核心板, "DEV_BOARD_V1.2"=V1.2开发板, "DEV_BOARD_V1.3"=V1.3开发板
-- 如果内核固件支持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)
-- 加载显示屏驱动管理功能模块,有以下两种:
-- 1、使用lcd核心库驱动的lcd_drv.lua
-- 2、使用exlcd扩展库驱动的exlcd_drv.lua
-- 根据自己的需求,启用两者中的任何一种都可以
-- 也可以不启用任何一种,不使用显示屏功能
lcd_drv = require "lcd_drv"
-- lcd_drv = require "exlcd_drv"
-- 加载按键驱动管理功能模块
key_drv = require "key_drv"
-- 加载字库驱动管理功能模块
-- 使用外部自定义字体不需要require "customer_font_drv",可以参照customer_font_drv.lua内的使用说明进行创建和加载字体文件
-- 加载输入法驱动管理功能模块(正在开发中,后续补充)
-- 加载lcd核心库实现的用户界面功能模块
-- 实现多页面切换、按键事件分发和界面渲染功能
-- 包含主页、lcd核心库功能演示页、GTFont演示页和自定义字体演示页
require "ui_main"
-- 用户代码已结束
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,329 @@
# LCD、按键与字体演示系统
## 一、功能模块介绍
### 1.1 核心主程序模块
1. **main.lua** - 主程序入口,负责系统初始化和任务调度
2. **ui_main.lua** - 用户界面主控模块,管理页面切换和事件分发
### 1.2 显示页面模块
3. **home_page.lua** - 主页模块,提供应用入口和导航功能
4. **lcd_page.lua** - LCD图形绘制演示模块
5. **customer_font_page.lua** - 自定义字体演示模块
6. **camera_preview_page.lua** - 摄像头预览页面,支持预览→拍照→保存/删除
### 1.3 驱动模块
7. **lcd_drv.lua** - LCD显示驱动模块,基于lcd核心库,lcd_drv和exlcd_drv二选一使用
8. **exlcd_drv.lua** - LCD显示驱动模块,基于exlcd扩展库,lcd_drv和exlcd_drv二选一使用
9. **key_drv.lua** - 按键驱动模块,管理BOOT键和PWR键
10. **customer_font_drv.lua** - 自定义外部字体驱动功能模块
**注意**:当前设备仅支持自定义点阵字体显示中文,不支持内置12号中文字体和GTFont矢量字体。
## 二、按键消息介绍
1. **"KEY_EVENT"** - 按键事件消息,包含按键类型和状态
- boot键事件:`boot_down`(按下)、`boot_up`(释放)
- pwr键事件:`pwr_down`(按下)、`pwr_up`(释放)
- 按键功能定义:
- 主页:boot键(按下)选择/切换选项,pwr键(按下)确认
- LCD页面:pwr键(按下)返回,boot键无功能
- 自定义字体页面:pwr键(按下)返回,boot键无功能
- 摄像头预览页面:预览完成后PWR键拍照,Boot键返回主页;照片显示页Boot键切换焦点,PWR键执行操作
## 三、显示效果
<table>
<tr><td>主页<br/></td><td>lcd核心库页面<br/></td><td>自定义字体页面<br/></td></tr><tr>
<td rowspan="2"><img src="https://docs.openluat.com/cdn/image/Air780EPM_AirLCD_1000_主页.png" width="80" /><br/></td>
<td rowspan="2"><img src="https://docs.openluat.com/cdn/image/Air780EPM_AirLCD_1000_lcd演示页.png" width="80" /><br/></td>
<td><img src="https://docs.openluat.com/cdn/image/Air780EPM_AirLCD_1000_自定义字体演示页.png" width="80" /><br/></td></tr>
</table>
### 4.1 LCD图形绘制演示
1. **基本图形绘制** - 展示点、线、矩形、圆形等基本图形绘制功能
2. **图片显示** - 支持外部图片文件显示
3. **二维码生成** - 动态生成并显示二维码
4. **xbm格式位图示例** - 显示16*16 xbm点阵
5. **自定义字体示例** - 使用自定义字体显示文字
### 4.2 自定义字体演示
1. **外部字体加载** - 支持加载外部自定义字体文件
2. **多颜色文字** - 支持不同颜色的文字显示
### 4.3 按键交互功能
1. **页面导航** - 支持多页面之间的切换
2. **按钮响应** - 按键的点击响应功能
## 五、演示硬件环境
本Demo支持三种硬件环境,请根据实际使用的硬件选择对应的配置:
| 硬件环境 | HARDWARE_ENV值 | LCD电源控制 | LCD背光控制 |
|---------|---------------|------------|------------|
| 核心板直连 | "CORE_BOARD" | 直接供电,无需GPIO控制 | GPIO1控制 |
| V1.2开发板 | "DEV_BOARD_V1.2" | GPIO28控制 | 未接背光,无需控制 |
| V1.3开发板 | "DEV_BOARD_V1.3" | GPIO29控制 | GPIO30控制(低电平亮) |
**配置方法:** 在`main.lua`中修改`HARDWARE_ENV`变量的值即可切换硬件环境。
### 5.1 核心板直连模式(CORE_BOARD
#### 5.1.1 硬件清单
- Air780EPM核心板 × 1
- AirLCD_1000 配件板 × 1
- 母对母杜邦线 × 14
- TYPE-C 数据线 × 1
#### 5.1.2 硬件连接与控制
**核心板供电**
- Air780EPM核心板通过TYPE-C USB口供电(将核心板正面开关拨到 ON 一端)
- 使用TYPE-C USB数据线连接核心板与电脑USB口
**配件板接线**
- 使用母对母杜邦线按照下表连接AirLCD_1000配件板
**LCD 显示屏接线:**
<table>
<tr>
<td>Air780EPM核心板<br/></td><td>AirLCD_1000配件板<br/></td></tr>
<tr>
<td>53/LCD_CLK<br/></td><td>SCLK/CLK<br/></td></tr>
<tr>
<td>52/LCD_CS<br/></td><td>CS<br/></td></tr>
<tr>
<td>49/LCD_RST<br/></td><td>RES/RST<br/></td></tr>
<tr>
<td>50/LCD_SDA<br/></td><td>SDA/MOS<br/></td></tr>
<tr>
<td>51/LCD_RS<br/></td><td>DC/RS<br/></td></tr>
<tr>
<td>22/GPIO1<br/></td><td>BLK<br/></td></tr>
<tr>
<td>VDD_EXT<br/></td><td>VCC<br/></td></tr>
<tr>
<td>GND<br/></td><td>GND<br/></td></tr>
</table>
**接线图:**
![](https://docs.openLuat.com/cdn/image/Air780EPM_AirLCD_1000_lcd演示接线图.png)
**LCD电源控制**
- 通过核心板上的VDD_EXT引脚直接供电,无需GPIO控制
- 此种供电方式下,VDD_EXT引脚为3.3V
> **注意**:为了演示方便,本模式直接通过VDD_EXT给配件板供电。客户在设计实际项目时,建议通过GPIO控制LDO给配件板供电,以灵活控制功耗。
**背光控制**
- 使用GPIO1控制LCD背光
- 系统会自动设置`pin_pwr = 1`,通过lcd库自动管理背光开关
#### 5.1.3 配置说明
`main.lua`中设置:
```lua
HARDWARE_ENV = "CORE_BOARD"
```
驱动会自动完成以下操作:
1. 无需控制GPIO供电(通过核心板上的VDD_EXT引脚供电)
2. `pin_pwr = 1` - 通过lcd库使用GPIO1控制背光
### 5.2 V1.2开发板(DEV_BOARD_V1.2
#### 5.2.1 硬件清单
- Air780EPM v1.2开发板 × 1
- AirLCD_1000 配件板 × 1
- TYPE-C 数据线 × 1
#### 5.2.2 硬件连接与控制
**开发板供电**
- Air780EPM V1.2版本开发板通过TYPE-C USB口供电
- 将开发板上的"外部供电/USB供电"拨动开关拨到"USB供电"一端
- 使用TYPE-C USB数据线连接开发板与电脑USB口
**配件板连接**
- 将AirLCD_1000配件板按照下图所示方向插入开发板
![image](https://docs.openLuat.com/cdn/image/Air780EPM/Air780EPM_demo_accessory_board_lcd_v1.2.jpg)
**LCD电源控制**
- 使用GPIO28控制LCD电源供电
- 系统会自动执行`gpio.setup(28, 1)`打开供电
**背光控制**
- 本版本开发板未接屏幕背光脚,无法控制背光
- 系统会自动设置`pin_pwr = nil`,不通过lcd库控制背光
#### 5.2.3 配置说明
`main.lua`中设置:
```lua
HARDWARE_ENV = "DEV_BOARD_V1.2"
```
驱动会自动完成以下操作:
1. `gpio.setup(28, 1)` - 打开GPIO28给LCD供电
2. `pin_pwr = nil` - 不通过lcd库控制背光
### 5.3 V1.3开发板(DEV_BOARD_V1.3
#### 5.3.1 硬件清单
- Air780EPM v1.3开发板 × 1
- AirLCD_1000 配件板 × 1
- TYPE-C 数据线 × 1
#### 5.3.2 硬件连接与控制
**开发板供电**
- Air780EPM V1.3版本开发板通过TYPE-C USB口供电
- 将开发板上的"外部供电/USB供电"拨动开关拨到"USB供电"一端
- 使用TYPE-C USB数据线连接开发板与电脑USB口
**配件板连接**
- 将AirLCD_1000配件板按照下图所示方向插入开发板
![image](https://docs.openLuat.com/cdn/image/Air780EPM/Air780EPM_demo_accessory_board_lcd_v1.3.jpg)
**LCD电源控制**
- 使用GPIO29控制LCD电源供电
- 系统会自动执行`gpio.setup(29, 1)`打开供电
**背光控制**
- 使用GPIO30控制LCD背光,通过NPN三极管控制
- 系统会自动执行`gpio.setup(30, 0)`开启背光
- 系统会自动设置`pin_pwr = nil`,不通过lcd库控制背光(避免lcd库逻辑与硬件电路冲突)
**背光控制原理:**
V1.3开发板的背光控制电路采用NPN三极管:
- **GPIO30 拉高** → 三极管导通 → BL脚接地 → **灭屏**
- **GPIO30 拉低** → 三极管截止 → BL脚悬空 → **亮屏**
**手动控制背光:**
如需在代码中动态控制背光:
```lua
-- 关闭背光(灭屏)
gpio.setup(30, 1)
-- 开启背光(亮屏)
gpio.setup(30, 0)
```
#### 5.3.3 配置说明
`main.lua`中设置:
```lua
HARDWARE_ENV = "DEV_BOARD_V1.3"
```
驱动会自动完成以下操作:
1. `gpio.setup(29, 1)` - 打开GPIO29给LCD供电
2. `gpio.setup(30, 0)` - GPIO30拉低,开启背光
3. `pin_pwr = nil` - 不通过lcd库控制背光(避免lcd库逻辑与硬件电路冲突)
## 六、演示软件环境
### 6.1 开发工具
- [Luatools下载调试工具](https://docs.openluat.com/air780epm/luatos/common/download/) - 固件烧录和代码调试
### 6.2 内核固件
- [点击下载Air780EPM系列最新版本内核固件](https://docs.openluat.com/air780epm/luatos/firmware/version/)demo所使用的是LuatOS-SoC_V2018_Air780EPM 1号固件
### 6.3 字体文件
- 自定义字体文件:`customer_font_12.bin``customer_font_22.bin`(和lua脚本文件一起烧录,会自动放置在`/luadb/`目录下)
- 演示图片文件:`logo.jpg`(和lua脚本文件一起烧录,会自动放置在`/luadb/`目录下)
- [点击查看自定义字体生成和使用说明](https://docs.openluat.com/osapi/core/lcd/?h=lcd#_6)
## 七、演示核心步骤
### 7.1 硬件准备
1. 按照硬件接线表连接所有设备
2. 确保电源连接正确,通过TYPE-C USB口供电
3. 检查所有接线无误,避免短路
### 7.2 软件配置
`main.lua`中选择加载对应的驱动模块:
```lua
-- 加载显示屏驱动管理功能模块,有以下两种:
-- 1、使用lcd核心库驱动的lcd_drv.lua
-- 2、使用exlcd扩展库驱动的exlcd_drv.lua
-- 根据自己的需求,启用两者中的任何一种都可以
-- 也可以不启用任何一种,不使用显示屏功能
lcd_drv = require "lcd_drv"
-- lcd_drv = require "exlcd_drv"
-- 加载按键驱动管理功能模块
key_drv = require "key_drv"
-- 加载字库驱动管理功能模块
-- 使用外部自定义字体不需要require "customer_font_drv",可以参照customer_font_drv.lua内的使用说明进行创建和加载字体文件
-- 加载输入法驱动管理功能模块(正在开发中,后续补充)
-- 加载lcd核心库实现的用户界面功能模块
-- 实现多页面切换、按键事件分发和界面渲染功能
-- 包含主页、lcd核心库功能演示页、GTFont演示页和自定义字体演示页
require "ui_main"
```
### 7.3 软件烧录
1. 使用Luatools烧录最新内核固件
2. 下载并烧录本项目所有脚本文件
3. 将字体文件和图片文件随脚本文件一起烧录到脚本分区
4. 烧录成功后设备自动重启后开始运行
### 7.4 功能测试
#### 7.4.1 主页面操作
1. 设备启动后显示主页面,包含三个功能按钮(摄像头预览、LCD演示、自定义字体),作为按键页面切换演示
2. 使用boot键切换选择,pwr键进入对应演示页面
#### 7.4.2 LCD演示页面
1. 查看基本图形绘制示例(点、线、矩形、圆形)
2. 查看图片显示区域(显示logo图片)
3. 查看二维码区域(合宙文档二维码)
4. 查看位图和字体示例
5. 按pwr键返回主页
#### 7.4.3 自定义字体页面
1. 查看外部字体文件显示效果
2. 查看多颜色文字显示(红色、绿色、蓝色)
3. 查看字体使用说明和接口信息
4. 按pwr键返回主页
#### 7.4.4 摄像头预览页面
1. 按PWR键拍照
2. Boot键返回主页
3. 拍照后进入照片显示页面,Boot键切换焦点(保存/删除/返回),PWR键执行操作
4. 点击保存后文字变绿显示"已保存"
5. 点击删除后重新进入预览模式
### 7.5 预期效果
- **主页面**:正常显示,使用boot键切换选项,pwr键确认
- **LCD演示页面**:图形绘制清晰,图片和二维码显示正常,颜色示例完整,pwr键返回
- **自定义字体页面**:外部字体加载正确,多颜色文字显示正常,pwr键返回
- **摄像头预览页面**:预览画面正常显示,拍照响应及时,照片保存/删除功能正常,焦点切换流畅
- **按键交互**:所有按键操作响应及时准确,页面切换流畅
### 7.6 故障排除
1. **显示异常**:检查LCD接线是否正确,确认电源供电稳定
2. **按键无响应**:检查参数是否配置正确,确认按键驱动初始化成功
3. **字体显示异常**:检查所需显示内容是否包含在字体内,确认字体文件路径正确
4. **图片无法显示**:确认图片文件已正确烧录到指定路径
5. **系统卡顿**:检查内存使用情况,适当调整刷新频率
@@ -0,0 +1,70 @@
--[[
@module key_drv
@summary BOOT键和PWR键的GPIO中断处理
@version 1.0
@date 2025.12.8
@author 访
@usage
1BOOT键(GPIO0)PWR键(gpio.PWR_KEY)GPIO引脚
2GPIO中断处理函数沿沿
350ms防抖功能
4"KEY_EVENT"
1
1key_drv.init()GPIO中断和防抖参数
]]
local key_drv = {}
-- 按键定义
local key_boot = 0 -- GPIO0按键(BOOT键)
local key_pwr = gpio.PWR_KEY -- 电源按键
-- 按键事件处理函数
local function handle_boot_key(val)
-- print("key_boot", val)
if val == 1 then
sys.publish("KEY_EVENT", "boot_down")
else
sys.publish("KEY_EVENT", "boot_up")
end
end
local function handle_pwr_key(val)
-- print("key_pwr", val)
if val == 1 then
sys.publish("KEY_EVENT", "pwr_up")
else
sys.publish("KEY_EVENT", "pwr_down")
end
end
--[[
GPIO
BOOT键和PWR键的GPIO中断
@api key_drv.init()
@summary BOOT键和PWR键的GPIO中断
@return bool true
@usage
local result = key_drv.init()
if result then
log.info("按键驱动初始化成功")
end
]]
function key_drv.init()
gpio.setup(key_boot, handle_boot_key, gpio.PULLDOWN, gpio.BOTH)
gpio.debounce(key_boot, 50, 0) -- 防抖,防止频繁触发
gpio.setup(key_pwr, handle_pwr_key, gpio.PULLUP, gpio.BOTH)
gpio.debounce(key_pwr, 50, 0) -- 防抖,防止频繁触发
log.info("key_drv", "按键初始化完成")
return true
end
key_drv.init()
return key_drv
@@ -0,0 +1,260 @@
--[[
@module camera_preview_page
@summary /Air780EPM自定义字体
@version 2.2
@date 2026.06.24
@author
]]
require "gc032a"
local excamera = require "excamera"
local camera_preview_page = {}
-- 硬件参数(Air780EPMi2c1, camera_pwr=2
local I2C_ID = 1
local CAM_PWR = 2
local CAM_PWDN = 5
-- 页面状态
local page_state = "idle" -- idle / preview / taking_photo / show_photo
local camera_opened = false
local photo_saved = false
local preview_task_running = false
-- 左上角返回(show_photo时有效)
local back_button = { x1 = 10, y1 = 10, x2 = 80, y2 = 50 }
-- 显示照片页按钮(仅保留保存和删除,下方两个按钮)
-- 保存/删除按钮在照片下方的操作区
local photo_buttons = {
save = { x1 = 30, y1 = 380, x2 = 140, y2 = 450 },
delete = { x1 = 180, y1 = 380, x2 = 290, y2 = 450 }
}
-- show_photo 状态下按键选择焦点:1=保存, 2=删除, 3=返回
local focus_index = 1
-- Air780EPM 使用自定义点阵字体显示中文
local function set_cn_font()
lcd.setFontFile("/luadb/customer_font_12.bin")
end
-- 绘制焦点光标
local function draw_focus()
if page_state ~= "show_photo" then
return
end
local x1, y1, x2, y2
if focus_index == 1 then
-- 保存按钮
x1 = photo_buttons.save.x1 - 3
y1 = photo_buttons.save.y1 - 3
x2 = photo_buttons.save.x2 + 3
y2 = photo_buttons.save.y2 + 3
elseif focus_index == 2 then
-- 删除按钮
x1 = photo_buttons.delete.x1 - 3
y1 = photo_buttons.delete.y1 - 3
x2 = photo_buttons.delete.x2 + 3
y2 = photo_buttons.delete.y2 + 3
elseif focus_index == 3 then
-- 返回按钮
x1 = back_button.x1 - 3
y1 = back_button.y1 - 3
x2 = back_button.x2 + 3
y2 = back_button.y2 + 3
end
if x1 then
lcd.drawRectangle(x1, y1, x2, y2, 0xFFFF)
lcd.drawRectangle(x1 - 1, y1 - 1, x2 + 1, y2 + 1, 0x0000)
end
end
-- 启动预览→拍照→显图的完整流程task
local function start_camera_flow()
preview_task_running = true
sys.taskInit(function()
-- === 阶段1:纯画面预览 ===
local param_preview = {
id = "gc032a", i2c_id = I2C_ID, work_mode = 2,
save_path = "ZBUFF",
camera_pwr = CAM_PWR, camera_pwdn = CAM_PWDN, camera_light = nil
}
local ok = excamera.open(param_preview)
camera_opened = ok
if ok then
excamera.preview()
page_state = "preview"
log.info("camera_preview", "纯画面预览中,按PWR键拍照")
-- 预览循环:轮询 page_state,等待 handle_key 触发
while page_state == "preview" and preview_task_running do
sys.wait(100)
end
-- 关闭预览模式
excamera.close()
sys.wait(1000)
camera_opened = false
end
-- === 阶段2:拍照 ===
if page_state == "taking_photo" then
local param_capture = {
id = "gc032a", i2c_id = I2C_ID, work_mode = 0,
save_path = "/photo.jpg",
camera_pwr = CAM_PWR, camera_pwdn = CAM_PWDN, camera_light = nil
}
local ok = excamera.open(param_capture)
if ok then
local result, _ = excamera.photo()
excamera.close()
if result then
photo_saved = false
focus_index = 1
page_state = "show_photo"
log.info("camera_preview", "拍照成功")
else
log.error("camera_preview", "拍照失败,重新预览")
page_state = "idle"
end
else
log.error("camera_preview", "摄像头初始化失败")
page_state = "idle"
end
end
preview_task_running = false
end)
end
function camera_preview_page.on_enter()
page_state = "idle"
photo_saved = false
focus_index = 1
start_camera_flow()
end
function camera_preview_page.is_preview_raw()
return page_state == "preview"
end
function camera_preview_page.draw()
set_cn_font()
if page_state == "taking_photo" then
lcd.clear()
lcd.setColor(0xFFFF, 0x0000)
lcd.drawStr(110, 230, "拍照中...", 0x0000)
elseif page_state == "show_photo" then
lcd.clear()
-- 全屏显示照片
lcd.showImage(0, 0, "/photo.jpg")
-- 返回按钮(灰色,和其他页面一致)
lcd.fill(back_button.x1, back_button.y1, back_button.x2, back_button.y2, 0xC618)
lcd.setColor(0x07E0, 0x0000)
lcd.drawStr(35, 35, "返回", 0x0000)
-- 底部操作区
lcd.drawLine(0, 350, 320, 350, 0x8410)
lcd.setColor(0xFFFF, 0x0000)
if photo_saved then
lcd.drawStr(130, 358, "已保存", 0x07E0)
end
-- 保存(绿色)
lcd.fill(photo_buttons.save.x1, photo_buttons.save.y1,
photo_buttons.save.x2, photo_buttons.save.y2, 0x07E0)
lcd.drawStr(65, 415, "保存", 0xFFFF)
-- 删除(红色)
lcd.fill(photo_buttons.delete.x1, photo_buttons.delete.y1,
photo_buttons.delete.x2, photo_buttons.delete.y2, 0xF800)
lcd.drawStr(215, 415, "删除", 0xFFFF)
-- 绘制焦点光标
draw_focus()
elseif page_state == "preview" then
-- 预览状态下显示左上角返回按钮
set_cn_font()
lcd.fill(back_button.x1, back_button.y1, back_button.x2, back_button.y2, 0xC618)
lcd.setColor(0x07E0, 0x0000)
lcd.drawStr(35, 35, "返回", 0x0000)
end
end
function camera_preview_page.handle_key(key_type, switch_page)
if page_state == "show_photo" then
if key_type == "next" or key_type == "boot_up" then
-- Boot键:切换焦点 1→2→3→1
focus_index = focus_index % 3 + 1
return true
elseif key_type == "confirm" or key_type == "pwr_up" then
-- PWR键:执行焦点对应的操作
if focus_index == 1 then
-- 保存
photo_saved = true
return true
elseif focus_index == 2 then
-- 删除 → 回到预览
os.remove("/photo.jpg")
photo_saved = false
focus_index = 1
start_camera_flow()
-- 等待新 task 执行到 preview 状态,让 ui_main 进入预览分支(不刷新 LCD)
sys.wait(100)
return true
elseif focus_index == 3 then
-- 返回 → 回到主页
preview_task_running = false
page_state = "idle"
switch_page("home")
return true
end
end
elseif page_state == "preview" then
if key_type == "pwr_up" or key_type == "confirm" then
-- PWR键:触发拍照
page_state = "taking_photo"
return true
elseif key_type == "boot_up" or key_type == "next" then
-- Boot键:返回主页
preview_task_running = false
page_state = "idle"
switch_page("home")
return true
end
elseif page_state == "taking_photo" then
return false
end
return false
end
function camera_preview_page.on_leave()
preview_task_running = false
page_state = "idle"
if camera_opened then
excamera.close()
camera_opened = false
end
i2c.setup(I2C_ID, i2c.SLOW)
end
return camera_preview_page
@@ -0,0 +1,120 @@
--[[
@module customer_font_page
@summary
@version 1.0
@date 2025.12.8
@author 访
@usage
11222
2绿
3使
4使
3
1customer_font_page.draw()
2customer_font_page.handle_key(key_type, switch_page)
3customer_font_page.on_leave()
]]
local customer_font_page = {}
--[[
@api customer_font_page.draw()
@summary UI元素和字体内容
@return nil
]]
function customer_font_page.draw()
lcd.clear()
-- 显示标题,使用22号自定义字体
-- 设置背景颜色
lcd.setColor(0xFFFF, 0x0000)
lcd.setFontFile("/luadb/customer_font_22.bin")
-- 显示标题区域
lcd.fill(0, 0, 320, 40, 0x001F) -- 蓝色标题栏 lcd.setColor(0xFFFF, 0x0000)
-- 显示标题
lcd.drawStr(70, 30, "自定义点阵字体演示", 0xFFFF)
-- 显示演示内容,使用12号自定义字体
lcd.setFontFile("/luadb/customer_font_12.bin")
-- 字体支持说明区域(带背景色)
lcd.fill(15, 60, 305, 155, 0xF7DE) -- 浅灰色背景
lcd.drawRectangle(15, 60, 305, 155, 0x8410) -- 边框
-- 显示字体支持说明
lcd.drawStr(30, 80, "当前设备字体支持:", 0x0000)
lcd.drawStr(40, 105, "- 仅支持自定义字体", 0x0000)
lcd.drawStr(40, 120, "- 不支持内置12号中文", 0x0000)
lcd.drawStr(40, 135, "- 不支持GTFont矢量字体", 0x0000)
lcd.drawLine(20, 165, 300, 165, 0x8410)
-- 设置22号自定义字体文件显示示例文字
lcd.setFontFile("/luadb/customer_font_22.bin")
-- 示例文字区域(居中显示)
lcd.drawStr(100, 200, "上海合宙", 0xF800) -- 红色
lcd.drawStr(105, 230, "LuatOS", 0x07E0) -- 绿色
lcd.drawStr(95, 260, "演示demo", 0x001F) -- 蓝色
-- 恢复12号字体显示说明
lcd.setFontFile("/luadb/customer_font_12.bin")
lcd.setColor(0xFFFF, 0x0000)
-- 字体文件信息区域
lcd.drawLine(20, 280, 300, 280, 0x8410)
lcd.drawStr(20, 300, "字体文件信息:", 0x0000)
lcd.drawStr(30, 325, "- 12号字体: /luadb/customer_font_12.bin", 0x0000)
lcd.drawStr(30, 350, "- 22号字体: /luadb/customer_font_22.bin", 0x0000)
-- 显示使用接口
lcd.drawStr(20, 380, "使用接口:", 0x0000)
lcd.drawStr(35, 405, "- lcd.setFontFile(字体路径)", 0x0000)
lcd.drawStr(35, 425, "- lcd.drawStr(x, y, 文本, 颜色)", 0x0000)
-- 底部状态栏
lcd.fill(0, 440, 320, 480, 0x001F) -- 蓝色背景
lcd.drawStr(20, 465, "按PWR键返回主页", 0xFFFF)
end
--[[
@api customer_font_page.handle_key(key_type, switch_page)
@summary
@string key_type
@valid_values "pwr_up"
@function switch_page
@return bool truefalse
]]
function customer_font_page.handle_key(key_type, switch_page)
log.info("customer_font_page.handle_key", "key_type:", key_type)
if key_type == "pwr_up" then
-- PWR键:返回首页
switch_page("home")
return true
end
-- BOOT键无功能
return false
end
--[[
@api customer_font_page.on_leave()
@summary 12
@return nil
]]
function customer_font_page.on_leave()
-- 恢复使用12号自定义字体
lcd.setFontFile("/luadb/customer_font_12.bin")
log.info("customer_font_page", "已恢复12号自定义字体")
end
return customer_font_page
@@ -0,0 +1,145 @@
--[[
@module home_page
@summary
@version 1.1
@date 2026.06.24
@author 访
@usage
1
2
3
4BOOT键选择和PWR键确认
使
4
1home_page.draw()UI
2home_page.handle_key(key_type, switch_page)
3home_page.on_enter()
4home_page.on_leave()
]]
local home_page = {}
-- 按钮区域定义(竖排三等宽居中)
local buttons = {
{name = "lcd", text = "LCD演示", x1 = 60, y1 = 100, x2 = 260, y2 = 180, color = 0x001F},
{name = "camera_preview", text = "摄像头:点击拍照", x1 = 60, y1 = 200, x2 = 260, y2 = 280, color = 0xFCC0},
{name = "customer_font", text = "自定义字体", x1 = 60, y1 = 300, x2 = 260, y2 = 380, color = 0x07E0}
}
-- 当前选中项索引
local selected_index = 1
local title = "合宙lcd演示系统"
local content1 = "本页面使用的是12号自定义点阵字体"
local hint = "boot键:选择 pwr键:确认"
-- Air780EPM 使用自定义点阵字体显示中文
local function set_title_font()
lcd.setFontFile("/luadb/customer_font_22.bin")
end
local function set_body_font()
lcd.setFontFile("/luadb/customer_font_12.bin")
end
--[[
@local
@return nil
]]
local function draw_cursor()
local btn = buttons[selected_index]
-- 在选中按钮周围绘制矩形光标
lcd.drawRectangle(btn.x1 - 2, btn.y1 - 2, btn.x2 + 2, btn.y2 + 2, 0x3186) -- 蓝色外框
lcd.drawRectangle(btn.x1 - 1, btn.y1 - 1, btn.x2 + 1, btn.y2 + 1, 0x0000) -- 黑色内框
end
--[[
@api home_page.draw()
@summary UI元素
@return nil
]]
function home_page.draw()
lcd.clear()
lcd.setColor(0xFFFF, 0x0000)
-- 显示标题,使用22号自定义字体
set_title_font()
lcd.drawStr(106, 30, title, 0x0000)
-- 显示说明文字,使用12号自定义字体
set_body_font()
lcd.drawStr(46, 50, content1, 0x0000)
-- 绘制所有按钮
for i, btn in ipairs(buttons) do
local color = btn.color
if i == selected_index then
-- 选中状态:颜色稍微变亮
color = color + 0x0842
end
lcd.fill(btn.x1, btn.y1, btn.x2, btn.y2, color)
-- 绘制按钮文字
if btn.name == "lcd" then
lcd.drawStr(130, 138, "LCD演示", 0xFFFF)
elseif btn.name == "camera_preview" then
lcd.drawStr(120, 238, "摄像头:点击拍照", 0xFFFF)
elseif btn.name == "customer_font" then
lcd.drawStr(125, 338, "自定义字体", 0xFFFF)
end
end
-- 绘制光标指示
draw_cursor()
end
--[[
@api home_page.handle_key(key_type, switch_page)
@summary
@string key_type
@valid_values "confirm", "next", "prev", "back"
@function switch_page
@return bool truefalse
]]
function home_page.handle_key(key_type, switch_page)
log.info("home_page.handle_key", "key_type:", key_type, "selected_index:", selected_index)
if key_type == "confirm" then
local btn = buttons[selected_index]
switch_page(btn.name)
return true
elseif key_type == "right" or key_type == "next" then
selected_index = selected_index % #buttons + 1
return true
elseif key_type == "left" or key_type == "prev" then
selected_index = (selected_index - 2) % #buttons + 1
return true
elseif key_type == "back" then
return false
end
return false
end
--[[
@api home_page.on_enter()
@summary
@return nil
]]
function home_page.on_enter()
selected_index = 1
end
function home_page.on_leave()
end
return home_page
@@ -0,0 +1,146 @@
--[[
@module lcd_page
@summary LCD图形绘制演示模块lcd核心库的图形绘制功能
@version 1.0
@date 2025.12.8
@author 访
@usage
LCD图形绘制演示功能模块
1LCD核心库的基本图形绘制功能线
2JPG图片文件
3
4xbm格式位图示例16x16点阵显示
使
2
1lcd_page.draw()LCD演示页面所有UI元素
2lcd_page.handle_key(key_type, switch_page)LCD页面按键事件
]]
local lcd_page = {}
--[[
LCD演示页面
@api lcd_page.draw()
@summary LCD演示页面的所有图形和UI元素
@return nil
]]
function lcd_page.draw()
lcd.clear()
-- 设置背景颜色
lcd.setColor(0xFFFF, 0x0000)
-- 显示标题区域
lcd.fill(0, 0, 320, 40, 0x001F) -- 蓝色标题栏 lcd.setColor(0xFFFF, 0x0000)
-- 显示标题,使用22号自定义字体
lcd.setFontFile("/luadb/customer_font_22.bin")
lcd.drawStr(95, 31, "核心库演示", 0xFFFF)
-- 显示演示内容,使用12号自定义字体
lcd.setFontFile("/luadb/customer_font_12.bin")
lcd.drawStr(20, 75, "基本图形绘制", 0x0000)
lcd.drawLine(20, 55, 300, 55, 0x8410)
lcd.drawStr(30, 98, "", 0x0000)
lcd.drawPoint(55, 98, 0xFCC0)
lcd.drawPoint(65, 98, 0x07E0)
lcd.drawPoint(75, 98, 0x001F)
lcd.drawStr(30, 125, "线", 0x0000)
lcd.drawLine(55, 113, 115, 113, 0xFCC0)
lcd.drawLine(55, 118, 115, 123, 0x07E0)
lcd.drawLine(55, 123, 115, 118, 0x001F)
-- 绘制矩形(预留右侧空间)
lcd.drawStr(30, 160, "矩形", 0x0000)
lcd.drawRectangle(65, 138, 105, 163, 0x7296)
lcd.fill(120, 138, 160, 163, 0x07E0)
-- 绘制圆(预留右侧空间)
lcd.drawStr(30, 200, "圆形", 0x0000)
lcd.drawCircle(90, 193, 15, 0x001F)
lcd.drawCircle(130, 193, 15, 0xFCC0)
lcd.drawLine(170, 70, 170, 300, 0x8410) -- 垂直分隔线
-- === 第二区域:图片和二维码 ===
lcd.drawStr(180, 75, "图片二维码", 0x0000)
-- 图片显示区域 (80x80)
lcd.drawStr(180, 100, "LOGO", 0x0000)
lcd.drawRectangle(200, 110, 290, 200, 0x0000)
lcd.showImage(205, 115, "/luadb/logo.jpg")
-- 二维码区域 (80x80)
lcd.drawStr(180, 215, "二维码", 0x0000)
lcd.drawRectangle(200, 225, 290, 308, 0x0000)
lcd.drawQrcode(205, 226, "https://docs.openluat.com/air780epm/", 80)
lcd.drawLine(20, 325, 300, 325, 0x8410)
-- === 第三区域:位图 ===
lcd.drawLine(20, 235, 160, 235, 0x8410)
-- 位图区域
lcd.drawStr(20, 255, "位图示例", 0x0000)
-- 绘制位图
local x_start = 30
local y_start = 265
lcd.drawXbm(x_start, y_start, 16, 16, string.char(
0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x3F, 0x80, 0x00,
0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0xFE, 0x7F, 0x00, 0x00))
lcd.drawXbm(x_start + 20, y_start, 16, 16, string.char(
0x00, 0x00, 0x80, 0x00, 0xC4, 0x7F, 0x28, 0x00, 0x10, 0x00, 0xD0, 0x3F, 0x42, 0x20, 0x44, 0x22,
0x40, 0x24, 0xF0, 0xFF, 0x24, 0x20, 0x24, 0x22, 0x24, 0x20, 0xE2, 0x7F, 0x02, 0x20, 0x02, 0x1E))
lcd.drawXbm(x_start, y_start + 20, 16, 16, string.char(
0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0x40, 0x02, 0x20, 0x04, 0x18, 0x18, 0xF4, 0x6F, 0x02, 0x00,
0x00, 0x00, 0xF8, 0x1F, 0x08, 0x10, 0x08, 0x10, 0x08, 0x10, 0x08, 0x10, 0xF8, 0x1F, 0x08, 0x10))
lcd.drawXbm(x_start + 20, y_start + 20, 16, 16, string.char(
0x00, 0x00, 0x80, 0x00, 0x00, 0x01, 0xFE, 0x7F, 0x02, 0x40, 0x02, 0x40, 0x00, 0x01, 0xFC, 0x3F,
0x04, 0x21, 0x04, 0x21, 0xFC, 0x3F, 0x04, 0x21, 0x04, 0x21, 0x04, 0x21, 0xFC, 0x3F, 0x04, 0x20))
-- === 第四区域:字体 ===
lcd.drawStr(20, 345, "中英文字体示例", 0x0000)
lcd.drawStr(30, 368, "- 中文字体ABC123", 0x0000)
lcd.drawStr(30, 394, "- 英文符号,-_./()[]{}<>!@#$%^&*_+=|?", 0x0000)
-- 显示当前字体信息
lcd.drawStr(20, 420, "字体路径:/luadb/customer_font_12.bin", 0x0000)
-- 底部状态栏
lcd.fill(0, 440, 320, 480, 0x001F) -- 蓝色背景
lcd.drawStr(20, 465, "按PWR键返回主页", 0xFFFF)
end
--[[
@api lcd_page.handle_key(key_type, switch_page)
@summary LCD页面按键事件
@string key_type
@valid_values "pwr_up"
@function switch_page
@return bool truefalse
]]
function lcd_page.handle_key(key_type, switch_page)
log.info("lcd_page.handle_key", "key_type:", key_type)
if key_type == "pwr_up" then
-- PWR键:返回首页
switch_page("home")
return true
end
-- BOOT键无功能
return false
end
return lcd_page
@@ -0,0 +1,162 @@
--[[
@module ui_main
@summary
@version 1.0
@date 2025.12.8
@author 访
@usage
1LCD演示页和自定义字体演示页
2
3
4
sys.taskInit(ui_main)UI主循环
]]
-- 加载子页面
local home_page = require "home_page"
local lcd_page = require "lcd_page"
local customer_font_page = require "customer_font_page"
local camera_preview_page = require "camera_preview_page"
-- 当前页面状态
local current_page = "home"
local last_page = ""
frame_time = 20 * 1000 -- 超时刷新时间,进入刷新率要求高的页面可以更此变量,退出再更改回来
--[[
@api switch_page(new_page)
@summary
@string new_page
@valid_values "home", "lcd", "customer_font", "camera_preview"
@return nil
]]
local function switch_page(new_page)
log.info("switch_page", "", current_page, "切换到", new_page)
-- 调用旧页面的退出函数
if current_page == "home" and home_page.on_leave then
home_page.on_leave()
elseif current_page == "lcd" and lcd_page.on_leave then
lcd_page.on_leave()
elseif current_page == "customer_font" and customer_font_page.on_leave then
customer_font_page.on_leave()
elseif current_page == "camera_preview" and camera_preview_page.on_leave then
camera_preview_page.on_leave()
end
last_page = current_page
current_page = new_page
-- 调用新页面的进入函数
if new_page == "home" and home_page.on_enter then
home_page.on_enter()
elseif new_page == "lcd" and lcd_page.on_enter then
lcd_page.on_enter()
elseif new_page == "customer_font" and customer_font_page.on_enter then
customer_font_page.on_enter()
elseif new_page == "camera_preview" and camera_preview_page.on_enter then
camera_preview_page.on_enter()
end
log.info("ui_main", "已切换到页面:", current_page)
end
--[[
@api handle_key_event(key_event)
@summary
@string key_event
@valid_values "boot_up", "pwr_up"
@return bool truefalse
]]
local function handle_key_event(key_event)
log.info("按键事件", "event:", key_event, "当前页面:", current_page)
-- 只在按键释放时处理
if key_event == "boot_up" then
-- BOOT键:在主页作为方向键
if current_page == "home" then
return home_page.handle_key("next", switch_page)
elseif current_page == "camera_preview" then
if camera_preview_page.handle_key then
return camera_preview_page.handle_key("boot_up", switch_page)
end
end
-- 其他页面BOOT键无功能
return false
elseif key_event == "pwr_up" then
-- PWR键:确认/返回功能
if current_page == "home" then
return home_page.handle_key("confirm", switch_page)
elseif current_page == "lcd" then
if lcd_page.handle_key then
return lcd_page.handle_key("pwr_up", switch_page)
end
elseif current_page == "customer_font" then
if customer_font_page.handle_key then
return customer_font_page.handle_key("pwr_up", switch_page)
end
elseif current_page == "camera_preview" then
if camera_preview_page.handle_key then
return camera_preview_page.handle_key("pwr_up", switch_page)
end
end
end
return false
end
--[[
@api ui_main()
@summary UI主循环
@return nil
]]
local function ui_main()
-- 使用自定义字体(12号)
lcd.setFontFile("/luadb/customer_font_12.bin")
-- 调用主页的进入函数
if home_page.on_enter then
home_page.on_enter()
end
while true do
-- 纯画面预览模式:硬件直接输出画面,跳过draw/flush
if current_page == "camera_preview" and camera_preview_page.is_preview_raw() then
-- 等待按键事件500ms,不刷新LCD
local result, key_event = sys.waitUntil("KEY_EVENT", 500)
if result then
handle_key_event(key_event)
end
else
-- 其他页面:正常绘制和刷新
-- 根据当前页面绘制内容
if current_page == "home" then
home_page.draw()
elseif current_page == "lcd" then
lcd_page.draw()
elseif current_page == "customer_font" then
customer_font_page.draw()
elseif current_page == "camera_preview" then
camera_preview_page.draw()
end
lcd.flush()
-- 等待按键事件
local result, key_event = sys.waitUntil("KEY_EVENT", frame_time)
if result then
handle_key_event(key_event)
end
end
end
end
sys.taskInit(ui_main)
@@ -0,0 +1,86 @@
--[[
@module hw_default_font_drv
@summary lcd和tp驱动模块使12
@version 1.0
@date 2025.11.25
@author 访
@usage
lcd和tp驱动模块
1使lcd内核固件中自带的12号英文点阵字体
2lcd和tp参数exEasyUI默认使用的字体
3
require加载后自动执行初始化
@api ui.hw_init(config)
@summary exEasyUI硬件系统
@table config LCD配置和触摸配置
@field lcd_config table LCD显示配置参数
@field tp_config table
@return nil
@usage
]]
-- 使用默认12号中文字体初始化exEasyUI硬件
ui.hw_init({
-- lcd_config参数填写可以参考合宙exlcd显示扩展库exlcd.init(param)接口说明:https://docs.openluat.com/osapi/ext/exlcd/#31-exlcdinitparam
lcd_config = {
lcd_model = "AirLCD_1010", -- LCD型号
-- pin_vcc = 24, -- 供电引脚,使用GPIO控制屏幕供电可配置
pin_rst = 36, -- 复位引脚
pin_pwr = 1, -- 背光控制引脚GPIO ID号
pin_pwm = 0, -- 背光控制引脚PWM ID号
port = lcd.HWID_0, -- 驱动端口
-- pin_dc = 0xFF, -- lcd数据/命令选择引脚GPIO ID号,使用lcd 专用 SPI 接口 lcd.HWID_0不需要填此参数,使用通用SPI接口需要赋值
direction = 0, -- lcd屏幕方向 0:0° 1:90° 2:180° 3:270°,屏幕方向和分辨率保存一致
w = 320, -- lcd 水平分辨率
h = 480, -- lcd 竖直分辨率
xoffset = 0, -- x偏移(不同屏幕ic 不同屏幕方向会有差异)
yoffset = 0, -- y偏移(不同屏幕ic 不同屏幕方向会有差异)
sleepcmd = 0X10, -- 睡眠命令,默认0X10
wakecmd = 0X11, -- 唤醒命令,默认0X11
-- bus_speed = 50*1000*1000, -- SPI总线速度,不填默认50M,若速率要求更高需要进行设置
-- interface_mode = lcd.WIRE_4_BIT_8_INTERFACE_I, -- lcd模式,默认lcd.WIRE_4_BIT_8_INTERFACE_I
-- direction0 = {0x36,0x00}, -- 0°方向的命令,(不同屏幕ic会有差异)
-- direction90 = {0x36,0x60}, -- 90°方向的命令,(不同屏幕ic会有差异)
-- direction180 ={0x36,0xc0} , -- 180°方向的命令,(不同屏幕ic会有差异)
-- direction270 = {0x36,0xA0}, -- 270°方向的命令,(不同屏幕ic会有差异)
-- hbp = nil, -- 水平后廊
-- hspw = nil, -- 水平同步脉冲宽度
-- hfp = 0, -- 水平前廊
-- vbp = 0, -- 垂直后廊
-- vspw = 0, -- 垂直同步脉冲宽度
-- vfp = 0, -- 垂直前廊
-- initcmd = nil, -- 自定义屏幕初始化命令表
-- flush_rate = nil, -- 刷新率
-- spi_dev = nil, -- spi设备,当port = "device"时有效,当port ≠ "device"时可不填或者填nil
-- init_in_service = false, -- 允许初始化在lcd service里运行,在后台初始化LCD,默认是falseAir8000/G/W/T/A、Air780EHM/EGH/EHV 支持填true,可加快初始化速度,默认SPI总线速度80M
},
-- tp_config参数填写可以参考合宙extp触摸扩展库以下三个接口说明:https://docs.openluat.com/osapi/ext/extp/#41-extpinitparam
-- 按extp.init(param)接口说明填写tp_model、i2c_id、pin_rst、pin_int参数
-- 按extp.set_publish_enabled(msg_type, enabled)接口说明和实际需求填写message_enabled{}列表内参数
-- 按extp.set_swipe_threshold(threshold)接口说明填写swipe_threshold和long_press_threshold参数
tp_config = {
tp_model = "AirLCD_1010", -- 触摸芯片/设备型号
i2c_id = 1, -- I2C总线ID
pin_rst = 0xFF, -- 触摸芯片复位引脚(AirLCD_1010上没有引出该引脚)
pin_int = 24, -- 触摸芯片中断引脚
-- @param message_enabled 消息类型 ("ALL", "RAW_DATA", "TOUCH_DOWN", "MOVE_X", "MOVE_Y", "SWIPE_LEFT", "SWIPE_RIGHT", "SWIPE_UP", "SWIPE_DOWN", "SINGLE_TAP", "LONG_PRESS")
message_enabled = {
TOUCH_DOWN = true, -- 启用按下检测
MOVE_X = true, -- 启用横向滑动
MOVE_Y = true, -- 启用纵向滑动
SWIPE_LEFT = true, -- 启用左滑手势
SWIPE_RIGHT = true, -- 启用右滑手势
SWIPE_UP = true, -- 启用上滑手势
SWIPE_DOWN = true, -- 启用下滑手势
SINGLE_TAP = true, -- 启用点击手势
LONG_PRESS = false -- 禁用长按手势
},
swipe_threshold = 10, -- 设置滑动阈值
long_press_threshold = 2000 -- 设置长按阈值(毫秒)
}
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,84 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口
@version 1.0
@date 2025.12.10
@author 访
@usage
demo演示的核心功能为
1exeasyui扩展库
2
3
4
readme.md文件
]]
--[[
PROJECT和VERSION变量Luatools工具会用到这两个变量
PROJECTascii string类型
便使,
VERSIONascii string类型
使iot.openluat.com进行远程升级"XXX.YYY.ZZZ"
XYZ各表示1位数字X表示的数字可以相同Y和三个Z表示的数字也是可以相同
YYY这三位数字必须存在999
使iot.openluat.com进行远程升级
]]
-- main.lua - 程序入口文件
-- 定义项目名称和版本号
PROJECT = "ui_demo" -- 项目名称
VERSION = "001.999.000" -- 版本号
-- 在日志中打印项目名和项目版本号
log.info("ui_demo", PROJECT, VERSION)
-- 设置日志输出风格为样式2(建议调试时开启)
-- log.style(2)
-- 如果内核固件支持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)
-- 必须加载才能启用exeasyui的功能
ui = require("exeasyui")
-- 加载lcd、tp驱动管理功能模块
-- 1、按lcd显示驱动配置和tp触摸驱动配置进行初始化,默认使用lcd内核固件中自带的12号英文点阵字体
require("hw_default_font_drv")
-- 加载exeassyui扩展库实现的用户界面功能模块
-- 实现多页面切换、触摸事件分发和界面渲染功能
-- 包含主页、组件演示页、默认字体演示页
require("ui_main")
-- 用户代码已结束
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,172 @@
# exEasyUI 演示
## 一、功能模块介绍
### 1.1 核心主程序模块
1. **main.lua** - 主程序入口,负责系统初始化和任务调度
2. **ui_main.lua** - exeasyui 主程序,负责执行exeasyui的任务调度
### 1.2 显示页面模块
1. **home_page.lua** - 主页模块,提供应用入口和导航功能
2. **component_page.lua** - UI 组件演示模块
3. **default_font_page.lua** - 默认字体演示模块
### 1.3 硬件驱动模块
1. **hw_default_font_drv.lua** - lcd显示驱动配置、tp触摸驱动配置和驱动模块,使用内置12号英文点阵字体
## 二、演示效果
<table>
<tr>
<td>主页<br/></td><td>组件演示页<br/></td><td>默认字体页<br/></td></tr>
<tr>
<td><img src="https://docs.openluat.com/cdn/image/Air780EPM_AirLCD_1010__exeasyui_homepage.png" width="80" /><br/></td><td><img src="https://docs.openluat.com/cdn/image/Air780EPM_AirLCD_1010__exeasyui_componentpage.png" width="80" /><br/></td><td><img src="https://docs.openluat.com/cdn/image/Air780EPM_AirLCD_1010__exeasyui_fontpage.png" width="80" /><br/></td></tr>
</table>
## 三、演示硬件环境
### 3.1 硬件清单
- Air780EPM 核心板 × 1
- AirLCD_1010 触摸配件板 × 1
- 母对母杜邦线 × 11,杜邦线太长的话,会出现 spi 通信不稳定的现象;
- TYPE-C 数据线 × 1
- Air780EPM 核心板和 AirLCD_1010 配件板的硬件接线方式为
- Air780EPM 核心板通过 TYPE-C USB 口供电(核心板正面开关拨到 ON 一端),此种供电方式下,VBAT 引脚为 3.3V,可以直接给 AirLCD_1010 配件板供电;
- 为了演示方便,所以 Air780EPM 核心板上电后直接通过 VBAT 引脚给 AirLCD_1010 配件板供电;
- 客户在设计实际项目时,一般来说,需要通过一个 GPIO 来控制 LDO 给配件板供电,这样可以灵活地控制配件板的供电,可以使项目的整体功耗降到最低;
### 3.2 接线配置
#### 3.2.1 显示屏接线
<table>
<tr>
<td>Air780EPM 核心板<br/></td><td>AirLCD_1010配件板<br/></td></tr>
<tr>
<td>53/LCD_CLK<br/></td><td>SCLK/CLK<br/></td></tr>
<tr>
<td>52/LCD_CS<br/></td><td>CS<br/></td></tr>
<tr>
<td>49/LCD_RST<br/></td><td>RES/RST<br/></td></tr>
<tr>
<td>50/LCD_SDA<br/></td><td>SDA/MOS<br/></td></tr>
<tr>
<td>51/LCD_RS<br/></td><td>DC/RS<br/></td></tr>
<tr>
<td>22/GPIO1<br/></td><td>BLK<br/></td></tr>
<tr>
<td>VBAT<br/></td><td>VCC<br/></td></tr>
<tr>
<td>67/I2C1_SCL<br/></td><td>SCL<br/></td></tr>
<tr>
<td>66/I2C1_SDA<br/></td><td>SDA<br/></td></tr>
<tr>
<td>20/GPIO24<br/></td><td></td><td>INT<br/></td></tr>
</table>
### 3.3 实际接线图
![](https://docs.openLuat.com/cdn/image/Air780EPM_AirLCD_1010接线图.jpg)
## 四、演示软件环境
### 4.1 开发工具
- [Luatools下载调试工具](https://docs.openluat.com/air780epm/luatos/common/download/) - 固件烧录和代码调试
### 4.2 内核固件
- [点击下载Air780EPM系列最新版本内核固件](https://docs.openluat.com/air780epm/luatos/firmware/version/)demo所使用的是LuatOS-SoC_V2018_Air780EPMM 1号固件
## 五、演示核心步骤
### 5.1 硬件准备
1. 按照硬件接线表连接所有设备
2. 通过 TYPE-C USB 口供电
3. 检查所有接线无误
### 5.2 软件配置
`main.lua` 中配置系统参数:
```lua
-- 必须加载才能启用exeasyui的功能
ui = require("exeasyui")
-- 加载lcd、tp驱动管理功能模块
-- 1、按lcd显示驱动配置和tp触摸驱动配置进行初始化,默认使用lcd内核固件中自带的12号英文点阵字体
require("hw_default_font_drv")
-- 加载exeassyui扩展库实现的用户界面功能模块
-- 实现多页面切换、触摸事件分发和界面渲染功能
-- 包含主页、组件演示页、默认字体演示页
require("ui_main")
```
### 5.3 屏幕参数配置
在对应的驱动文件中根据实际硬件调整硬件参数:
- **hw_default_font_drv.lua** - lcd显示驱动配置、tp触摸驱动配置驱动模块,使用内置 12 号英文点阵字体
### 5.4 软件烧录
1. 使用 Luatools 烧录对应型号的最新内核固件
2. 下载并烧录本项目所有脚本文件
3. 将图片文件随脚本文件一起烧录到脚本分区
4. 设备自动重启后开始运行
5. [点击查看Luatools 下载和详细使用](https://docs.openluat.com/air780epm/common/Luatools/)
### 5.5 功能测试
#### 5.5.1 主页面操作
1. 设备启动后显示主页面,包含两个功能按钮
2. 查看系统标题和版本信息
3. 点击各功能按钮进入对应演示页面
#### 5.5.2 组件演示页面
1. 测试进度条组件的动态更新
2. 体验消息框的弹出和按钮响应
3. 操作复选框查看状态变化
4. 在输入框中输入文本测试
5. 使用下拉框选择选项
6. 查看图片轮播效果(如有图片文件)
#### 5.5.3 字体演示页面
1. **默认字体页**:查看固定 12 号字体的颜色和英文显示
2. 在各页面使用返回按钮回到主页
### 5.6 预期效果
- **系统启动**:正常初始化,显示主页面
- **页面切换**:流畅的页面过渡效果
- **组件交互**:所有 UI 组件响应灵敏
- **字体显示**:各字体页面正常显示,动态调整功能正常
- **触摸操作**:准确的触摸定位和事件响应
### 5.7 故障排除
1. **显示异常**:检查 LCD 接线,确认对应驱动文件中的硬件参数正确
2. **触摸无响应**:检查 I2C 接线,确认触摸芯片型号配置正确
3. **图片无法显示**:确认图片文件已正确烧录到指定路径
4. **系统卡顿**:调整 `ui_main.lua` 中的刷新率参数
## 六、扩展开发
本演示 demo 所有接口都在 [exeasyUI UI 扩展库](https://docs.openluat.com/osapi/ext/exeasyui/)内有详细说明,如需实现更丰富的自定义功能可按接口说明实现。
@@ -0,0 +1 @@
-- 无触摸仅显示,通过按键方式进行UI交互的初始化方式正在开发中
@@ -0,0 +1,323 @@
--[[
@module component_page
@summary exEasyUI组件演示页面模块UI组件的使用方法
@version 1.0
@date 2025.12.10
@author 访
@usage
1
2UI组件
3
4
1
1component_page.create(ui)
]]
local component_page = {}
--[[
@api component_page.create(ui)
@summary
@table ui UI库对象
@return table
@usage
-- 在子页面工厂中调用创建组件演示页面
local component_page = require("component_page").create(ui)
]]
function component_page.create(ui)
local win = ui.window({
background_color = ui.COLOR_WHITE,
x = 0,
y = 0,
w = 320,
h = 480
})
-- 启用滚动
win:enable_scroll({
direction = "vertical",
content_height = 800,
threshold = 10
})
-- 标题
local title = ui.label({
x = 120,
y = 25,
text = "Component Demo Page",
color = ui.COLOR_BLACK,
size = 12
})
-- 返回按钮
local btn_back = ui.button({
x = 20,
y = 20,
w = 60,
h = 30,
text = "Back",
on_click = function()
win:back()
end
})
-- ==================== 1. 进度条组件演示 ====================
local progress_label = ui.label({
x = 20,
y = 70,
text = "1. Progress Bar:",
color = ui.COLOR_BLACK,
size = 12
})
local progress_value = 0
local progress_bar = ui.progress_bar({
x = 20,
y = 100,
w = 200,
h = 26,
progress = progress_value
})
local btn_progress = ui.button({
x = 230,
y = 100,
w = 70,
h = 26,
text = "+10%",
on_click = function()
progress_value = progress_value + 10
if progress_value > 100 then
progress_value = 0
end
progress_bar:set_progress(progress_value)
progress_bar:set_text("Progress: " .. progress_value .. "%")
end
})
-- ==================== 2. 消息框组件演示 ====================
local msgbox_label = ui.label({
x = 20,
y = 140,
text = "2. Message Box:",
color = ui.COLOR_BLACK,
size = 12
})
local btn_msgbox = ui.button({
x = 20,
y = 170,
w = 180,
h = 30,
text = "Show MessageBox",
on_click = function()
local message_box = ui.message_box({
x = 10,
y = 100,
w = 300,
h = 150,
title = "Info",
message = "This is an exEasyUI message box demo",
buttons = { "OK", "Cancel" },
on_result = function(button_index)
log.info("component_page", "Button clicked:", button_index)
end
})
ui.add(message_box)
end
})
-- ==================== 3. 复选框组件演示 ====================
local checkbox_label = ui.label({
x = 20,
y = 220,
text = "3. Check Box:",
color = ui.COLOR_BLACK,
size = 12
})
local checkbox1 = ui.check_box({
x = 20,
y = 250,
text = "Option A",
checked = false,
on_change = function(checked)
log.info("component_page", "Option A:", checked)
end
})
local checkbox2 = ui.check_box({
x = 120,
y = 250,
text = "Option B",
checked = true,
on_change = function(checked)
log.info("component_page", "Option B:", checked)
end
})
-- ==================== 4. 输入框组件演示 ====================
local input_label = ui.label({
x = 20,
y = 300,
text = "4. Input Box:",
color = ui.COLOR_BLACK,
size = 12
})
-- 普通文本输入框
local text_input = ui.input({
x = 20,
y = 330,
w = 200,
h = 30,
placeholder = "Enter text...",
max_length = 20
})
-- 密码输入框
local password_input = ui.input({
x = 20,
y = 370,
w = 200,
h = 30,
placeholder = "Enter password...",
input_type = "password",
max_length = 16
})
-- 数字输入框
local number_input = ui.input({
x = 20,
y = 410,
w = 200,
h = 30,
placeholder = "Enter number...",
input_type = "number",
max_length = 10
})
-- ==================== 5. 下拉框组件演示 ====================
local combo_label = ui.label({
x = 20,
y = 460,
text = "5. Combo Box:",
color = ui.COLOR_BLACK,
size = 12
})
local combo_box = ui.combo_box({
x = 20,
y = 490,
w = 200,
h = 30,
options = { "Option 1", "Option 2", "Option 3" },
placeholder = "Please select",
selected = 1,
on_select = function(value, index, text)
log.info("component_page", "Selected:", text, "Index:", index)
end
})
-- ==================== 6. 图片轮播组件演示 ====================
local picture_label = ui.label({
x = 20,
y = 540,
text = "6. Image Carousel:",
color = ui.COLOR_BLACK,
size = 12
})
local picture = ui.picture({
x = 20,
y = 570,
w = 280,
h = 100,
sources = { "/luadb/1.jpg", "/luadb/2.jpg", "/luadb/3.jpg" },
autoplay = true,
interval = 2000
})
-- ==================== 7. 按钮组件演示 ====================
local button_label = ui.label({
x = 20,
y = 705,
text = "7. Buttons:",
color = ui.COLOR_BLACK,
size = 12
})
-- 普通按钮
local normal_btn = ui.button({
x = 20,
y = 735,
w = 120,
h = 30,
text = "Normal Button",
on_click = function()
log.info("component_page", "Normal button clicked")
end
})
-- 带颜色的按钮
local colored_btn = ui.button({
x = 150,
y = 735,
w = 90,
h = 30,
text = "Blue Button",
bg_color = ui.COLOR_BLUE,
text_color = ui.COLOR_WHITE,
on_click = function()
log.info("component_page", "Blue button clicked")
end
})
-- 图片按钮
local image_btn = ui.button({
x = 250,
y = 720,
w = 64,
h = 64,
src = "/luadb/4.jpg",
src_toggled = "/luadb/5.jpg",
toggle = true,
on_click = function()
log.info("component_page", "Image button clicked")
end
})
-- 添加所有组件到窗口
win:add(title)
win:add(btn_back)
win:add(progress_label)
win:add(progress_bar)
win:add(btn_progress)
win:add(msgbox_label)
win:add(btn_msgbox)
win:add(checkbox_label)
win:add(checkbox1)
win:add(checkbox2)
win:add(input_label)
win:add(text_input)
win:add(password_input)
win:add(number_input)
win:add(combo_label)
win:add(combo_box)
win:add(picture_label)
win:add(picture)
win:add(button_label)
win:add(normal_btn)
win:add(colored_btn)
win:add(image_btn)
return win
end
return component_page
@@ -0,0 +1,147 @@
--[[
@module default_font_page
@summary 使12
@version 1.0
@date 2025.12.10
@author 访
@usage
112
2
3
4
1
1default_font_page.create(ui)
]]
local default_font_page = {}
--[[
@api default_font_page.create(ui)
@summary
@table ui UI库对象
@return table
@usage
-- 在子页面工厂中调用创建默认字体演示页面
local default_font_page = require("default_font_page").create(ui)
]]
function default_font_page.create(ui)
local win = ui.window({
background_color = ui.COLOR_WHITE,
x = 0, y = 0, w = 320, h = 480
})
-- 标题 - 居中显示
local title = ui.label({
x = 120, y = 25,
text = "Default Font Demo",
color = ui.COLOR_BLACK,
size = 12
})
-- 返回按钮
local btn_back = ui.button({
x = 20, y = 20,
w = 60, h = 30,
text = "Back",
on_click = function()
win:back()
end
})
-- 字体演示标题
local demo_title = ui.label({
x = 20, y = 70,
text = "Font Demo (Fixed 12pt):",
color = ui.COLOR_BLACK,
size = 12
})
-- 数字演示 - 蓝色12号
local number_demo = ui.label({
x = 20, y = 100,
text = "1. Numbers: 0123456789",
color = ui.COLOR_BLUE,
size = 12
})
-- 符号演示 - 橙色12号
local symbol_demo = ui.label({
x = 20, y = 130,
text = "2. Symbols: !@#$%^&*()_+-=[]",
color = ui.COLOR_ORANGE,
size = 12
})
-- 英文演示 - 红色12号
local text_demo = ui.label({
x = 20, y = 160,
text = "3. English: Hello World ABC",
color = ui.COLOR_RED,
size = 12
})
-- 默认字体特性说明
local feature_title = ui.label({
x = 20, y = 200,
text = "Default Font Features:",
color = ui.COLOR_BLACK,
size = 12
})
local feature1 = ui.label({
x = 20, y = 230,
text = "- Built-in 12pt bitmap font",
color = ui.COLOR_GRAY,
size = 12
})
local feature2 = ui.label({
x = 20, y = 260,
text = "- Supports English, numbers, symbols",
color = ui.COLOR_GRAY,
size = 12
})
local feature3 = ui.label({
x = 20, y = 290,
text = "- Fast startup, low resource usage",
color = ui.COLOR_GRAY,
size = 12
})
local feature4 = ui.label({
x = 20, y = 320,
text = "- Fixed 12pt font size",
color = ui.COLOR_GRAY,
size = 12
})
-- 启用滚动以显示完整内容
win:enable_scroll({
direction = "vertical",
content_height = 400,
threshold = 10
})
-- 添加所有组件到窗口
win:add(title)
win:add(btn_back)
win:add(demo_title)
win:add(number_demo)
win:add(symbol_demo)
win:add(text_demo)
win:add(feature_title)
win:add(feature1)
win:add(feature2)
win:add(feature3)
win:add(feature4)
return win
end
return default_font_page
@@ -0,0 +1,96 @@
--[[
@module home_page
@summary
@version 1.0
@date 2025.12.10
@author 访
@usage
1
2
3
4
1
1home_page.create()
]]
local home_page = {}
--[[
@api home_page.create()
@summary
@return nil
@usage
-- 在UI主程序中调用创建主页
home_page.create()
]]
function home_page.create()
-- 创建主页
local home = ui.window({ background_color = ui.COLOR_WHITE })
home.visible = true
-- 配置子页面工厂
home:configure_subpages({
component = function() return require("component_page").create(ui) end,
default_font = function() return require("default_font_page").create(ui) end
})
-- 标题
local title = ui.label({
x = 80,
y = 30,
text = "exEasyUI Demo System",
color = ui.COLOR_BLACK,
size = 12
})
local subtitle = ui.label({
x = 75,
y = 60,
text = "Based on exEasyUI v1.7.0",
color = ui.COLOR_GRAY,
size = 12
})
-- 组件演示按钮
local btn_component = ui.button({
x = 20,
y = 100,
w = 280,
h = 50,
text = "Component Demo",
bg_color = ui.COLOR_BLUE,
text_color = ui.COLOR_WHITE,
on_click = function()
home:show_subpage("component")
end
})
-- 默认字体演示按钮
local btn_default_font = ui.button({
x = 20,
y = 170,
w = 280,
h = 50,
text = "Default Font Demo",
bg_color = ui.COLOR_RED,
text_color = ui.COLOR_WHITE,
on_click = function()
home:show_subpage("default_font")
end
})
-- 添加所有组件到窗口
home:add(title)
home:add(subtitle)
home:add(btn_component)
home:add(btn_default_font)
ui.add(home)
end
return home_page
@@ -0,0 +1,29 @@
--[[
@module home_page
@summary exEasyUI实现
@version 1.0
@date 2025.12.10
@author 访
@usage
exeasyui主程序模块
1
2
3
]]
local home_page = require("home_page")
-- 启动UI主任务
local function ui_main()
-- 初始化UI主题
ui.sw_init({ theme = "light" })
home_page.create()
end
sys.taskInit(ui_main)
@@ -0,0 +1,16 @@
--[[
customer_font_drv是用户外部自定义外部点阵字体驱动功能模块,使
lcd核心库自定义字体制作说明https://docs.openluat.com/osapi/core/lcd/#_6
1 https://gitee.com/Dozingfiretruck/u8g2_font_tool
2
- .ttf和.otf格式字体加载到制作工具内
-
-
- lcd核心库可加载的.bin格式的字体文件
3使lcd.setFontFile(font)
- font为字体文件路径
- customer_font_24.bin luatools工具同代码一起烧录到脚本分区lcd.setFontFile("/luadb/customer_font_24.bin")
- customer_font_24.bin luatools工具同固件和代码一起烧录到文件系统lcd.setFontFile("/customer_font_24.bin")
4使lcd.drawStr(x,y,str,fg_color)
]]
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,31 @@
--[[
@module exlcd_drv
@summary LCD显示驱动模块exlcd扩展库
@version 1.0
@date 2025.12.3
@author 访
@usage
LCD显示驱动功能模块
1AirLCD_1010扩展LCD显示
2
exlcd_drv模块后LCD显示驱动初始化
]]
local exlcd = require "exlcd"
local function exlcd_drv_init()
local result = exlcd.init({lcd_model = "AirLCD_1010"})
log.info("exlcd.init", result)
if result then
-- 显示设置
lcd.setupBuff(nil, true)
lcd.autoFlush(false)
end
end
exlcd_drv_init()
@@ -0,0 +1,60 @@
--[[
@module lcd_drv
@summary LCD显示驱动模块lcd核心库
@version 1.0
@date 2025.12.3
@author 访
@usage
LCD显示驱动功能模块
1AirLCD_1010 IC ST7796
2LCD显示参数和显示缓冲区
3
lcd_drv模块后LCD显示驱动初始化
]]
local function lcd_drv_init()
local result = lcd.init("st7796",
{
pin_rst = 36, -- 复位引脚
pin_pwr = 1, -- 背光控制引脚GPIO的ID号
port = lcd.HWID_0, -- 驱动端口
-- pin_dc = 0xFF, -- lcd数据/命令选择引脚GPIO ID号,使用lcd 专用 SPI 接口 lcd.HWID_0不需要填此参数,使用通用SPI接口需要赋值
direction = 0, -- lcd屏幕方向 0:0° 1:90° 2:180° 3:270°,屏幕方向和分辨率保存一致
w = 320, -- lcd 水平分辨率
h = 480, -- lcd 竖直分辨率
xoffset = 0, -- x偏移(不同屏幕ic 不同屏幕方向会有差异)
yoffset = 0, -- y偏移(不同屏幕ic 不同屏幕方向会有差异)
sleepcmd = 0X10, -- 睡眠命令,默认0X10
wakecmd = 0X11, -- 唤醒命令,默认0X11
-- bus_speed = 50*1000*1000, -- SPI总线速度,不填默认50M,若速率要求更高需要进行设置
-- interface_mode = lcd.WIRE_4_BIT_8_INTERFACE_I, -- lcd模式,默认lcd.WIRE_4_BIT_8_INTERFACE_I
-- direction0 = {0x36,0x00}, -- 0°方向的命令,(不同屏幕ic会有差异)
-- direction90 = {0x36,0x60}, -- 90°方向的命令,(不同屏幕ic会有差异)
-- direction180 ={0x36,0xc0} , -- 180°方向的命令,(不同屏幕ic会有差异)
-- direction270 = {0x36,0xA0}, -- 270°方向的命令,(不同屏幕ic会有差异)
-- hbp = nil, -- 水平后廊
-- hspw = nil, -- 水平同步脉冲宽度
-- hfp = 0, -- 水平前廊
-- vbp = 0, -- 垂直后廊
-- vspw = 0, -- 垂直同步脉冲宽度
-- vfp = 0, -- 垂直前廊
-- initcmd = nil, -- 自定义屏幕初始化命令表
-- flush_rate = nil, -- 刷新率
-- spi_dev = nil, -- spi设备,当port = "device"时有效,当port ≠ "device"时可不填或者填nil
-- init_in_service = false, -- 允许初始化在lcd service里运行,在后台初始化LCD,默认是falseAir8000/G/W/T/A、Air780EHM/EGH/EHV 支持填true,可加快初始化速度,默认SPI总线速度80M
})
log.info("lcd.init", result)
if result then
-- 显示设置
lcd.setupBuff(nil, true)
lcd.autoFlush(false)
end
end
lcd_drv_init()
@@ -0,0 +1,102 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口
@version 1.0
@date 2025.11.20
@author 访
@usage
demo演示的核心功能为
1LCD显示屏驱动初始化LCD驱动模式
2
3
4
5
readme.md文件
]]
--[[
PROJECT和VERSION变量Luatools工具会用到这两个变量
PROJECTascii string类型
便使,
VERSIONascii string类型
使iot.openluat.com进行远程升级"XXX.YYY.ZZZ"
XYZ各表示1位数字X表示的数字可以相同Y和三个Z表示的数字也是可以相同
YYY这三位数字必须存在999
使iot.openluat.com进行远程升级
]]
-- main.lua - 程序入口文件
-- 定义项目名称和版本号
PROJECT = "ui_demo" -- 项目名称
VERSION = "001.999.000" -- 版本号
-- 在日志中打印项目名和项目版本号
log.info("ui_demo", PROJECT, VERSION)
-- 设置日志输出风格为样式2(建议调试时开启)
-- 设置日志输出风格为样式2(建议调试时开启)
-- log.style(2)
-- 如果内核固件支持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)
-- 加载显示屏驱动管理功能模块,有以下两种:
-- 1、使用lcd核心库驱动的lcd_drv.lua
-- 2、使用exlcd扩展库驱动的exlcd_drv.lua
-- 根据自己的需求,启用两者中的任何一种都可以
-- 也可以不启用任何一种,不使用显示屏功能
require "lcd_drv"
--require "exlcd_drv"
-- 加载触摸面板驱动管理功能模块,有以下两种:
-- 1、使用tp核心库驱动的tp_drv.lua
-- 2、使用extp扩展库驱动的extp_drv.lua
-- 根据自己的需求,启用两者中的任何一种都可以
-- 也可以不启用任何一种,不使用触摸面板功能
require "tp_drv"
--require "extp_drv"
-- 加载字库驱动管理功能模块
-- 使用外部自定义字体不需要require "customer_font_drv",可以参照customer_font_drv.lua内的使用说明进行创建和加载字体文件
-- 加载输入法驱动管理功能模块(正在开发中,后续补充)
-- 加载lcd核心库实现的用户界面功能模块
-- 实现多页面切换、触摸事件分发和界面渲染功能
-- 包含主页、lcd核心库功能演示页、GTFont演示页和自定义字体演示页
require "ui_main"
-- 用户代码已结束
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,207 @@
# LCD、触摸与字体演示系统
## 一、功能模块介绍
### 1.1 核心主程序模块
1. **main.lua** - 主程序入口,负责系统初始化和任务调度
2. **ui_main.lua** - 用户界面主控模块,管理页面切换和事件分发
### 1.2 显示页面模块
1. **home_page.lua** - 主页模块,提供应用入口和导航功能
2. **lcd_page.lua** - LCD图形绘制演示模块
3. **customer_font_page.lua** - 自定义字体演示模块
4. **camera_preview_page.lua** - 摄像头预览页面,支持预览→拍照→保存/删除
### 1.3 驱动模块
1. **lcd_drv.lua** - LCD显示驱动模块,基于lcd核心库,lcd_drv和exlcd_drv二选一使用
2. **exlcd_drv.lua** - LCD显示驱动模块,基于exlcd扩展库,lcd_drv和exlcd_drv二选一使用
3. **tp_drv.lua** - 触摸驱动模块,基于tp核心库,tp_drv和extp_drv二选一使用
4. **extp_drv.lua** - 触摸驱动模块,基于extp扩展库,tp_drv和extp_drv二选一使用
## 二、触摸消息介绍
1. **"BASE_TOUCH_EVENT"** - 基础触摸事件消息,包含触摸坐标和事件类型
- tp触摸库事件类型:`tp.EVENT_DOWN`(按下)、`tp.EVENT_MOVE`(移动)、`tp.EVENT_UP`(抬起)
- extp触摸库事件类型:`SINGLE_TAP`(单击)、`LONG_PRESS`(长按)可根据UI需求打开滑动事件
- 坐标范围:X: 0-319Y: 0-479
## 三、显示效果
<table>
<tr><td>主页<br/></td><td>lcd核心库页面<br/></td><td>自定义字体页面<br/></td></tr><tr>
<td rowspan="2"><img src="https://docs.openluat.com/cdn/image/Air780EPM_AirLCD_1010_主页.png" width="80" /><br/></td>
<td rowspan="2"><img src="https://docs.openluat.com/cdn/image/Air780EPM_AirLCD_1010_lcd演示页.png" width="80" /><br/></td>
<td><img src="https://docs.openluat.com/cdn/image/Air780EPM_AirLCD_1010_自定义字体演示页.png" width="80" /><br/></td></tr>
</table>
## 四、演示功能概述
### 4.1 LCD图形绘制演示
1. **基本图形绘制** - 展示点、线、矩形、圆形等基本图形绘制功能
2. **图片显示** - 支持外部图片文件显示
3. **二维码生成** - 动态生成并显示二维码
4. **xbm格式位图示例** - 显示16*16 xbm点阵
5. **字体示例** - 显示12号和22号自定义字体
### 4.2 自定义字体演示
1. **外部字体加载** - 支持加载外部自定义字体文件
2. **多颜色文字** - 支持不同颜色的文字显示
3. **字体大小切换** - 支持12号和22号字体切换
### 4.3 触摸交互功能
1. **页面导航** - 支持多页面之间的触摸切换
2. **按钮响应** - 触摸按钮的点击响应功能
## 五、演示硬件环境
### 5.1 硬件清单
- Air780EPM 核心板 × 1
- AirLCD_1010 触摸配件板 × 1
- 母对母杜邦线 × 11,杜邦线太长的话,会出现 spi 通信不稳定的现象;
- TYPE-C 数据线 × 1
- Air780EPM 核心板和 AirLCD_1010 配件板的硬件接线方式为
- Air780EPM 核心板通过 TYPE-C USB 口供电(核心板正面开关拨到 ON 一端),此种供电方式下,VBAT 引脚为 3.3V,可以直接给 AirLCD_1010 配件板;
- 为了演示方便,所以 Air780EPM 核心板上电后直接通过 VBAT 引脚给 AirLCD_1010 配件板供电;
- 客户在设计实际项目时,一般来说,需要通过一个 GPIO 来控制 LDO 给配件板供电,这样可以灵活地控制配件板的供电,可以使项目的整体功耗降到最低;
### 5.2 接线配置
#### 5.2.1 显示屏接线
<table>
<tr>
<td>Air780EPM 核心板<br/></td><td>AirLCD_1010配件板<br/></td></tr>
<tr>
<td>53/LCD_CLK<br/></td><td>SCLK/CLK<br/></td></tr>
<tr>
<td>52/LCD_CS<br/></td><td>CS<br/></td></tr>
<tr>
<td>49/LCD_RST<br/></td><td>RES/RST<br/></td></tr>
<tr>
<td>50/LCD_SDA<br/></td><td>SDA/MOS<br/></td></tr>
<tr>
<td>51/LCD_RS<br/></td><td>DC/RS<br/></td></tr>
<tr>
<td>22/GPIO1<br/></td><td>BLK<br/></td></tr>
<tr>
<td>VBAT<br/></td><td>VCC<br/></td></tr>
<tr>
<td>67/I2C1_SCL<br/></td><td>SCL<br/></td></tr>
<tr>
<td>66/I2C1_SDA<br/></td><td>SDA<br/></td></tr>
<tr>
<td>20/GPIO24<br/></td><td></td><td>INT<br/></td></tr>
</table>
### 5.3 实际接线图
![](https://docs.openLuat.com/cdn/image/Air780EPM_AirLCD_1010_lcd演示接线图.png)
## 六、演示软件环境
### 6.1 开发工具
- [Luatools下载调试工具](https://docs.openluat.com/air780epm/luatos/common/download/) - 固件烧录和代码调试
### 6.2 内核固件
- [点击下载Air780EPM系列最新版本内核固件](https://docs.openluat.com/air780epm/luatos/firmware/version/)demo所使用的是LuatOS-SoC_V2018_Air780EPM 1号固件
### 6.3 字体文件
- 自定义字体文件:`customer_font_12.bin``customer_font_22.bin`(和lua脚本文件一起烧录,会自动放置在`/luadb/`目录下)
- 演示图片文件:`logo.jpg`(和lua脚本文件一起烧录,会自动放置在`/luadb/`目录下)
- [点击查看自定义字体生成和使用说明](https://docs.openluat.com/osapi/core/lcd/?h=lcd#_6)
## 七、演示核心步骤
### 7.1 硬件准备
1. 按照硬件接线表连接所有设备
2. 确保电源连接正确,通过TYPE-C USB口供电
3. 检查所有接线无误,避免短路
### 7.2 软件配置
`main.lua`中选择加载对应的驱动模块:
```lua
-- 加载显示屏驱动管理功能模块,有以下两种:
-- 1、使用lcd核心库驱动的lcd_drv.lua
-- 2、使用exlcd扩展库驱动的exlcd_drv.lua
-- 根据自己的需求,启用两者中的任何一种都可以
-- 也可以不启用任何一种,不使用显示屏功能
lcd_drv = require "lcd_drv"
-- lcd_drv = require "exlcd_drv"
-- 加载触摸面板驱动管理功能模块,有以下两种:
-- 1、使用tp核心库驱动的tp_drv.lua
-- 2、使用extp扩展库驱动的extp_drv.lua
-- 根据自己的需求,启用两者中的任何一种都可以
-- 也可以不启用任何一种,不使用触摸面板功能
tp_drv = require "tp_drv"
-- tp_drv = require "extp_drv"
-- 加载字库驱动管理功能模块
-- 使用外部自定义字体不需要require "customer_font_drv",可以参照customer_font_drv.lua内的使用说明进行创建和加载字体文件
-- 加载输入法驱动管理功能模块(正在开发中,后续补充)
-- 加载lcd核心库实现的用户界面功能模块
-- 实现多页面切换、触摸事件分发和界面渲染功能
-- 包含主页、lcd核心库功能演示页、GTFont演示页和自定义字体演示页
require "ui_main"
```
### 7.3 软件烧录
1. 使用Luatools烧录最新内核固件
2. 下载并烧录本项目所有脚本文件
3. 将字体文件和图片文件随脚本文件一起烧录到脚本分区或者单独烧录到文件系统
4. 设备自动重启后开始运行
### 7.4 功能测试
#### 7.4.1 主页面操作
1. 设备启动后显示主页面,包含三个功能按钮(摄像头预览、LCD演示、自定义字体)
2. 查看系统功能概览信息
3. 点击各功能按钮进入对应演示页面
#### 7.4.2 LCD演示页面
1. 查看基本图形绘制示例(点、线、矩形、圆形)
2. 查看图片显示区域(显示logo图片)
3. 查看二维码区域(合宙文档二维码)
4. 查看位图和字体示例
5. 点击"返回"按钮回到主页
#### 7.4.3 自定义字体页面
1. 查看外部字体文件显示效果
2. 查看多颜色文字显示(红色、绿色、蓝色)
3. 查看字体使用说明和接口信息
4. 点击"返回"按钮回到主页
#### 7.4.4 摄像头预览页面
1. 进入页面即开始摄像头预览,点击屏幕任意位置拍照
2. 拍照后进入照片显示页面,触摸保存/删除/返回按钮操作
3. 点击保存后文字变绿显示"已保存"
4. 点击删除后重新进入预览模式
### 7.5 预期效果
- **主页面**:正常显示,三个功能按钮响应灵敏
- **LCD演示页面**:图形绘制清晰,图片和二维码显示正常,颜色示例完整
- **自定义字体页面**:外部字体加载正确,多颜色文字显示正常
- **摄像头预览页面**:预览画面正常显示,拍照响应及时,照片保存/删除功能正常
- **触摸交互**:所有触摸操作响应及时准确,页面切换流畅
### 7.6 故障排除
1. **显示异常**:检查LCD接线是否正确,确认电源供电稳定
2. **触摸无响应**:检查I2C接线,确认触摸芯片初始化成功
3. **字体显示异常**:检查所需显示内容是否包含在字体内,确认字体文件路径正确
4. **图片无法显示**:确认图片文件已正确烧录到指定路径
5. **系统卡顿**:检查内存使用情况,适当调整刷新频率
@@ -0,0 +1,51 @@
--[[
@module extp_drv
@summary extp扩展库
@version 1.0
@date 2025.11.20
@author 访
@usage
1AirLCD_1010扩展触摸面板
2I2C通信接口和触摸参数
3UI系统处理
extp_drv.init()extp扩展库内
1sys.publish("BASE_TOUCH_EVENT", "SINGLE_TAP", x, y) --单击消息,x和y表示坐标
2sys.publish("BASE_TOUCH_EVENT", "LONG_PRESS", x, y) --长按消息,x和y表示坐标
extp扩展库中的extp.set_publish_enabled(msg_type, enabled)
1extp_drv.init()
]]
local extp = require "extp"
local extp_drv = {}
--[[
@usage
extp_drv模块后
]]
local function extp_drv_init()
-- 初始化I2C
local result = i2c.setup(1, i2c.SLOW)
if result==0 then
log.error("extp_drv.init i2c.setup error")
return false
end
result = extp.init({tp_model = "AirLCD_1010", i2c_id = 1, pin_rst = 0xFF, pin_int = 24, w = 320, h = 480})
log.info("extp.init", result)
end
extp_drv_init()
@@ -0,0 +1,57 @@
--[[
@module tp_drv
@summary tp核心库
@version 1.0
@date 2025.11.20
@author 访
@usage
1GT911触摸控制器
2I2C通信接口和触摸回调函数
3UI系统处理
1tp_drv.init()
]]
--[[
@local
@function tp_callback(tp_device, tp_data)
@userdata tp_device
@table tp_data
@return nil
]]
local function tp_callback(tp_device, tp_data)
if tp_data[1].event == tp.EVENT_DOWN then
-- log.info("tp_drv tp_callback", tp_data[1].event, tp_data[1].x, tp_data[1].y)
sys.publish("BASE_TOUCH_EVENT", tp_data[1].event, tp_data[1].x, tp_data[1].y)
end
end
--[[
]]
local function tp_drv_init()
-- 初始化I2C
local result = i2c.setup(1, i2c.SLOW)
if result==0 then
log.error("tp_drv.init i2c.setup error")
return false
end
result = tp.init("gt911", {port = 1, pin_rst = 0xFF, pin_int = 24, w = 320, h = 480}, tp_callback)
log.info("tp.init", result)
return result
end
tp_drv_init()
@@ -0,0 +1,221 @@
--[[
@module camera_preview_page
@summary /AirLCD_1010触摸版Air780EPM
@version 1.0
@date 2026.06.24
@author
]]
require "gc032a"
local excamera = require "excamera"
local camera_preview_page = {}
-- 硬件参数(Air780EPMi2c1, camera_pwr=2
local I2C_ID = 1
local CAM_PWR = 2
local CAM_PWDN = 5
-- 页面状态
local page_state = "idle" -- idle / preview / taking_photo / show_photo
local camera_opened = false
local photo_saved = false
local preview_task_running = false
-- 左上角返回(show_photo时有效)
local back_button = { x1 = 10, y1 = 10, x2 = 80, y2 = 50 }
-- 显示照片页按钮(保存和删除,下方两个按钮)
local photo_buttons = {
save = { x1 = 30, y1 = 380, x2 = 140, y2 = 450 },
delete = { x1 = 180, y1 = 380, x2 = 290, y2 = 450 }
}
-- Air780EPM 使用自定义点阵字体,不依赖系统内置中文字体
local function set_cn_font()
lcd.setFontFile("/luadb/customer_font_12.bin")
end
local function is_in_button(x, y, btn)
return x >= btn.x1 and x <= btn.x2 and y >= btn.y1 and y <= btn.y2
end
-- 启动预览→拍照→显图的完整流程task
local function start_camera_flow()
preview_task_running = true
sys.taskInit(function()
-- === 阶段1:纯画面预览 ===
local param_preview = {
id = "gc032a", i2c_id = I2C_ID, work_mode = 2,
save_path = "ZBUFF",
camera_pwr = CAM_PWR, camera_pwdn = CAM_PWDN, camera_light = nil
}
local ok = excamera.open(param_preview)
camera_opened = ok
if ok then
excamera.preview()
page_state = "preview"
log.info("camera_preview", "纯画面预览中,点击任意位置拍照")
-- 预览循环:等待触摸事件触发拍照
while page_state == "preview" and preview_task_running do
local has_touch, ev, tx, ty = sys.waitUntil("BASE_TOUCH_EVENT", 500)
if has_touch and ev then
log.info("camera_preview", "触摸触发拍照")
page_state = "taking_photo"
break
end
end
-- 关闭预览模式
excamera.close()
sys.wait(1000)
camera_opened = false
end
-- === 阶段2:拍照 ===
if page_state == "taking_photo" then
local param_capture = {
id = "gc032a", i2c_id = I2C_ID, work_mode = 0,
save_path = "/photo.jpg",
camera_pwr = CAM_PWR, camera_pwdn = CAM_PWDN, camera_light = nil
}
local ok = excamera.open(param_capture)
if ok then
local result, _ = excamera.photo()
excamera.close()
if result then
photo_saved = false
page_state = "show_photo"
log.info("camera_preview", "拍照成功")
else
log.error("camera_preview", "拍照失败,重新预览")
page_state = "idle"
end
else
log.error("camera_preview", "摄像头初始化失败")
page_state = "idle"
end
sys.publish("BASE_TOUCH_EVENT", nil, 0, 0)
end
preview_task_running = false
end)
end
function camera_preview_page.on_enter()
page_state = "idle"
photo_saved = false
start_camera_flow()
end
function camera_preview_page.is_preview_raw()
return page_state == "preview"
end
function camera_preview_page.draw()
set_cn_font()
if page_state == "taking_photo" then
lcd.clear()
lcd.setColor(0xFFFF, 0x0000)
lcd.drawStr(110, 230, "拍照中...", 0x0000)
elseif page_state == "show_photo" then
lcd.clear()
-- 全屏显示照片
lcd.showImage(0, 0, "/photo.jpg")
-- 返回按钮(灰色)
lcd.fill(back_button.x1, back_button.y1, back_button.x2, back_button.y2, 0xC618)
lcd.setColor(0x07E0, 0x0000)
lcd.drawStr(35, 35, "返回", 0x0000)
-- 底部操作区
lcd.drawLine(0, 350, 320, 350, 0x8410)
lcd.setColor(0xFFFF, 0x0000)
if photo_saved then
lcd.drawStr(130, 358, "已保存", 0x07E0)
end
-- 保存(绿色)
lcd.fill(photo_buttons.save.x1, photo_buttons.save.y1,
photo_buttons.save.x2, photo_buttons.save.y2, 0x07E0)
lcd.drawStr(65, 415, "保存", 0xFFFF)
-- 删除(红色)
lcd.fill(photo_buttons.delete.x1, photo_buttons.delete.y1,
photo_buttons.delete.x2, photo_buttons.delete.y2, 0xF800)
lcd.drawStr(215, 415, "删除", 0xFFFF)
elseif page_state == "preview" then
-- 预览状态下显示左上角返回按钮
lcd.fill(back_button.x1, back_button.y1, back_button.x2, back_button.y2, 0xC618)
lcd.setColor(0x07E0, 0x0000)
lcd.drawStr(35, 35, "返回", 0x0000)
end
end
function camera_preview_page.handle_touch(x, y, switch_page)
if page_state == "show_photo" then
-- 保存
if is_in_button(x, y, photo_buttons.save) then
photo_saved = true
return true
end
-- 删除 → 回到预览
if is_in_button(x, y, photo_buttons.delete) then
os.remove("/photo.jpg")
photo_saved = false
start_camera_flow()
-- 等待新 task 执行到 preview 状态,让 ui_main 进入预览分支(不刷新 LCD)
sys.wait(100)
return true
end
-- 左上角返回 → 回到主页
if x >= back_button.x1 and x <= back_button.x2 and
y >= back_button.y1 and y <= back_button.y2 then
switch_page("home")
return true
end
elseif page_state == "preview" then
-- 预览期间左上角返回主页
if x >= back_button.x1 and x <= back_button.x2 and
y >= back_button.y1 and y <= back_button.y2 then
preview_task_running = false
page_state = "idle"
switch_page("home")
return true
end
return false
elseif page_state == "taking_photo" then
return false
end
return false
end
function camera_preview_page.on_leave()
preview_task_running = false
page_state = "idle"
if camera_opened then
excamera.close()
camera_opened = false
end
i2c.setup(I2C_ID, i2c.SLOW)
end
return camera_preview_page
@@ -0,0 +1,137 @@
--[[
@module customer_font_page
@summary
@version 1.0
@date 2025.12.8
@author 访
@usage
11222
2绿
3使
4使
3
1customer_font_page.draw()
2customer_font_page.handle_touch()
3customer_font_page.on_leave()
]]
local customer_font_page = {}
-- 按钮区域定义
local back_button = { x1 = 10, y1 = 445, x2 = 50, y2 = 475 }
--[[
@api customer_font_page.draw()
@summary UI元素和字体内容
@return nil
]]
function customer_font_page.draw()
lcd.clear()
-- 设置默认颜色
lcd.setColor(0xFFFF, 0x0000)
-- 显示标题,使用22号自定义字体
lcd.setFontFile("/luadb/customer_font_22.bin")
-- 显示标题区域
lcd.fill(0, 0, 320, 40, 0x001F) -- 蓝色标题栏
lcd.drawStr(70, 30, "自定义点阵字体演示", 0xFFFF)
-- 显示演示内容,使用12号自定义字体
lcd.setFontFile("/luadb/customer_font_12.bin")
-- 字体支持说明区域(带背景色)
lcd.fill(15, 60, 305, 155, 0xF7DE) -- 浅灰色背景
lcd.drawRectangle(15, 60, 305, 155, 0x8410) -- 边框
-- 显示字体支持说明
lcd.drawStr(30, 80, "当前设备字体支持:", 0x0000)
lcd.drawStr(40, 105, "- 仅支持自定义字体", 0x0000)
lcd.drawStr(40, 120, "- 不支持内置12号中文", 0x0000)
lcd.drawStr(40, 135, "- 仅支持自定义点阵字体", 0x0000)
lcd.drawLine(20, 165, 300, 165, 0x8410)
-- 设置22号自定义字体文件显示示例文字
lcd.setFontFile("/luadb/customer_font_22.bin")
-- 示例文字区域(居中显示)
lcd.drawStr(100, 200, "上海合宙", 0xF800) -- 红色
lcd.drawStr(105, 230, "LuatOS", 0x07E0) -- 绿色
lcd.drawStr(95, 260, "演示demo", 0x001F) -- 蓝色
-- 恢复12号字体显示说明
lcd.setFontFile("/luadb/customer_font_12.bin")
lcd.setColor(0xFFFF, 0x0000)
-- 字体文件信息区域
lcd.drawLine(20, 280, 300, 280, 0x8410)
lcd.drawStr(20, 300, "字体文件信息:", 0x0000)
lcd.drawStr(30, 325, "- 12号字体: /luadb/customer_font_12.bin", 0x0000)
lcd.drawStr(30, 350, "- 22号字体: /luadb/customer_font_22.bin", 0x0000)
-- 显示使用接口
lcd.drawStr(20, 380, "使用接口:", 0x0000)
lcd.drawStr(35, 405, "- lcd.setFontFile(字体路径)", 0x0000)
lcd.drawStr(35, 425, "- lcd.drawStr(x, y, 文本, 颜色)", 0x0000)
-- 底部状态栏
-- 绘制返回按钮
lcd.setFontFile("/luadb/customer_font_12.bin")
lcd.fill(0, 440, 320, 480, 0x001F) -- 蓝色背景
lcd.fill(back_button.x1, back_button.y1, back_button.x2, back_button.y2, 0xC618)
lcd.drawStr(20, 465, "返回", 0x0000)
lcd.drawStr(180, 465, "触摸返回按钮回到主页", 0xFFFF)
end
--[[
@api customer_font_page.handle_touch(x, y, switch_page)
@summary
@number x X坐标
@number y Y坐标
@function switch_page
@return bool truefalse
]]
function customer_font_page.handle_touch(x, y, switch_page)
log.info("customer_font_page.handle_touch", "x:", x, "y:", y)
-- 检查返回按钮
if x >= back_button.x1 and x <= back_button.x2 and
y >= back_button.y1 and y <= back_button.y2 then
switch_page("home")
return true
end
return false
end
--[[
@api customer_font_page.on_enter()
@summary
@return nil
]]
function customer_font_page.on_enter()
-- 使用12号自定义字体
lcd.setFontFile("/luadb/customer_font_12.bin")
end
--[[
@api customer_font_page.on_leave()
@summary 12
@return nil
]]
function customer_font_page.on_leave()
-- 恢复使用12号自定义字体
lcd.setFontFile("/luadb/customer_font_12.bin")
log.info("customer_font_page", "已恢复12号自定义字体")
end
return customer_font_page
@@ -0,0 +1,115 @@
--[[
@module home_page
@summary
@version 1.1
@date 2026.06.24
@author 访
@usage
1
2
3LCD演示
4
使
3
1home_page.draw()UI
2home_page.handle_touch()
3home_page.on_enter()
]]
local home_page = {}
-- 按钮区域定义(竖排三等宽居中)
local buttons = {
{ name = "lcd", text = "LCD演示", x1 = 60, y1 = 100, x2 = 260, y2 = 180, color = 0x001F },
{ name = "camera_preview", text = "摄像头:点击拍照", x1 = 60, y1 = 200, x2 = 260, y2 = 280, color = 0xFCC0 },
{ name = "customer_font", text = "自定义字体", x1 = 60, y1 = 300, x2 = 260, y2 = 380, color = 0x07E0 }
}
local title = "合宙lcd演示系统"
local content1 = "本页面使用的是12号自定义点阵字体"
-- Air780EPM 使用自定义点阵字体显示中文
local function set_title_font()
lcd.setFontFile("/luadb/customer_font_22.bin")
end
local function set_body_font()
lcd.setFontFile("/luadb/customer_font_12.bin")
end
--[[
@api home_page.draw()
@summary UI元素
@return nil
]]
function home_page.draw()
lcd.clear()
lcd.setColor(0xFFFF, 0x0000)
-- 显示标题,使用22号自定义字体
set_title_font()
lcd.drawStr(106, 30, title, 0x0000)
-- 显示说明文字,使用12号自定义字体
set_body_font()
lcd.drawStr(46, 50, content1, 0x0000)
-- 绘制所有按钮
for i, btn in ipairs(buttons) do
local color = btn.color
lcd.fill(btn.x1, btn.y1, btn.x2, btn.y2, color)
-- 绘制按钮文字
if btn.name == "lcd" then
lcd.drawStr(130, 138, "LCD演示", 0xFFFF)
elseif btn.name == "camera_preview" then
lcd.drawStr(120, 238, "摄像头:点击拍照", 0xFFFF)
elseif btn.name == "customer_font" then
lcd.drawStr(125, 338, "自定义字体", 0xFFFF)
end
end
end
--[[
@api home_page.handle_touch(x, y, switch_page)
@summary
@number x X坐标
@number y Y坐标
@function switch_page
@return bool truefalse
]]
function home_page.handle_touch(x, y, switch_page)
log.info("home_page.handle_touch", "x:", x, "y:", y)
-- 检查触摸是否在按钮区域内
for i, btn in ipairs(buttons) do
if x >= btn.x1 and x <= btn.x2 and
y >= btn.y1 and y <= btn.y2 then
switch_page(btn.name)
return true
end
end
return false
end
--[[
@api home_page.on_enter()
@summary
@return nil
]]
function home_page.on_enter()
end
function home_page.on_leave()
end
return home_page
@@ -0,0 +1,174 @@
--[[
@module lcd_page
@summary LCD图形绘制演示模块
@version 1.0
@date 2025.11.20
@author 访
@usage
LCD图形绘制演示功能模块
1LCD核心库的基本图形绘制功能
2线
3
4
1lcd_page.draw()LCD演示页面
2lcd_page.handle_touch()LCD页面触摸事件
]]
local lcd_page = {}
-- 按钮区域定义
local back_button = { x1 = 10, y1 = 445, x2 = 50, y2 = 475 }
--[[
LCD演示页面
@api lcd_page.draw()
@summary LCD演示页面的所有图形和UI元素
@return nil
]]
function lcd_page.draw()
lcd.clear()
-- 显示标题,使用22号自定义字体
lcd.setFontFile("/luadb/customer_font_22.bin")
-- 设置默认颜色
lcd.setColor(0xFFFF, 0x0000)
-- 显示标题区域
lcd.fill(0, 0, 320, 40, 0x001F) -- 蓝色标题栏
lcd.drawStr(80, 30, "LCD核心库演示", 0xFFFF)
lcd.drawLine(20, 55, 300, 55, 0x8410)
-- 显示演示内容,使用12号自定义字体
-- === 第一区域:基本图形 ===
lcd.setFontFile("/luadb/customer_font_12.bin")
lcd.drawStr(20, 75, "基本图形绘制:", 0x0000)
-- 绘制点
lcd.drawStr(30, 98, "点:", 0x0000)
lcd.drawPoint(55, 98, 0xFCC0)
lcd.drawPoint(65, 98, 0x07E0)
lcd.drawPoint(75, 98, 0x001F)
-- 绘制线
lcd.drawStr(30, 125, "线:", 0x0000)
lcd.drawLine(55, 113, 115, 113, 0xFCC0)
lcd.drawLine(55, 118, 115, 123, 0x07E0)
lcd.drawLine(55, 123, 115, 118, 0x001F)
-- 绘制矩形(预留右侧空间)
lcd.drawStr(30, 160, "矩形:", 0x0000)
lcd.drawRectangle(65, 138, 105, 163, 0x7296)
lcd.fill(120, 138, 160, 163, 0x07E0)
-- 绘制圆(预留右侧空间)
lcd.drawStr(30, 200, "圆形:", 0x0000)
lcd.drawCircle(90, 193, 15, 0x001F)
lcd.drawCircle(130, 193, 15, 0xFCC0)
lcd.drawLine(170, 70, 170, 300, 0x8410) -- 垂直分隔线
-- === 第二区域:图片和二维码 ===
lcd.drawStr(180, 75, "图片/二维码:", 0x0000)
-- 图片显示区域 (80x80)
lcd.drawStr(180, 100, "LOGO", 0x0000)
lcd.drawRectangle(200, 110, 290, 200, 0x0000)
lcd.showImage(205, 115, "/luadb/logo.jpg")
-- 二维码区域 (80x80)
lcd.drawStr(180, 215, "二维码", 0x0000)
lcd.drawRectangle(200, 225, 290, 308, 0x0000)
lcd.drawQrcode(205, 226, "https://docs.openluat.com/air780epm/", 80)
lcd.drawLine(20, 325, 300, 325, 0x8410)
-- === 第三区域:位图 ===
lcd.drawLine(20, 235, 160, 235, 0x8410)
-- 位图区域
lcd.drawStr(20, 255, "位图示例:", 0x0000)
-- 绘制位图
local x_start = 30
local y_start = 265
lcd.drawXbm(x_start, y_start, 16, 16, string.char(
0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x3F, 0x80, 0x00,
0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0xFE, 0x7F, 0x00, 0x00))
lcd.drawXbm(x_start + 20, y_start, 16, 16, string.char(
0x00, 0x00, 0x80, 0x00, 0xC4, 0x7F, 0x28, 0x00, 0x10, 0x00, 0xD0, 0x3F, 0x42, 0x20, 0x44, 0x22,
0x40, 0x24, 0xF0, 0xFF, 0x24, 0x20, 0x24, 0x22, 0x24, 0x20, 0xE2, 0x7F, 0x02, 0x20, 0x02, 0x1E))
lcd.drawXbm(x_start, y_start + 20, 16, 16, string.char(
0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0x40, 0x02, 0x20, 0x04, 0x18, 0x18, 0xF4, 0x6F, 0x02, 0x00,
0x00, 0x00, 0xF8, 0x1F, 0x08, 0x10, 0x08, 0x10, 0x08, 0x10, 0x08, 0x10, 0xF8, 0x1F, 0x08, 0x10))
lcd.drawXbm(x_start + 20, y_start + 20, 16, 16, string.char(
0x00, 0x00, 0x80, 0x00, 0x00, 0x01, 0xFE, 0x7F, 0x02, 0x40, 0x02, 0x40, 0x00, 0x01, 0xFC, 0x3F,
0x04, 0x21, 0x04, 0x21, 0xFC, 0x3F, 0x04, 0x21, 0x04, 0x21, 0x04, 0x21, 0xFC, 0x3F, 0x04, 0x20))
-- === 第四区域:字体 ===
lcd.drawStr(20, 345, "字体示例:", 0x0000)
-- 12号字体
lcd.setFontFile("/luadb/customer_font_12.bin")
lcd.drawStr(20, 368, "12px字体示例", 0x0000)
-- 22号字体
lcd.setFontFile("/luadb/customer_font_22.bin")
lcd.drawStr(20, 398, "22px字体示例", 0x0000)
-- 底部状态栏
-- 绘制返回按钮
lcd.setFontFile("/luadb/customer_font_12.bin")
lcd.fill(0, 440, 320, 480, 0x001F) -- 蓝色背景
lcd.fill(back_button.x1, back_button.y1, back_button.x2, back_button.y2, 0xC618)
lcd.drawStr(20, 465, "返回", 0x0000)
lcd.drawStr(180, 465, "触摸返回按钮回到主页", 0xFFFF)
end
--[[
LCD页面触摸事件
@api lcd_page.handle_touch(x, y, switch_page)
@number x X坐标0-319
@number y Y坐标0-479
@function switch_page
@return boolean truefalse
]]
function lcd_page.handle_touch(x, y, switch_page)
-- 检查返回按钮
if x >= back_button.x1 and x <= back_button.x2 and
y >= back_button.y1 and y <= back_button.y2 then
switch_page("home")
return true
end
return false
end
--[[
@api lcd_page.on_enter()
@summary
@return nil
]]
function lcd_page.on_enter()
-- 使用12号自定义字体
lcd.setFontFile("/luadb/customer_font_12.bin")
end
--[[
@api lcd_page.on_leave()
@summary
@return nil
]]
function lcd_page.on_leave()
-- 恢复使用12号自定义字体
lcd.setFontFile("/luadb/customer_font_12.bin")
end
return lcd_page
@@ -0,0 +1,150 @@
--[[
@module ui_main
@summary
@version 1.0
@date 2025.11.20
@author 访
@usage
UI控制模块
1LCD显示和触摸面板驱动
2
3
4
1ui_main()UI任务入口函数
2switch_page()
3handle_touch_event()
]]
-- 加载子页面
local home_page = require "home_page"
local lcd_page = require "lcd_page"
local customer_font_page = require "customer_font_page"
local camera_preview_page = require "camera_preview_page"
-- 当前页面状态
local current_page = "home"
local last_page = ""
frame_time = 20*1000
--[[
@api switch_page(new_page)
@string new_page
@valid_values "home", "lcd", "customer_font", "camera_preview"
@return nil
@usage
-- 切换到LCD演示页面
switch_page("lcd")
]]
local function switch_page(new_page)
-- 调用旧页面的退出函数(如果存在)
if current_page == "home" and home_page.on_leave then
home_page.on_leave()
elseif current_page == "lcd" and lcd_page.on_leave then
lcd_page.on_leave()
elseif current_page == "customer_font" and customer_font_page.on_leave then
customer_font_page.on_leave()
elseif current_page == "camera_preview" and camera_preview_page.on_leave then
camera_preview_page.on_leave()
end
last_page = current_page
current_page = new_page
-- 调用新页面的进入函数(如果存在)
if new_page == "home" and home_page.on_enter then
home_page.on_enter()
elseif new_page == "lcd" and lcd_page.on_enter then
lcd_page.on_enter()
elseif new_page == "customer_font" and customer_font_page.on_enter then
customer_font_page.on_enter()
elseif new_page == "camera_preview" and camera_preview_page.on_enter then
camera_preview_page.on_enter()
end
log.info("ui_main", "切换到页面:", current_page)
end
--[[
@api handle_touch_event(event, x, y)
@number event
@valid_values tp.EVENT_DOWN, tp.EVENT_MOVE, tp.EVENT_UP
@number x X坐标0-319
@number y Y坐标0-479
@return boolean truefalse
]]
-- 处理触摸事件
local function handle_touch_event(event, x, y)
log.info("触摸事件", "event:", event, "x:", x, "y:", y)
if event then -- 只在抬起时处理点击
if current_page == "home" then
return home_page.handle_touch(x, y, switch_page)
elseif current_page == "lcd" then
return lcd_page.handle_touch(x, y, switch_page)
elseif current_page == "customer_font" then
return customer_font_page.handle_touch(x, y, switch_page)
elseif current_page == "camera_preview" then
return camera_preview_page.handle_touch(x, y, switch_page)
end
end
return false
end
--[[
@api ui_main()
@summary UI主循环
@return nil
]]
local function ui_main()
-- 默认使用12号自定义字体
lcd.setFontFile("/luadb/customer_font_12.bin")
-- 调用主页的进入函数
if home_page.on_enter then
home_page.on_enter()
end
while true do
-- 纯画面预览模式:硬件直接输出画面,跳过draw/flush
if current_page == "camera_preview" and camera_preview_page.is_preview_raw() then
-- 等待触摸事件500ms,不刷新LCD
local result, event, x, y = sys.waitUntil("BASE_TOUCH_EVENT", 500)
if result then
handle_touch_event(event, x, y)
end
else
-- 其他页面:正常绘制和刷新
if current_page == "home" then
home_page.draw()
elseif current_page == "lcd" then
lcd_page.draw()
elseif current_page == "customer_font" then
customer_font_page.draw()
elseif current_page == "camera_preview" then
camera_preview_page.draw()
end
lcd.flush()
-- 等待触摸事件
local result, event, x, y = sys.waitUntil("BASE_TOUCH_EVENT",frame_time)
if result then
handle_touch_event(event, x, y)
end
end
end
end
sys.taskInit(ui_main)
@@ -0,0 +1,188 @@
--[[
@module AirRC522_1000
@summary AirRC522_1000测试功能模块
@version 1.0
@date 2025.07.23
@author
@usage
demo演示的功能为使Air780EPM核心板通过SPI实现对RC522的操作
Air780EPM核心板为例, 线:
Air780EPM核心板 AirRC522_1000
GND() GND
3V3 3.3V
83/SPI0CS SDA
86/SPI0CLK SCK
85/SPI0MOSI MOSI
84/SPI0MISO MISO
19/GPIO22(IO) RST
1. spi,退
2. RC522模块,退
3.
4.
5.
]]
rc522 = require "rc522"
-- 硬件配置参数(Air780EPM适配)
local RC522_CONFIG = {
spi_id = 0, -- SPI通道
cs_pin = 8, -- 片选引脚
rst_pin = 22, -- 复位引脚
spi_baud = 2 * 1000 * 1000, -- SPI波特率
}
-- 全局变量(模块内可见)
local spi_dev = nil -- SPI设备对象
-- 1. 初始化SPI接口
local function init_spi()
log.info("RC522", "初始化SPI接口")
-- 配置SPI参数:模式0,8位数据,高位在前
spi_dev = spi.setup(
RC522_CONFIG.spi_id,
RC522_CONFIG.cs_pin,
0, -- 极性0
0, -- 相位0
8, -- 数据位
RC522_CONFIG.spi_baud,
spi.MSB, -- 高位优先
spi.master, --主模式
spi.half --半双工
)
if not spi_dev then
log.error("RC522", "SPI初始化失败")
return false
end
log.info("RC522", "SPI初始化成功")
return true
end
-- 2. 初始化RC522模块
local function init_rc522()
log.info("RC522", "初始化RC522传感器")
-- 初始化RC522硬件(SPI ID、CS引脚、RST引脚)
local init_ok = rc522.init(
RC522_CONFIG.spi_id,
RC522_CONFIG.cs_pin,
RC522_CONFIG.rst_pin
)
if not init_ok then
log.error("RC522", "传感器初始化失败")
return false
end
log.info("RC522", "传感器初始化成功")
return true
end
-- 3. 写数据
local function write_ic_card(block, data)
-- 步骤1:验证数据长度
if #data > 16 then
log.error("RC522", "数据过长(最大16字节)")
return false
end
-- 步骤2:写入数据块
local write_ok = rc522.write_datablock(block, data)
if not write_ok then
log.error("RC522", "数据写入失败,块号:", block)
return false
end
log.info("RC522", "数据写入成功,块号:", block, "写入数据是:", string.char(table.unpack(data)):toHex())
return true
end
-- 4. 检测并读取IC卡数据
local function read_ic_card()
-- 检测是否有卡片靠近
local status, array_id = rc522.request(rc522.reqall)
if not status then
log.info("RC522", "未检测到卡片")
return false
end
log.info("RC522", "检测到卡片,类型:", array_id:toHex())
-- 防冲突检测,获取卡片唯一ID
local status, card_uid = rc522.anticoll(array_id)
if not status then
log.error("RC522", "防冲突检测失败")
return false
end
log.info("RC522", "卡片UID", card_uid:toHex())
-- -- 选择卡片,激活卡片进行后续操作
local select_ok = rc522.select(card_uid)
if not select_ok then
log.error("RC522", "卡片选择失败")
return false
end
-- 写数据测试
local TEST_BLOCK = 9
-- 待写入的数据
local TEST_DATA = { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }
if write_ic_card(TEST_BLOCK, TEST_DATA) then
-- 读数据验证
local read_ok, read_data = rc522.read_datablock(TEST_BLOCK)
if read_ok and read_data == string.char(table.unpack(TEST_DATA)) then
log.info("RC522", "写入验证成功,数据是:", read_data:toHex())
else
log.warn("RC522", "写入验证失败,实际读取的数据是:", read_data:toHex())
end
end
-- 读取卡片数据块(0-63块)
log.info("RC522", "开始读取卡片数据...")
for block = 0, 63 do
local read_ok, data = rc522.read_datablock(block)
if read_ok and data then
log.info(string.format("块[%d]", block), data:toHex())
else
log.warn(string.format("块[%d]读取失败", block))
end
--每读取完一个块等待20ms(时间可按需修改)
sys.wait(20)
end
-- 停止卡片操作
rc522.halt()
return true
end
-- 5. 关闭SPI设备,成功返回0
local function spi_close_func()
log.info("关闭spi", spi.close(RC522_CONFIG.spi_id))
end
-- 6. 主测试任务
local function rc522_main_test()
-- 初始化硬件
if not init_spi() then
spi_close_func()
return
end
if not init_rc522() then
spi_close_func()
return
end
-- 循环检测卡片
log.info("RC522", "开始检测卡片")
while true do
read_ic_card()
-- 循环检测间隔:2s
sys.wait(2000)
end
end
-- 启动主任务
sys.taskInit(rc522_main_test)
@@ -0,0 +1,65 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口
@version 001.999.000
@date 2025.9.05
@author
@usage
demo是使用Air780EPM核心板挂载合宙AirRC522_1000配件板rc522的功能使用方法
1.rc522.lua rc522扩展库文件AirRC522_1000.lua功能模块中require使用
2.AirRC522_1000.luamain.lua中加载运行AirRC522_1000.lua
]]
--[[
PROJECT和VERSION变量Luatools工具会用到这两个变量
PROJECTascii string类型
便使,
VERSIONascii string类型
使iot.openluat.com进行远程升级"XXX.YYY.ZZZ"
XYZ各表示1位数字X表示的数字可以相同Y和三个Z表示的数字也是可以相同
YYY这三位数字必须存在999
使iot.openluat.com进行远程升级
]]
PROJECT = "Air780EPM_rc522_DEMO"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 加载AirRC522_1000功能模块
require "AirRC522_1000"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,646 @@
--[[
@module rc522
@summary rc522
@version 1.0
@date 2022.06.14
@author Dozingfiretruck
@usage
--注意:因使用了sys.wait()所有api需要在task中使用
-- 用法实例
local rc522 = require "rc522"
local function rc522_test()
spi_rc522 = spi.setup(0,nil,0,0,8,10*1000*1000,spi.MSB, spi.master,spi.half)
rc522.init(0,12,16)
wdata = {0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}
while 1 do
rc522.write_datablock(8,wdata)
for i=0,63 do
local a,b = rc522.read_datablock(i)
if a then
log.info("read",i,b:toHex())
end
end
sys.wait(500)
end
end)
sys.taskInit(rc522_test)
]]
local rc522 = {}
local rc522_spi, rc522_rst, rc522_irq, rc522_cs
---器件所用地址
local rc522_idle = 0x00 --取消当前命令
local rc522_authent = 0x0E --验证密钥
local rc522_receive = 0x08 --接收数据
local rc522_transmit = 0x04 --发送数据
local rc522_transceive = 0x0C --发送并接收数据
local rc522_resetphase = 0x0F --复位
local rc522_calccrc = 0x03 --CRC计算
--Mifare_One卡片命令字
rc522.reqidl = 0x26 --寻天线区内未进入休眠状态
rc522.reqall = 0x52 --寻天线区内全部卡
local rc522_anticoll1 = 0x93 --防冲撞
local rc522_anticoll2 = 0x95 --防冲撞
local rc522_authent1a = 0x60 --验证A密钥
local rc522_authent1b = 0x61 --验证B密钥
local rc522_read = 0x30 --读块
local rc522_write = 0xA0 --写块
local rc522_decrement = 0xC0 --扣款
local rc522_increment = 0xC1 --充值
local rc522_restore = 0xC2 --调块数据到缓冲区
local rc522_transfer = 0xB0 --保存缓冲区中数据
local rc522_halt = 0x50 --休眠
--MF522寄存器定义
-- PAGE 0
local rc522_rfu00 = 0x00 --保留为将来之用
local rc522_com_mand = 0x01 --启动和停止命令的执行
local rc522_com_ie = 0x02 --中断请求传递的使能和禁能控制位
local rc522_divl_en = 0x03 --中断请求传递的使能和禁能控制位
local rc522_com_irq = 0x04 --包含中断请求标志
local rc522_div_irq = 0x05 --包含中断请求标志
local rc522_error = 0x06 --错误标志,指示执行的上个命令的错误状态
local rc522_status1 = 0x07 --包含通信的状态标志
local rc522_status2 = 0x08 --包含接收器和发送器的状态标志
local rc522_fifo_data = 0x09 --64 字节 FIFO 缓冲区的输入和输出
local rc522_fifo_level = 0x0A --指示 FIFO 中存储的字节数
local rc522_water_level = 0x0B --定义 FIFO 下溢和上溢报警的 FIFO 深度
local rc522_control = 0x0C --不同的控制寄存器
local rc522_bit_framing = 0x0D --面向位的帧的调节
local rc522_coll = 0x0E --RF 接口上检测到的第一个位冲突的位的位置
local rc522_rfu0f = 0x0F --保留为将来之用
-- PAGE 1
local RFU10 = 0x10 --保留为将来之用
local rc522_mode = 0x11 --定义发送和接收的常用模式
local rc522_tx_mode = 0x12 --定义发送过程的数据传输速率
local rc522_rx_mode = 0x13 --定义接收过程中的数据传输速率
local rc522_tx_control = 0x14 --控制天线驱动器管脚 TX1 和 TX2 的逻辑特性
local rc522_tx_ayto = 0x15 --控制天线驱动器的设置
local rc522_tx_sel = 0x16 --选择天线驱动器的内部源
local rc522_rx_sel = 0x17 --选择内部的接收器设置
local rc522_rx_threshold = 0x18 --选择位译码器的阈值
local rc522_demod = 0x19 --定义解调器的设置
local rc522_rfu1a = 0x1A --保留为将来之用
local rc522_rfu1b = 0x1B --保留为将来之用
local rc522_mifare = 0x1C --控制 ISO 14443/MIFARE 模式中 106kbit/s 的通信
local rc522_rfu1d = 0x1D --保留为将来之用
local rc522_rfu1e = 0x1E --保留为将来之用
local rc522_serial_speed = 0x1F --选择串行 UART 接口的速率
-- PAGE 2
local rc522_rfu20 = 0x20 --保留为将来之用
local rc522_crcresult_m = 0x21 --显示 CRC 计算的实际 MSB 值
local rc522_crcresult_l = 0x22 --显示 CRC 计算的实际 LSB 值
local rc522_rfu23 = 0x23 --保留为将来之用
local rc522_mod_width = 0x24 --控制 ModWidth 的设置
local rc522_rfu25 = 0x25 --保留为将来之用
local rc522_rfcfg = 0x26 --配置接收器增益
local rc522_gsn = 0x27 --选择天线驱动器管脚 TX1 和 TX2 的调制电导
local rc522_cwgscfg = 0x28 --选择天线驱动器管脚 TX1 和 TX2 的调制电导
local rc522_modgscfg = 0x29 --选择天线驱动器管脚 TX1 和 TX2 的调制电导
local rc522_tmode = 0x2A --定义内部定时器的设置
local rc522_tprescaler = 0x2B --定义内部定时器的设置
local rc522_tpreload_h = 0x2C --描述 16 位长的定时器重装值
local rc522_tpreload_l = 0x2D --描述 16 位长的定时器重装值
local rc522_tcounter_value_h = 0x2E --描述 16 位长的定时器重装值
local rc522_tcounter_value_l = 0x2F --描述 16 位长的定时器重装值
-- PAGE 3
local rc522_rfu30 = 0x30 --保留为将来之用
local rc522_testsel1 = 0x31 --常用测试信号的配置
local rc522_testsel2 = 0x32 --常用测试信号的配置和 PRBS 控制
local rc522_testpin_en = 0x33 --D1-D7 输出驱动器的使能管脚(注:仅用于串行接口)
local rc522_testpin_value = 0x34 --定义 D1-D7 用作 I/O 总线时的值
local rc522_testbus = 0x35 --显示内部测试总线的状态
local rc522_autotest = 0x36 --控制数字自测试
local rc522_version = 0x37 --显示版本
local rc522_analogtest = 0x38 --控制管脚 AUX1 和 AUX2
local rc522_testadc1 = 0x39 --定义 TestDAC1 的测试值
local rc522_testadc2 = 0x3A --定义 TestDAC2 的测试值
local rc522_testadc = 0x3B --显示 ADC I 和 Q 通道的实际值
local rc522_rfu3c = 0x3C --保留用于产品测试
local rc522_rfu3d = 0x3D --保留用于产品测试
local rc522_rfu3e = 0x3E --保留用于产品测试
local rc522_rfu3f = 0x3F --保留用于产品测试
local Key_A = string.char(0xff, 0xff, 0xff, 0xff, 0xff, 0xff)
local Key_B = string.char(0xff, 0xff, 0xff, 0xff, 0xff, 0xff)
--[[
rc522寄存器
@api rc522.set_bit_mask(address, value)
@number address
@number value
@usage
write_rawrc(rc522_bit_framing,0x07)
]]
local function write_rawrc(address, value)
rc522_cs(0)
local data = string.char((address << 1) & 0x7E) .. string.char(value)
spi.send(rc522_spi, data)
-- rc522_spi:send(data)
rc522_cs(1)
end
--[[
rc522寄存器
@api rc522.read_rawrc(address)
@number address
@return number
@usage
local n = read_rawrc(rc522_com_irq)
]]
local function read_rawrc(address)
rc522_cs(0)
local data = string.char(((address << 1) & 0x7E)|0x80)
spi.send(rc522_spi, data)
local val = spi.recv(rc522_spi, 1)
-- rc522_spi:send(data)
-- local val = rc522_spi:recv(1)
rc522_cs(1)
return string.byte(val)
end
--[[
rc522寄存器置位
@api rc522.set_bit_mask(address, mask)
@number address
@number mask
@usage
rc522.set_bit_mask (rc522_fifo_level, 0x80)
]]
function rc522.set_bit_mask(address, mask)
local current = read_rawrc(address)
write_rawrc(address, bit.bor(current, mask))
end
--[[
rc522寄存器清位
@api rc522.clear_bit_mask(address, mask)
@number address
@number mask
@usage
rc522.clear_bit_mask(rc522_com_irq, 0x80 )
]]
function rc522.clear_bit_mask(address, mask)
local current = read_rawrc(address)
write_rawrc(address, bit.band(current, bit.bnot(mask)))
end
--[[
@api rc522.command(command,data)
@number command
@number data
@return status data len ,,
@usage
rc522.version()
]]
function rc522.command(command, data)
local out_data = {}
local len = 0
local status = false
local Irq_en = 0x00
local waitfor = 0x00
local last_bits, n, l
if command == rc522_authent then
Irq_en = 0x12
waitfor = 0x10
elseif command == rc522_transceive then
Irq_en = 0x77
waitfor = 0x30
end
write_rawrc(0x02, bit.bor(Irq_en, 0x80))
rc522.clear_bit_mask(rc522_com_irq, 0x80)
write_rawrc(rc522_com_mand, rc522_idle)
rc522.set_bit_mask(rc522_fifo_level, 0x80)
for i = 1, #data do
write_rawrc(rc522_fifo_data, data[i])
end
write_rawrc(rc522_com_mand, command)
if (command == rc522_transceive) then
rc522.set_bit_mask(rc522_bit_framing, 0x80)
end
l = 12
while true do
sys.wait(1)
n = read_rawrc(rc522_com_irq)
l = l - 1
if not ((l ~= 0) and (bit.band(n, 0x01) == 0) and (bit.band(n, waitfor) == 0)) then
break
end
end
rc522.clear_bit_mask(rc522_bit_framing, 0x80)
if (l ~= 0) then
if bit.band(read_rawrc(rc522_error), 0x1B) == 0x00 then
status = true
if bit.band(n, Irq_en, 0x01) ~= 0 then
status = false
end
if (command == rc522_transceive) then
n = read_rawrc(rc522_fifo_level)
last_bits = bit.band(read_rawrc(rc522_control), 0x07)
if last_bits ~= 0 then
len = (n - 1) * 8 + last_bits
else
len = n * 8
end
if n == 0 then
n = 1
end
for i = 1, n do
out_data[i] = read_rawrc(rc522_fifo_data)
end
end
end
else
status = false
end
rc522.set_bit_mask(rc522_control, 0x80)
write_rawrc(rc522_com_mand, rc522_idle)
return status, out_data, len
end
--[[
@api rc522.anticoll(id)
@string id 4
@return status uid ,uid
@usage
local status,uid = rc522.anticoll(array_id)
]]
function rc522.anticoll(id)
local check = 0
local uid
rc522.clear_bit_mask(rc522_status2, 0x08)
write_rawrc(rc522_bit_framing, 0x00)
rc522.clear_bit_mask(rc522_coll, 0x80)
local status, back_data = rc522.command(rc522_transceive, { 0x93, 0x20 })
if back_data and #back_data >= 5 then
for i = 1, 4 do
check = bit.bxor(check, back_data[i])
end
if check ~= back_data[5] then
status = false
end
uid = string.char(table.unpack(back_data, 1, 4))
end
rc522.clear_bit_mask(rc522_coll, 0x80)
return status, uid
end
--[[
crc计算
@api calculate_crc(data)
@table data
@return table crc值
@usage
local crc = calculate_crc(buff)
]]
local function calculate_crc(data)
local ret_data = {}
rc522.clear_bit_mask(rc522_div_irq, 0x04)
write_rawrc(rc522_com_mand, rc522_idle)
rc522.set_bit_mask(rc522_fifo_level, 0x80)
for i = 1, #data do
write_rawrc(rc522_fifo_data, data[i])
end
write_rawrc(rc522_com_mand, rc522_calccrc)
local i = 0xFF
while true do
local n = read_rawrc(0x05)
i = i - 1
if not ((i ~= 0) and not (n & 0x04)) then
break
end
end
ret_data[1] = read_rawrc(0x22)
ret_data[2] = read_rawrc(0x21)
return ret_data
end
--[[
@api authstate(mode, addr,key,uid )
@number mode
@number addr
@string key
@string uid uid
@return bool
@usage
status = authstate(rc522_authent1b, addr,Key_B,uid )
]]
local function authstate(mode, addr, key, uid)
local buff = {}
buff[1] = mode
buff[2] = addr
for i = 1, 6 do
buff[i + 2] = key:byte(i)
end
for i = 1, 4 do
buff[i + 8] = uid:byte(i)
end
local status, back_data = rc522.command(rc522_authent, buff)
if status == true and (read_rawrc(rc522_status2) & 0x08) ~= 0 then
return true
end
return false
end
--[[
M1卡一块
@api rc522.write(addr, data)
@number addr (0-63)M1卡总共有16个扇区(3+1),64
@table data
@return bool
@usage
rc522.write(addr, data)
]]
function rc522.write(addr, data)
local buff = {}
buff[1] = rc522_write
buff[2] = addr
local crc = calculate_crc(buff)
buff[3] = crc[1]
buff[4] = crc[2]
local status, back_data, back_bits = rc522.command(rc522_transceive, buff)
if status == true and back_bits == 4 and (back_data[1] & 0x0F) == 0x0A then
local buf_w = {}
for i = 0, 16 do
table.insert(buf_w, data[i])
end
local crc = calculate_crc(buf_w)
table.insert(buf_w, crc[1])
table.insert(buf_w, crc[2])
status, back_data, back_bits = rc522.command(rc522_transceive, buf_w)
if status == true and back_bits == 4 and (back_data[1] & 0x0F) == 0x0A then
return status
end
end
return status
end
--[[
M1卡一块
@api rc522.read(addr)
@number addr (0-63)M1卡总共有16个扇区(3+1),64
@return bool,string
@usage
rc522.read(addr, data)
]]
local function read(addr)
local buff = {}
buff[1] = rc522_read
buff[2] = addr
local crc = calculate_crc(buff)
buff[3] = crc[1]
buff[4] = crc[2]
local status, back_data, back_bits = rc522.command(rc522_transceive, buff)
if status == true and back_bits == 0x90 then
local data = string.char(table.unpack(back_data, 1, 16))
return status, data
end
return false
end
--[[
rc522
@api rc522.version()
@return number
@usage
rc522.version()
]]
function rc522.version()
return read_rawrc(rc522_version)
end
--[[
rc522
@api rc522.halt()
@return bool
@usage
rc522.halt()
]]
function rc522.halt()
local buff = {}
buff[1] = rc522_halt
buff[2] = 0
local crc = calculate_crc(buff)
buff[3] = crc[1]
buff[4] = crc[2]
local status = rc522.command(rc522_transceive, buff)
return status
end
--[[
rc522
@api rc522.reset()
@usage
rc522.reset()
]]
function rc522.reset()
rc522_rst(1)
rc522_rst(0)
rc522_rst(1)
write_rawrc(rc522_com_mand, 0x0f)
write_rawrc(rc522_mode, 0x3D)
write_rawrc(rc522_tpreload_l, 30)
write_rawrc(rc522_tpreload_h, 0)
write_rawrc(rc522_tmode, 0x8D)
write_rawrc(rc522_tprescaler, 0x3E)
write_rawrc(rc522_tx_ayto, 0x40)
end
--[[
线
@api rc522.antenna_on()
@usage
rc522.antenna_on()
]]
function rc522.antenna_on()
local uc = read_rawrc(rc522_tx_control)
if ((uc & 0x03) == 0) then
rc522.set_bit_mask(rc522_tx_control, 0x03)
end
end
--[[
线
@api rc522.antenna_on()
@usage
rc522.antenna_on()
]]
function rc522.antenna_off()
rc522.clear_bit_mask(rc522_tx_control, 0x03)
end
--[[
rc522工作方式为ISO14443_A
@api rc522_config_isotype()
@usage
rc522_config_isotype()
]]
local function rc522_config_isotype()
rc522.clear_bit_mask(rc522_status2, 0x08)
write_rawrc(rc522_mode, 0x3D)
write_rawrc(rc522_rx_sel, 0x86)
write_rawrc(rc522_rfcfg, 0x7F)
write_rawrc(rc522_tpreload_l, 30)
write_rawrc(rc522_tpreload_h, 0)
write_rawrc(rc522_tmode, 0x8D)
write_rawrc(rc522_tprescaler, 0x3E)
rc522.antenna_on()
end
--[[
rc522
@api rc522.request(req_code)
@number req_code rc522.reqidl 线 rc522.reqall 线
@return bool tagtype
@usage
status,array_id = rc522.request(rc522.reqall)
]]
function rc522.request(req_code)
rc522.clear_bit_mask(rc522_status2, 0x08)
write_rawrc(rc522_bit_framing, 0x07)
rc522.set_bit_mask(rc522_tx_control, 0x03)
local tagtype
local status, back_data, back_bits = rc522.command(rc522_transceive, { req_code })
if status == true and back_bits == 0x10 then
tagtype = string.char(table.unpack(back_data, 1, 2))
return status, tagtype
end
return false
end
--[[
@api rc522.select(id)
@number id 4
@return bool
@usage
status = rc522.select(id)
]]
function rc522.select(id)
if not id then
return false
end
local buff = {}
buff[1] = rc522_anticoll1
buff[2] = 0x70
buff[7] = 0
for i = 1, 4 do
buff[i + 2] = id:byte(i)
buff[7] = bit.bxor(buff[7], id:byte(i))
end
local crc = calculate_crc(buff)
buff[8] = crc[1]
buff[9] = crc[2]
rc522.clear_bit_mask(rc522_status2, 0x08)
local status, back_data, back_bits = rc522.command(rc522_transceive, buff)
if status == true and back_bits == 0x18 then
return true
end
return false
end
--[[
rc522操作流程写入16字节数据到块
@api rc522.write_datablock(addr,data)
@number addr .M1卡总共有16个扇区(3+1),64
@table data ,16
@return bool
@usage
rc522.write_datablock(addr,data)
]]
function rc522.write_datablock(addr, data)
if #data ~= 16 then
return false
end
local status, array_id = rc522.request(rc522.reqall)
if status ~= true then
status, array_id = rc522.request(rc522.reqall)
end
if status == true then
local status, uid = rc522.anticoll(array_id)
if not uid then
return false
end
if status == true and uid then
rc522.select(uid)
status = authstate(rc522_authent1b, addr, Key_B, uid)
if status == true then
status = rc522.write(addr, data)
rc522.halt()
return status
end
end
end
return false
end
--[[
rc522操作流程读取块
@api rc522.read_datablock(addr)
@number addr .M1卡总共有16个扇区(3+1),64
@return bool string
@usage
for i=0,63 do
local a,b = rc522.read_datablock(i)
if a then
print("read",i,b:toHex())
end
end
]]
function rc522.read_datablock(addr)
local status, array_id = rc522.request(rc522.reqall)
if status ~= true then
status, array_id = rc522.request(rc522.reqall)
end
if status == true then
local status, uid = rc522.anticoll(array_id)
if status == true and uid then
rc522.select(uid)
status = authstate(rc522_authent1b, addr, Key_B, uid)
if status == true then
local status, data = read(addr)
if status == true then
return true, data
end
rc522.halt()
end
end
end
return false
end
--[[
rc522
@api rc522.init(spi_id, cs, rst)
@number spi_id spi端口号
@number cs cs引脚
@number rst rst引脚
@return bool
@usage
spi_rc522 = spi.setup(0,nil,0,0,8,10*1000*1000,spi.MSB,1,1)
rc522.init(0,pin.PB04,pin.PB01)
]]
function rc522.init(spi_id, cs, rst)
rc522_spi = spi_id
rc522_cs = gpio.setup(cs, 0, gpio.PULLUP)
rc522_cs(1)
rc522_rst = gpio.setup(rst, 0, gpio.PULLUP)
rc522_rst(1)
rc522.reset()
rc522_config_isotype()
log.debug("rc522.version", rc522.version())
return true
end
function rc522.test()
end
return rc522
@@ -0,0 +1,152 @@
## 功能模块介绍:
1. main.lua:主程序入口
2. rc522.lua rc522扩展库文件,在AirRC522_1000.lua功能模块中require使用
3. AirRC522_1000.lua:功能演示核心文件,在main.lua中加载运行,详细逻辑请看AirRC522_1000.lua 文件
## 演示功能概述:
核心逻辑:
1. 初始化并启用spi,如果初始化失败,退出程序
2. 初始化RC522模块,如果初始化失败,退出程序
3. 循环检测卡片。
4. 向卡片指定块号写入数据,并读取数据验证一致性
5. 读取卡片所有数据
## 演示硬件环境:
![](https://docs.openluat.com/accessory/AirSPINORFLASH_1000/image/780EPM.jpg)
![](https://docs.openluat.com/accessory/AirSPINORFLASH_1000/image/rc522.jpg)
1. 合宙 Air780EPM 核心板一块
2. 合宙 AirRC522_1000 一套
3. TYPE-C USB 数据线一根 Air780EPM 核心板和数据线的硬件接线方式为:
* Air780EPM 核心板通过 TYPE-C USB 口供电;(USB的拨码开关off/on,拨到on
* TYPE-C USB 数据线直接插到核心板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;
4. 杜邦线 7根
Air780EPM 核心板与 AirRC522_1000 按以下方式接线:
| Air780EPM核心板 | AirRC522_1000 |
| ----------------------------------------------- | ------------- |
| GND(任意) | GND |
| 3V3 | 3.3V |
| 83/SPI0CS | SDA |
| 86/SPI0CLK | SCK |
| 85/SPI0MOSI | MOSI |
| 84/SPI0MISO | MISO |
| 19/GPIO22(可选任意空闲IO) | RST |
## 演示软件环境:
1. Luatools 下载调试工具
2. 固件版本:LuatOS-SoC_V2018_Air780EPM_1,固件地址,如有最新固件请用最新[https://docs.openluat.com/air780epm/luatos/firmware/version/](https://docs.openluat.com/air780epm/luatos/firmware/version/)
3. pc 系统 win11win10 及以上)
## 演示核心步骤:
1. 搭建好硬件环境
2. Luatools 烧录内核固件和demo 脚本代码.
AirRC522_1000.lua脚本中,硬件配置参数中rst_pin是使用Air780EPM核心板的GPIO22,如果接其他IO,注意修改硬件配置参数中rst_pin对应的管脚号。
3. 烧录成功后,代码会自动运行,查看打印日志,如果正常运行,会打印相关信息,spi 初始化,rc522初始化,写入数据读取数据等。
4. 如下 log 显示:
```
[2025-12-01 17:35:36.521][000000000.205] I/user.main Air780EPM_rc522_DEMO 001.000.000
[2025-12-01 17:35:36.529][000000000.226] I/user.RC522 初始化SPI接口
[2025-12-01 17:35:36.538][000000000.227] SPI_HWInit 552:spi0 speed 2000000,1994805,154
[2025-12-01 17:35:36.546][000000000.227] I/user.RC522 SPI初始化成功
[2025-12-01 17:35:36.553][000000000.227] I/user.RC522 初始化RC522传感器
[2025-12-01 17:35:36.562][000000000.230] D/user.rc522.version 178
[2025-12-01 17:35:36.568][000000000.230] I/user.RC522 传感器初始化成功
[2025-12-01 17:35:36.574][000000000.230] I/user.RC522 开始检测卡片
[2025-12-01 17:35:36.579][000000000.236] I/user.RC522 检测到卡片,类型: 0400 4
[2025-12-01 17:35:36.584][000000000.243] I/user.RC522 卡片UID E3E2AD14 8
[2025-12-01 17:35:36.589][000000000.335] I/user.RC522 数据写入成功,块号: 9 写入数据是: 01000000000000000000000000000000 32
[2025-12-01 17:35:36.595][000000000.378] I/user.RC522 写入验证成功,数据是: 01000000000000000000000000000000 32
[2025-12-01 17:35:36.601][000000000.378] I/user.RC522 开始读取卡片数据...
[2025-12-01 17:35:36.608][000000000.429] I/user.块[0] E3E2AD14B80804006263646566676869 32
[2025-12-01 17:35:36.613][000000000.506] I/user.块[1] 00000000000000000000000000000000 32
[2025-12-01 17:35:36.618][000000000.576] I/user.块[2] 00000000000000000000000000000000 32
[2025-12-01 17:35:36.623][000000000.648] I/user.块[3] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:36.630][000000000.720] I/user.块[4] 00000000000000000000000000000000 32
[2025-12-01 17:35:36.677][000000000.793] I/user.块[5] 00000000000000000000000000000000 32
[2025-12-01 17:35:36.682][000000000.861] I/user.块[6] 00000000000000000000000000000000 32
[2025-12-01 17:35:36.690][000000000.927] I/user.块[7] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:36.698][000000000.992] I/user.块[8] 00000000000000000000000000000000 32
[2025-12-01 17:35:36.702][000000001.059] I/user.块[9] 01000000000000000000000000000000 32
[2025-12-01 17:35:36.734][000000001.124] I/user.块[10] 00000000000000000000000000000000 32
[2025-12-01 17:35:36.796][000000001.191] I/user.块[11] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:36.874][000000001.256] I/user.块[12] 00000000000000000000000000000000 32
[2025-12-01 17:35:36.935][000000001.322] I/user.块[13] 00000000000000000000000000000000 32
[2025-12-01 17:35:37.154][000000001.403] I/user.块[14] 00000000000000000000000000000000 32
[2025-12-01 17:35:37.174][000000001.473] I/user.块[15] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:37.192][000000001.585] I/user.块[16] 00000000000000000000000000000000 32
[2025-12-01 17:35:37.264][000000001.650] I/user.块[17] 00000000000000000000000000000000 32
[2025-12-01 17:35:37.325][000000001.718] I/user.块[18] 00000000000000000000000000000000 32
[2025-12-01 17:35:37.387][000000001.784] I/user.块[19] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:37.463][000000001.850] I/user.块[20] 00000000000000000000000000000000 32
[2025-12-01 17:35:37.541][000000001.926] I/user.块[21] 00000000000000000000000000000000 32
[2025-12-01 17:35:37.603][000000001.993] I/user.块[22] 00000000000000000000000000000000 32
[2025-12-01 17:35:37.668][000000002.060] I/user.块[23] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:37.786][000000002.138] I/user.块[24] 00000000000000000000000000000000 32
[2025-12-01 17:35:37.793][000000002.139] D/mobile cid1, state0
[2025-12-01 17:35:37.800][000000002.139] D/mobile bearer act 0, result 0
[2025-12-01 17:35:37.811][000000002.140] D/mobile NETIF_LINK_ON -> IP_READY
[2025-12-01 17:35:37.815][000000002.205] I/user.块[25] 00000000000000000000000000000000 32
[2025-12-01 17:35:37.826][000000002.211] D/mobile TIME_SYNC 0
[2025-12-01 17:35:37.884][000000002.276] I/user.块[26] 00000000000000000000000000000000 32
[2025-12-01 17:35:37.961][000000002.343] I/user.块[27] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:38.022][000000002.409] I/user.块[28] 00000000000000000000000000000000 32
[2025-12-01 17:35:38.085][000000002.476] I/user.块[29] 00000000000000000000000000000000 32
[2025-12-01 17:35:38.146][000000002.542] I/user.块[30] 00000000000000000000000000000000 32
[2025-12-01 17:35:38.224][000000002.608] I/user.块[31] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:38.287][000000002.672] I/user.块[32] 00000000000000000000000000000000 32
[2025-12-01 17:35:38.350][000000002.738] I/user.块[33] 00000000000000000000000000000000 32
[2025-12-01 17:35:38.413][000000002.804] I/user.块[34] 00000000000000000000000000000000 32
[2025-12-01 17:35:38.490][000000002.872] I/user.块[35] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:38.552][000000002.940] I/user.块[36] 00000000000000000000000000000000 32
[2025-12-01 17:35:38.614][000000003.005] I/user.块[37] 00000000000000000000000000000000 32
[2025-12-01 17:35:38.676][000000003.070] I/user.块[38] 00000000000000000000000000000000 32
[2025-12-01 17:35:38.753][000000003.136] I/user.块[39] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:38.813][000000003.203] I/user.块[40] 00000000000000000000000000000000 32
[2025-12-01 17:35:38.877][000000003.269] I/user.块[41] 00000000000000000000000000000000 32
[2025-12-01 17:35:38.938][000000003.334] I/user.块[42] 00000000000000000000000000000000 32
[2025-12-01 17:35:39.015][000000003.400] I/user.块[43] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:39.079][000000003.464] I/user.块[44] 00000000000000000000000000000000 32
[2025-12-01 17:35:39.142][000000003.529] I/user.块[45] 00000000000000000000000000000000 32
[2025-12-01 17:35:39.204][000000003.596] I/user.块[46] 00000000000000000000000000000000 32
[2025-12-01 17:35:39.268][000000003.662] I/user.块[47] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:39.345][000000003.727] I/user.块[48] 00000000000000000000000000000000 32
[2025-12-01 17:35:39.409][000000003.792] I/user.块[49] 00000000000000000000000000000000 32
[2025-12-01 17:35:39.473][000000003.857] I/user.块[50] 00000000000000000000000000000000 32
[2025-12-01 17:35:39.535][000000003.923] I/user.块[51] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:39.597][000000003.988] I/user.块[52] 00000000000000000000000000000000 32
[2025-12-01 17:35:39.660][000000004.054] I/user.块[53] 00000000000000000000000000000000 32
[2025-12-01 17:35:39.723][000000004.118] I/user.块[54] 00000000000000000000000000000000 32
[2025-12-01 17:35:39.786][000000004.183] I/user.块[55] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:39.864][000000004.250] I/user.块[56] 00000000000000000000000000000000 32
[2025-12-01 17:35:39.927][000000004.317] I/user.块[57] 00000000000000000000000000000000 32
[2025-12-01 17:35:39.988][000000004.382] I/user.块[58] 00000000000000000000000000000000 32
[2025-12-01 17:35:40.052][000000004.448] I/user.块[59] 000000000000FF078069FFFFFFFFFFFF 32
[2025-12-01 17:35:40.129][000000004.514] I/user.块[60] 00000000000000000000000000000000 32
[2025-12-01 17:35:40.191][000000004.580] I/user.块[61] 00000000000000000000000000000000 32
[2025-12-01 17:35:40.254][000000004.646] I/user.块[62] 00000000000000000000000000000000 32
[2025-12-01 17:35:40.318][000000004.714] I/user.块[63] 000000000000FF078069FFFFFFFFFFFF 32
```
@@ -0,0 +1,129 @@
--[[
@module AirSHT30_1000
@summary AirSHT30_1000应用功能模块
@version 1.0
@date 2025.10.22
@author
@usage
AirSHT30_1000驱动配置文件
1AirSHT30_1000
2湿
require "AirSHT30_1000"
]]
--本文件中的主机是指I2C主机,具体指Air780EPM
--本文件中的从机是指I2C从机,具体指AirSHT30_1000配件板上的sht30温湿度传感器芯片
local AirSHT30_1000 =
{
-- i2c_id:主机的i2c id
}
-- 从机地址为0x44
local slave_addr = 0x44
-- 计算数据表data中所有数据元素的crc8校验值
local function crc8(data)
local crc = 0xFF
for i = 1, #data do
crc = bit.bxor(crc, data[i])
for j = 1, 8 do
crc = crc * 2
if crc >= 0x100 then
crc = bit.band(bit.bxor(crc, 0x31), 0xff)
end
end
end
return crc
end
--打开AirSHT30_1000
--i2c_idnumber类型;
-- 主机使用的I2C ID,用来控制AirSHT30_1000
-- 取值范围:仅支持0和1;
-- 如果没有传入此参数,则默认为0;
--返回值:成功返回true,失败返回false
function AirSHT30_1000.open(i2c_id)
--如果i2c_id为nil,则赋值为默认值0
if i2c_id==nil then i2c_id=0 end
--检查参数的合法性
if not (i2c_id == 0 or i2c_id == 1) then
log.error("AirSHT30_1000.open", "invalid i2c_id", i2c_id)
return false
end
AirSHT30_1000.i2c_id = i2c_id
--初始化I2C
if i2c.setup(i2c_id, i2c.FAST) ~= 1 then
log.error("AirSHT30_1000.open", "i2c.setup error", i2c_id)
return false
end
return true
end
--读取温湿度数据;
--返回值:失败返回false
-- 成功返回两个值,第一个为摄氏温度值(number类型,例如23.6表示23.6摄氏度),第二个为百分比湿度值(number类型,例如67表示67%的湿度)
function AirSHT30_1000.read()
-- 发送启动测量命令(高精度)
i2c.send(AirSHT30_1000.i2c_id, slave_addr, {0x24, 0x00})
-- 等待测量完成(SHT30高精度测量需~15ms)
sys.wait(20)
-- 读取6字节数据(温度高/低 + CRC,湿度高/低 + CRC
local data = i2c.recv(AirSHT30_1000.i2c_id, slave_addr, 6)
-- 如果没有读取到6字节数据
if type(data)~="string" or data:len()~=6 then
log.error("AirSHT30_1000.read", "i2c.recv error")
return false
end
-- log.info("AirSHT30_1000.read", data:toHex())
--如果校验值正确
if crc8({data:byte(1), data:byte(2)}) == data:byte(3) and crc8({data:byte(4), data:byte(5)}) == data:byte(6) then
-- 提取原始温度值
local temp_raw = (data:byte(1) << 8) | data:byte(2)
-- 提取原始湿度值
local hum_raw = (data:byte(4) << 8) | data:byte(5)
-- 转换为实际值(根据SHT30数据手册公式)
local temprature = (-45 + 175 * temp_raw / 65535.0)
local humidity = (100 * hum_raw / 65535.0)
-- 打印输出结果(保留2位小数)
-- log.info("AirSHT30_1000.read", "temprature", string.format("%.2f ℃", temprature))
-- log.info("AirSHT30_1000.read", "temprature", string.format("%.2f %%RH", humidity))
return temprature, humidity
else
log.error("AirSHT30_1000.read", "crc error", i2c_id)
return false
end
end
--关闭AirSHT30_1000
--返回值:成功返回true,失败返回false
function AirSHT30_1000.close()
--close接口没有返回值,理论上不会关闭失败
i2c.close(AirSHT30_1000.i2c_id)
return true
end
return AirSHT30_1000
@@ -0,0 +1,64 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口
@version 1.0
@date 2025.10.24
@author
@usage
AirSHT30_1000是合宙设计生产的一款I2C接口的SHT30温湿度传感器配件板
demo演示的核心功能为
Air780EPM核心板+AirSHT30_1000配件板11湿
readme.md文件
]]
--[[
PROJECT和VERSION变量Luatools工具会用到这两个变量
PROJECTascii string类型
便使,
VERSIONascii string类型
使iot.openluat.com进行远程升级"XXX.YYY.ZZZ"
XYZ各表示1位数字X表示的数字可以相同Y和三个Z表示的数字也是可以相同
YYY这三位数字必须存在999
使iot.openluat.com进行远程升级
]]
PROJECT = "AirSHT30_1000"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 加载sht30应用模块
require "sht30_app"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,64 @@
## 功能模块介绍
1、main.lua:主程序入口;
2、sht30_app.lua:每隔1秒读取一次温湿度数据;
3、AirSHT30_1000.luaAirSHT30_1000驱动文件;
## 演示功能概述
AirSHT30_1000是合宙设计生产的一款I2C接口的SHT30温湿度传感器配件板;
本demo演示的核心功能为:
Air780EPM核心板+AirSHT30_1000配件板,每隔1秒读取1次温湿度数据;
## 核心板+配件板资料
[Air780EPM核心板](https://docs.openluat.com/air780epm/product/shouce/)
[AirSHT30_1000配件板相关资料](https://docs.openluat.com/accessory/AirSHT30_1000/)
## 演示硬件环境
![](https://docs.openluat.com/accessory/AirSHT30_1000/image/connect_780epm.png)
1、Air780EPM核心板
2、AirSHT30_1000配件板
3、母对母的杜邦线4根
| Air780EPM核心板 | AirSHT30_1000配件板|
| ------------ | ------------------ |
| 3V3 | 3V3 |
| GND | GND |
| 66/I2C1SDA | SDA |
| 67/I2C1SCL | SCL |
## 演示软件环境
1、Luatools下载调试工具
2、[Air780EPM最新版本的内核固件](https://docs.openluat.com/air780epm/luatos/firmware/version/)
## 演示操作步骤
1、搭建好演示硬件环境
2、不需要修改demo脚本代码
3、Luatools烧录内核固件和demo脚本代码
4、烧录成功后,自动开机运行
5、通过观察Luatools的运行日志,每隔1秒出现一次类似于下面的打印,就表示测试正常
``` lua
[2025-09-18 15:29:03.155][000000001.262] I/user.read_sht30_task_func temprature 27.43 ℃
[2025-09-18 15:29:03.159][000000001.262] I/user.read_sht30_task_func humidity 57.58 %RH
@@ -0,0 +1,48 @@
--[[
@module sht30_app
@summary sht30_app应用功能模块
@version 1.0
@date 2025.10.22
@author
@usage
sht30_app应用功能模块
11湿
main.lua中require "sht30_app"
]]
--加载AirSHT30_1000驱动文件
local air_sht30 = require "AirSHT30_1000"
--每隔1秒读取一次温湿度数据
local function read_sht30_task_func()
--打开sht30硬件
air_sht30.open(1)
while true do
--读取温湿度数据
local temprature, humidity = air_sht30.read()
--读取成功
if temprature then
-- 打印输出结果(保留2位小数)
log.info("read_sht30_task_func", "temprature", string.format("%.2f ℃", temprature))
log.info("read_sht30_task_func", "humidity", string.format("%.2f %%RH", humidity))
--读取失败
else
log.error("read_sht30_task_func", "read error")
end
--等待1秒
sys.wait(1000)
end
--关闭sht30硬件
air_sht30.close()
end
--创建一个task,并且运行task的主函数read_sht30_task_func
sys.taskInit(read_sht30_task_func)
@@ -0,0 +1,203 @@
--[[
@module lf_fs
@summary lf_fs测试功能模块
@version 1.0
@date 2025.9.05
@author
@usage
demo演示的功能为使Air780EPM核心板通过SPI核心库/lf核心库/io核心库实现对 NAND Flash的操作
Air780EPM核心板为例, 线:
Air780EPM AirSPINAND_1000配件板
GND() GND
VDD_EXT VCC
GPIO8/SPI0_CS CS,
SPI0_SLK CLK,
SPI0_MOSI DI,,
SPI0_MISO DO,,
--使用SPI0,硬件SPI CS接在gpio8上
1.SPISPI对象
2.SPI对象初始化flash设备flash设备对象
3.lf库挂载flash设备对象为LittleFS文件系统
4.
5.
]]
-- SPI配置参数
local SPI_ID = 0 -- SPI总线ID,根据实际情况修改
local CS_PIN = 8 -- CS引脚,根据实际情况修改
local CPHA = 0 -- 时钟相位
local CPOL = 0 -- 时钟极性
local data_Width = 8 -- 数据宽度(位)
local bandrate = 2*1000*1000 -- 波特率(Hz),初始化为2MHz
-- 1. 以对象方式设置并启用 SPI,返回设备对象
local function spiDev_init_func()
log.info("lf_fs", "SPI_ID", SPI_ID, "CS_PIN", CS_PIN)
--以对象的方式初始化spi,高位在前,主模式,半双工模式
--spi nand flash只支持半双工模式
local spi_device = spi.deviceSetup(SPI_ID, CS_PIN, CPHA, CPOL, data_Width, bandrate, spi.MSB, 1, 0)
log.info("硬件spi", "初始化,波特率:", spi_device, bandrate)
if not spi_device then
log.error("SPI初始化", "失败")
return nil
end
log.info("SPI初始化", "成功,波特率",bandrate)
return spi_device
end
-- 2. 初始化Flash设备,返回设备对象
local function init_flash_device(spi_device)
log.info("Flash初始化", "开始")
local flash_device = lf.init(spi_device)
if not flash_device then
log.error("Flash初始化", "失败")
return nil
end
log.info("Flash初始化", "成功,设备:", flash_device)
return flash_device
end
-- 3. 挂载文件系统
local function mount_filesystem(flash_device, mount_point)
log.info("文件系统", "开始挂载:", mount_point)
-- 检查是否支持挂载功能
if not lf.mount then
log.error("文件系统", "lf模块不支持挂载功能")
return false
end
-- 尝试挂载
local mount_ok = lf.mount(flash_device, mount_point)
if not mount_ok then
log.warn("文件系统lf", "挂载失败,尝试重新挂载...")
mount_ok = lf.mount(flash_device, mount_point)
if not mount_ok then
log.error("文件系统", "仍挂载失败")
return false
end
end
log.info("文件系统", "挂载成功:", mount_point)
return true
end
-- 4. 打印文件系统信息
local function print_filesystem_info(mount_point)
log.info("文件系统信息", "开始查询:", mount_point)
-- 获取文件系统详细信息,总块数/已用块数等
local ok, total_blocks, used_blocks, block_size, fs_type = fs.fsstat(mount_point)
if ok then
log.info(" 总block数:", total_blocks)
log.info(" 已用block数:", used_blocks)
log.info(" block大小:", block_size, "字节")
log.info(" 文件系统类型:", fs_type)
else
log.warn(" 无法获取详细信息")
end
end
-- 5. 执行文件操作测试
local function test_file_operations(mount_point)
log.info("文件操作测试", "开始")
-- 测试写入文件
local test_file = mount_point .. "/test.txt"
local f, err = io.open(test_file, "w")
if not f then
log.error(" 写入失败", test_file, "错误:", err)
return false
end
local write_data = "当前时间: " .. os.date()
f:write(write_data)
f:close()
log.info(" 写入成功", test_file, "内容:", write_data)
-- 测试读取文件
local read_data, read_err = io.readFile(test_file)
if not read_data then
log.error(" 读取失败", test_file, "错误:", read_err)
return false
end
log.info(" 读取成功", test_file, "内容:", read_data)
-- 验证内容一致性
if read_data ~= write_data then
log.warn(" 内容不一致", "写入:", write_data, "读取:", read_data)
end
-- 测试文件追加
local append_file = mount_point .. "/append.txt"
os.remove(append_file) -- 清除旧文件
io.writeFile(append_file, "LuatOS 测试") -- 初始写入
local f_append, append_err = io.open(append_file, "a+")
if not f_append then
log.error(" 追加失败", append_file, "错误:", append_err)
return false
end
local append_data = " - 追加时间: " .. os.date()
f_append:write(append_data)
-- 执行完操作后,一定要关掉文件
f_append:close()
local final_data = io.readFile(append_file)
log.info(" 追加后内容:", final_data)
log.info("文件操作测试", "完成")
return true
end
-- 7. 关闭SPI设备,成功返回0
local function spi_close_func()
log.info("关闭spi", spi_device:close())
end
-- 主任务函数:按流程调用各功能函数
local function spinand_test_func()
--1.判断SPI初始化
spi_device = spiDev_init_func()
if not spi_device then
log.error("主流程", "SPI初始化失败,终止")
return
end
-- 流程2:初始化Flash设备
local flash_device = init_flash_device(spi_device)
if not flash_device then
log.error("主流程", "Flash初始化失败,终止")
spi_close_func()
return
end
-- 流程3:挂载文件系统
local mount_point = "/little_flash"
if not mount_filesystem(flash_device, mount_point) then
log.error("主流程", "文件系统挂载失败,终止")
spi_close_func()
return
end
-- 流程4:打印文件系统信息
print_filesystem_info(mount_point)
-- 流程5:执行文件操作测试
if not test_file_operations(mount_point) then
log.warn("主流程", "文件操作测试部分失败")
end
-- 6.关闭SPI设备
spi_close_func()
end
sys.taskInit(spinand_test_func)
@@ -0,0 +1,70 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口
@version 001.999.000
@date 2025.9.05
@author
@usage
demo是演示合宙AirSPINAND_1000配件版的功能使
lf_fslittleFS文件系统,nand flash模块以文件系统的方式进行读写数据操作lf_fs.lua
]]
--[[
PROJECT和VERSION变量Luatools工具会用到这两个变量
PROJECTascii string类型
便使,
VERSIONascii string类型
使iot.openluat.com进行远程升级"XXX.YYY.ZZZ"
XYZ各表示1位数字X表示的数字可以相同Y和三个Z表示的数字也是可以相同
YYY这三位数字必须存在999
使iot.openluat.com进行远程升级
]]
PROJECT = "SPI_NAND"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 加载lf_fs功能模块
require "lf_fs"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,112 @@
## 功能模块介绍:
1. main.lua:主程序入口,加载lf_fs脚本运行
2. lf_fs:通过littleFS文件系统,对nand flash模块以文件系统的方式进行读写数据操作,详细逻辑请看lf_fs.lua 文件
## 演示功能概述:
### lf_fs
1.以对象的方式配置参数,初始化启用 SPI,返回 SPI 对象
2.用 SPI 对象初始化 flash 设备,返回 flash 设备对象
3.用 lf 库挂载 flash 设备对象为LittleFS文件系统
4.读取文件系统的信息,以确认内存情况
5.操作文件读写,并验证写入一致性,追加文件等。
## 演示硬件环境:
![](https://docs.openluat.com/accessory/AirSPINORFLASH_1000/image/780EPM.jpg)
![](https://docs.openluat.com/accessory/AirSPINAND_1000/image/nand.jpg)
1. 合宙Air780EPM 核心板一块
2. 合宙 AirSPINAND_1000配件板 一块
3. TYPE-C USB 数据线一根 Air780EPM 核心板和数据线的硬件接线方式为:
- Air780EPM 核心板通过 TYPE-C USB 口供电;(外部供电/USB 供电 拨动开关 拨到 USB 供电一端)
- TYPE-C USB 数据线直接插到开发板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;
4. 杜邦线 6 根
    Air780EPM 核心板与 AirSPINAND_1000配件板 按以下方式接线:
<table>
<tr>
<td>Air780EPM核心板<br/></td><td>AirSPINAND_1000配件板<br/></td></tr>
<tr>
<td>GND(任意) <br/></td><td>GND<br/></td></tr>
<tr>
<td>VDD_EXT<br/></td><td>VCC<br/></td></tr>
<tr>
<td>GPIO8/<br/>SPI0_CS<br/></td><td>CS<br/></td></tr>
<tr>
<td>SPI0_SLK<br/></td><td>SCK<br/></td></tr>
<tr>
<td>SPI0_MOSI<br/></td><td>MOSI<br/></td></tr>
<tr>
<td>SPI0_MISO<br/></td><td>MISO<br/></td></tr>
</table>
## 演示软件环境:
1. Luatools 下载调试工具
2. 固件版本:LuatOS-SoC_V2018_Air780EPM_1,固件地址,如有最新固件请用最新 [https://docs.openluat.com/air780epm/luatos/firmware/version/](https://docs.openluat.com/air780ehm/luatos/firmware/version/)
3. pc 系统 win11win10 及以上)
## 演示核心步骤:
1. 搭建好硬件环境
2. main.lua 中加载lf_fs功能模块
3. Luatools 烧录内核固件和 demo 脚本
4. 烧录成功后,代码会自动运行,查看打印日志,如果正常运行,会打印相关信息,spi 初始化,数据读写,文件操作等。
5. lf_fs.lua 如下 log 显示:
```bash
[2025-11-15 16:41:28.932][000000000.215] I/user.main SPI_NAND 001.000.000
[2025-11-15 16:41:28.945][000000000.224] I/user.lf_fs SPI_ID 0 CS_PIN 8
[2025-11-15 16:41:28.952][000000000.224] SPI_HWInit 552:spi0 speed 2000000,1994805,154
[2025-11-15 16:41:28.961][000000000.225] I/user.硬件spi 初始化,波特率: SPI*: 0C1A6A68 2000000
[2025-11-15 16:41:28.977][000000000.225] I/user.SPI初始化 成功,波特率 2000000
[2025-11-15 16:41:28.985][000000000.225] I/user.Flash初始化 开始
[2025-11-15 16:41:28.997][000000000.225] I/little_flash SFDP header not found.
[2025-11-15 16:41:29.005][000000000.226] I/little_flash JEDEC ID: manufacturer_id:0xEF device_id:0xAA21
[2025-11-15 16:41:29.011][000000000.226] I/little_flash little flash found flash W25N01GVZEIG
[2025-11-15 16:41:29.016][000000000.277] I/user.Flash初始化 成功,设备: userdata: 0C1CA134
[2025-11-15 16:41:29.025][000000000.277] I/user.文件系统 开始挂载: /little_flash
[2025-11-15 16:41:29.799][000000001.737] D/little_flash lfs_mount 0
[2025-11-15 16:41:29.806][000000001.738] D/little_flash vfs mount /little_flash ret 0
[2025-11-15 16:41:29.811][000000001.738] I/user.文件系统 挂载成功: /little_flash
[2025-11-15 16:41:29.818][000000001.738] I/user.文件系统信息 开始查询: /little_flash
[2025-11-15 16:41:30.812][000000002.735] I/user. 总block数: 1024
[2025-11-15 16:41:30.827][000000002.735] I/user. 已用block数: 2
[2025-11-15 16:41:30.835][000000002.736] I/user. block大小: 131072 字节
[2025-11-15 16:41:30.845][000000002.736] I/user. 文件系统类型: lfs
[2025-11-15 16:41:30.852][000000002.736] I/user.文件操作测试 开始
[2025-11-15 16:41:31.326][000000003.259] I/user. 写入成功 /little_flash/test.txt 内容: 当前时间: Sat Nov 15 16:41:36 2025
[2025-11-15 16:41:31.837][000000003.771] I/user. 读取成功 /little_flash/test.txt 内容: 当前时间: Sat Nov 15 16:41:36 2025
[2025-11-15 16:41:34.095][000000006.020] I/user. 追加后内容: LuatOS 测试 - 追加时间: Sat Nov 15 16:41:38 2025
[2025-11-15 16:41:34.106][000000006.020] I/user.文件操作测试 完成
[2025-11-15 16:41:34.117][000000006.021] I/user.关闭spi true
[2025-11-15 16:41:34.129][000000006.023] D/mobile cid1, state0
[2025-11-15 16:41:34.136][000000006.023] D/mobile bearer act 0, result 0
[2025-11-15 16:41:34.146][000000006.024] D/mobile NETIF_LINK_ON -> IP_READY
[2025-11-15 16:41:34.160][000000006.024] D/mobile TIME_SYNC 0
```
@@ -0,0 +1,204 @@
--[[
@module lf_fs
@summary lf_fs测试功能模块
@version 1.0
@date 2025.9.05
@author
@usage
demo演示的功能为使Air780EPM核心板通过SPI核心库/lf核心库/io核心库实现对 NOR Flash的操作
Air780EPM核心板为例, 线:
Air780EPM核心板 AirSPINORFLASH_1000配件版
GND() GND
VDD_EXT VCC
GPIO8/SPI0_CS CS,
SPI0_SLK CLK,
SPI0_MOSI DI,,
SPI0_MISO DO,,
--使用SPI0,硬件SPI CS接在gpio8上
1.SPISPI对象
2.SPI对象初始化flash设备flash设备对象
3.lf库挂载flash设备对象为文件系统
4.
5.
]]
-- SPI配置参数
local SPI_ID = 0 -- SPI总线ID,根据实际情况修改
local CS_PIN = 8 -- CS引脚,根据实际情况修改
local CPHA = 0 -- 时钟相位
local CPOL = 0 -- 时钟极性
local data_Width = 8 -- 数据宽度(位)
local bandrate = 2*1000*1000 -- 波特率(Hz),初始化为2MHz
-- 1. 以对象方式设置并启用 SPI,返回设备对象
local function spiDev_init_func()
log.info("lf_fs", "SPI_ID", SPI_ID, "CS_PIN", CS_PIN)
--以对象的方式初始化spi,高位在前,主模式,半双工模式
--spi flash只支持半双工模式
local spi_device = spi.deviceSetup(SPI_ID, CS_PIN, CPHA, CPOL, data_Width, bandrate, spi.MSB, 1, 0)
log.info("硬件spi", "初始化,波特率:", spi_device, bandrate)
if not spi_device then
log.error("SPI初始化", "失败")
return nil
end
log.info("SPI初始化", "成功,波特率:",bandrate)
return spi_device
end
-- 2. 初始化Flash设备,返回设备对象
local function init_flash_device(spi_device)
log.info("Flash初始化", "开始")
local flash_device = lf.init(spi_device)
if not flash_device then
log.error("Flash初始化", "失败")
return nil
end
log.info("Flash初始化", "成功,设备:", flash_device)
return flash_device
end
-- 3. 挂载文件系统
local function mount_filesystem(flash_device, mount_point)
log.info("文件系统", "开始挂载:", mount_point)
-- 检查是否支持挂载功能
if not lf.mount then
log.error("文件系统", "lf模块不支持挂载功能")
return false
end
-- 尝试挂载
local mount_ok = lf.mount(flash_device, mount_point)
if not mount_ok then
log.warn("文件系统lf", "挂载失败,尝试重新挂载...")
mount_ok = lf.mount(flash_device, mount_point)
if not mount_ok then
log.error("文件系统", "仍挂载失败")
return false
end
end
log.info("文件系统", "挂载成功:", mount_point)
return true
end
-- 4. 打印文件系统信息
local function print_filesystem_info(mount_point)
log.info("文件系统信息", "开始查询:", mount_point)
-- 获取文件系统详细信息,总块数/已用块数等
local ok, total_blocks, used_blocks, block_size, fs_type = fs.fsstat(mount_point)
if ok then
log.info(" 总block数:", total_blocks)
log.info(" 已用block数:", used_blocks)
log.info(" block大小:", block_size, "字节")
log.info(" 文件系统类型:", fs_type)
else
log.warn(" 无法获取详细信息")
end
end
-- 5. 执行文件操作测试
local function test_file_operations(mount_point)
log.info("文件操作测试", "开始")
-- 测试写入文件
local test_file = mount_point .. "/test.txt"
local f, err = io.open(test_file, "w")
if not f then
log.error(" 写入失败", test_file, "错误:", err)
return false
end
local write_data = "当前时间: " .. os.date()
f:write(write_data)
f:close()
log.info(" 写入成功", test_file, "内容:", write_data)
-- 测试读取文件
local read_data, read_err = io.readFile(test_file)
if not read_data then
log.error(" 读取失败", test_file, "错误:", read_err)
return false
end
log.info(" 读取成功", test_file, "内容:", read_data)
-- 验证内容一致性
if read_data ~= write_data then
log.warn(" 内容不一致", "写入:", write_data, "读取:", read_data)
end
-- 测试文件追加
local append_file = mount_point .. "/append.txt"
os.remove(append_file) -- 清除旧文件
io.writeFile(append_file, "LuatOS 测试") -- 初始写入
local f_append, append_err = io.open(append_file, "a+")
if not f_append then
log.error(" 追加失败", append_file, "错误:", append_err)
return false
end
local append_data = " - 追加时间: " .. os.date()
f_append:write(append_data)
-- 执行完操作后,一定要关掉文件
f_append:close()
local final_data = io.readFile(append_file)
log.info(" 追加后内容:", final_data)
log.info("文件操作测试", "完成")
return true
end
-- 7. 关闭SPI设备对象,成功返回true
local function spi_close_func()
log.info("关闭spi", spi_device:close())
end
-- 主任务函数:按流程调用各功能函数
local function spinor_test_func()
--1.判断SPI初始化
spi_device = spiDev_init_func()
if not spi_device then
log.error("主流程", "SPI初始化失败,终止")
return
end
-- 流程2:初始化Flash设备
local flash_device = init_flash_device(spi_device)
if not flash_device then
log.error("主流程", "Flash初始化失败,终止")
spi_close_func()
return
end
-- 流程3:挂载文件系统
local mount_point = "/little_flash"
if not mount_filesystem(flash_device, mount_point) then
log.error("主流程", "文件系统挂载失败,终止")
spi_close_func()
return
end
-- 流程4:打印文件系统信息
print_filesystem_info(mount_point)
-- 流程5:执行文件操作测试
if not test_file_operations(mount_point) then
log.warn("主流程", "文件操作测试部分失败")
end
-- 6.关闭SPI设备
spi_close_func()
end
sys.taskInit(spinor_test_func)
@@ -0,0 +1,77 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口
@version 001.999.000
@date 2025.9.05
@author
@usage
demo是演示合宙AirSPINORFLASH_1000配件版的功能使用flash模块
1. raw_spi
spi接口对flash模块进行读写数据操作raw_spi.lua
2. lf_fs
littleFS文件系统,flash模块以文件系统的方式进行读写数据操作lf_fs.lua
]]
--[[
PROJECT和VERSION变量Luatools工具会用到这两个变量
PROJECTascii string类型
便使,
VERSIONascii string类型
使iot.openluat.com进行远程升级"XXX.YYY.ZZZ"
XYZ各表示1位数字X表示的数字可以相同Y和三个Z表示的数字也是可以相同
YYY这三位数字必须存在999
使iot.openluat.com进行远程升级
]]
PROJECT = "AirSPINORFLASH_1000"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 加载raw_spi功能模块
-- require "raw_spi"
-- 加载lf_fs功能模块
require"lf_fs"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

Some files were not shown because too many files have changed in this diff Show More