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,564 @@
--[[
@module aircloud
@summary 合宙IoT平台(excloud) + MQTT 双通道数据上报模块
@version 1.0
@date 2026.07.23
@author 江访
@usage
本文件为合宙IoT平台(excloud)和MQTT双通道数据上报功能模块,核心业务逻辑为:
1、excloud初始化、连接认证、心跳维护
2、通过excloud上报传感器数据和FOTA信息
3、通过MQTT短连接上报JSON格式完整数据
本文件的对外接口有5个:
1、aircloud.init(cfg):初始化excloud并等待连接/认证完成
2、aircloud.report_excloud(sensor_data, fota_info):通过excloud上报数据
3、aircloud.report_fota_status(status_text, sensor_data):通过excloud上报FOTA状态
4、aircloud.report_mqtt(topic, payload, broker, port):通过MQTT短连接上报JSON数据
5、aircloud.build_mqtt_payload(sensor_data, fota_info):构建MQTT上报payload
]]
-- ==================== 加载扩展库 ====================
-- excloud: 合宙IoT平台通信扩展库,基于自有TLV协议
-- 支持TCP和MQTT两种传输方式,本模块使用TCP方式
local excloud = require "excloud"
-- ==================== 模块表 ====================
-- aircloud 为模块对外接口表,所有对外函数注册在此表上
local aircloud = {}
-- ==================== 内部状态 ====================
-- g_mqtt_publish_done: MQTT发布是否已完成(成功或失败)
-- g_mqtt_publish_success: MQTT发布是否成功
-- 这两个变量用于MQTT短连接模式下的同步等待
local g_mqtt_publish_done = false
local g_mqtt_publish_success = false
-- ==================== MQTT事件回调 ====================
--[[
MQTT客户端事件回调处理函数
本回调注册到 mqtt_client:on(),处理以下事件:
conack — MQTT连接成功(broker确认)
sent — 数据发布成功
disconnect — MQTT连接断开(正常或异常)
error — MQTT通信错误
@param userdata mqtt_client MQTT客户端对象(由mqtt.create返回)
@param string event 事件类型
"conack" — TCP连接成功且收到broker的CONNACK
"sent" — PUBLISH报文已发送完成
"disconnect" — 连接断开(包括主动disconnect和被动断开)
"error" — 通信过程发生错误
@param any data 事件附加数据
conack事件: nil
sent事件: 报文标识符
disconnect事件: 断开原因码
error事件: 错误描述字符串
@param string payload 收到的消息负载(仅message事件有值,本模块未使用)
@param table metas 消息元数据(仅message事件有值,本模块未使用)
]]
local function mqtt_event_callback(mqtt_client, event, data, payload, metas)
log.info("aircloud", "MQTT事件:", event, data)
if event == "conack" then
-- MQTT连接成功:发布MQTT_CONNECT_OK消息,携带客户端对象
-- 上层等待MQTT_CONNECT_OK后获取connected_client进行publish
log.info("aircloud", "MQTT连接成功")
sys.publish("MQTT_CONNECT_OK", mqtt_client)
elseif event == "sent" then
-- 数据发送成功:标记发布完成并发布MQTT_SENT_OK
log.info("aircloud", "MQTT数据发送成功")
g_mqtt_publish_done = true
g_mqtt_publish_success = true
sys.publish("MQTT_SENT_OK")
elseif event == "disconnect" then
-- 连接断开:标记发布完成(未成功),防止上层永久等待
log.warn("aircloud", "MQTT断开")
g_mqtt_publish_done = true
sys.publish("MQTT_SENT_OK")
elseif event == "error" then
-- 通信错误:标记发布完成(未成功)
log.warn("aircloud", "MQTT错误:", data)
g_mqtt_publish_done = true
sys.publish("MQTT_SENT_OK")
end
end
-- ==================== excloud事件回调 ====================
--[[
excloud平台事件回调处理函数
本回调注册到 excloud.on(),处理以下事件:
connect_result — TCP连接结果
auth_result — 设备认证结果
disconnect — 与服务器断开连接
send_result — 数据发送结果
@param string event 事件类型
"connect_result" — 与excloud服务器的TCP连接结果
"auth_result" — 通过auth_key认证的结果
"disconnect" — 与服务器断开连接
"send_result" — TLV数据发送结果
@param table data 事件数据
connect_result: {success=boolean, error=string}
auth_result: {success=boolean, message=string}
disconnect: 空table
send_result: {success=boolean, sequence_num=number, error_msg=string}
]]
local function excloud_event_callback(event, data)
if event == "connect_result" then
-- TCP连接结果
-- data.success=true 表示TCP三次握手成功
-- 后续会进行auth认证
if data.success then
log.info("aircloud", "excloud连接成功")
else
log.warn("aircloud", "excloud连接失败:", data.error or "未知")
end
elseif event == "auth_result" then
-- 设备认证结果
-- data.success=true 表示通过auth_key认证通过
-- 认证通过后发布EXCLOUD_READY,通知上层可以开始数据交互
if data.success then
log.info("aircloud", "excloud认证成功")
sys.publish("EXCLOUD_READY")
else
log.warn("aircloud", "excloud认证失败:", data.message)
end
elseif event == "disconnect" then
-- 与服务器断开连接
log.warn("aircloud", "excloud断开连接")
elseif event == "send_result" then
-- TLV数据发送结果
-- data.sequence_num 为消息流水号,用于日志追踪
if data.success then
log.info("aircloud", "excloud发送成功, 流水号:", data.sequence_num)
else
log.warn("aircloud", "excloud发送失败:", data.error_msg)
end
end
end
-- ==================== API:初始化excloud ====================
--[[
初始化excloud连接并等待认证完成
aircloud.init(cfg)
本函数完成以下工作:
1、注册事件回调 excloud_event_callback
2、调用 excloud.setup 配置传输参数
3、调用 excloud.open 开启服务
4、启动自动心跳(间隔300秒)
5、等待认证完成(最长20秒)
注意:
1、本函数内部有 sys.waitUntil(),必须在协程中调用
2、如果超时(20秒未认证成功),认为初始化失败
3、excloud.setup 已不再需要主动配置 use_getip 和 device_type 参数
@param table cfg
含义:excloud配置参数表
是否必选:必选
cfg包含字段:
transport : string, 传输协议类型, 默认"tcp"
"tcp" — TCP长连接,适合频繁上报场景
"mqtt" — MQTT协议,适合标准MQTT生态
auto_reconnect : boolean, 是否自动重连, 默认true
true — 断开后自动尝试重连
false — 断开后不自动重连
reconnect_interval: number, 重连间隔(秒), 默认10
取值范围:5~300
max_reconnect : number, 最大重连次数, 默认3
达到最大次数后不再重连
mtn_log_enabled : boolean, 是否启用运维日志上传, 默认false
@return boolean
含义:初始化是否成功(含TCP连接和认证)
true — excloud已就绪,可进行数据上报
false — 初始化失败,可能是网络问题或PRODUCT_KEY无效
@usage
local ok = aircloud.init({
transport = "tcp",
auto_reconnect = true,
reconnect_interval = 10,
max_reconnect = 3,
})
if not ok then
log.error("aircloud", "初始化失败,跳过excloud上报")
end
]]
function aircloud.init(cfg)
log.info("aircloud", "正在初始化excloud...")
-- 第一步:注册事件回调
-- excloud扩展库内部维护此回调,收到平台事件时回调本函数
excloud.on(excloud_event_callback)
-- 第二步:配置excloud参数
-- excloud.setup会自动通过PRODUCT_KEY获取服务器地址和端口
-- 不再需要手动指定host和port
local ok, err_msg = excloud.setup({
transport = cfg.transport or "tcp",
auto_reconnect = (cfg.auto_reconnect ~= false),
reconnect_interval = cfg.reconnect_interval or 10,
max_reconnect = cfg.max_reconnect or 3,
mtn_log_enabled = cfg.mtn_log_enabled or false,
})
if not ok then
log.error("aircloud", "excloud.setup失败:", err_msg)
return false
end
-- 第三步:开启excloud服务
-- open()会发起TCP连接到服务器
ok, err_msg = excloud.open()
if not ok then
log.error("aircloud", "excloud.open失败:", err_msg)
return false
end
-- 第四步:启动自动心跳
-- 默认间隔300秒,保持TCP长连接活跃
excloud.start_heartbeat()
-- 第五步:等待认证完成,最长20秒
-- EXCLOUD_READY由excloud_event_callback在auth_result成功时发布
sys.waitUntil("EXCLOUD_READY", 20000)
return true
end
-- ==================== APIexcloud数据上报 ====================
--[[
通过excloud平台上报传感器数据和FOTA信息
aircloud.report_excloud(sensor_data, fota_info)
本函数构建TLV列表并通过excloud.send上报服务器。
TLV字段使用 CONTROL_RESPONSE(自定义数据)类型。
@param table sensor_data
含义:传感器数据表(由sensor_vl53l1x.collect_data返回)
是否必选:可选
传入nil表示不上报传感器数据
当传入非nil时,上报 sensor_data.avg_distance 作为TEXT类型值
@param string fota_info
含义:FOTA升级信息描述字符串
是否必选:可选
传入nil表示不上报FOTA信息
示例值:
"FOTA升级成功: 升级前: VERSION=001.999.000 ; 升级后: VERSION=001.999.001"
"FOTA升级包下载成功, 升级前版本: 001.999.000"
@return boolean
含义:上报是否成功
true — 数据已通过TCP发送到服务器
false — excloud未连接或发送失败
@usage
-- 上报传感器数据
aircloud.report_excloud(sensor_data, nil)
-- 上报传感器数据+FOTA信息
aircloud.report_excloud(sensor_data, "FOTA升级成功: xxx")
-- 仅上报FOTA信息
aircloud.report_excloud(nil, "FOTA升级包下载成功")
]]
function aircloud.report_excloud(sensor_data, fota_info)
-- 检查excloud连接状态
local status = excloud.status()
if not status or not status.is_connected then
log.warn("aircloud", "excloud未连接,跳过数据上报")
return false
end
-- 构建TLV列表
-- TLV (Tag-Length-Value) 是excloud协议的数据单元格式
-- field_meaning 用 CONTROL_RESPONSE(0x14) 表示自定义数据
-- data_type 使用 UNICODE/String 类型
local tlvs = {}
-- 添加传感器数据TLV
if sensor_data then
tlvs[#tlvs + 1] = {
field_meaning = excloud.FIELD_MEANINGS.CONTROL_RESPONSE,
data_type = excloud.DATA_TYPES.UNICODE,
value = tostring(sensor_data.avg_distance),
}
end
-- 添加FOTA信息TLV
if fota_info then
tlvs[#tlvs + 1] = {
field_meaning = excloud.FIELD_MEANINGS.CONTROL_RESPONSE,
data_type = excloud.DATA_TYPES.UNICODE,
value = fota_info,
}
end
-- 没有TLV时直接返回失败
if #tlvs == 0 then
return false
end
-- 发送TLV列表(不需要服务器回复)
-- 第二个参数false表示不需要服务器回复ACK
local ok, err_msg = excloud.send(tlvs, false)
if ok then
log.info("aircloud", "excloud数据上报成功")
else
log.warn("aircloud", "excloud数据上报失败:", err_msg)
end
return ok
end
-- ==================== APIexcloud FOTA状态上报 ====================
--[[
通过excloud上报FOTA状态文本(升级中状态)
aircloud.report_fota_status(status_text, sensor_data)
本函数专门用于上报"FOTA正在检查/升级中"等过程状态。
与report_excloud的区别在于本函数强制要求status_text不为空。
@param string status_text
含义:FOTA状态描述文本
是否必选:必选
示例值:
"FOTA: 正在检查/升级中"
"FOTA: 下载进度 50%"
@param table sensor_data
含义:传感器数据(可选,附带上报)
是否必选:可选
@return boolean
含义:上报是否成功
@usage
aircloud.report_fota_status("FOTA: 正在检查/升级中", sensor_data)
]]
function aircloud.report_fota_status(status_text, sensor_data)
-- 检查excloud连接状态
local status = excloud.status()
if not status or not status.is_connected then
log.warn("aircloud", "excloud未连接,跳过FOTA状态上报")
return false
end
-- 构建TLV列表
local tlvs = {}
-- 可选:附带传感器数据
if sensor_data then
tlvs[#tlvs + 1] = {
field_meaning = excloud.FIELD_MEANINGS.CONTROL_RESPONSE,
data_type = excloud.DATA_TYPES.UNICODE,
value = tostring(sensor_data.avg_distance),
}
end
-- 必选:FOTA状态文本
tlvs[#tlvs + 1] = {
field_meaning = excloud.FIELD_MEANINGS.CONTROL_RESPONSE,
data_type = excloud.DATA_TYPES.UNICODE,
value = status_text,
}
-- 发送TLV列表
local ok, err_msg = excloud.send(tlvs, false)
if ok then
log.info("aircloud", "FOTA状态已上报:", status_text)
else
log.warn("aircloud", "FOTA状态上报失败:", err_msg)
end
return ok
end
-- ==================== APIMQTT数据上报 ====================
--[[
通过MQTT短连接上报JSON格式数据
aircloud.report_mqtt(topic, payload_table, mqtt_broker, mqtt_port)
本函数每次调用都会创建一个新的MQTT短连接:
创建客户端 → 连接broker → 发布数据 → 断开连接 → 释放客户端
这样可以避免维护长连接,但每次有额外的TCP握手开销。
MQTT认证方式:client_id = IMEI + "_vl53l1x"
@param string topic
含义:MQTT发布主题
取值范围:任意合法的MQTT主题字符串
是否必选:必选
示例值:"864317083866528/vl53l1x/up"
@param table payload_table
含义:待上报的数据表(会被json.encode编码为JSON字符串)
取值范围:任意可被json.encode序列化的table
是否必选:必选
示例值:{distance_mm=265, project="Air8780P_VL53L1X"}
@param string mqtt_broker
含义:MQTT服务器地址(域名或IP)
是否必选:必选
示例值:"lbsmqtt.airm2m.com"
@param number mqtt_port
含义:MQTT服务器端口号
取值范围:1~65535
是否必选:可选
默认值:1884
@return boolean
含义:上报是否成功
true — 数据已成功发布到broker
false — 连接失败或发布失败
@usage
local payload = {distance_mm=265, project="Air8780P_VL53L1X"}
aircloud.report_mqtt("IMEI/vl53l1x/up", payload, "lbsmqtt.airm2m.com", 1884)
]]
function aircloud.report_mqtt(topic, payload_table, mqtt_broker, mqtt_port)
-- 参数有效性检查
if not topic or not payload_table or not mqtt_broker then
return false
end
log.info("aircloud", "MQTT上报数据到:", topic)
-- 重置MQTT同步变量
g_mqtt_publish_done = false
g_mqtt_publish_success = false
-- 第一步:创建MQTT客户端
-- mqtt.create(adapter, host, port, ssl)
-- adapter传nil使用默认网卡
local mqtt_client = mqtt.create(nil, mqtt_broker, mqtt_port or 1884)
if not mqtt_client then
log.error("aircloud", "MQTT创建失败")
return false
end
-- 第二步:设置认证信息和事件回调
-- client_id使用IMEI保证唯一性
-- username/password为空表示匿名登录
-- clean_session=false表示不清理session
mqtt_client:auth(mobile.imei() .. "_vl53l1x", "", "", false)
mqtt_client:on(mqtt_event_callback)
-- 第三步:发起TCP连接
if not mqtt_client:connect() then
log.error("aircloud", "MQTT连接失败")
mqtt_client:close()
return false
end
-- 第四步:等待连接确认(CONNACK),最长5秒
local no_timeout, connected_client = sys.waitUntil("MQTT_CONNECT_OK", 5000)
if not no_timeout or not connected_client then
log.warn("aircloud", "MQTT连接超时")
mqtt_client:disconnect()
sys.wait(500)
mqtt_client:close()
return false
end
-- 第五步:发布数据
-- qos=1, retain=0
local payload_json = json.encode(payload_table)
connected_client:publish(topic, payload_json, 1, 0)
log.info("aircloud", "MQTT已发布:", payload_json)
-- 第六步:等待发布结果,最长8秒
sys.waitUntil("MQTT_SENT_OK", 8000)
-- 第七步:断开连接并释放资源
connected_client:disconnect()
sys.wait(500)
connected_client:close()
log.info("aircloud", "MQTT短连接结束,发送" .. (g_mqtt_publish_success and "成功" or "失败"))
return g_mqtt_publish_success
end
-- ==================== API:构建MQTT payload ====================
--[[
构建MQTT上报的JSON payload数据表
aircloud.build_mqtt_payload(sensor_data, fota_info)
本函数生成统一格式的MQTT上报数据,包含以下通用字段:
imei — 设备IMEI号,用于设备识别
project — 项目名称,用于区分不同项目的数据
version — 脚本版本号,用于追踪固件版本
ts — Unix时间戳,记录数据采集时间
@param table sensor_data
含义:传感器数据表(由sensor_vl53l1x.collect_data返回)
是否必选:可选
当传入非nil时,payload增加以下字段:
distance_mm — 平均距离(number
distances — 原始距离数组(table)
valid_samples— 有效帧数(number
@param string fota_info
含义:FOTA升级信息(可选)
是否必选:可选
当传入非nil时,payload增加以下字段:
fota_info — FOTA升级描述字符串
@return table
含义:MQTT上报用的数据表,可直接传入json.encode
@usage
local payload = aircloud.build_mqtt_payload(sensor_data, "FOTA升级成功")
-- 返回: {imei="xxx", project="Air8780P_VL53L1X", version="001.999.000",
-- ts=1784623861, distance_mm=265, distances={267,261,267},
-- valid_samples=3, fota_info="FOTA升级成功"}
]]
function aircloud.build_mqtt_payload(sensor_data, fota_info)
-- 基础payload:所有上报必含设备标识和时间戳
local payload = {
imei = mobile.imei(),
project = PROJECT,
version = VERSION,
ts = os.time(),
}
-- 可选:添加传感器数据字段
if sensor_data then
payload.distance_mm = sensor_data.avg_distance
payload.distances = sensor_data.distance_list
payload.valid_samples = sensor_data.valid_count
end
-- 可选:添加FOTA信息字段
if fota_info then
payload.fota_info = fota_info
end
return payload
end
return aircloud
@@ -0,0 +1,64 @@
--[[
@module drv_led
@summary LED指示灯驱动配置功能模块
@version 1.0
@date 2026.07.23
@author 江访
@usage
本文件为LED指示灯驱动配置功能模块,提供了LED灯在正常工作状态和PSM+休眠状态下的控制逻辑。
LED状态说明:
1、正常工作状态(LED点亮):设备初始化完成、数据采集中、网络通信中
2、PSM+模式(LED熄灭):进入深度休眠前关闭,降低功耗
本文件的对外接口有2个:
1、sys.subscribe("LED_SET_HIGH", led_set_high_func):拉高LED指示灯
2、sys.subscribe("LED_SET_LOW", led_set_low_func):拉低LED指示灯
其他模块通过 sys.publish 控制LED
sys.publish("LED_SET_HIGH", gpio_led) -- 拉高LED
sys.publish("LED_SET_LOW", gpio_led) -- 拉低LED
]]
-- ==================== LED拉高事件处理 ====================
--[[
LED拉高事件处理函数
订阅LED_SET_HIGH消息,收到后拉高指定GPIO引脚。
LED点亮表示设备处于正常工作状态。
@param number gpio_led GPIO引脚号(例如27
]]
local function led_set_high_func(gpio_led)
if not gpio_led then return end
gpio.setup(gpio_led, 1)
gpio.set(gpio_led, 1)
log.info("drv_led", "GPIO" .. gpio_led .. " 拉高")
end
-- ==================== LED拉低事件处理 ====================
--[[
LED拉低事件处理函数
订阅LED_SET_LOW消息,收到后拉低指定GPIO引脚。
LED熄灭表示设备即将进入低功耗模式。
@param number gpio_led GPIO引脚号(例如27
]]
local function led_set_low_func(gpio_led)
if not gpio_led then return end
gpio.set(gpio_led, 0)
log.info("drv_led", "GPIO" .. gpio_led .. " 拉低")
end
-- ==================== 事件订阅 ====================
sys.subscribe("LED_SET_HIGH", led_set_high_func)
sys.subscribe("LED_SET_LOW", led_set_low_func)
log.info("drv_led", "模块已加载,等待LED_SET_HIGH/LED_SET_LOW消息")
-- 如果项目启动后需要LED保持某种状态,直接在此处配置初始状态
-- 此处的配置取决于硬件设计,合宙核心板上电后GPIO默认状态为高阻态
@@ -0,0 +1,92 @@
--[[
@module drv_psm
@summary PSM+模式(pm.power WORK_MODE 3)驱动配置功能模块
@version 1.0
@date 2026.07.23
@author 江访
@usage
本文件为PSM+模式驱动配置功能模块,提供了PSM+模式的配置模板,包括以下几点:
1、在进入PSM+模式前,配置深度休眠定时器(dtimer),设定唤醒时间
2、提供进入PSM+模式的入口函数,包括pm.power(pm.WORK_MODE, 3)、等待和重启兜底
3、在进入PSM+模式前,可以根据实际项目需求配置其他功能引脚
PSM+模式说明:
1、pm.power(pm.WORK_MODE, 3)配置后,并不会立即进入休眠
2、等所有协程都处于阻塞状态时,才会自动进入PSM+模式
3、进入PSM+模式后,RAM掉电,不执行任何脚本代码
4、RTC定时器到达或外部中断唤醒后,系统冷启动
本文件的对外接口只有1个:
1、sys.subscribe("DRV_SET_PSM", set_drv_psm):订阅"DRV_SET_PSM"消息;
其他模块如果需要进入PSM+模式,先发布"DRV_SET_PSM"消息,然后阻塞本task即可。
]]
-- ==================== PSM+入口主函数 ====================
--[[
PSM+模式任务函数
订阅DRV_SET_PSM消息后,在task协程中执行此函数。
函数流程:
1、配置深度休眠定时器(设定唤醒时间)
2、调用pm.power(pm.WORK_MODE, 3)进入PSM+模式
3、等待80秒——如果未成功进入PSM+,强制重启
注意:
1、必须在调用前关闭所有外设(传感器等)
2、必须在调用前拉低LED
3、进入PSM+前需要先配置好深度休眠定时器dtimer
4、PSM+模式下,所有引脚状态保持进入前的电平
@param number sleep_min_s PSM+休眠时间(分钟)
]]
local function psm_task(sleep_min_s)
log.info("drv_psm", "进入PSM+模式,休眠" .. sleep_min_s .. "分钟后唤醒")
-- 设置RTC深度休眠定时器
-- pm.dtimerStart(id, ms)
-- id=0表示使用定时器0,到达时间后系统自动唤醒
-- 此处的时长不要小于80秒
local sleep_ms = sleep_min_s * 60 * 1000
pm.dtimerStart(0, sleep_ms)
log.info("drv_psm", "深度休眠定时器已设置:", sleep_ms, "ms")
-- 在进入PSM+模式前,可以根据实际项目需求配置以下功能项:
-- 1、飞行模式:PSM+模式下飞行模式自动开启,无需手动操作
-- 2、USB功能:内核固件已自动关闭USB功能(2025年3月后版本)
-- 3、AGPIO配置:根据硬件设计配置GPIO24~28的电平状态
-- 详情可参考 lowpower/drv/drv_psm.lua 中的 set_psm_func_item()
-- 配置最低功耗模式为PSM+模式
-- pm.power(pm.WORK_MODE, 3)表示允许系统进入PSM+模式
-- 执行后不会立即进入休眠,等所有协程阻塞后自动进入
pm.power(pm.WORK_MODE, 3)
-- 等待80秒:给内核固件足够时间进入PSM+
-- 如果成功进入PSM+,此后的代码不会执行(RAM掉电)
-- 如果没有进入(例如有协程未阻塞),80秒后强制重启
sys.wait(80000)
log.info("drv_psm", "进入PSM+失败,重启")
rtos.reboot()
end
-- ==================== 事件处理 ====================
--[[
DRV_SET_PSM 事件处理函数
其他模块通过 sys.publish("DRV_SET_PSM", sleep_min_s) 触发:
sleep_min_s参数为休眠时间(分钟)
注意:本订阅处理函数使用sys.taskInit创建独立协程执行psm_task
这是因为pm.power和内部sys.wait需要在协程中运行
]]
local function set_drv_psm(sleep_min_s)
sys.taskInit(psm_task, sleep_min_s or 15)
end
-- ==================== 事件订阅 ====================
sys.subscribe("DRV_SET_PSM", set_drv_psm)
log.info("drv_psm", "模块已加载,等待DRV_SET_PSM消息")
@@ -0,0 +1,16 @@
--[[
@module netdrv_device
@summary 网络驱动设备功能模块
@version 1.0
@date 2026.07.21
@author 江访
@usage
本文件为网络驱动设备功能模块,核心业务逻辑为:
根据Air8780P项目需求,选择并且配置合适的网卡(网络适配器)
本demo使用4G网卡(LWIP_GP
本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行
]]
-- 加载"4G网卡"驱动模块
require "netdrv_4g"
@@ -0,0 +1,331 @@
--[[
@module fota_mgr
@summary libfota2远程升级管理模块,含版本文件管理和首次启动检测
@version 1.0
@date 2026.07.23
@author 江访
@usage
本文件为libfota2远程升级的管理模块,核心业务逻辑为:
1、FOTA升级检查与下载
2、版本文件管理(保存/读取)
3、FOTA升级后的首次启动检测
升级流程:
1. 调用 check_and_upgrade() 检查是否有新版本
2. 有新版 → 下载升级包 → 下载成功返回 true → 调用方重启设备
3. 重启后首次运行 → check_first_boot() 检测到版本不一致 → 上报升级结果
4. 上报完成后调用 clear_version_file() 清除标记
本文件的对外接口有4个:
1、fota_mgr.check_first_boot():检测是否为FOTA升级后的首次启动
2、fota_mgr.check_and_upgrade(max_wait_s):检查并执行FOTA升级
3、fota_mgr.get_last_result():获取最后一次FOTA结果
4、fota_mgr.clear_version_file():清除版本标记文件
]]
-- ==================== 加载扩展库 ====================
-- libfota2: 合宙FOTA升级扩展库
-- 实现HTTP下载升级包的底层逻辑,自动拼接URL和版本号
-- 依赖PRODUCT_KEY在main.lua中定义
local libfota2 = require "libfota2"
-- ==================== 模块表 ====================
-- fota_mgr 为模块对外接口表,所有对外函数注册在此表上
local fota_mgr = {}
-- ==================== 内部状态 ====================
-- g_fota_running: FOTA升级是否正在进行中
-- 用于防止重复调用
local g_fota_running = false
-- g_fota_result: FOTA回调返回码
-- nil=尚未执行,0=成功下载,非0=各种失败原因
-- 用于任务1(PSM+超时管理)判断是否需要等待
local g_fota_result = nil
-- G_FOTA_FILE: 版本标记文件的路径
-- 文件内容格式: "VERSION,PROJECT"
-- 例如: "001.999.000,Air8780P_VL53L1X"
-- 用于检测FOTA升级后的首次启动
local G_FOTA_FILE = "/fota_version.txt"
-- ==================== FOTA回调 ====================
--[[
libfota2升级回调处理函数
当libfota2框架完成升级检查或下载后,自动调用此回调。
回调参数 ret 表示FOTA状态码。
回调完成后发布FOTA_END消息,通知PSM+管理任务:
1、如果 ret==0(成功下载),任务1看到后会让系统进入重启流程
2、如果 ret!=0(失败或无升级),任务1在等待超时后进入PSM+
@param number ret FOTA返回码
0 — 升级包下载成功,需要重启以应用升级
1 — 连接FOTA服务器失败(网络不通或DNS解析失败)
2 — 升级包URL错误(PRODUCT_KEY不匹配)
3 — 与FOTA服务器的连接断开
4 — 接收报文错误(下载不完整或校验失败),也可能是已是最新版本
5 — 版本号格式错误(VERSION不符合XXX.YYY.ZZZ格式)
]]
local function fota_callback(ret)
log.info("fota_mgr", "FOTA回调 ret=", ret)
-- 清除运行中标志
g_fota_running = false
g_fota_result = ret
-- 根据返回码打印详细日志
-- 参考libfota2文档中各返回码的含义
if ret == 0 then
log.info("fota_mgr", "FOTA升级包下载成功,准备重启")
elseif ret == 1 then
log.info("fota_mgr", "FOTA连接失败")
elseif ret == 2 then
log.info("fota_mgr", "FOTA URL错误")
elseif ret == 3 then
log.info("fota_mgr", "FOTA服务器断开")
elseif ret == 4 then
log.info("fota_mgr", "FOTA接收报文错误或已是最新版本")
elseif ret == 5 then
log.info("fota_mgr", "FOTA版本号格式错误")
else
log.info("fota_mgr", "FOTA未知返回码", ret)
end
-- 发布FOTA结束消息,通知PSM+管理任务(任务1)
-- 任务1等待此消息后检查g_fota_result,决定是否重启或进入PSM+
sys.publish("FOTA_END")
end
-- ==================== 版本文件管理(内部函数) ====================
--[[
保存当前版本信息到标记文件
文件中保存当前PROJECT和VERSION,格式:VERSION,PROJECT
下次启动时通过read_version读取并比对,判断是否为FOTA后的首次启动
文件路径:/fota_version.txt(存储在文件系统中,重启后保留)
]]
local function save_version()
local info = VERSION .. "," .. PROJECT
io.writeFile(G_FOTA_FILE, info)
log.info("fota_mgr", "保存版本信息到文件:", info)
end
--[[
从标记文件读取版本信息
@return string 文件内容,格式"VERSION,PROJECT"
例如:"001.999.000,Air8780P_VL53L1X"
@return nil 文件不存在或内容为空
首次运行时文件不存在,属于正常情况
]]
local function read_version()
local data = io.readFile(G_FOTA_FILE)
if data and #data > 0 then
return data
end
return nil
end
-- ==================== API:检测FOTA后首次启动 ====================
--[[
检测是否为FOTA升级后的首次启动
fota_mgr.check_first_boot()
本函数通过比对标记文件中的版本号与当前运行版本号来判断:
1、如果标记文件不存在 → 首次运行,保存版本信息,返回nil
2、如果版本号一致 → 正常启动,更新标记文件,返回nil
3、如果版本号不一致 → FOTA升级后首次启动,返回升级对比信息
本函数应在main.lua中require完成后尽早调用,以便在数据上报时包含升级信息。
调用后会自动更新标记文件为当前版本。
@return string
含义:若为FOTA后首次启动,返回包含升级前后版本信息的字符串
示例值:
"升级前: PROJECT=Air8780P_VL53L1X VERSION=001.999.000 ; 升级后: PROJECT=Air8780P_VL53L1X VERSION=001.999.001"
@return nil
含义:非FOTA首次启动(正常启动或首次运行)
@usage
local fota_info = fota_mgr.check_first_boot()
if fota_info then
log.info("fota_mgr", "检测到FOTA升级:", fota_info)
-- 后续数据上报时包含此信息
end
]]
function fota_mgr.check_first_boot()
-- 读取上次保存的版本信息
local old_info = read_version()
-- 文件不存在说明是第一次运行,直接保存当前版本
if not old_info then
save_version()
return nil
end
-- 解析文件内容:格式为 "VERSION,PROJECT"
-- 使用逗号分隔
local comma_pos = string.find(old_info, ",")
if not comma_pos then
-- 文件格式异常,覆盖写入当前版本
save_version()
return nil
end
-- 提取升级前的版本号和项目名
local old_version = string.sub(old_info, 1, comma_pos - 1)
local old_project = string.sub(old_info, comma_pos + 1)
-- 比对版本号或项目名是否发生变化
if old_version ~= VERSION or old_project ~= PROJECT then
-- 版本变化:说明经历了FOTA升级并重启
local result_str = string.format(
"升级前: PROJECT=%s VERSION=%s ; 升级后: PROJECT=%s VERSION=%s",
old_project, old_version, PROJECT, VERSION
)
log.info("fota_mgr", "检测到FOTA升级后的首次启动", result_str)
save_version()
return result_str
else
-- 版本一致:正常启动
save_version()
return nil
end
end
-- ==================== API:获取FOTA结果 ====================
--[[
获取最后一次FOTA回调的结果
fota_mgr.get_last_result()
本函数供PSM+管理任务(任务1)读取FOTA结果。
任务1在收到FOTA_END消息后调用此函数判断是否需要重启:
0 → FOTA成功,重启设备
非0 → FOTA失败或无更新,进入PSM+
@return number
含义:FOTA返回码
nil — 尚未执行过FOTA检查
0 — 升级包下载成功
1 — 连接失败
2 — URL错误
3 — 服务器断开
4 — 接收报文错误或已是最新版本
5 — 版本号格式错误
@usage
if fota_mgr.get_last_result() == 0 then
log.info("fota_mgr", "FOTA成功,准备重启")
end
]]
function fota_mgr.get_last_result()
return g_fota_result
end
-- ==================== API:执行FOTA升级 ====================
--[[
检查并执行FOTA升级
fota_mgr.check_and_upgrade(max_wait_s)
本函数完成以下工作:
1、保存当前版本信息到标记文件
2、发布FOTA_UPGRADING消息通知PSM+任务延长等待
3、调用libfota2.request发起升级检查/下载
4、等待FOTA结果,最长max_wait_s秒
5、返回是否需要重启
注意:
1、libfota2内部会自动检查是否需要升级
2、如果有新版本,libfota2会自动下载
3、下载完成后fota_callback会被调用,ret=0表示下载成功
4、本函数返回true后,调用方需执行rtos.reboot()
@param number max_wait_s
含义:FOTA等待最长时间(秒)
取值范围:正整数
如果网络状况好,通常10~30秒可完成
建议设置300~600秒(5~10分钟),为弱网环境留出重试时间
是否必选:必选
建议值:600
@return boolean
含义:true=升级包已下载需要重启,false=无升级或升级失败
@usage
local need_reboot = fota_mgr.check_and_upgrade(600)
if need_reboot then
rtos.reboot()
end
]]
function fota_mgr.check_and_upgrade(max_wait_s)
log.info("fota_mgr", "开始检查FOTA升级...")
-- 第一步:保存当前版本(用于下次启动时比对)
save_version()
g_fota_running = true
-- 第二步:通知PSM+管理任务(任务1)延长等待
-- 任务1原本在psm_entry_max_s秒后强制进PSM+
-- 收到FOTA_UPGRADING后会改为等待FOTA_END
sys.publish("FOTA_UPGRADING")
-- 第三步:发起FOTA请求
-- libfota2.request内部流程:
-- 1. 构造URLHTTP GET iot.openluat.com/api/...
-- 2. 下载升级包(如果有)
-- 3. 下载完成后调用fota_callback
-- opts可以传自定义参数,此处传空table使用默认配置
local opts = {}
libfota2.request(fota_callback, opts)
-- 第四步:等待FOTA结果,最长max_wait_s秒
-- 如果超时仍未收到FOTA_END,说明网络不好
sys.waitUntil("FOTA_END", max_wait_s * 1000)
-- 第五步:判断结果
if g_fota_result == 0 then
log.info("fota_mgr", "FOTA升级包已下载,需要重启模块")
return true
end
log.info("fota_mgr", "FOTA检查完成(无升级或升级失败)")
return false
end
-- ==================== API:清除版本标记文件 ====================
--[[
清除版本标记文件
fota_mgr.clear_version_file()
在以下场景需要调用:
1、FOTA升级后首次启动,上报完成升级信息后
2、无网络时跳过上报,需要清除文件以免下次启动再次上报
3、版本文件已不再需要时
清除后,下次启动时check_first_boot()会认为是最新版本,不会再返回FOTA信息。
@usage
fota_mgr.clear_version_file()
]]
function fota_mgr.clear_version_file()
os.remove(G_FOTA_FILE)
log.info("fota_mgr", "已删除版本标记文件")
end
return fota_mgr
@@ -0,0 +1,76 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2026.07.23
@author 江访
@usage
本demo演示Air8780P工业模组+VL53L1X激光测距传感器的低功耗数据采集与上报流程:
1、VL53L1X激光测距传感器数据采集
2、通过合宙iot平台(excloud)和MQTT上报传感器数据
3、支持libfota2远程升级
4、PSM+低功耗模式
更多说明参考本目录下的readme.md文件
]]
--[[
必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
PROJECT:项目名,ascii string类型
可以随便定义,只要不使用,就行
VERSION:项目版本号,ascii string类型
如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
]]
-- main.lua - 程序入口文件
PROJECT = "Air8780P_VL53L1X"
VERSION = "001.999.000"
PRODUCT_KEY = "PhbD7kCQg9Jt7vwCGgtdjQ6jIscQ2gJJ" -- 用于合宙IOT平台FOTA升级,此处仅为示例,实际使用请修改为自己的PRODUCT_KEY
-- 在日志中打印项目名和项目版本号
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)
-- 加载网络驱动设备功能模块(4G网卡)
require "netdrv_device"
-- 加载VL53L1X项目编排模块
-- 该模块会级联加载:
-- drv/drv_led — 驱动层:LED控制(事件驱动)
-- drv/drv_psm — 驱动层:PSM+模式(事件驱动)
-- sensor/sensor_vl53l1x — 应用层:传感器驱动
-- cloud/aircloud — 应用层:云平台上报
-- fota/fota_mgr — 应用层:FOTA升级管理
require "prj_vl53l1x"
-- 用户代码已结束
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
@@ -0,0 +1,34 @@
--[[
@module netdrv_4g
@summary "4G网卡"驱动模块
@version 1.0
@date 2026.07.21
@author 江访
@usage
本文件为4G网卡驱动模块,核心业务逻辑为:
1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
]]
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_GP then
-- 设置DNS服务器IP地址
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
log.info("netdrv_4g", "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")
end
end
-- 订阅"IP_READY"和"IP_LOSE"两种消息
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
-- 在Air780EPM/Air8780P上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
@@ -0,0 +1,237 @@
--[[
@module prj_vl53l1x
@summary Air8780P VL53L1X激光测距传感器低功耗数据采集与上报项目编排模块
@version 2.0
@date 2026.07.23
@author 江访
@usage
本文件为项目主业务编排模块,负责协调各功能模块完成完整业务流程。
三层架构说明:
drv/ 硬件驱动层 — 通过 sys.subscribe 注册,事件驱动
drv_led → LED_SET_HIGH / LED_SET_LOW
drv_psm → DRV_SET_PSM
app/ 应用功能层 — return table,直接调用
sensor_vl53l1x — 传感器驱动封装
aircloud — 云平台数据上报
fota_mgr — FOTA远程升级管理
prj/ 项目编排层 — 本文件,协调业务流程
完整业务流程:
① 发布LED_SET_HIGH → drv_led点亮LED
② 检查FOTA首次启动(fota_mgr.check_first_boot
③ 初始化传感器(sensor_vl53l1x.init
④ 采集传感器数据(sensor_vl53l1x.collect_data
⑤ 关闭传感器(sensor_vl53l1x.close
⑥ 等待4G网络就绪
⑦ 初始化excloud连接(aircloud.init
⑧ 双通道上报(aircloud.report_excloud + report_mqtt
⑨ FOTA升级检查(fota_mgr.check_and_upgrade
⑩ 关闭传感器 + 发布LED_SET_LOW → drv_led熄灭LED
⑪ 发布DRV_SET_PSM → drv_psm进入PSM+
消息协议(发布/订阅):
发布: LED_SET_HIGH(gpio) → drv_led: 拉高LED
发布: LED_SET_LOW(gpio) → drv_led: 拉低LED
发布: DRV_SET_PSM(sleep_min) → drv_psm: 进入PSM+模式
本文件没有对外接口,require "prj_vl53l1x"即可加载运行
]]
-- ==================== 加载应用功能模块(app层,return table直接调用) ====================
-- sensor_vl53l1x: VL53L1X传感器驱动封装,提供init/collect_data/close接口
local sensor_vl53l1x = require "sensor_vl53l1x"
-- aircloud: 云平台数据上报,封装excloud和MQTT双通道上报
local aircloud = require "aircloud"
-- fota_mgr: FOTA远程升级管理,含版本文件管理和首次启动检测
local fota_mgr = require "fota_mgr"
-- ==================== 加载硬件驱动模块(drv层,事件订阅,不返回接口表) ====================
-- drv_led: LED指示灯控制,订阅LED_SET_HIGH/LED_SET_LOW消息
-- 通过sys.publish("LED_SET_HIGH", gpio)控制LED点亮
-- 通过sys.publish("LED_SET_LOW", gpio)控制LED熄灭
require "drv_led"
-- drv_psm: PSM+低功耗模式驱动,订阅DRV_SET_PSM消息
-- 通过sys.publish("DRV_SET_PSM", sleep_min)进入PSM+模式
-- 进入前需要先关闭外设和熄灭LED
require "drv_psm"
-- ==================== 可配置参数 ====================
-- 用户可根据项目需求修改以下参数
-- 这些参数决定了设备的工作行为和功耗表现
local CFG = {
-- PSM+休眠时间(分钟)
-- 唤醒后重新开始完整的数据采集和上报循环
-- 合宙IoT平台建议最多10分钟请求一次,建议≤10
psm_sleep_min_s = 15,
-- 开机到进入PSM+的超时上限(秒)
-- 到时间后不管任务2是否完成,都强制进入PSM+
psm_entry_max_s = 90,
-- FOTA等待最长时间(秒)
-- 超时说明网络不好,进入PSM+下次再试
fota_wait_max_s = 600,
-- 每次唤醒后采集传感器的次数
-- 多次采集取平均值可以降低单次测量误差
sensor_samples = 3,
-- 测距模式
-- "short" — 短距离(~1.36m),抗环境光能力强
-- "standard" — 标准距离(~2.9m),通用场景
-- "long" — 长距离(~4.6m),需较好的环境光条件
sensor_range_mode = "standard",
-- I2C引脚定义(软件I2C模式)
gpio_i2c_scl = 1, -- I2C SCLGPIO1
gpio_i2c_sda = 2, -- I2C SDAGPIO2
gpio_led_ctrl = 27, -- LED指示灯,高电平点亮,PSM+前熄灭
-- MQTT配置(使用合宙测试服务器)
mqtt_broker = "lbsmqtt.airm2m.com",
mqtt_port = 1884,
}
-- ==================== 网络等待工具函数 ====================
--[[
等待4G网络就绪
通过socket.adapter判断默认网卡是否已获得IP地址。
每秒检查一次,同时等待IP_READY消息,兼顾即时性和兜底。
@param number timeout_s 等待超时时间(秒),默认120秒
@return boolean true=网络就绪,false=超时未就绪
]]
local function wait_network_ready(timeout_s)
timeout_s = timeout_s or 120
log.info("prj_vl53l1x", "等待4G网络就绪,最长" .. timeout_s .. "秒...")
for i = 1, timeout_s do
if socket.adapter(socket.dft()) then
log.info("prj_vl53l1x", "4G网络已就绪,IP:", socket.localIP(socket.LWIP_GP))
return true
end
log.warn("prj_vl53l1x", "等待IP_READY", socket.dft())
sys.waitUntil("IP_READY", 1000)
end
log.warn("prj_vl53l1x", "4G网络等待超时(未插卡或无信号),跳过联网操作")
return false
end
-- ==================== 项目主任务 ====================
local function vl53l1x_task()
log.info("prj_vl53l1x", "========== Air8780P VL53L1X 主任务启动 ==========")
-- ==================== 第一步:LED点亮 ====================
-- 通过事件驱动drv_led模块控制GPIO
-- drv_led接收到LED_SET_HIGH消息后,将指定GPIO拉高
sys.publish("LED_SET_HIGH", CFG.gpio_led_ctrl)
-- ==================== 第二步:检查FOTA首次启动 ====================
-- 比对版本标记文件中的版本号与当前运行版本号
-- 如果版本不一致,说明经历了FOTA升级并重启
-- 返回的字符串包含升级前后版本信息,需在上报时附带
local fota_first_boot_info = fota_mgr.check_first_boot()
if fota_first_boot_info then
log.info("prj_vl53l1x", "FOTA升级后首次启动:", fota_first_boot_info)
end
-- ==================== 第三步:初始化传感器 ====================
-- 配置I2C引脚并初始化VL53L1X传感器
-- 如果初始化失败,跳过数据采集步骤
local sensor_ok = sensor_vl53l1x.init(CFG.gpio_i2c_scl, CFG.gpio_i2c_sda, CFG.sensor_range_mode)
-- ==================== 第四步:采集传感器数据 ====================
-- 采集多次后取平均值,降低单次测量误差
local sensor_data = nil
if sensor_ok then
sensor_data = sensor_vl53l1x.collect_data(CFG.sensor_samples)
if sensor_data then
log.info("prj_vl53l1x", string.format("测距结果 = %d mm (有效%d帧)",
sensor_data.avg_distance, sensor_data.valid_count))
else
log.warn("prj_vl53l1x", "测距结果 = 无效(所有帧均未测到目标)")
end
-- 采集完成后立即关闭传感器,降低功耗
sensor_vl53l1x.close()
end
-- ==================== 第五步:等待4G网络就绪 ====================
-- 如果超时,跳过所有需要联网的操作
local net_ok = wait_network_ready()
if net_ok then
-- ==================== 第六步:初始化excloud连接 ====================
-- 连接到合宙IoT平台,等待认证完成
aircloud.init({transport = "tcp", auto_reconnect = true, reconnect_interval = 10, max_reconnect = 3})
-- ==================== 第七步:双通道数据上报 ====================
-- 通过excloud(合宙IoT平台)和MQTT双通道同时上报
local mqtt_topic = mobile.imei() .. "/vl53l1x/up"
local mqtt_payload
if fota_first_boot_info then
-- FOTA升级后首次启动:上报升级信息+传感器数据
local fota_msg = "FOTA升级成功: " .. fota_first_boot_info
aircloud.report_excloud(sensor_data, fota_msg)
mqtt_payload = aircloud.build_mqtt_payload(sensor_data, fota_msg)
-- 上报完成后删除版本文件,避免下次重启再次上报
fota_mgr.clear_version_file()
else
-- 正常启动:仅上报传感器数据
aircloud.report_excloud(sensor_data, nil)
mqtt_payload = aircloud.build_mqtt_payload(sensor_data, nil)
end
aircloud.report_mqtt(mqtt_topic, mqtt_payload, CFG.mqtt_broker, CFG.mqtt_port)
-- ==================== 第八步:FOTA升级检查 ====================
-- 非FOTA后首次启动才检查FOTA升级
if not fota_first_boot_info then
aircloud.report_fota_status("FOTA: 正在检查/升级中", sensor_data)
local need_reboot = fota_mgr.check_and_upgrade(CFG.fota_wait_max_s)
if need_reboot then
aircloud.report_excloud(nil, "FOTA升级包下载成功, 升级前版本: " .. VERSION)
sys.wait(1000)
rtos.reboot()
end
end
else
-- 无网络:跳过所有联网操作,直接进入PSM+
if fota_first_boot_info then
fota_mgr.clear_version_file()
end
log.info("prj_vl53l1x", "无网络,跳过上报和FOTA检查")
end
-- ==================== 第九步:关闭外设并进入PSM+ ====================
-- 关闭传感器
sensor_vl53l1x.close()
-- 熄灭LED(通过事件驱动drv_led)
-- drv_led接收到LED_SET_LOW消息后,将指定GPIO拉低
sys.publish("LED_SET_LOW", CFG.gpio_led_ctrl)
log.info("prj_vl53l1x", "所有业务完成,准备进入PSM+模式,休眠" .. CFG.psm_sleep_min_s .. "分钟")
-- 发布PSM+进入消息,通知drv_psm模块
-- drv_psm接收到DRV_SET_PSM消息后,在独立协程中执行:
-- 1. 设置深度休眠定时器(dtimer)
-- 2. 调用pm.power(pm.WORK_MODE, 3)
-- 3. 等待80秒后如果未进入PSM+则强制重启
-- drv_psm模块内部已有中断唤醒引脚和功能引脚的配置说明
sys.publish("DRV_SET_PSM", CFG.psm_sleep_min_s)
end
-- ==================== 启动项目主任务 ====================
sys.taskInit(vl53l1x_task)
@@ -0,0 +1,517 @@
# Air8780P + VL53L1X 激光测距传感器低功耗数据采集与上报 Demo
## 一、功能模块介绍
### 1.1 核心主程序模块
1. **main.lua** - 主程序入口,负责 PRODUCT_KEY 定义、errDump 开启、网络驱动加载和业务模块调度
2. **app_vl53l1x_main.lua** - 业务逻辑主协调模块,实现双任务架构(PSM+超时管理 + 业务逻辑),调用各模块完成完整流程
3. **sensor_vl53l1x.lua** - VL53L1X 传感器驱动封装模块,含初始化、数据采集、关闭接口
4. **aircloud.lua** - 云平台数据上报模块,封装 excloud 和 MQTT 双通道上报,含 payload 构建
5. **fota_mgr.lua** - FOTA 远程升级管理模块,含版本文件管理、首次启动检测、升级检查
6. **psm_mgr.lua** - PSM+ 低功耗模式管理模块,含 LED 指示控制和深度休眠入口
7. **netdrv_device.lua** - 网络驱动设备模块
8. **netdrv/netdrv_4g.lua** - 4G 网卡驱动
### 1.2 扩展库模块
1. **exs_vl53l1x** - VL53L1X 激光测距传感器扩展库
2. **libfota2** - 远程升级扩展库
3. **excloud** - 合宙 IoT 平台数据交互扩展库
## 二、演示流程介绍
本 demo 采用双任务架构:
- **任务1(PSM+超时管理)**:到设定时间就进入 PSM+,期间等待 FOTA 消息,收到后等待升级结果(最长 10 分钟),超时或失败也进入 PSM+ 下次再试
- **任务2(业务逻辑)**:传感器采集 → 联网 → 双通道上报 → FOTA 检查 → PSM+
开机 → GPIO27 拉高(LED亮) → 检查FOTA后首次启动 → 传感器初始化 → 采集3次测距数据 → 关闭传感器 → 等待4G网络 → excloud连接 → 双通道(aircloud+MQTT)上报 → FOTA检查 → 进入PSM+模式休眠 → 定时唤醒后重复
## 三、硬件准备
### 3.1 硬件清单
- Air8780P 工业模组 × 1
- VL53L1X 激光测距传感器模块 × 1
- SIM 卡 × 14G 联网)
- 母对母杜邦线 × 4
- TYPE-C 数据线 × 1
### 3.2 接线配置
#### 3.2.1 VL53L1X 传感器接线
| Air8780P | VL53L1X |
|----------|---------|
| 3.3V | VIN |
| GND | GND |
| GPIO1 | SCL |
| GPIO2 | SDA |
> 说明:VL53L1X 传感器 I2C 地址默认 0x29,支持软件 I2C 和硬件 I2C 两种模式,本 demo 使用 GPIO1/GPIO2 软件 I2C 模式。
![](https://docs.openluat.com/cdn/image/Air8780P_vl53l1x.png)
#### 3.2.2 指示灯说明
| Air8780P | 功能 |
|----------|------|
| GPIO27 | LED 指示灯,正常工作状态拉高点亮,进入 PSM+ 前拉低熄灭 |
## 四、演示软件环境
### 4.1 开发工具
- [Luatools下载调试工具](https://docs.openluat.com/air780egh/luatos/common/download/) - 固件烧录和代码调试
### 4.2 内核固件
- [点击下载Air780EPM系列最新版本内核固件](https://docs.openluat.com/air780epm/luatos/firmware/780epm_version/)demo所使用的是 LuatOS-SoC_V2046_Air780EPM 1号固件
### 4.3 脚本文件
1. **main.lua** - 主程序入口
2. **prj/prj_vl53l1x.lua** - 项目编排模块(业务协调/事件驱动)
3. **drv/netdrv_device.lua** - 网络驱动设备模块
4. **drv/drv_led.lua** - LED指示灯驱动(事件订阅:LED_SET_HIGH/LED_SET_LOW
5. **drv/drv_psm.lua** - PSM+低功耗模式驱动(事件订阅:DRV_SET_PSM
6. **sensor/sensor_vl53l1x.lua** - VL53L1X 传感器驱动封装模块
7. **cloud/aircloud.lua** - 云平台(excloud + MQTT)数据上报模块
8. **fota/fota_mgr.lua** - FOTA 远程升级管理模块
9. **netdrv/netdrv_4g.lua** - 4G 网卡驱动
9. **exs_vl53l1x** (扩展库) - VL53L1X 测距传感器驱动
10. **libfota2** (扩展库) - 远程升级
11. **excloud** (扩展库) - 合宙 IoT 平台
## 五、演示核心步骤
### 5.1 硬件准备
1. 按照接线表将 VL53L1X 传感器模块连接到 Air8780P 模组
2. 确认 SIM 卡已正确安装
3. 通过 TYPE-C USB 口供电
4. 检查所有接线无误,避免短路
### 5.2 软件配置
`main.lua` 中配置 PRODUCT_KEY
```lua
PRODUCT_KEY = "PhbD7kCQg9Jt7vwCGgtdjQ6jIscQ2gJJ" -- 请改为实际的PRODUCT_KEY
```
`app_vl53l1x_main.lua` 中可根据需求调整以下参数:
```lua
local CFG = {
psm_entry_max_s = 90, -- 开机到进入PSM+的超时上限(秒)
psm_sleep_min_s = 15, -- PSM+休眠时间(分钟),唤醒后重新开始循环
fota_wait_max_s = 600, -- FOTA等待最长时间(秒),默认10分钟
sensor_samples = 3, -- 每次唤醒后采集传感器的次数
}
```
如需更换 MQTT 服务器,修改 `app_vl53l1x_main.lua` 中 CFG 的 `mqtt_broker``mqtt_port`
### 5.3 软件烧录
1. 使用 Luatools 选择最新内核固件
2. 下载本项目所有脚本文件
3. 将固件和脚本一起烧录到设备
4. 烧录成功后设备自动重启后开始运行
### 5.4 功能流程说明
#### 5.4.1 双任务架构
本 demo 同时运行两个任务:
**任务1PSM+超时管理)**:开机后立即启动计时,最大等待 `psm_entry_max_s` 秒。期间等待 FOTA 消息:
- 未收到 FOTA 消息 → 到时间直接进入 PSM+
- 收到 `FOTA_UPGRADING` → 延长等待 FOTA 结果,最长 `fota_wait_max_s` 秒(默认 10 分钟)
- FOTA 成功 → 上报升级结果后重启
- FOTA 失败或超时 → 进入 PSM+,下次唤醒再试
**任务2(业务逻辑)**:完成传感器采集、联网上报、FOTA 检查后进入 PSM+
> 两个任务任意一个先到 PSM+ 入口时,系统在所有任务都阻塞后自动进入深度休眠。
#### 5.4.2 开机自检
1. 设备启动后,GPIO27 拉高(LED 点亮),表示进入正常工作状态
2. 检查版本标记文件:如果检测到 FOTA 升级后的首次启动,会在日志中打印升级前后版本信息
3. 初始化 VL53L1X 传感器,等待固件就绪、写入预设模式配置
#### 5.4.2 数据采集
1. 传感器初始化成功后,开始采集测距数据,默认采集 3 次
2. 每次采集间隔 500ms,日志中会打印每次采集的距离值和状态
3. 计算有效数据的平均值作为本次上报的测距结果
#### 5.4.3 数据上报
1. 等待 4G 网络就绪
2. 连接合宙 IoT 平台(aircloud
3. 通过 aircloud 和 MQTT 双通道上报传感器数据:
- aircloud:上报平均距离
- MQTT:上报 JSON 格式完整数据,Topic 为 `IMEI/vl53l1x/up`
#### 5.4.4 FOTA 升级
1. 非 FOTA 后首次启动时,任务2检查 libfota2 远程升级,同时发布 `FOTA_UPGRADING` 消息通知任务1
2. 任务1收到消息后延长 PSM+ 等待时间,最长 `fota_wait_max_s` 秒(默认 10 分钟)
3. 任务2上报"FOTA: 正在检查/升级中"状态
4. 升级包下载成功后上报升级结果,然后重启模块
5. 重启后首次运行会上报升级前后版本对比信息,上报完成后删除版本标记文件
6. FOTA 超时(如网络不好)→ 进入 PSM+,下次唤醒再试
#### 5.4.5 PSM+ 低功耗
1. 所有业务处理完成后,配置深度休眠定时器
2. GPIO27 拉低(LED 熄灭)
3. 调用 `pm.power(pm.WORK_MODE, 3)` 进入 PSM+ 模式
4. 定时唤醒后系统重新启动,从 main.lua 开始执行
### 5.5 预期效果
- **数据采集**:每次开机自动采集 3 次距离数据,日志实时打印
- **联网上报**aircloud 和 MQTT 双通道同时上报,数据格式完整
- **低功耗**:上报完成后进入 PSM+ 模式,功耗降至 uA 级别
- **定时唤醒**:默认 15 分钟(可配置)后唤醒,重复整个流程
- **日志如下:**
```
[2026-07-21 16:50:57.264][000000000.000] main_entry 708:SDK base line V017_p001.026
[2026-07-21 16:50:57.267][000000000.007] am_service_init 1372:Air780EPM_A11
[2026-07-21 16:50:57.268][000000000.007] am_get_chip_type 868:6bef6,8,4,31,8,EC718PM
[2026-07-21 16:50:57.270][000000000.007] am_service_init 1380:APB MP 102400000
[2026-07-21 16:50:57.273][000000000.048] bsp_user_init_io 417:io volt 3.3v 21
[2026-07-21 16:50:57.276][000000000.048] BSP_CustomInit 558:hardfault mode init 4
[2026-07-21 16:50:57.278][000000000.049] Uart_ChangeBR 1461:uart0, 6000000 6028985 26000000 69
[2026-07-21 16:50:57.284][000000000.070] I/pm poweron: Power/Reset
[2026-07-21 16:50:57.287][000000000.070] luat_pm_get_poweron_reason 336:ap 1, cp 1
[2026-07-21 16:50:57.290][000000000.070] I/pm poweron reason: 0 0 0
[2026-07-21 16:50:57.293][000000000.178] self_info 125:model Air780EPM_A11 imei 864317083866528 dbversion 0x405e3b06
[2026-07-21 16:50:57.295][000000000.178] self_info 127:firmware[1] BASIC fs 168kbyte script 256kbyte
[2026-07-21 16:50:57.300][000000000.178] I/main LuatOS@Air780EPM base 26.04 bsp V2046 32bit
[2026-07-21 16:50:57.304][000000000.178] I/main ROM Build: Jul 7 2026 20:39:17
[2026-07-21 16:50:57.307][000000000.180] W/pins /luadb/pins_air780epm.json not exist!!
[2026-07-21 16:50:57.311][000000000.182] D/main loadlibs luavm 1048568 14784 14784
[2026-07-21 16:50:57.315][000000000.183] D/main loadlibs sys 2366664 82956 89556
[2026-07-21 16:50:57.318][000000000.183] D/main loadlibs psram 2366664 82956 89556
[2026-07-21 16:50:57.320][000000000.200] I/user.main Air8780P_VL53L1X 001.999.000
[2026-07-21 16:50:57.322][000000000.236] D/user.libfota2 version -> 202607021200
[2026-07-21 16:50:57.327][000000000.272] D/user.httpplus version -> 202607021200
[2026-07-21 16:50:57.330][000000000.284] D/user.exmtn version -> 202607021200
[2026-07-21 16:50:57.332][000000000.289] D/user.excloud version -> 202607091431
[2026-07-21 16:50:57.334][000000000.293] I/user.app_vl53l1x ========== Air8780P VL53L1X 主任务启动 ==========
[2026-07-21 16:50:57.337][000000000.294] I/user.app_vl53l1x 配置: wait=60s psm=5min samples=3
[2026-07-21 16:50:57.343][000000000.294] I/user.app_vl53l1x GPIO27 HIGH
[2026-07-21 16:50:57.347][000000000.301] I/user.app_vl53l1x 保存版本信息到文件: 001.999.000,Air8780P_VL53L1X
[2026-07-21 16:50:57.373][000000000.302] I/user.app_vl53l1x 正在初始化VL53L1X传感器...
[2026-07-21 16:50:57.384][000000000.318] I/user.exs_vl53l1x 固件就绪 0x00E5=0x03
[2026-07-21 16:50:57.394][000000000.436] I/user.exs_vl53l1x 预设模式配置写入完成 (mode=standard)
[2026-07-21 16:50:57.402][000000000.440] I/user.exs_vl53l1x 芯片 MID=0xEA MT=0xCC
[2026-07-21 16:50:57.418][000000000.642] I/user.exs_vl53l1x 初始化完成,Standard Ranging 测距已启动
[2026-07-21 16:50:57.424][000000000.643] I/user.app_vl53l1x VL53L1X初始化成功
[2026-07-21 16:50:57.429][000000000.643] I/user.app_vl53l1x 开始采集传感器数据,采集次数=3
[2026-07-21 16:50:57.878][000000001.195] I/user.app_vl53l1x 采集[1/3] 距离=267mm 状态=测距成功
[2026-07-21 16:50:58.147][000000001.704] I/user.app_vl53l1x 采集[2/3] 距离=261mm 状态=测距成功
[2026-07-21 16:50:58.158][000000002.215] I/user.app_vl53l1x 采集[3/3] 距离=267mm 状态=测距成功
[2026-07-21 16:50:58.166][000000002.215] I/user.app_vl53l1x 采集完成: 有效3帧, 平均距离=265mm
[2026-07-21 16:50:58.177][000000002.216] I/user.app_vl53l1x 测距结果 = 265 mm (有效3帧, 原始数据=267,261,267)
[2026-07-21 16:50:58.188][000000002.216] I/user.app_vl53l1x 关闭VL53L1X传感器
[2026-07-21 16:50:58.196][000000002.222] I/user.exs_vl53l1x 传感器已关闭
[2026-07-21 16:50:58.208][000000002.222] I/user.app_vl53l1x 等待4G网络就绪...
[2026-07-21 16:50:58.214][000000002.222] W/user.app_vl53l1x 等待IP_READY 1 1
[2026-07-21 16:50:58.465][000000002.250] I/mobile sim0 sms ready
[2026-07-21 16:50:58.474][000000002.251] D/mobile cid1, state0
[2026-07-21 16:50:58.483][000000002.252] D/mobile bearer act 0, result 0
[2026-07-21 16:50:58.493][000000002.252] D/mobile NETIF_LINK_ON -> IP_READY
[2026-07-21 16:50:58.503][000000002.253] I/user.netdrv_4g IP_READY 10.51.56.19 255.255.255.255 0.0.0.0 nil
[2026-07-21 16:50:58.522][000000002.254] I/user.app_vl53l1x 4G网络已就绪 10.51.56.19 255.255.255.255 0.0.0.0 nil
[2026-07-21 16:50:58.538][000000002.255] I/user.app_vl53l1x 正在初始化excloud...
[2026-07-21 16:50:58.549][000000002.255] W/user.excloud.setup 不再需要主动配置use_getip
[2026-07-21 16:50:58.562][000000002.255] W/user.excloud.setup 不再需要主动配置device_type
[2026-07-21 16:50:58.582][000000002.257] I/mobile l_mobile_muid called
[2026-07-21 16:50:58.587][000000002.263] I/mobile l_mobile_muid ret=32
[2026-07-21 16:50:58.600][000000002.263] I/user.[excloud]4G设备 IMEI: 864317083866528 MUID: 20260702152525A860510A9885483051
[2026-07-21 16:50:58.613][000000002.265] I/user.[excloud]setup 初始化成功 设备ID: 864317083866528
[2026-07-21 16:50:58.626][000000002.265] I/user.[excloud]首次连接,获取服务器信息...
[2026-07-21 16:50:58.635][000000002.266] I/mobile l_mobile_muid called
[2026-07-21 16:50:58.652][000000002.267] I/mobile l_mobile_muid ret=32
[2026-07-21 16:50:58.670][000000002.268] I/user.[excloud]getip 类型: 3 key: unusedkey-864317083866528-20260702152525A860510A9885483051
[2026-07-21 16:50:58.680][000000002.278] D/socket connect to gps.openluat.com,443
[2026-07-21 16:50:58.694][000000002.279] dns_run 676:gps.openluat.com state 0 id 1 ipv6 0 use dns server0, try 0
[2026-07-21 16:50:58.707][000000002.297] D/mobile TIME_SYNC 0 tm 1784623860
[2026-07-21 16:50:58.716][000000002.321] dns_run 693:dns all done ,now stop
[2026-07-21 16:50:58.936][000000003.217] I/user.httpplus 服务器已完成响应
[2026-07-21 16:50:58.945][000000003.219] I/user.[excloud]getip响应 HTTP: 200 Body:
[2026-07-21 16:50:58.950][000000003.220] {"msg":"ok","conninfo":{"ipv4":"124.71.128.165","port":9108,"auth_key":"PhbD7kCQg9Jt7vwCGgtdjQ6jIscQ2gJJ"},"imginfo":{"url":"https://api.luatos.com/iot/air_up/image","data_key":"f","data_param":{"key":"7nTSPfh2Pcporb2dNDrS2jforVJaacDWfQbuA","tip":""}},"audinfo":{"url":"https://api.luatos.com/iot/air_up/audio","data_key":"f","data_param":{"key":"7nTSPfh2Pcporb2dNDrS2jforVJaacDWfQbuA","tip":""}},"mtninfo":{"url":"https://api.luatos.com/iot/air_up/file","data_key":"f","data_param":{"key":"7nTSPfh2Pcporb2dNDrS2jforVJaacDWfQbuA","tip":""}}}
[2026-07-21 16:50:58.968][000000003.221] I/user.[excloud]TCP/UDP连接信息 host: 124.71.128.165 port: 9108 key: nil
[2026-07-21 16:50:58.973][000000003.221] I/user.[excloud]获取到图片上传信息
[2026-07-21 16:50:58.979][000000003.222] I/user.[excloud]获取到音频上传信息
[2026-07-21 16:50:58.994][000000003.222] I/user.[excloud]获取到运维日志上传信息
[2026-07-21 16:50:59.005][000000003.222] W/user.[excloud]未获取到二维码信息
[2026-07-21 16:50:59.012][000000003.223] I/user.[excloud]自动获取到auth_key
[2026-07-21 16:50:59.022][000000003.223] I/user.[excloud]getip 更新配置: 124.71.128.165 9108
[2026-07-21 16:50:59.029][000000003.223] I/user.[excloud]getip 成功: true
[2026-07-21 16:50:59.038][000000003.224] I/user.[excloud]服务器信息获取成功 host: 124.71.128.165 port: 9108 transport: tcp
[2026-07-21 16:50:59.043][000000003.224] I/user.[excloud]创建TCP连接
[2026-07-21 16:50:59.056][000000003.225] D/socket connect to 124.71.128.165,9108
[2026-07-21 16:50:59.067][000000003.227] I/user.[excloud]TCP连接结果 true false
[2026-07-21 16:50:59.071][000000003.227] I/user.[excloud]excloud service started
[2026-07-21 16:50:59.081][000000003.228] I/user.[excloud]excloud 自动心跳已启动,间隔 300 秒
[2026-07-21 16:50:59.088][000000003.282] I/user.[excloud]TCP socket cb userdata: 0C185A00 33554449 0
[2026-07-21 16:50:59.093][000000003.282] I/user.[excloud]TCP socket TCP连接成功
[2026-07-21 16:50:59.103][000000003.283] I/user.app_vl53l1x excloud连接成功
[2026-07-21 16:50:59.111][000000003.283] I/mobile l_mobile_muid called
[2026-07-21 16:50:59.117][000000003.285] I/mobile l_mobile_muid ret=32
[2026-07-21 16:50:59.120][000000003.285] I/user.[excloud] 发送鉴权请求
[2026-07-21 16:50:59.126][000000003.286] I/user.[excloud]构建发送数据 field: 16 type: 3 value: PhbD7kCQg9Jt7vwCGgtdjQ6jIscQ2gJJ-864317083866528-20260702152525A860510A9885483051
[2026-07-21 16:50:59.134][000000003.287] I/user.[excloud]tlv发送数据长度4 85
[2026-07-21 16:50:59.137][000000003.287] I/user.[excloud]构建消息头 seq: 2 len: 85 flags: 18 dev: 0186431708386652
[2026-07-21 16:50:59.147][000000003.290] I/user.app_vl53l1x excloud发送成功, 流水号: 2
[2026-07-21 16:50:59.151][000000003.290] I/user.[excloud]数据发送成功 101 字节
[2026-07-21 16:50:59.155][000000003.351] I/user.[excloud]TCP socket cb userdata: 0C185A00 33554450 0
[2026-07-21 16:50:59.161][000000003.352] I/user.[excloud]TCP socket 发送完成
[2026-07-21 16:50:59.165][000000003.411] I/user.[excloud]TCP socket cb userdata: 0C185A00 33554452 0
[2026-07-21 16:50:59.171][000000003.412] I/user.[excloud]TCP socket 收到数据 22 字节 01864317083866520001000600000000301100026F6B 44
[2026-07-21 16:50:59.181][000000003.413] I/user.[excloud]解析消息头 设备ID: 864317083866528 序列号: 1 消息长度: 6 协议版本: 0 需要回复: false UDP承载: false 包含auth_key: false
[2026-07-21 16:50:59.187][000000003.414] I/user.[excloud]鉴权成功 ok
[2026-07-21 16:50:59.193][000000003.415] I/user.app_vl53l1x excloud认证成功
[2026-07-21 16:50:59.198][000000003.417] I/user.[excloud]构建发送数据 field: 20 type: 5 value: 265
[2026-07-21 16:50:59.204][000000003.418] I/user.[excloud]tlv发送数据长度4 7
[2026-07-21 16:50:59.207][000000003.419] I/user.[excloud]构建消息头 seq: 3 len: 7 flags: 2 dev: 0186431708386652
[2026-07-21 16:50:59.211][000000003.421] I/user.app_vl53l1x excloud发送成功, 流水号: 3
[2026-07-21 16:50:59.215][000000003.421] I/user.[excloud]数据发送成功 23 字节
[2026-07-21 16:50:59.222][000000003.422] I/user.app_vl53l1x excloud数据上报成功
[2026-07-21 16:50:59.227][000000003.424] I/user.app_vl53l1x MQTT上报数据到: 864317083866528/vl53l1x/up
[2026-07-21 16:50:59.231][000000003.428] dns_run 676:lbsmqtt.airm2m.com state 0 id 2 ipv6 0 use dns server0, try 0
[2026-07-21 16:50:59.238][000000003.461] I/user.[excloud]TCP socket cb userdata: 0C185A00 33554450 0
[2026-07-21 16:50:59.243][000000003.462] I/user.[excloud]TCP socket 发送完成
[2026-07-21 16:50:59.247][000000003.471] dns_run 693:dns all done ,now stop
[2026-07-21 16:50:59.313][000000003.599] I/user.app_vl53l1x MQTT事件: conack nil
[2026-07-21 16:50:59.322][000000003.599] I/user.app_vl53l1x MQTT连接成功
[2026-07-21 16:50:59.328][000000003.601] I/user.app_vl53l1x MQTT已发布: {"distance_mm":265,"project":"Air8780P_VL53L1X","distances":[267,261,267],"imei":"864317083866528","valid_samples":3,"ts":1784623861,"version":"001.999.000"}
[2026-07-21 16:50:59.391][000000003.679] I/user.app_vl53l1x MQTT事件: sent 1
[2026-07-21 16:50:59.399][000000003.679] I/user.app_vl53l1x MQTT数据发送成功
[2026-07-21 16:50:59.405][000000003.681] I/mqtt mqtt closing socket netc:c1b7f28 mqtt_state:3
[2026-07-21 16:50:59.414][000000003.682] I/user.app_vl53l1x MQTT事件: disconnect 0
[2026-07-21 16:50:59.418][000000003.682] W/user.app_vl53l1x MQTT断开
[2026-07-21 16:50:59.429][000000003.683] I/user.app_vl53l1x MQTT事件: close nil
[2026-07-21 16:50:59.899][000000004.181] W/mqtt network_tx ret -1, closing socket
[2026-07-21 16:50:59.903][000000004.182] I/mqtt mqtt closing socket netc:c1b7f28 mqtt_state:0
[2026-07-21 16:50:59.907][000000004.182] I/mqtt mqtt closing socket netc:c1b7f28 mqtt_state:0
[2026-07-21 16:50:59.912][000000004.183] I/user.app_vl53l1x MQTT短连接结束,发送 成功
[2026-07-21 16:50:59.919][000000004.183] I/user.app_vl53l1x 开始检查FOTA升级...
[2026-07-21 16:50:59.924][000000004.186] I/user.app_vl53l1x 保存版本信息到文件: 001.999.000,Air8780P_VL53L1X
[2026-07-21 16:50:59.929][000000004.186] I/user.[excloud]构建发送数据 field: 20 type: 5 value: FOTA: 正在检查/升级中
[2026-07-21 16:50:59.940][000000004.188] I/user.[excloud]tlv发送数据长度4 32
[2026-07-21 16:50:59.944][000000004.188] I/user.[excloud]构建消息头 seq: 4 len: 32 flags: 2 dev: 0186431708386652
[2026-07-21 16:50:59.949][000000004.190] I/user.app_vl53l1x excloud发送成功, 流水号: 4
[2026-07-21 16:50:59.953][000000004.191] I/user.[excloud]数据发送成功 48 字节
[2026-07-21 16:50:59.957][000000004.191] I/user.app_vl53l1x 已上报FOTA升级状态
[2026-07-21 16:50:59.962][000000004.192] I/user.app_vl53l1x FOTA期间同时上报传感器数据: 265 mm
[2026-07-21 16:50:59.970][000000004.194] I/user.libfota2.url GET http://iot.openluat.com/api/site/firmware_upgrade?imei=864317083866528&project_key=PhbD7kCQg9Jt7vwCGgtdjQ6jIscQ2gJJ&firmware_name=Air8780P_VL53L1X_LuatOS-SoC_Air780EPM&version=2046.001.000
[2026-07-21 16:50:59.974][000000004.194] I/user.libfota2.imei/mac imei=864317083866528
[2026-07-21 16:50:59.978][000000004.195] I/user.libfota2.project_key PhbD7kCQg9Jt7vwCGgtdjQ6jIscQ2gJJ
[2026-07-21 16:50:59.982][000000004.195] I/user.libfota2.firmware_name Air8780P_VL53L1X_LuatOS-SoC_Air780EPM
[2026-07-21 16:50:59.985][000000004.195] I/user.libfota2.version 2046.001.000
[2026-07-21 16:50:59.988][000000004.197] dns_run 676:iot.openluat.com state 0 id 3 ipv6 0 use dns server0, try 0
[2026-07-21 16:50:59.995][000000004.241] dns_run 693:dns all done ,now stop
[2026-07-21 16:51:00.003][000000004.243] I/user.[excloud]TCP socket cb userdata: 0C185A00 33554450 0
[2026-07-21 16:51:00.006][000000004.243] I/user.[excloud]TCP socket 发送完成
[2026-07-21 16:51:00.149][000000004.393] I/fota write common data
[2026-07-21 16:51:00.573][000000004.809] I/fota common data done, now checking 0
[2026-07-21 16:51:00.583][000000004.815] I/fota common data md5 ok
[2026-07-21 16:51:00.586][000000004.815] I/fota only common data
[2026-07-21 16:51:00.590][000000004.815] I/fota 脚本地址匹配
[2026-07-21 16:51:00.594][000000004.849] I/fota fota type 0 ok!, wait reboot
[2026-07-21 16:51:00.599][000000004.852] I/http http close c1d7174
[2026-07-21 16:51:00.604][000000004.853] I/user.app_vl53l1x FOTA回调 ret= 0
[2026-07-21 16:51:00.611][000000004.853] I/user.app_vl53l1x FOTA升级包下载成功,准备重启
[2026-07-21 16:51:00.615][000000004.854] I/user.app_vl53l1x FOTA升级包已下载,重启模块
[2026-07-21 16:51:00.619][000000004.855] I/user.[excloud]构建发送数据 field: 20 type: 5 value: FOTA升级包下载成功, 升级前版本: 001.999.000
[2026-07-21 16:51:00.623][000000004.856] I/user.[excloud]tlv发送数据长度4 59
[2026-07-21 16:51:00.627][000000004.856] I/user.[excloud]构建消息头 seq: 5 len: 59 flags: 2 dev: 0186431708386652
[2026-07-21 16:51:00.633][000000004.859] I/user.app_vl53l1x excloud发送成功, 流水号: 5
[2026-07-21 16:51:00.646][000000004.859] I/user.[excloud]数据发送成功 75 字节
[2026-07-21 16:51:00.650][000000004.859] I/user.app_vl53l1x excloud数据上报成功
[2026-07-21 16:51:00.654][000000004.901] I/user.[excloud]TCP socket cb userdata: 0C185A00 33554450 0
[2026-07-21 16:51:00.657][000000004.902] I/user.[excloud]TCP socket 发送完成
[2026-07-21 16:51:01.663] 工具提示: diag com USB 断开连接 COM12 CommError,[WinError 22] 设备不识别此命令。
[2026-07-21 16:51:03.048] 工具提示: soc log port COM10打开成功
[2026-07-21 16:51:03.147] 工具提示: ap log port COM12打开成功
[2026-07-21 16:51:03.170] 工具提示: 用户虚拟串口 COM11
[2026-07-21 16:51:04.039][000000000.000] main_entry 708:SDK base line V017_p001.026
[2026-07-21 16:51:04.045][000000000.007] am_service_init 1372:Air780EPM_A11
[2026-07-21 16:51:04.050][000000000.007] am_get_chip_type 868:6bef6,8,4,31,8,EC718PM
[2026-07-21 16:51:04.064][000000000.007] am_service_init 1380:APB MP 102400000
[2026-07-21 16:51:04.078][000000000.048] bsp_user_init_io 417:io volt 3.3v 21
[2026-07-21 16:51:04.088][000000000.049] BSP_CustomInit 558:hardfault mode init 4
[2026-07-21 16:51:04.098][000000000.049] Uart_ChangeBR 1461:uart0, 6000000 6028985 26000000 69
[2026-07-21 16:51:04.105][000000000.070] I/pm poweron: Power/Reset
[2026-07-21 16:51:04.110][000000000.071] luat_pm_get_poweron_reason 336:ap 3, cp 2
[2026-07-21 16:51:04.121][000000000.071] I/pm poweron reason: 0 0 3
[2026-07-21 16:51:04.129][000000000.180] self_info 125:model Air780EPM_A11 imei 864317083866528 dbversion 0x405e3b06
[2026-07-21 16:51:04.144][000000000.180] self_info 127:firmware[1] BASIC fs 168kbyte script 256kbyte
[2026-07-21 16:51:04.151][000000000.181] I/main LuatOS@Air780EPM base 26.04 bsp V2046 32bit
[2026-07-21 16:51:04.156][000000000.181] I/main ROM Build: Jul 7 2026 20:39:17
[2026-07-21 16:51:04.161][000000000.183] W/pins /luadb/pins_air780epm.json not exist!!
[2026-07-21 16:51:04.177][000000000.185] D/main loadlibs luavm 1048568 14784 14784
[2026-07-21 16:51:04.184][000000000.185] D/main loadlibs sys 2366664 82956 89556
[2026-07-21 16:51:04.191][000000000.185] D/main loadlibs psram 2366664 82956 89556
[2026-07-21 16:51:04.206][000000000.203] I/user.main Air8780P_VL53L1X 001.999.001
[2026-07-21 16:51:04.216][000000000.241] D/user.libfota2 version -> 202607021200
[2026-07-21 16:51:04.228][000000000.276] D/user.httpplus version -> 202607021200
[2026-07-21 16:51:04.236][000000000.290] D/user.exmtn version -> 202607021200
[2026-07-21 16:51:04.249][000000000.294] D/user.excloud version -> 202607091431
[2026-07-21 16:51:04.272][000000000.298] I/user.app_vl53l1x ========== Air8780P VL53L1X 主任务启动 ==========
[2026-07-21 16:51:04.286][000000000.299] I/user.app_vl53l1x 配置: wait=60s psm=5min samples=3
[2026-07-21 16:51:04.300][000000000.299] I/user.app_vl53l1x GPIO27 HIGH
[2026-07-21 16:51:04.308][000000000.302] I/user.app_vl53l1x 检测到FOTA升级后的首次启动 升级前: PROJECT=Air8780P_VL53L1X VERSION=001.999.000 ; 升级后: PROJECT=Air8780P_VL53L1X VERSION=001.999.001
[2026-07-21 16:51:04.314][000000000.308] I/user.app_vl53l1x 保存版本信息到文件: 001.999.001,Air8780P_VL53L1X
[2026-07-21 16:51:04.321][000000000.309] I/user.app_vl53l1x FOTA升级后首次启动: 升级前: PROJECT=Air8780P_VL53L1X VERSION=001.999.000 ; 升级后: PROJECT=Air8780P_VL53L1X VERSION=001.999.001
[2026-07-21 16:51:04.336][000000000.309] I/user.app_vl53l1x 正在初始化VL53L1X传感器...
[2026-07-21 16:51:04.342][000000000.327] I/user.exs_vl53l1x 固件就绪 0x00E5=0x03
[2026-07-21 16:51:04.347][000000000.446] I/user.exs_vl53l1x 预设模式配置写入完成 (mode=standard)
[2026-07-21 16:51:04.361][000000000.449] I/user.exs_vl53l1x 芯片 MID=0xEA MT=0xCC
[2026-07-21 16:51:04.369][000000000.652] I/user.exs_vl53l1x 初始化完成,Standard Ranging 测距已启动
[2026-07-21 16:51:04.375][000000000.652] I/user.app_vl53l1x VL53L1X初始化成功
[2026-07-21 16:51:04.382][000000000.652] I/user.app_vl53l1x 开始采集传感器数据,采集次数=3
[2026-07-21 16:51:04.868][000000001.205] I/user.app_vl53l1x 采集[1/3] 距离=260mm 状态=测距成功
[2026-07-21 16:51:05.189][000000001.713] I/user.app_vl53l1x 采集[2/3] 距离=258mm 状态=测距成功
[2026-07-21 16:51:05.556][000000002.194] I/mobile sim0 sms ready
[2026-07-21 16:51:05.572][000000002.195] D/mobile cid1, state0
[2026-07-21 16:51:05.585][000000002.196] D/mobile bearer act 0, result 0
[2026-07-21 16:51:05.593][000000002.196] D/mobile NETIF_LINK_ON -> IP_READY
[2026-07-21 16:51:05.614][000000002.197] I/user.netdrv_4g IP_READY 10.32.104.76 255.255.255.255 0.0.0.0 nil
[2026-07-21 16:51:05.619][000000002.227] I/user.app_vl53l1x 采集[3/3] 距离=257mm 状态=测距成功
[2026-07-21 16:51:05.628][000000002.228] I/user.app_vl53l1x 采集完成: 有效3帧, 平均距离=258mm
[2026-07-21 16:51:05.640][000000002.229] I/user.app_vl53l1x 测距结果 = 258 mm (有效3帧, 原始数据=260,258,257)
[2026-07-21 16:51:05.654][000000002.229] I/user.app_vl53l1x 关闭VL53L1X传感器
[2026-07-21 16:51:05.669][000000002.234] I/user.exs_vl53l1x 传感器已关闭
[2026-07-21 16:51:05.682][000000002.234] I/user.app_vl53l1x 等待4G网络就绪...
[2026-07-21 16:51:05.695][000000002.234] I/user.app_vl53l1x 4G网络已就绪 10.32.104.76 255.255.255.255 0.0.0.0 nil
[2026-07-21 16:51:05.712][000000002.235] I/user.app_vl53l1x 正在初始化excloud...
[2026-07-21 16:51:05.724][000000002.235] W/user.excloud.setup 不再需要主动配置use_getip
[2026-07-21 16:51:05.734][000000002.236] W/user.excloud.setup 不再需要主动配置device_type
[2026-07-21 16:51:05.745][000000002.237] I/mobile l_mobile_muid called
[2026-07-21 16:51:05.753][000000002.239] I/mobile l_mobile_muid ret=32
[2026-07-21 16:51:05.768][000000002.239] I/user.[excloud]4G设备 IMEI: 864317083866528 MUID: 20260702152525A860510A9885483051
[2026-07-21 16:51:05.775][000000002.242] I/user.[excloud]setup 初始化成功 设备ID: 864317083866528
[2026-07-21 16:51:05.782][000000002.242] I/user.[excloud]首次连接,获取服务器信息...
[2026-07-21 16:51:05.794][000000002.243] I/mobile l_mobile_muid called
[2026-07-21 16:51:05.801][000000002.244] I/mobile l_mobile_muid ret=32
[2026-07-21 16:51:05.816][000000002.245] I/user.[excloud]getip 类型: 3 key: unusedkey-864317083866528-20260702152525A860510A9885483051
[2026-07-21 16:51:05.822][000000002.252] D/socket connect to gps.openluat.com,443
[2026-07-21 16:51:05.827][000000002.253] dns_run 676:gps.openluat.com state 0 id 1 ipv6 0 use dns server0, try 0
[2026-07-21 16:51:05.836][000000002.254] D/mobile TIME_SYNC 0 tm 1784623867
[2026-07-21 16:51:05.842][000000002.290] dns_run 693:dns all done ,now stop
[2026-07-21 16:51:05.848][000000003.123] I/user.httpplus 服务器已完成响应
[2026-07-21 16:51:05.862][000000003.125] I/user.[excloud]getip响应 HTTP: 200 Body:
[2026-07-21 16:51:05.867][000000003.126] {"msg":"ok","conninfo":{"ipv4":"124.71.128.165","port":9108,"auth_key":"PhbD7kCQg9Jt7vwCGgtdjQ6jIscQ2gJJ"},"imginfo":{"url":"https://api.luatos.com/iot/air_up/image","data_key":"f","data_param":{"key":"7nTSPfh2Pcporb2dNDrS2jforVJaacDWfQbuA","tip":""}},"audinfo":{"url":"https://api.luatos.com/iot/air_up/audio","data_key":"f","data_param":{"key":"7nTSPfh2Pcporb2dNDrS2jforVJaacDWfQbuA","tip":""}},"mtninfo":{"url":"https://api.luatos.com/iot/air_up/file","data_key":"f","data_param":{"key":"7nTSPfh2Pcporb2dNDrS2jforVJaacDWfQbuA","tip":""}}}
[2026-07-21 16:51:05.877][000000003.127] I/user.[excloud]TCP/UDP连接信息 host: 124.71.128.165 port: 9108 key: nil
[2026-07-21 16:51:05.888][000000003.127] I/user.[excloud]获取到图片上传信息
[2026-07-21 16:51:05.898][000000003.128] I/user.[excloud]获取到音频上传信息
[2026-07-21 16:51:05.912][000000003.128] I/user.[excloud]获取到运维日志上传信息
[2026-07-21 16:51:05.917][000000003.129] W/user.[excloud]未获取到二维码信息
[2026-07-21 16:51:05.923][000000003.129] I/user.[excloud]自动获取到auth_key
[2026-07-21 16:51:05.944][000000003.129] I/user.[excloud]getip 更新配置: 124.71.128.165 9108
[2026-07-21 16:51:05.951][000000003.130] I/user.[excloud]getip 成功: true
[2026-07-21 16:51:05.958][000000003.130] I/user.[excloud]服务器信息获取成功 host: 124.71.128.165 port: 9108 transport: tcp
[2026-07-21 16:51:05.977][000000003.131] I/user.[excloud]创建TCP连接
[2026-07-21 16:51:05.981][000000003.132] D/socket connect to 124.71.128.165,9108
[2026-07-21 16:51:05.987][000000003.133] I/user.[excloud]TCP连接结果 true false
[2026-07-21 16:51:05.997][000000003.133] I/user.[excloud]excloud service started
[2026-07-21 16:51:06.008][000000003.134] I/user.[excloud]excloud 自动心跳已启动,间隔 300 秒
[2026-07-21 16:51:06.018][000000003.170] I/user.[excloud]TCP socket cb userdata: 0C1859C8 33554449 0
[2026-07-21 16:51:06.024][000000003.170] I/user.[excloud]TCP socket TCP连接成功
[2026-07-21 16:51:06.028][000000003.171] I/user.app_vl53l1x excloud连接成功
[2026-07-21 16:51:06.038][000000003.171] I/mobile l_mobile_muid called
[2026-07-21 16:51:06.048][000000003.173] I/mobile l_mobile_muid ret=32
[2026-07-21 16:51:06.053][000000003.173] I/user.[excloud] 发送鉴权请求
[2026-07-21 16:51:06.056][000000003.173] I/user.[excloud]构建发送数据 field: 16 type: 3 value: PhbD7kCQg9Jt7vwCGgtdjQ6jIscQ2gJJ-864317083866528-20260702152525A860510A9885483051
[2026-07-21 16:51:06.061][000000003.175] I/user.[excloud]tlv发送数据长度4 85
[2026-07-21 16:51:06.069][000000003.175] I/user.[excloud]构建消息头 seq: 2 len: 85 flags: 18 dev: 0186431708386652
[2026-07-21 16:51:06.082][000000003.177] I/user.app_vl53l1x excloud发送成功, 流水号: 2
[2026-07-21 16:51:06.087][000000003.178] I/user.[excloud]数据发送成功 101 字节
[2026-07-21 16:51:06.090][000000003.230] I/user.[excloud]TCP socket cb userdata: 0C1859C8 33554450 0
[2026-07-21 16:51:06.094][000000003.230] I/user.[excloud]TCP socket 发送完成
[2026-07-21 16:51:06.104][000000003.275] I/user.[excloud]TCP socket cb userdata: 0C1859C8 33554452 0
[2026-07-21 16:51:06.109][000000003.276] I/user.[excloud]TCP socket 收到数据 22 字节 01864317083866520001000600000000301100026F6B 44
[2026-07-21 16:51:06.115][000000003.277] I/user.[excloud]解析消息头 设备ID: 864317083866528 序列号: 1 消息长度: 6 协议版本: 0 需要回复: false UDP承载: false 包含auth_key: false
[2026-07-21 16:51:06.120][000000003.278] I/user.[excloud]鉴权成功 ok
[2026-07-21 16:51:06.124][000000003.279] I/user.app_vl53l1x excloud认证成功
[2026-07-21 16:51:06.127][000000003.281] I/user.[excloud]构建发送数据 field: 20 type: 5 value: 258
[2026-07-21 16:51:06.137][000000003.282] I/user.[excloud]构建发送数据 field: 20 type: 5 value: FOTA升级成功: 升级前: PROJECT=Air8780P_VL53L1X VERSION=001.999.000 ; 升级后: PROJECT=Air8780P_VL53L1X VERSION=001.999.001
[2026-07-21 16:51:06.140][000000003.283] I/user.[excloud]tlv发送数据长度4 142
[2026-07-21 16:51:06.149][000000003.284] I/user.[excloud]构建消息头 seq: 3 len: 142 flags: 2 dev: 0186431708386652
[2026-07-21 16:51:06.154][000000003.286] I/user.app_vl53l1x excloud发送成功, 流水号: 3
[2026-07-21 16:51:06.158][000000003.287] I/user.[excloud]数据发送成功 158 字节
[2026-07-21 16:51:06.166][000000003.287] I/user.app_vl53l1x excloud数据上报成功
[2026-07-21 16:51:06.170][000000003.289] I/user.app_vl53l1x MQTT上报数据到: 864317083866528/vl53l1x/up
[2026-07-21 16:51:06.174][000000003.293] dns_run 676:lbsmqtt.airm2m.com state 0 id 2 ipv6 0 use dns server0, try 0
[2026-07-21 16:51:06.181][000000003.339] I/user.[excloud]TCP socket cb userdata: 0C1859C8 33554450 0
[2026-07-21 16:51:06.186][000000003.340] I/user.[excloud]TCP socket 发送完成
[2026-07-21 16:51:06.191][000000003.356] dns_run 693:dns all done ,now stop
[2026-07-21 16:51:06.196][000000003.476] I/user.app_vl53l1x MQTT事件: conack nil
[2026-07-21 16:51:06.200][000000003.476] I/user.app_vl53l1x MQTT连接成功
[2026-07-21 16:51:06.205][000000003.478] I/user.app_vl53l1x MQTT已发布: {"fota_info":"FOTA升级成功: 升级前: PROJECT=Air8780P_VL53L1X VERSION=001.999.000 ; 升级后: PROJECT=Air8780P_VL53L1X VERSION=001.999.001","distance_mm":258,"project":"Air8780P_VL53L1X","distances":[260,258,257],"imei":"864317083866528","valid_samples":3,"ts":1784623867,"version":"001.999.001"}
[2026-07-21 16:51:06.215][000000003.550] I/user.app_vl53l1x MQTT事件: sent 1
[2026-07-21 16:51:06.219][000000003.551] I/user.app_vl53l1x MQTT数据发送成功
[2026-07-21 16:51:06.229][000000003.552] I/mqtt mqtt closing socket netc:c1b7f28 mqtt_state:3
[2026-07-21 16:51:06.233][000000003.554] I/user.app_vl53l1x MQTT事件: disconnect 0
[2026-07-21 16:51:06.240][000000003.554] W/user.app_vl53l1x MQTT断开
[2026-07-21 16:51:06.244][000000003.555] I/user.app_vl53l1x MQTT事件: close nil
[2026-07-21 16:51:06.610][000000004.054] W/mqtt network_tx ret -1, closing socket
[2026-07-21 16:51:06.616][000000004.054] I/mqtt mqtt closing socket netc:c1b7f28 mqtt_state:0
[2026-07-21 16:51:06.621][000000004.054] I/mqtt mqtt closing socket netc:c1b7f28 mqtt_state:0
[2026-07-21 16:51:06.625][000000004.055] I/user.app_vl53l1x MQTT短连接结束,发送 成功
[2026-07-21 16:51:06.635][000000004.055] I/user.app_vl53l1x 发送完成,进入PSM+模式
[2026-07-21 16:51:06.639][000000004.055] I/user.app_vl53l1x 准备进入PSM+模式,休眠5分钟后唤醒
[2026-07-21 16:51:06.651][000000004.056] I/user.app_vl53l1x 关闭VL53L1X传感器
[2026-07-21 16:51:06.655][000000004.056] I/user.app_vl53l1x GPIO27 LOW
[2026-07-21 16:51:06.663][000000004.057] I/user.app_vl53l1x 深度休眠定时器已设置: 300000 ms
[2026-07-21 16:51:06.922][000000004.367] net_lwip_tcp_err_cb 662:adapter 1 socket 1 not closing, but error -13
```
### 5.6 故障排除
1. **VL53L1X 传感器初始化失败**
- 检查 I2C 接线是否正确(GPIO1-SCL、GPIO2-SDA、3.3V、GND
- 确认传感器供电正常(3.3V
- 检查传感器 I2C 地址是否默认 0x29(可通过读写 0x010F 寄存器验证,应返回 0xEA)
2. **测距数据一直为无效(串扰信号)**
- 传感器前方可能有遮挡物
- 传感器距离目标过近(< 10mm)
- 环境光太强或目标表面反射率过低
- 可通过 `calibrate_xtalk()` API 进行串扰校准
3. **无法联网**
- 检查 SIM 卡是否正确安装、是否有流量
- 检查天线是否连接
- 检查网络信号强度
4. **无法进入 PSM+ 模式**
- 检查是否有其他任务未阻塞
- 确认 `pm.power(pm.WORK_MODE, 3)` 已执行
5. **PSM+ 模式下传感器功耗偏高(320µA)**
- 传感器模块始终上电,软件待机下模块上的 LDO/电平转换芯片仍会消耗电流
- 如需降低,可使用 VL53L1X 的 XSHUT 引脚(硬件断电),接线时把 XSHUT 接到模组空闲 GPIO
`setup` 时传入 `{xshut = GPIO号}`,进入 PSM+ 前会自动拉低 XSHUT 彻底关断传感器
5. **MQTT 数据未收到**
- 确认 Topic 正确(格式:`IMEI/vl53l1x/up`
- 检查 MQTT 服务器地址和端口配置是否正确
- 日志中查看 `MQTT数据发送成功` 是否打印
### 5.7 扩展功能建议
- 串扰校准:使用 `exs_vl53l1x.calibrate_xtalk()` API 配合白纸板进行校准,校准值传入 `setup({xtalk_offset = value})` 即可
- 中断模式:VL53L1X 支持 GPIO1 中断,可配置为回调模式或轮询模式,详见 [exs_vl53l1x 扩展库说明](https://docs.openluat.com/osapi/ext/sensor/exs_vl53l1x/)
- 三档测距模式:支持 standard(约 2.9m/ short(约 1.36m,抗强光)/ long(约 4.6m)三种模式,在 `setup` 时通过 `range_mode` 参数设置
@@ -0,0 +1,239 @@
--[[
@module sensor_vl53l1x
@summary Air8780P VL53L1X激光测距传感器驱动封装模块
@version 1.0
@date 2026.07.23
@author 江访
@usage
本文件为VL53L1X激光测距传感器的驱动封装模块,核心业务逻辑为:
1、传感器初始化(I2C配置、模式选择)
2、传感器数据采集(多次采样取平均)
3、传感器关闭
本文件的对外接口有3个:
1、sensor_vl53l1x.init(scl, sda, range_mode):初始化传感器
2、sensor_vl53l1x.collect_data(sensor_samples):采集传感器数据,返回平均值
3、sensor_vl53l1x.close():关闭传感器
]]
-- ==================== 加载扩展库 ====================
-- exs_vl53l1x: VL53L1X传感器扩展库,提供底层I2C通信和测距控制
-- 使用前需确认固件中包含此扩展库
local exs_vl53l1x = require "exs_vl53l1x"
-- ==================== 模块表 ====================
-- sensor_vl53l1x 为模块对外接口表,所有对外函数注册在此表上
-- 调用方通过 local sensor_vl53l1x = require "sensor_vl53l1x" 获取本表
local sensor_vl53l1x = {}
-- ==================== API:初始化 ====================
--[[
初始化VL53L1X激光测距传感器
sensor_vl53l1x.init(scl, sda, range_mode)
本函数完成以下工作:
1、配置I2C引脚(软件I2C模式)
2、等待传感器固件就绪
3、写入预设模式配置(standard/short/long
4、校验芯片IDMID=0xEA, MT=0xCC
@param number scl
含义:I2C时钟线SCL的GPIO引脚号
取值范围:0~最大GPIO号,视具体模组而定
是否必选:必选
示例值:1Air8780P上的GPIO1
@param number sda
含义:I2C数据线SDA的GPIO引脚号
取值范围:0~最大GPIO号,视具体模组而定
是否必选:必选
示例值:2Air8780P上的GPIO2
@param string range_mode
含义:测距模式
取值范围:
"short" — 短距离模式,最远约1.36m,抗环境光能力强
"standard"— 标准模式,最远约2.9m,通用场景
"long" — 长距离模式,最远约4.6m,需要较好的环境光条件
是否必选:可选
默认值:"standard"
@return boolean
含义:初始化是否成功
取值范围:
true — 初始化成功,传感器可正常使用
false — 初始化失败,请检查I2C接线和传感器供电
@usage
-- 基本用法:GPIO1=SCL, GPIO2=SDA, 标准模式
local ok = sensor_vl53l1x.init(1, 2, "standard")
if not ok then
log.error("sensor_vl53l1x", "传感器初始化失败")
return
end
-- 短距离模式(抗强光)
local ok = sensor_vl53l1x.init(1, 2, "short")
-- 长距离模式
local ok = sensor_vl53l1x.init(1, 2, "long")
]]
function sensor_vl53l1x.init(scl, sda, range_mode)
range_mode = range_mode or "standard"
log.info("sensor_vl53l1x", "正在初始化VL53L1X传感器, mode=" .. range_mode)
-- 调用扩展库setup接口,传入I2C引脚和测距模式
-- exs_vl53l1x.setup内部会完成:
-- 1. i2c.setup初始化I2C总线
-- 2. 等待传感器固件启动(轮询寄存器0x00E5)
-- 3. 写入预设模式配置
-- 4. 验证芯片型号
local ok = exs_vl53l1x.setup({
scl = scl,
sda = sda,
range_mode = range_mode,
})
if not ok then
log.error("sensor_vl53l1x", "VL53L1X初始化失败,请检查接线和供电")
return false
end
log.info("sensor_vl53l1x", "VL53L1X初始化成功")
return true
end
-- ==================== API:数据采集 ====================
--[[
采集传感器数据,多次采样后取平均值
sensor_vl53l1x.collect_data(sensor_samples)
本函数完成以下工作:
1、按指定次数循环采集测距数据
2、每次采集间隔500ms,等待传感器就绪
3、过滤无效数据(status不为0的帧)
4、计算有效数据的平均值(四舍五入取整)
注意:本函数内部有 sys.wait(),必须在协程中调用
@param number sensor_samples
含义:每次唤醒后采集的次数
取值范围:正整数,建议1~10
1 — 单次采集,速度快但可能因瞬间干扰产生偏差
3 — 默认值,三次取平均,兼顾速度和精度
5~10 — 多次采集,精度高但耗时较长(每次间隔500ms)
是否必选:可选
默认值:3
@return table
含义:采集成功时返回数据表,包含以下字段
distance_list : table,原始距离值数组(单位mm),例如 {267, 261, 265}
avg_distance : number,有效数据的平均值(单位mm,四舍五入取整)
valid_count : number,有效数据的帧数
@return nil
含义:所有采集数据均无效时返回nil
可能原因:
1、传感器前方有遮挡物
2、传感器距离目标过近(<10mm)
3、环境光过强或目标表面反射率过低
@usage
-- 默认采集3次
local data = sensor_vl53l1x.collect_data(3)
if data then
log.info("sensor_vl53l1x", "平均距离=" .. data.avg_distance .. "mm")
log.info("sensor_vl53l1x", "原始数据=" .. table.concat(data.distance_list, ","))
else
log.warn("sensor_vl53l1x", "未测到有效距离")
end
]]
function sensor_vl53l1x.collect_data(sensor_samples)
sensor_samples = sensor_samples or 3
log.info("sensor_vl53l1x", "开始采集传感器数据,采集次数=" .. sensor_samples)
-- distances: 存储有效距离值的数组
-- valid_count: 有效数据帧数计数器
local distances = {}
local valid_count = 0
-- 循环采集:每次采集前等待500ms让传感器完成测距
for i = 1, sensor_samples do
sys.wait(500)
-- exs_vl53l1x.get_data() 返回数据结构:
-- {
-- distance = number, -- 测距值(单位mm
-- status = number, -- 状态码,0=成功
-- status_str = string, -- 状态描述文字
-- sigma = number, -- 标准差(可选)
-- }
local data = exs_vl53l1x.get_data()
-- 判断数据是否有效:status==0 表示测距成功
if data and data.status == 0 then
distances[#distances + 1] = data.distance
valid_count = valid_count + 1
log.info("sensor_vl53l1x", string.format("采集[%d/%d] 距离=%dmm 状态=%s",
i, sensor_samples, data.distance, data.status_str))
else
local status_str = (data and data.status_str) or "无数据"
log.warn("sensor_vl53l1x", string.format("采集[%d/%d] 无效数据 状态=%s",
i, sensor_samples, status_str))
end
end
-- 没有有效数据时返回nil
if valid_count == 0 then
log.error("sensor_vl53l1x", "所有采集数据均无效")
return nil
end
-- 计算平均值(四舍五入取整)
local sum = 0
for _, d in ipairs(distances) do
sum = sum + d
end
local avg_distance = math.floor(sum / #distances + 0.5)
log.info("sensor_vl53l1x", string.format("采集完成: 有效%d帧, 平均距离=%dmm",
valid_count, avg_distance))
-- 返回数据表
return {
distance_list = distances, -- 原始距离数组,用于MQTT上报详细数据
avg_distance = avg_distance, -- 平均距离,用于excloud上报
valid_count = valid_count, -- 有效帧数,用于判断数据质量
}
end
-- ==================== API:关闭传感器 ====================
--[[
关闭VL53L1X传感器
sensor_vl53l1x.close()
本函数完成以下工作:
1、调用扩展库close接口停止测距
2、释放I2C总线资源
3、传感器进入待机模式,降低功耗
建议在以下场景调用:
1、数据采集完成后立即关闭,减少功耗
2、进入PSM+休眠前必须关闭
3、设备异常重启前
@usage
sensor_vl53l1x.close()
]]
function sensor_vl53l1x.close()
log.info("sensor_vl53l1x", "关闭VL53L1X传感器")
exs_vl53l1x.close()
end
return sensor_vl53l1x
@@ -0,0 +1,130 @@
--[[
@module fota
@summary 使用合宙iot平台远程升级功能模块
@version 1.0
@date 2025.09.25
@author 王城钧
@usage
SHT30获取温湿度
]]
--本文件中的主机是指I2C主机,具体指Air780EHV
--本文件中的从机是指I2C从机,具体指AirSHT30_1000配件板上的sht30温湿度传感器芯片
local AirSHT30_1000 =
{
-- i2c_id:主机的i2c id
}
-- 从机地址为0x44
local slave_addr = 0x44
--电平设为3.3v
pm.ioVol(pm.IOVOL_ALL_GPIO, 3300)
--设置gpio2输出,给camera_sda、camera_scl引脚提供上拉
gpio.setup(2, 1)
-- 计算数据表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,286 @@
--[[
@module excloud_test
@summary excloud测试文件
@version 1.0
@date 2025.09.25
@author 王城钧
@usage
本demo演示的功能为:
演示excloud扩展库的使用。
]]
-- 导入excloud库
local excloud = require("excloud")
--加载AirSHT30_1000驱动文件
local air_sht30 = require "AirSHT30_1000"
--加载lbsLoc2库
local lbsLoc2 = require("lbsLoc2")
-- 注册回调函数
excloud.on(function(event, data)
log.info("用户回调函数", event, json.encode(data))
if event == "connect_result" then
if data.success then
log.info("连接成功")
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 == "message" then
log.info("收到消息, 流水号: " .. data.header.sequence_num)
-- 处理服务器下发的消息
for _, tlv in ipairs(data.tlvs) do
if tlv.field == excloud.FIELD_MEANINGS.CONTROL_COMMAND then
log.info("收到控制命令: " .. tostring(tlv.value))
-- 处理控制命令并发送响应
local response_ok, err_msg = excloud.send({
{
field_meaning = excloud.FIELD_MEANINGS.CONTROL_RESPONSE,
data_type = excloud.DATA_TYPES.ASCII,
value = "命令执行成功"
}
}, false)
if not response_ok then
log.info("发送控制响应失败: " .. err_msg)
end
end
end
elseif event == "disconnect" then
log.warn("与服务器断开连接")
elseif event == "reconnect_failed" then
log.info("重连失败,已尝试 " .. data.count .. "")
elseif event == "send_result" then
if data.success then
log.info("发送成功,流水号: " .. data.sequence_num)
else
log.info("发送失败: " .. data.error_msg)
end
end
end)
local getCellInfo1
local band1
local lat, lng
-- 定期轮训式
sys.taskInit(function()
sys.wait(3000)
while 1 do
mobile.reqCellInfo(15)
sys.waitUntil("CELL_INFO_UPDATE", 30000)
getCellInfo1 = mobile.getCellInfo()
band1 = getCellInfo1[1].band
log.info("驻留频段", band1, json.encode(getCellInfo1))
lat, lng, t = lbsLoc2.request(5000, nil, nil, true) --需要经纬度和当前时间
--(时间格式{"year":2024,"min":56,"month":11,"day":12,"sec":44,"hour":14})
log.info("lbsLoc2", lat, lng, (json.encode(t or {}))) --打印经纬度和时间
sys.wait(60000)
end
end)
local temprature, humidity
sys.taskInit(function()
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("tcp_client_main_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
sys.wait(1000)
-- 配置excloud参数
local ok, err_msg = excloud.setup({
-- device_id = "862419074073247", -- 设备ID (IMEI前14位)
device_type = 1, -- 设备类型: 4G
host = "124.71.128.165", -- 服务器地址
port = 9108, -- 服务器端口
auth_key = "SIsRml1ImTsP6XR4lvRAQVuksbZZuUpO", -- 鉴权密钥
transport = "tcp", -- 使用TCP传输
auto_reconnect = true, -- 自动重连
reconnect_interval = 10, -- 重连间隔(秒)
max_reconnect = 5, -- 最大重连次数
timeout = 30, -- 超时时间(秒)
})
-- 配置MQTT连接参数
-- local ok, err_msg = excloud.setup({
-- device_type = 1, -- 设备类型: 4G设备
-- transport = "mqtt", -- 使用MQTT传输协议
-- host = "airtest.openluat.com", -- MQTT服务器地址
-- port = 1883, -- MQTT服务器端口
-- auth_key = "VmhtOb81EgZau6YyuuZJzwF6oUNGCbXi", -- 鉴权密钥(请替换为实际密钥)
-- username = "root", -- MQTT用户名(可选)
-- password = "Luat123456", -- MQTT密码(可选)
-- keepalive = 300, -- 心跳间隔(秒)
-- -- auto_reconnect = true, -- 自动重连
-- reconnect_interval = 10, -- 重连间隔(秒)
-- max_reconnect = 5, -- 最大重连次数
-- timeout = 30, -- 超时时间(秒)
-- qos = 1, -- MQTT QoS等级(0/1/2)
-- -- retain = false, -- MQTT retain标志
-- -- clean_session = true, -- MQTT clean session
-- ssl = false -- 不使用SSL
-- })
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服务已开启")
-- 在主循环中定期上报数据
while true do
-- 每60秒上报一次数据
sys.wait(60000)
-- 检查连接状态
-- local status = excloud.status()
-- if not status.is_connected or not status.is_authenticated then
-- log.info("设备未连接或未认证,跳过数据上报")
-- else
-- 获取信号强度
local rssi = mobile.csq()
log.info("信号强度", rssi)
-- 获取sim卡的iccid
local iccid = mobile.iccid()
log.info("iccid", iccid)
-- 获取驻留频段
local buff = zbuff.create(40)
-- 获取驻留小区
local cell_info = mobile.scell()
local cell_id = cell_info.cid
log.info("小区ID", cell_id)
-- 获取环境温度
--打开sht30硬件
air_sht30.open(1)
--读取温湿度数据
temprature, humidity = air_sht30.read()
log.info("温度", temprature, humidity)
-- 获取cpu温度
adc.open(adc.CH_CPU) --打开adc.CH_CPU通道
local data5 = adc.get(adc.CH_CPU) --获取adc.CH_CPU计算值,将获取到的值赋给data5
-- adc.close(adc.CH_CPU)--关闭adc.CH_CPU通道
log.info("CPU温度", data5)
if not lat then
lat = 0
end
if not lng then
lng = 0
end
if not rssi then
rssi = 0
end
if not cell_id then
cell_id = 0
end
if not temprature then
temprature = 0
end
if not humidity then
humidity = 0
end
if not band1 then
band1 = 0
end
if not iccid then
iccid = ""
end
if not data5 then
data5 = 0
end
local ok, err_msg = excloud.send({
{
--信号强度
field_meaning = excloud.FIELD_MEANINGS.SIGNAL_STRENGTH_4G,
data_type = excloud.DATA_TYPES.INTEGER,
value = rssi -- 信号强度
},
{
--iccid
field_meaning = excloud.FIELD_MEANINGS.SIM_ICCID,
data_type = excloud.DATA_TYPES.ASCII,
value = iccid -- sim卡iccid
},
{
--驻留频段
field_meaning = excloud.FIELD_MEANINGS.SERVING_CELL,
data_type = excloud.DATA_TYPES.INTEGER,
value = band1 -- 驻留频段
},
{
--驻留小区
field_meaning = excloud.FIELD_MEANINGS.CELL_INFO,
data_type = excloud.DATA_TYPES.INTEGER,
value = cell_id -- 驻留小区
},
{
--环境温度
field_meaning = excloud.FIELD_MEANINGS.TEMPERATURE,
data_type = excloud.DATA_TYPES.FLOAT,
value = temprature -- 环境温度
},
{
--环境湿度
field_meaning = excloud.FIELD_MEANINGS.HUMIDITY,
data_type = excloud.DATA_TYPES.FLOAT,
value = humidity -- 环境湿度
},
{
--cpu温度
field_meaning = excloud.FIELD_MEANINGS.ENV_TEMPERATURE,
data_type = excloud.DATA_TYPES.INTEGER,
value = data5 -- cpu温度
},
{
--经度
field_meaning = excloud.FIELD_MEANINGS.GNSS_LONGITUDE,
data_type = excloud.DATA_TYPES.ASCII,
value = lng
},
{
--纬度
field_meaning = excloud.FIELD_MEANINGS.GNSS_LATITUDE,
data_type = excloud.DATA_TYPES.ASCII,
value = lat
}
}, false) -- 不需要服务器回复
if not ok then
log.info("发送数据失败: " .. err_msg)
else
log.info("数据发送成功")
end
-- end
end
end)
@@ -0,0 +1,29 @@
--[[
@module fota
@summary 使用合宙iot平台远程升级功能模块
@version 1.0
@date 2025.09.25
@author 王城钧
@usage
实现远程升级功能
]]
-- 使用合宙iot平台时需要这个参数
PRODUCT_KEY = "SIsRml1ImTsP6XR4lvRAQVuksbZZuUpO" -- 到 iot.openluat.com 创建项目,获取正确的项目id
--加在libfota2扩展库
libfota2 = require "libfota2"
local function fota_cb(ret)
log.info("fota", ret)
-- fota结束,无论成功还是失败,都释放fota_running标志
if ret == 0 then
log.info("升级包下载成功,重启模块")
rtos.reboot()
end
end
local opts = {}
libfota2.request(fota_cb, opts)
sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, opts)
@@ -0,0 +1,65 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.09.25
@author 王城钧
@usage
本demo演示的功能为:
演示excloud扩展库的使用。
]]
--[[
必须定义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 = "excloud_test"
VERSION = "001.999.000"
-- 如果内核固件支持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)
-- 加载fota升级功能模块
require "fota"
-- 加载网络驱动设备功能模块
require "netdrv_device"
-- 加载excloud测试模块
require "excloud_test"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
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
-- 如果使用的硬件和以上描述的环境不同,根据自己的硬件配置修改以下参数
exnetif.set_priority_order({
{
ETHERNET = {
pwrpin = 20,
tp = netdrv.CH390,
opts = {spi = 0, cs = 8}
}
}
})
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}
}
},
-- 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,85 @@
--[[
@module netdrv_wifi
@summary “WIFI STA网卡”驱动模块
@version 1.0
@date 2025.07.01
@author 马梦阳
@usage
本文件为WIFI STA网卡驱动模块,核心业务逻辑为:
1、初始化WIFI网络;
2、连接WIFI路由器;
3、和WIFI路由器之间的连接状态发生变化时,在日志中进行打印;
本文件没有对外接口,直接在其他功能模块中require "netdrv_wifi"就可以加载运行;
]]
local exnetif = require "exnetif"
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_STA 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_wifi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_STA))
end
end
local function ip_lose_func(adapter)
if adapter == socket.LWIP_STA then
log.warn("netdrv_wifi.ip_lose_func", "IP_LOSE")
end
end
--WIFI联网成功(做为STATION成功连接AP,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
--各个功能模块可以订阅"IP_READY"消息实时处理WIFI联网成功的事件
--也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功
--WIFI断网后,内核固件会产生一个"IP_LOSE"消息
--各个功能模块可以订阅"IP_LOSE"消息实时处理WIFI断网的事件
--也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功
--此处订阅"IP_READY"和"IP_LOSE"两种消息
--在消息的处理函数中,仅仅打印了一些信息,便于实时观察WIFI的连接状态
--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
local function wifi_sta_func(evt, data)
-- evt 可能的值有: "CONNECTED", "DISCONNECTED"
-- 当evt=CONNECTED, data是连接的AP的ssid, 字符串类型
-- 当evt=DISCONNECTED, data断开的原因, 整数类型
log.info("收到STA事件", evt, data)
end
-- wifi的STA相关事件
sys.subscribe("WLAN_STA_INC", wifi_sta_func)
local function netdrv_wifi_task_func()
-- 配置WiFi设备模式的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_STA
-- ssid为要连接的WiFi路由器名称;
-- password为要连接的WiFi路由器密码;
-- 注意:仅支持2.4G的WiFi,不支持5G的WiFi
-- 实际测试时,根据自己要连接的WiFi热点信息修改以下参数
exnetif.set_priority_order({
{
WIFI = {
ssid = "茶室-降功耗,找合宙!",
password = "Air123456"
}
}
})
end
-- 启动一个tasktask的处理函数为netdrv_wifi_task_func
-- 在处理函数中调用exnetif.set_priority_order设置网卡优先级
-- 因为exnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
sys.taskInit(netdrv_wifi_task_func)
@@ -0,0 +1,33 @@
--[[
@module netdrv_device
@summary 网络驱动设备功能模块
@version 1.0
@date 2025.07.24
@author 朱天华
@usage
本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
1、netdrv_4gsocket.LWIP_GP4G网卡;
2、netdrv_wifisocket.LWIP_STAWIFI STA网卡;
3、netdrv_eth_spisocket.LWIP_USER1,通过SPI外挂CH390H芯片的以太网卡;
4、netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
根据自己的项目需求,只需要require以上四种中的一种即可;
本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
]]
-- 根据自己的项目需求,只需要require以下四种中的一种即可;
-- 加载“4G网卡”驱动模块
require "netdrv_4g"
-- 加载“WIFI STA网卡”驱动模块
-- require "netdrv_wifi"
-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
-- require "netdrv_eth_spi"
-- 加载“可以配置优先级的多种网卡”驱动模块
-- require "netdrv_multiple"
@@ -0,0 +1,119 @@
--[[
@module excloud_app
@summary AirCloud(云平台)连接管理功能模块
@version 1.0
@date 2026.08.06
@author 王城钧
@usage
本文件为AirCloud连接管理功能模块,核心业务逻辑为:
1、setup配置excloud库(传输方式、自动重连、运维日志等参数);
2、等待短信服务就绪和网络就绪后,调用open启动excloud服务;
3、启动心跳定时上报,维持与服务器的连接活性;
4、启动运维日志定时上传任务;
5、注册平台消息回调函数,处理鉴权结果、连接结果、平台下发的消息以及连接断开事件:
- 收到服务器下发的消息后,分发给sms_bridge桥接功能模块做短信指令处理;
本文件没有对外接口,直接在main.lua中require "excloud_app"就可以加载运行;
]]
-- 变量
local excloud = require("excloud")
local sms_bridge = require("sms_bridge")
_G.EXCLOUD = excloud
local TRANSPORT = "tcp"
local AUTO_RECONNECT = true
local RECONNECT_INTERVAL = 10
local MAX_RECONNECT = 5
local HEARTBEAT_INTERVAL = 300
local MTN_LOG_ENABLED = true
local MTN_LOG_BLOCKS = 2
local MTN_LOG_WRITE_WAY = 1
local MTN_LOG_AIRCLOUD = true
local MTN_LOG_FIRST_UPLOAD = 60
local MTN_LOG_UPLOAD_INTERVAL = 1800
-- 平台消息回调
local function on_excloud_event(event, data)
if event == "auth_result" then
if data.success then
log.info("excloud_app", "鉴权成功")
excloud.mtn_log("main", "AirCloud鉴权成功")
else
log.error("excloud_app", "鉴权失败", data.message)
excloud.mtn_log("main", "AirCloud鉴权失败", data.message or "")
end
elseif event == "connect_result" then
log.info("excloud_app", "连接结果", data.success and "成功" or "失败")
elseif event == "message" then
log.info("excloud_app", "收到平台消息", "seq=", data.header and data.header.sequence_num)
for i, tlv in ipairs(data.tlvs or {}) do
local val = tlv.value
if type(val) == "string" and #val > 50 then
val = string.sub(val, 1, 50) .. "..."
end
log.info("excloud_app", string.format(" [%d] field=%d type=%d value=%s", i, tlv.field, tlv.type, tostring(val)))
end
sms_bridge.handle_message(data.tlvs)
elseif event == "disconnect" then
log.warn("excloud_app", "连接断开")
excloud.mtn_log("main", "AirCloud连接断开")
end
end
-- 主连接任务
sys.taskInit(function()
log.info("excloud_app", "等待网络就绪...")
sys.waitUntil("SMS_READY", 30000)
log.info("excloud_app", "短信服务就绪")
while not socket.adapter(socket.dft()) do
sys.waitUntil("IP_READY", 1000)
end
log.info("excloud_app", "网络就绪")
excloud.setup({
transport = TRANSPORT,
auto_reconnect = AUTO_RECONNECT,
reconnect_interval = RECONNECT_INTERVAL,
max_reconnect = MAX_RECONNECT,
mtn_log_enabled = MTN_LOG_ENABLED,
mtn_log_blocks = MTN_LOG_BLOCKS,
mtn_log_write_way = MTN_LOG_WRITE_WAY,
aircloud_mtn_log_enabled = MTN_LOG_AIRCLOUD,
})
excloud.on(on_excloud_event)
local ok, err = excloud.open()
if not ok then
log.error("excloud_app", "启动失败", err)
return
end
log.info("excloud_app", "AirCloud连接已启动")
excloud.mtn_log("main", "启动完成", "网络就绪", "短信就绪", "AirCloud已启动")
excloud.start_heartbeat(HEARTBEAT_INTERVAL)
log.info("excloud_app", "心跳已启动", HEARTBEAT_INTERVAL, "s")
excloud.mtn_log("main", "心跳已启动", "间隔", HEARTBEAT_INTERVAL)
sys.taskInit(function()
sys.wait(MTN_LOG_FIRST_UPLOAD * 1000)
log.info("excloud_app", "首次上传运维日志")
excloud.upload_mtnlogs()
while true do
sys.wait(MTN_LOG_UPLOAD_INTERVAL * 1000)
log.info("excloud_app", "定时上传运维日志")
excloud.upload_mtnlogs()
end
end)
log.info("excloud_app", "运维日志定时上传已启动", "首次", MTN_LOG_FIRST_UPLOAD, "s", "间隔", MTN_LOG_UPLOAD_INTERVAL, "s")
excloud.mtn_log("main", "运维日志定时上传已启动", "间隔", MTN_LOG_UPLOAD_INTERVAL)
end)
log.info("excloud_app", "已加载")
@@ -0,0 +1,60 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2026.08.06
@author 王城钧
@usage
本demo演示的核心功能为:
1、通过AirCloud连接管理功能模块excloud_app.lua,实现设备与云平台的MQTT连接,包括:
- 应用层鉴权:通过TLV报文携带用户key+IMEI+MUID完成鉴权;
- 连接保活:启动心跳定时上报,维持连接活性;
- 运维日志:本地缓冲运行日志,定时上传云平台;
2、短信收发功能模块sms_bridge.lua,实现短信与应用报文的双向转换:
- 设备发送短信后,将短信发送结果(SMS_SENT)转换为TLV报文上传云平台;
- 短信送达状态(SMS_REPORT)解析后上传云平台;
- 收到云平台下发的短信指令(SMS_SEND)后,调起模组短信能力发送短信;
3、短信测试功能模块sms_app.lua,开机后自动执行上行短信测试用例;
4、短信调试日志功能模块sms_callback.lua,订阅短信事件并打印本地调试日志;
更多说明参考本目录下的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 = "8783_SMS"
VERSION = "001.999.009"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 加载短信调试功能模块
require "sms_callback"
-- 加载短信与应用协议桥接功能模块
require "sms_bridge"
-- 加载AirCloud(云平台)连接管理功能模块
require "excloud_app"
-- 加载短信测试应用功能模块
require "sms_app"
-- 加载看门狗功能模块
require "wdt_app"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,221 @@
## 功能模块介绍
1、main.lua:主程序入口,加载并调度各功能模块;
2、excloud_app.luaAirCloud(云平台)连接管理功能模块,负责应用层鉴权、心跳保活、运维日志上传以及平台消息分发;
3、sms_bridge.lua:短信与AirCloud协议桥接功能模块,负责短信收发事件与TLV应用报文之间的双向转换和上传;
4、sms_callback.lua:短信事件本地调试日志功能模块,打印短信发送结果与投递状态报告;
5、sms_app.lua:短信测试应用功能模块,开机自动执行上行短信测试用例;
## 系统消息介绍
1、"SMS_READY":模组短信服务已经就绪;
2、"IP_READY":网络已经获取到ip信息,仅仅获取到了ip信息,能否和外网连通还不确认;
## 短信事件消息介绍
1、"SMS_SENT":短信发送结果事件,模组调用短信能力发送短信后由系统发布,msg_ref用于和SMS_REPORT关联;
2、"SMS_REPORT":短信投递状态报告事件,基站回传短信送达的最终结果;
3、"SMS_INC":设备收到新短信事件,包含号码、短信内容和元数据;
## 演示功能概述
1、连接云平台:设备上电后,短信服务就绪并且网络就绪后,通过MQTT与AirCloud平台建立连接,并通过TLV报文完成应用层鉴权(用户key+IMEI+MUID);
2、建立连接后,启动心跳定时上报(默认300秒一次),维持与服务器的连接活性,防止服务器断链;
3、短信上行:设备通过sms_bridge.send_sms发送短信,SMSC接受后触发SMS_SENT事件,桥接功能模块将该发送结果转换为TLV报文上传云平台,例如:
``` lua
field=29 SMS_SEND_RSP
field=1039 SMS_SEQ
field=1040 SMS_CALLEE
field=1041 SMS_CONTENT
field=1042 SMS_STATUS
field=1047 SMS_MSG_REF
```
4、短信投递状态报告:短信送达或失败后,基站回传SMS_REPORT(携带status原因码),桥接功能模块根据状态码映射(0-成功,0x41-空号,0x22-不在线/关机等)转换为TLV报文上传云平台;
5、短信下发:由模组内部代码触发,sms_app.lua直接向目标号码发送短信;
6、运维日志:excloud库将运行日志写进本地缓冲,首次上传(默认60秒)后每30分钟上传一次云平台;
## 演示硬件环境
1、Air8783开发板一块 + 可上网的sim卡一张 :
- sim卡插入开发板的sim卡卡槽;
2、TYPE-C USB数据线一根,Air8783开发板和数据线的硬件接线方式为:
- Air8783开发板通过USB-A 口接到电脑USB口供电;
![](https://docs.openluat.com/air8000/luatos/app/image/8783短信.jpg)
## 演示软件环境
1、Luatools下载调试工具;
2、[Air8783固件]([Air780EPM固件版本 - luatos@air780epm - 合宙模组资料中心](https://docs.openluat.com/air780epm/luatos/firmware/780epm_version/)),必须使用支持短信SMS功能的固件版本,本demo使用的是2048-106号固件;
3、云平台测试环境:[AirCloud云平台](https://iot.luatos.com/),需要在合宙iot平台创建项目并归属到对应项目下
## 演示核心步骤
1、搭建好硬件环境,插好sim卡后连接电脑;
2、Luatools烧录内核固件和修改后的demo脚本代码,在"sms_app"中sms_bridge.send_sms函数里面修改要发送的号码和短信内容;
3、烧录成功后,自动开机运行,连接云平台成功后,串口会出现类似日志:
``` lua
[2026-08-07 11:08:49.597][000000008.646] D/sms pdu len 19
[2026-08-07 11:08:49.952][000000009.005] I/user.httpplus 服务器已完成响应
[2026-08-07 11:08:49.956][000000009.008] I/user.[excloud]getip响应 HTTP: 200 Body:
[2026-08-07 11:08:49.960][000000009.008] {"msg":"ok","conninfo":{"ipv4":"124.71.128.165","port":9108,"auth_key":"gwTdT1TpYK7Q2vKAmVZkmsG2VvjyMqWt"},"imginfo":{"url":"https://api.luatos.com/iot/air_up/image","data_key":"f","data_param":{"key":"2h3WBCJ4UUYfLApJwvwJ4U4cC2mxAQgrUd6Q","tip":""}},"audinfo":{"url":"https://api.luatos.com/iot/air_up/audio","data_key":"f","data_param":{"key":"2h3WBCJ4UUYfLApJwvwJ4U4cC2mxAQgrUd6Q","tip":""}},"mtninfo":{"url":"https://api.luatos.com/iot/air_up/file","data_key":"f","data_param":{"key":"2h3WBCJ4UUYfLApJwvwJ4U4cC2mxAQgrUd6Q","tip":""}}}
[2026-08-07 11:08:49.963][000000009.010] I/user.[excloud]TCP/UDP连接信息 host: 124.71.128.165 port: 9108 key: nil
[2026-08-07 11:08:49.966][000000009.010] I/user.[excloud]获取到图片上传信息
[2026-08-07 11:08:49.969][000000009.011] I/user.[excloud]获取到音频上传信息
[2026-08-07 11:08:49.972][000000009.011] I/user.[excloud]获取到运维日志上传信息
[2026-08-07 11:08:49.975][000000009.011] W/user.[excloud]未获取到二维码信息
[2026-08-07 11:08:49.977][000000009.012] I/user.[excloud]自动获取到auth_key
[2026-08-07 11:08:49.980][000000009.012] I/user.[excloud]getip 更新配置: 124.71.128.165 9108
[2026-08-07 11:08:49.983][000000009.013] I/user.[excloud]getip 成功: true
[2026-08-07 11:08:49.986][000000009.013] I/user.[excloud]服务器信息获取成功 host: 124.71.128.165 port: 9108 transport: tcp
[2026-08-07 11:08:49.989][000000009.014] I/user.[excloud]创建TCP连接
[2026-08-07 11:08:49.991][000000009.015] D/socket connect to 124.71.128.165,9108
[2026-08-07 11:08:49.993][000000009.017] I/user.[excloud]TCP连接结果 true false
[2026-08-07 11:08:49.996][000000009.017] I/user.aircloud system excloud服务启动 transport tcp host 124.71.128.165 port 9108
[2026-08-07 11:08:49.999][000000009.024] I/user.[excloud]excloud service started
[2026-08-07 11:08:50.002][000000009.025] I/user.excloud_app AirCloud连接已启动
[2026-08-07 11:08:50.005][000000009.025] I/user.main 启动完成 网络就绪 短信就绪 AirCloud已启动
[2026-08-07 11:08:50.013][000000009.072] I/user.[excloud]excloud 自动心跳已启动,间隔 300 秒
[2026-08-07 11:08:50.018][000000009.073] I/user.excloud_app 心跳已启动 300 s
[2026-08-07 11:08:50.020][000000009.073] I/user.main 心跳已启动 间隔 300
[2026-08-07 11:08:50.091][000000009.143] I/user.excloud_app 运维日志定时上传已启动 首次 60 s 间隔 1800 s
[2026-08-07 11:08:50.094][000000009.144] I/user.main 运维日志定时上传已启动 间隔 1800
[2026-08-07 11:08:50.154][000000009.215] I/user.[excloud]TCP socket cb userdata: 0C184050 33554449 0
[2026-08-07 11:08:50.157][000000009.216] I/user.aircloud net_conn TCP连接成功 host 124.71.128.165 port 9108
[2026-08-07 11:08:50.218][000000009.266] I/user.[excloud]TCP socket TCP连接成功
[2026-08-07 11:08:50.220][000000009.267] I/user.excloud_app 连接结果 成功
[2026-08-07 11:08:50.223][000000009.267] I/mobile l_mobile_muid called
[2026-08-07 11:08:50.226][000000009.269] I/mobile l_mobile_muid ret=32
[2026-08-07 11:08:50.229][000000009.269] I/user.[excloud] 发送鉴权请求
[2026-08-07 11:08:50.231][000000009.270] I/user.[excloud]构建发送数据 field: 16 type: 3 value: gwTdT1TpYK7Q2vKAmVZkmsG2VvjyMqWt-862323089995180-20260605140024A391855A8910558372
[2026-08-07 11:08:50.236][000000009.271] I/user.[excloud]tlv发送数据长度4 85
[2026-08-07 11:08:50.240][000000009.272] I/user.[excloud]构建消息头 seq: 2 len: 85 flags: 18 dev: 0186232308999518
[2026-08-07 11:08:50.243][000000009.275] I/user.[excloud]数据发送成功 101 字节
[2026-08-07 11:08:50.245][000000009.275] D/mobile ims reg state 0
[2026-08-07 11:08:50.248][000000009.276] I/user.SMS_SENT result= true rp_cause= 0 msg_ref= 10 error_code= 0
[2026-08-07 11:08:50.250][000000009.277] I/user.SMS_SENT SMSC已接受, msg_ref= 10 等待回执...
[2026-08-07 11:08:50.253][000000009.277] I/user.sms_bridge SMS_SENT上报 plat_seq 1786072129 msg_ref 10 status 发送成功 callee 17538215008
[2026-08-07 11:08:50.255][000000009.278] I/user.[excloud]构建发送数据 field: 29 type: 0 value: 0
[2026-08-07 11:08:50.258][000000009.280] I/user.[excloud]构建发送数据 field: 1039 type: 3 value: 1786072129
[2026-08-07 11:08:50.260][000000009.281] I/user.[excloud]构建发送数据 field: 1040 type: 3 value: 17538215008
[2026-08-07 11:08:50.263][000000009.282] I/user.[excloud]构建发送数据 field: 1041 type: 5 value: 301
[2026-08-07 11:08:50.266][000000009.284] I/user.[excloud]构建发送数据 field: 1042 type: 5 value: 发送成功
[2026-08-07 11:08:50.268][000000009.285] I/user.[excloud]构建发送数据 field: 1047 type: 0 value: 10
[2026-08-07 11:08:50.271][000000009.287] I/user.[excloud]tlv发送数据长度4 68
[2026-08-07 11:08:50.275][000000009.287] I/user.[excloud]构建消息头 seq: 3 len: 68 flags: 2 dev: 0186232308999518
[2026-08-07 11:08:50.277][000000009.289] I/user.[excloud]数据发送成功 84 字节
[2026-08-07 11:08:50.279][000000009.290] I/user.sms SMS_SENT上报 plat_seq 1786072129 msg_ref 10 status 发送成功 callee 17538215008
[2026-08-07 11:08:50.343][000000009.401] I/user.[excloud]TCP socket cb userdata: 0C184050 33554452 0
[2026-08-07 11:08:50.346][000000009.402] I/user.[excloud]TCP socket 收到数据 22 字节 01862323089995180001000600000000301100026F6B 44
[2026-08-07 11:08:50.349][000000009.403] I/user.[excloud]解析消息头 设备ID: 862323089995180 序列号: 1 消息长度: 6 协议版本: 0 需要回复: false UDP承载: false 包含auth_key: false
[2026-08-07 11:08:50.351][000000009.404] I/user.[excloud]鉴权成功 ok
[2026-08-07 11:08:50.354][000000009.405] I/user.excloud_app 鉴权成功
[2026-08-07 11:08:50.356][000000009.406] I/user.main AirCloud鉴权成功
[2026-08-07 11:08:50.390][000000009.449] I/user.excloud_app 收到平台消息 seq= 1
[2026-08-07 11:08:50.393][000000009.449] I/user.excloud_app [1] field=17 type=3 value=ok
[2026-08-07 11:08:50.396][000000009.450] I/user.[excloud]TCP socket cb userdata: 0C184050 33554450 0
[2026-08-07 11:08:50.398][000000009.451] I/user.[excloud]TCP socket 发送完成
[2026-08-07 11:08:50.763][000000009.819] I/user.SMS_REPORT msg_ref= 10 status= 65 str= FAILED_INCOMPAT_DEST phone= 8617538215008 time= 26-08-07 11:08:51
[2026-08-07 11:08:50.765][000000009.819] I/user.SMS_REPORT 目标不可达(空号) 8617538215008
[2026-08-07 11:08:50.771][000000009.820] I/user.sms_bridge SMS_REPORT上报 plat_seq 1786072129 msg_ref 10 status 目标不可达(空号/离线) phone 8617538215008
[2026-08-07 11:08:50.774][000000009.821] I/user.[excloud]构建发送数据 field: 30 type: 0 value: 0
[2026-08-07 11:08:50.778][000000009.823] I/user.[excloud]构建发送数据 field: 1039 type: 3 value: 1786072129
[2026-08-07 11:08:50.781][000000009.824] I/user.[excloud]构建发送数据 field: 1040 type: 3 value: 8617538215008
[2026-08-07 11:08:50.786][000000009.826] I/user.[excloud]构建发送数据 field: 1041 type: 5 value: 301
[2026-08-07 11:08:50.790][000000009.827] I/user.[excloud]构建发送数据 field: 1042 type: 5 value: 目标不可达(空号/离线)
[2026-08-07 11:08:50.792][000000009.828] I/user.[excloud]构建发送数据 field: 1047 type: 0 value: 10
[2026-08-07 11:08:50.795][000000009.830] I/user.[excloud]tlv发送数据长度4 88
[2026-08-07 11:08:50.797][000000009.831] I/user.[excloud]构建消息头 seq: 4 len: 88 flags: 2 dev: 0186232308999518
[2026-08-07 11:08:50.800][000000009.834] I/user.[excloud]数据发送成功 104 字节
[2026-08-07 11:08:50.803][000000009.835] I/user.sms SMS_REPORT上报 plat_seq 1786072129 msg_ref 10 status 目标不可达(空号/离线) phone 8617538215008
[2026-08-07 11:08:50.839][000000009.892] I/user.[excloud]TCP socket cb userdata: 0C184050 33554450 0
[2026-08-07 11:08:50.842][000000009.892] I/user.[excloud]TCP socket 发送完成
[2026-08-07 11:08:51.278][000000010.337] D/sms pdu len 15
[2026-08-07 11:08:51.832][000000010.879] I/user.SMS_SENT result= false rp_cause= 111 msg_ref= 0 error_code= 65535
[2026-08-07 11:08:51.835][000000010.879] I/user.SMS_SENT 发送失败 PROTOCOL_ERROR error_code= 65535
[2026-08-07 11:08:51.838][000000010.880] I/user.sms_bridge SMS_SENT上报 plat_seq 1786072131 msg_ref 0 status 发送失败 callee 10001
[2026-08-07 11:08:51.841][000000010.881] I/user.[excloud]构建发送数据 field: 29 type: 0 value: 0
[2026-08-07 11:08:51.844][000000010.882] I/user.[excloud]构建发送数据 field: 1039 type: 3 value: 1786072131
[2026-08-07 11:08:51.847][000000010.884] I/user.[excloud]构建发送数据 field: 1040 type: 3 value: 10001
[2026-08-07 11:08:51.851][000000010.885] I/user.[excloud]构建发送数据 field: 1041 type: 5 value: 102
[2026-08-07 11:08:51.855][000000010.886] I/user.[excloud]构建发送数据 field: 1042 type: 5 value: 发送失败
[2026-08-07 11:08:51.858][000000010.888] I/user.[excloud]构建发送数据 field: 1047 type: 0 value: 0
[2026-08-07 11:08:51.860][000000010.889] I/user.[excloud]tlv发送数据长度4 62
[2026-08-07 11:08:51.862][000000010.890] I/user.[excloud]构建消息头 seq: 5 len: 62 flags: 2 dev: 0186232308999518
[2026-08-07 11:08:51.867][000000010.893] I/user.[excloud]数据发送成功 78 字节
[2026-08-07 11:08:51.869][000000010.893] I/user.sms SMS_SENT上报 plat_seq 1786072131 msg_ref 0 status 发送失败 callee 10001
[2026-08-07 11:08:51.895][000000010.948] I/user.[excloud]TCP socket cb userdata: 0C184050 33554450 0
[2026-08-07 11:08:51.900][000000010.949] I/user.[excloud]TCP socket 发送完成
[2026-08-07 11:09:39.842][000000058.901] D/sms dcs 8 | 0 | 0 | 0
[2026-08-07 11:09:39.845][000000058.902] I/user.sms_bridge 收到短信上报 106980095566 seq= 1
[2026-08-07 11:09:39.847][000000058.903] I/user.[excloud]构建发送数据 field: 30 type: 0 value: 0
[2026-08-07 11:09:39.858][000000058.905] I/user.[excloud]构建发送数据 field: 1039 type: 3 value: 1
[2026-08-07 11:09:39.861][000000058.907] I/user.[excloud]构建发送数据 field: 1043 type: 3 value: 106980095566
[2026-08-07 11:09:39.863][000000058.909] I/user.[excloud]构建发送数据 field: 1041 type: 5 value: 【中国银行】立秋的第一杯奶茶,安排!点https://mbs.boc.cn/v/rYfyq 领淘宝闪购券,最高省15元。拒收请回复R
[2026-08-07 11:09:39.887][000000058.910] I/user.[excloud]tlv发送数据长度4 172
[2026-08-07 11:09:39.891][000000058.911] I/user.[excloud]构建消息头 seq: 6 len: 172 flags: 2 dev: 0186232308999518
[2026-08-07 11:09:39.893][000000058.914] I/user.[excloud]数据发送成功 188 字节
[2026-08-07 11:09:39.895][000000058.915] I/user.sms 收到短信上报 phone 106980095566 seq 1
[2026-08-07 11:09:39.935][000000058.985] I/user.[excloud]TCP socket cb userdata: 0C184050 33554450 0
[2026-08-07 11:09:39.938][000000058.985] I/user.[excloud]TCP socket 发送完成
[2026-08-07 11:09:50.081][000000069.141] I/user.excloud_app 首次上传运维日志
[2026-08-07 11:09:50.095][000000069.148] I/user.[excloud]运维日志文件检查 /hzmtn1.trc true 0
[2026-08-07 11:09:50.099][000000069.154] I/user.[excloud]运维日志文件检查 /hzmtn2.trc true 0
[2026-08-07 11:09:50.111][000000069.160] I/user.[excloud]运维日志文件检查 /hzmtn3.trc true 1640
[2026-08-07 11:09:50.116][000000069.167] I/user.[excloud]运维日志文件检查 /hzmtn4.trc true 0
[2026-08-07 11:09:50.121][000000069.167] I/user.[excloud]开始上传运维日志 文件数: 1
[2026-08-07 11:09:50.127][000000069.173] I/user.[excloud]开始文件上传 类型: 3 文件: /hzmtn3.trc 大小: 1640
[2026-08-07 11:09:50.131][000000069.174] I/user.[excloud]开始发送HTTP请求 URL: https://api.luatos.com/iot/air_up/file
[2026-08-07 11:09:50.134][000000069.179] D/socket connect to api.luatos.com,443
[2026-08-07 11:09:50.138][000000069.180] dns_run 845:api.luatos.com state 0 id 7523 ipv6 0 use dns server2, try 0
[2026-08-07 11:09:50.173][000000069.220] dns_run 862:dns all done ,now stop
[2026-08-07 11:09:50.794][000000069.852] I/zbuff create large size: 64 kbyte, trigger force GC
[2026-08-07 11:09:51.190][000000070.244] I/user.httpplus 服务器已完成响应
[2026-08-07 11:09:51.192][000000070.247] I/user.[excloud]文件上传响应 HTTP Code: 200 Body: {"info":"iot./iot/air_up/file","code":0,"trace":"iot./iot/air_up/file trcace:","log":"^^^","value":{"uri":"/vsa/aircloud_file/Dn...
[2026-08-07 11:09:51.195][000000070.248] I/user.[excloud]文件上传成功 URL: /vsa/aircloud_file/Dn2wZzL153pU5yZbufKb6S/2026-08/862323089995180/20260807110952_hzmtn3.trc
[2026-08-07 11:09:51.205][000000070.252] I/user.[excloud]文件上传完成
[2026-08-07 11:09:51.208][000000070.253] I/user.[excloud]运维日志上传成功 文件: hzmtn3.trc 大小: 1640
[2026-08-07 11:09:51.211][000000070.254] I/user.[excloud]构建发送数据 field: 27 type: 4 value: {"index":3,"timestamp":1786072191,"status":1}
[2026-08-07 11:09:51.213][000000070.255] I/user.[excloud]tlv发送数据长度4 49
[2026-08-07 11:09:51.215][000000070.256] I/user.[excloud]构建消息头 seq: 7 len: 49 flags: 2 dev: 0186232308999518
[2026-08-07 11:09:51.217][000000070.259] I/user.[excloud]数据发送成功 65 字节
[2026-08-07 11:09:51.220][000000070.263] I/user.[excloud]运维日志文件已清空 /hzmtn3.trc
[2026-08-07 11:09:51.223][000000070.264] I/user.[excloud]运维日志上传完成 成功文件数: 1 失败文件数: 0
[2026-08-07 11:09:51.269][000000070.321] I/user.[excloud]TCP socket cb userdata: 0C184050 33554450 0
[2026-08-07 11:09:51.272][000000070.322] I/user.[excloud]TCP socket 发送完成
```
4、Aircloud平台可以看到如下报文上报
![](https://docs.openluat.com/air8000/luatos/app/image/8783短信aircloud.jpg)
@@ -0,0 +1,37 @@
--[[
@module sms_app
@summary 短信测试应用功能模块
@version 1.0
@date 2026.08.06
@author 王城钧
@usage
本文件为短信测试应用功能模块,核心业务逻辑为:
1、开机等待8秒钟后,自动执行上行短信测试用例;
2、调用sms_bridge.send_sms向指定号码发送短信,验证短信上行链路是否正常;
3、测试结束后打印结果日志;
本文件没有对外接口,直接在main.lua中require "sms_app"就可以加载运行;
]]
-- 变量
local excloud = require("excloud")
local sms_bridge = require("sms_bridge")
local SMS_TEST_UPLINK = true
-- 上行测试:设备直接发送短信
if SMS_TEST_UPLINK then
sys.taskInit(function()
sys.wait(8000)
log.info("test_app", "=== 上行测试开始 ===")
sms_bridge.send_sms("17538215008", "301")
sys.waitUntil("SMS_SENT")
sys.wait(1000)
sms_bridge.send_sms("10001", "102")
end)
end
log.info("test_app", "已加载", "上行=", SMS_TEST_UPLINK)
@@ -0,0 +1,175 @@
--[[
@module sms_bridge
@summary 短信与AirCloud协议桥接功能模块
@version 1.0
@date 2026.08.06
@author 王城钧
@usage
本文件为短信与AirCloud协议桥接功能模块,核心业务逻辑为:
1、订阅短信事件SMS_SENT,设备发送短信被SMSC接受后,将发送结果转换为TLV报文上传云平台;
2、订阅短信事件SMS_REPORT,短信投递状态报告到达后,将送达状态转换为TLV报文上传云平台;
3、订阅短信事件SMS_INC,设备收到新短信后,将短信内容转换为TLV报文上传云平台;
4、处理云平台下发的短信指令(SMS_SEND),调起模组短信能力(sms.send/sms.sendLong)向目标号码发送短信;
5、维护msg_ref到平台seq的绑定关系,确保投递状态报告能关联到原始发送消息;
本文件对外接口有两个:
1、sms_bridge.send_sms(callee, content) - 发送短信,调用sms.send接口;
2、sms_bridge.handle_message(tlvs) - 处理云平台下发的TLV消息;
]]
local sms_bridge = {}
-- 变量
local excloud = require("excloud")
local FIELD = excloud.FIELD_MEANINGS
local DTYPE = excloud.DATA_TYPES
local _seq = 0
-- 平台下发的流水号 + 号码 + 内容(sms.send 时暂存,SMS_SENT 回调时取出上报)
local _pending_seq = ""
local _pending_callee = ""
local _pending_content = ""
-- msg_ref → 平台 seq 绑定表(SMS_SENT 时写入,SMS_REPORT 时查表取出原 seq
local _bindings = {}
local SMS_STATUS_MAP = {
[0] = "发送成功",
[1] = "发送失败",
[0x22] = "不在线/关机",
[0x23] = "服务拒绝(停机)",
[0x41] = "目标不可达(空号/离线)",
[0x43] = "无法获取",
[0x46] = "有效期过期",
}
-- 内部函数
local function send_tlvs(tlvs)
local ok, err = excloud.send(tlvs, false)
if not ok then
log.warn("sms_bridge", "TLV发送失败", err)
end
end
local function mtn(tag, ...)
excloud.mtn_log(tag, ...)
end
-- 事件订阅
sys.subscribe("SMS_SENT", function(result, rp_cause, rp_cause_str, msg_ref, error_code)
local plat_seq = _pending_seq or ""
local status_str = result and SMS_STATUS_MAP[0] or SMS_STATUS_MAP[1]
-- 绑定 msg_ref → 平台 seq(供 SMS_REPORT 查表)
if msg_ref and plat_seq ~= "" then
_bindings[msg_ref] = plat_seq
end
log.info("sms_bridge", "SMS_SENT上报", "plat_seq", plat_seq, "msg_ref", msg_ref, "status", status_str, "callee", _pending_callee)
send_tlvs({
{ field_meaning = FIELD.SMS_SEND_RSP, data_type = DTYPE.INTEGER, value = 0 },
{ field_meaning = FIELD.SMS_SEQ, data_type = DTYPE.ASCII, value = plat_seq },
{ field_meaning = FIELD.SMS_CALLEE, data_type = DTYPE.ASCII, value = _pending_callee },
{ field_meaning = FIELD.SMS_CONTENT, data_type = DTYPE.UNICODE, value = _pending_content },
{ field_meaning = FIELD.SMS_STATUS, data_type = DTYPE.UNICODE, value = status_str },
{ field_meaning = FIELD.SMS_MSG_REF, data_type = DTYPE.INTEGER, value = msg_ref or 0 },
})
mtn("sms", "SMS_SENT上报", "plat_seq", plat_seq, "msg_ref", msg_ref, "status", status_str, "callee", _pending_callee)
end)
sys.subscribe("SMS_REPORT", function(msg_ref, status, status_str, phone, discharge_time)
local plat_seq = _bindings[msg_ref] or ""
local report_status = SMS_STATUS_MAP[status] or status_str or ("未知状态:" .. tostring(status))
log.info("sms_bridge", "SMS_REPORT上报", "plat_seq", plat_seq, "msg_ref", msg_ref, "status", report_status, "phone", phone)
send_tlvs({
{ field_meaning = FIELD.SMS_REPORT, data_type = DTYPE.INTEGER, value = 0 },
{ field_meaning = FIELD.SMS_SEQ, data_type = DTYPE.ASCII, value = plat_seq },
{ field_meaning = FIELD.SMS_CALLEE, data_type = DTYPE.ASCII, value = tostring(phone or "") },
{ field_meaning = FIELD.SMS_CONTENT, data_type = DTYPE.UNICODE, value = _pending_content },
{ field_meaning = FIELD.SMS_STATUS, data_type = DTYPE.UNICODE, value = report_status },
{ field_meaning = FIELD.SMS_MSG_REF, data_type = DTYPE.INTEGER, value = msg_ref or 0 },
})
mtn("sms", "SMS_REPORT上报", "plat_seq", plat_seq, "msg_ref", msg_ref, "status", report_status, "phone", phone)
end)
sys.subscribe("SMS_INC", function(phone, content, metadata)
if not phone then return end
_seq = _seq + 1
log.info("sms_bridge", "收到短信上报", phone, "seq=", _seq)
send_tlvs({
{ field_meaning = FIELD.SMS_REPORT, data_type = DTYPE.INTEGER, value = 0 },
{ field_meaning = FIELD.SMS_SEQ, data_type = DTYPE.ASCII, value = tostring(_seq) },
{ field_meaning = FIELD.SMS_CALLER, data_type = DTYPE.ASCII, value = tostring(phone) },
{ field_meaning = FIELD.SMS_CONTENT, data_type = DTYPE.UNICODE, value = tostring(content or "") },
})
mtn("sms", "收到短信上报", "phone", phone, "seq", _seq)
if _G.ON_SMS_INCOMING then
_G.ON_SMS_INCOMING(phone, content, metadata)
end
end)
-- 公开接口
function sms_bridge.send_sms(callee, content)
_pending_seq = tostring(os.time()) -- 本地发送无平台 seq,用时间戳占位
_pending_callee = tostring(callee)
_pending_content = tostring(content)
return sms.send(tostring(callee), tostring(content), true, true)
end
function sms_bridge.handle_message(tlvs)
if not tlvs then return end
local is_sms_send = false
for _, tlv in ipairs(tlvs) do
if tlv.field == FIELD.SMS_SEND then
is_sms_send = true
break
end
end
if not is_sms_send then return end
local callee, content, seq, long_flag, long_total
for _, tlv in ipairs(tlvs) do
if tlv.field == FIELD.SMS_CALLEE then
callee = tlv.value
elseif tlv.field == FIELD.SMS_CONTENT then
content = tlv.value
elseif tlv.field == FIELD.SMS_SEQ then
seq = tostring(tlv.value or "")
elseif tlv.field == FIELD.SMS_LONG_FLAG then
long_flag = tlv.value
elseif tlv.field == FIELD.SMS_LONG_TOTAL then
long_total = tlv.value
end
end
if not callee or not content then
log.warn("sms_bridge", "SMS_SEND缺少callee或content")
send_tlvs({
{ field_meaning = FIELD.SMS_SEND_RSP, data_type = DTYPE.INTEGER, value = 0 },
{ field_meaning = FIELD.SMS_SEQ, data_type = DTYPE.ASCII, value = seq or "" },
{ field_meaning = FIELD.SMS_STATUS, data_type = DTYPE.UNICODE, value = SMS_STATUS_MAP[1] },
})
return
end
log.info("sms_bridge", "下发短信", callee, "plat_seq=", seq, "long=", long_flag)
mtn("sms", "收到平台下发短信", "callee", callee, "plat_seq", seq, "long_flag", long_flag)
_pending_seq = seq
_pending_callee = tostring(callee)
_pending_content = tostring(content)
if long_flag and long_flag == 1 and long_total and long_total > 1 then
sys.taskInit(function()
pcall(sms.sendLong, tostring(callee), tostring(content), true, true)
end)
else
sms.send(tostring(callee), tostring(content), true, true)
end
end
log.info("sms_bridge", "已加载")
return sms_bridge
@@ -0,0 +1,42 @@
--[[
@module sms_callback
@summary 短信事件本地调试日志功能模块
@version 1.0
@date 2026.08.06
@author 王城钧
@usage
本文件为短信事件本地调试日志功能模块,核心业务逻辑为:
1、订阅短信事件SMS_SENT,打印短信发送结果(SMSC是否接受,msq_ref等);
2、订阅短信事件SMS_REPORT,打印短信投递状态报告(送达成功/失败/空号等);
3、以上日志用于真机联调时观察短信链路的实时状态;
本文件没有对外接口,直接在main.lua中require "sms_callback"就可以加载运行;
]]
sys.subscribe("SMS_SENT", function(result, rp_cause, rp_cause_str, msg_ref, error_code)
log.info("SMS_SENT", "result=", result, "rp_cause=", rp_cause, "msg_ref=", msg_ref, "error_code=", error_code)
if result then
log.info("SMS_SENT", "SMSC已接受, msg_ref=", msg_ref, "等待回执...")
else
log.info("SMS_SENT", "发送失败", rp_cause_str or "", "error_code=", error_code)
end
end)
sys.subscribe("SMS_REPORT", function(msg_ref, status, status_str, phone, discharge_time)
log.info("SMS_REPORT", "msg_ref=", msg_ref, "status=", status, "str=", status_str, "phone=", phone, "time=", discharge_time)
if status == 0 then
log.info("SMS_REPORT", "送达成功", phone, discharge_time)
elseif status == 0x22 then
log.info("SMS_REPORT", "用户不在线/关机", phone)
elseif status == 0x23 then
log.info("SMS_REPORT", "服务拒绝(可能停机)", phone)
elseif status == 0x41 then
log.info("SMS_REPORT", "目标不可达(空号)", phone)
elseif status == 0x43 then
log.info("SMS_REPORT", "无法获取(可能空号/停机)", phone)
else
log.info("SMS_REPORT", "送达失败", status_str, phone)
end
end)
log.info("sms_callback", "已加载")
@@ -0,0 +1,42 @@
--[[
@module wdt_app
@summary 短信测试用例
@version 1.0
@date 2026.08.06
@author 王城钧
]]
-- 引入看门狗扩展库
local exair153x_wdt = require("exair153x_wdt")
-- 看门狗任务
local function wdt_task()
-- 初始化看门狗,使用 GPIO24 喂狗引脚
-- 默认参数:脉冲宽度 200ms,自动喂狗周期 180s
local success = exair153x_wdt.init({
wdt_pin = 23, -- 连接 WTDOG 的 GPIO 引脚号
})
-- 初始化失败则退出
if not success then
log.error("main", "看门狗初始化失败")
return
end
log.info("main", "看门狗初始化成功,自动喂狗已启动")
log.info("main", "当前版本: " .. exair153x_wdt.version())
-- 演示:等待 30 秒后手动补喂狗
sys.wait(30000)
log.info("main", "执行手动补喂狗")
exair153x_wdt.feed()
-- 业务主循环
while true do
log.info("main", "正常运行中...")
sys.wait(60000)
end
end
-- 启动看门狗任务
sys.taskInit(wdt_task)
@@ -0,0 +1,118 @@
--[[
@module http_upload_stream
@summary HTTP流式上传模块,通过UART获取数据后使用fill_data_func流式上传到服务器
@version 1.0
@date 2025.08.06
@author 马梦阳
@usage
本文件为HTTP流式上传模块,核心业务逻辑为:
1. 向uart_app模块请求文件大小(GET_FILE_SIZE_REQ/RESP消息对)
2. 向uart_app模块分片请求文件数据(GET_FILE_DATA_REQ/RESP消息对)
3. 使用httpplus扩展库的fill_data_func流式上传功能,将数据上传到服务器
4. 上传服务器地址:https://airtest.luatos.com/iot/luat_test_file/add
与uart_app模块的消息交互流程:
http_upload_stream uart_app
| |
|-- GET_FILE_SIZE_REQ ------------->|
|<----------- GET_FILE_SIZE_RESP ---|
| |
|-- GET_FILE_DATA_REQ(index) ------>| (循环,每次由fill_data_func触发)
|<----------- GET_FILE_DATA_RESP ---|
本文件没有对外接口,直接在main.lua中require "http_upload_stream"就可以加载运行
]]
local httpplus = require "httpplus"
-- fill_data_func: 每次被httpplus底层调用时,向uart_app请求下一片数据
-- @param total_sent 已发送的总字节数
-- @param sent_count 已调用fill_data_func的次数
-- @return zbuff数据 或 nil(数据源耗尽)
local function fill_stream_data(total_sent, sent_count)
log.info("fill_stream_data", total_sent, sent_count)
sys.publish("GET_FILE_DATA_REQ", total_sent + 1)
local result, data = sys.waitUntil("GET_FILE_DATA_RESP", 10000)
if result then
if type(data) == "string" then
log.info("fill_stream_data", "GET_FILE_DATA_RESP", data:len())
else
log.info("fill_stream_data", "GET_FILE_DATA_RESP", data:used())
end
return data
else
log.error("http_upload_stream", "GET_FILE_DATA_RESP timeout", total_sent)
return
end
end
-- HTTP流式上传任务的主处理函数
local function http_upload_task_func()
local code, response
while true do
-- 向uart_app请求文件大小
sys.publish("GET_FILE_SIZE_REQ")
local result, file_size = sys.waitUntil("GET_FILE_SIZE_RESP", 10000)
if not result then
log.error("http_upload_stream", "GET_FILE_SIZE_RESP timeout")
goto LOOP_PROC
end
log.info("http_upload_stream", "file_size", file_size)
-- 等待网络就绪
while not socket.adapter(socket.dft()) do
log.warn("http_upload_stream", "wait IP_READY", socket.dft())
sys.waitUntil("IP_READY", 1000)
end
log.info("http_upload_stream", "recv IP_READY", socket.dft())
-- 使用httpplus流式上传接口上传文件数据
-- files中["f"]的value为table类型,表示流式上传:
-- size: 文件总字节数,由uart_app通过串口获取
-- fill_data_func: 填充函数,httpplus底层循环调用获取分片数据
-- filename: 上传到服务器的文件名
-- 服务器要求文件name必须使用"f",文本name必须使用"params"
-- 测试接口响应:成功 HTTP 200 {"code":0,"value":"上传成功"}
-- 网页端查看:https://iot.luatos.com/#/p8000/netlab_file_server
code, response = httpplus.request(
{
url = "https://airtest.luatos.com/iot/luat_test_file/add",
files =
{
["f"] =
{
size = file_size,
filename = "uart_to_http_stream.bin",
fill_data_func = fill_stream_data
}
},
forms =
{
["params"] = json.encode({
username = "LuatOS",
password = "1234567890"
}),
},
-- 超时时间与数据长度关联: 1000bytes/s保守速率, 最低60秒
timeout = math.max(60, math.ceil(file_size / 1000))
})
log.info("http_upload_stream", code==200 and "success" or "error", code)
if code==200 then
log.info("http_upload_stream", "headers", json.encode(response.headers or {}))
local body = response.body:query()
log.info("http_upload_stream", "body", body and (body:len()>1024 and body:len() or body) or "nil")
end
::LOOP_PROC::
-- 60秒之后,循环测试
sys.wait(60000)
end
end
-- 创建并且启动一个task
sys.taskInit(http_upload_task_func)
@@ -0,0 +1,83 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,UART数据流式上传到HTTP服务器
@version 1.0
@date 2025.07.28
@author 马梦阳
@usage
本demo演示的核心功能为:
1、通过UART串口从PC端获取文件数据(文件大小+分片数据)
2、使用httpplus扩展库的fill_data_func流式上传功能,将UART接收的数据流式上传到HTTP服务器
3、netdrv_device:配置连接外网使用的网卡,目前支持以下三种选择(三选一)
(1) netdrv_4g4G网卡
(2) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
(3) netdrv_multiple:支持以上两种网卡,可以配置两种网卡的优先级
模块间的消息交互:
uart_app http_upload_stream
| |
|<--------- GET_FILE_SIZE_REQ ----------| (请求文件大小)
|---------- GET_FILE_SIZE_RESP -------->| (返回文件大小)
| |
|<--------- GET_FILE_DATA_REQ(index) ---| (请求第index片数据,由fill_data_func触发)
|---------- GET_FILE_DATA_RESP -------->| (返回zbuff数据)
更多说明参考本目录下的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 = "UART_TO_HTTP_STREAMER"
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"
-- 加载UART串口通信模块
require "uart_app"
-- 加载HTTP流式上传模块
require "http_upload_stream"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,43 @@
--[[
@module netdrv_4g
@summary “4G网卡”驱动模块
@version 1.0
@date 2025.07.01
@author 马梦阳
@usage
本文件为4G网卡驱动模块,核心业务逻辑为:
1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
]]
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_GP then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
end
end
local function ip_lose_func(adapter)
if adapter == socket.LWIP_GP then
log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
end
end
-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
-- 在Air780EPM上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
@@ -0,0 +1,77 @@
--[[
@module netdrv_eth_spi
@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
@version 1.0
@date 2025.07.24
@author 马梦阳
@usage
本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块,核心业务逻辑为:
1、打开CH390H芯片供电开关;
2、初始化spi0,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
3、以太网卡的连接状态发生变化时,在日志中进行打印;
直接使用Air780EPM V1.3版本开发板硬件测试即可;
本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
]]
local exnetif = require "exnetif"
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_ETH then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
end
end
local function ip_lose_func(adapter)
if adapter == socket.LWIP_ETH then
log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
end
end
-- 以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
-- 各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
-- 以太网断网后,内核固件会产生一个"IP_LOSE"消息
-- 各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
--此处订阅"IP_READY"和"IP_LOSE"两种消息
--在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI外挂CH390H芯片的以太网卡”的连接状态
--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
local function netdrv_eth_spi_task_func()
-- 配置SPI外接以太网芯片CH390H的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_ETH
-- 本demo使用Air780EPM V1.3版本开发板测试,开发板上的硬件配置为:
-- GPIO20为CH390H以太网芯片的供电使能控制引脚
-- 使用spi0,片选引脚使用GPIO8,中断引脚使用GPIO22
-- 如果使用的硬件和以上描述的环境不同,根据自己的硬件配置修改以下参数
exnetif.set_priority_order({
{
ETHERNET = {
pwrpin = 20,
tp = netdrv.CH390,
opts = {spi = 0, cs = 8, irq = 22}
}
}
})
end
-- 启动一个tasktask的处理函数为netdrv_eth_spi_task_func
-- 在处理函数中调用exnetif.set_priority_order设置网卡优先级
-- 因为exnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
sys.taskInit(netdrv_eth_spi_task_func)
@@ -0,0 +1,88 @@
--[[
@module netdrv_multiple
@summary 多网卡(4G网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块
@version 1.0
@date 2025.07.24
@author 马梦阳
@usage
本文件为多网卡驱动模块,核心业务逻辑为:
1、调用exnetif.set_priority_order配置多网卡的控制参数以及优先级;
直接使用Air780EPM V1.3版本开发板硬件测试即可;
本文件没有对外接口,直接在其他功能模块中require "netdrv_multiple"就可以加载运行;
]]
local exnetif = require "exnetif"
-- 网卡状态变化通知回调函数
-- 当exnetif中检测到网卡切换或者所有网卡都断网时,会触发调用此回调函数
-- 当网卡切换切换时:
-- net_typestring类型,表示当前使用的网卡字符串
-- adapternumber类型,表示当前使用的网卡id
-- 当所有网卡断网时:
-- net_type:为nil
-- adapternumber类型,为-1
local function netdrv_multiple_notify_cbfunc(net_type,adapter)
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
if type(net_type)=="string" then
log.info("netdrv_multiple_notify_cbfunc", "use new adapter", net_type, adapter)
elseif type(net_type)=="nil" then
log.warn("netdrv_multiple_notify_cbfunc", "no available adapter", net_type, adapter)
else
log.warn("netdrv_multiple_notify_cbfunc", "unknown status", net_type, adapter)
end
end
local function netdrv_multiple_task_func()
--设置网卡优先级
exnetif.set_priority_order(
{
-- “通过SPI外挂CH390H芯片”的以太网卡,使用Air780EPM V1.3版本开发板验证
{
ETHERNET = {
-- 供电使能GPIO
pwrpin = 20,
-- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
-- 如果没有传入此参数,exnetif会使用默认值10秒
ping_time = 3000,
-- 连通性检测ip(选填参数)
-- 如果没有传入ip地址,exnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
-- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
-- ping_ip = "填入可靠的并且可以ping通的ip地址",
-- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
tp = netdrv.CH390,
opts = {spi=0, cs=8, irq=22}
}
},
-- 4G网卡
{
LWIP_GP = true
}
}
)
end
-- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc
exnetif.notify_status(netdrv_multiple_notify_cbfunc)
-- 如果存在udp网络应用,并且udp网络应用中,根据应用层的心跳能够判断出来udp数据通信出现了异常;
-- 可以在判断出现异常的位置,调用一次exnetif.check_network_status()接口,强制对当前正式使用的网卡进行一次连通性检测;
-- 如果存在tcp网络应用,不需要用户调用exnetif.check_network_status()接口去控制,exnetif会在tcp网络应用通信异常时自动对当前使用的网卡进行连通性检测。
-- 启动一个tasktask的处理函数为netdrv_multiple_task_func
-- 在处理函数中调用exnetif.set_priority_order设置网卡优先级
-- 因为exnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
sys.taskInit(netdrv_multiple_task_func)
@@ -0,0 +1,45 @@
--[[
@module netdrv_pc
@summary “pc模拟器网卡”驱动模块
@version 1.0
@date 2025.07.01
@author 朱天华
@usage
本文件为pc模拟器网卡驱动模块,核心业务逻辑为:
1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
本文件没有对外接口,直接在其他功能模块中require "netdrv_pc"就可以加载运行;
]]
local function ip_ready_func(ip, adapter)
if adapter == socket.ETH0 then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
log.info("netdrv_pc.ip_ready_func", "IP_READY", socket.localIP(socket.ETH0))
end
end
local function ip_lose_func(adapter)
if adapter == socket.ETH0 then
log.warn("netdrv_pc.ip_lose_func", "IP_LOSE")
end
end
--此处订阅"IP_READY"和"IP_LOSE"两种消息
--在消息的处理函数中,仅仅打印了一些信息,便于实时观察pc模拟器网络的连接状态
--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
-- 设置默认网卡为socket.ETH0
-- pc模拟器上的默认网卡仍然需要使用接口(socket.ETH0)来设置,因为exnetif扩展库当前还不支持模拟器
socket.dft(socket.ETH0)
@@ -0,0 +1,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:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
根据自己的项目需求,只需要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,102 @@
## 功能模块介绍
1、main.lua:主程序入口;
2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的四种网卡(单4g网卡,单spi以太网卡,多网卡,PC模拟网卡)中的任何一种网卡;
3、uart_app.luaUART串口通信模块,负责与PC端串口交互获取文件数据;
4、http_upload_stream.luaHTTP流式上传模块,使用httpplus扩展库的fill_data_func功能,将UART接收的数据流式上传到服务器;
## 演示功能概述
1、uart_app:通过UART1串口与PC端通信,支持以下两种请求
- 请求文件大小:模组发送"getSize\r\n"PC端返回文件大小(数字字符串)
- 请求文件数据:模组发送"getData,{index}\r\n"PC端返回从index位置开始的文件数据(二进制)
2、http_upload_stream:使用httpplus扩展库的fill_data_func流式上传功能
- 向uart_app请求文件大小(GET_FILE_SIZE_REQ/RESP消息对)
- 向uart_app分片请求文件数据(GET_FILE_DATA_REQ/RESP消息对)
- 通过fill_data_func将数据流式上传到https://airtest.luatos.com/iot/luat_test_file/add
- http测试服务器要求文件name使用"f",文本name使用"params"
- 如果使用的是你自己的服务器,根据实际情况修改服务器地址、端口、文件name、文本name等参数
3、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
(1) netdrv_4g4G网卡
(2) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
(3) netdrv_multiple:支持以上两种网卡,可以配置两种网卡的优先级
(4) netdrv_pcPC模拟网卡(用于开发调试)
## 模块间消息交互
```
uart_app http_upload_stream
| |
|<--------- GET_FILE_SIZE_REQ ----------| (请求文件大小)
|---------- GET_FILE_SIZE_RESP -------->| (返回文件大小)
| |
|<--------- GET_FILE_DATA_REQ(index) ---| (请求第index片数据,由fill_data_func触发)
|---------- GET_FILE_DATA_RESP -------->| (返回zbuff或者string数据)
```
## 演示硬件环境
1、Air780EPM开发板一块+可上网的sim卡一张+4g天线一根:
- sim卡插入开发板的sim卡槽
- 天线装到开发板上
2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air780EPM开发板和数据线的硬件接线方式为:
- Air780EPM开发板通过TYPE-C USB口供电
- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口
- USB转串口数据线:白线连接开发板的UART1_TX,绿线连接开发板的UART1_RX,黑线连接核心板的GND,另外一端连接电脑USB口
## 演示软件环境
1、Luatools下载调试工具
2、PC端串口工具(如SSCOM、XCOM等),用于模拟文件数据源
3、[Air780EPM最新版本固件](https://docs.openluat.com/air780epm/luatos/firmware/version/)
## 演示核心步骤
1、搭建好硬件环境
2、demo脚本代码netdrv_device.lua中,按照自己的网卡需求启用对应的Lua文件
- 如果需要单4G网卡,打开require "netdrv_4g",其余注释掉
- 如果需要以太网卡,打开require "netdrv_eth_spi",其余注释掉
- 如果需要多网卡,打开require "netdrv_multiple",其余注释掉
- 如果在PC上调试,打开require "netdrv_pc",其余注释掉
3、Luatools烧录内核固件和修改后的demo脚本代码
4、烧录成功后,自动开机运行
5、PC端串口工具配置:波特率115200,数据位8,停止位1,无校验
6、PC端串口工具按照以下协议应答模组的请求:
- 收到getSize\r\n时,返回文件总字节数(纯数字字符串),例如163840
- 收到getData,{index}\r\n时,返回从index位置开始的文件二进制数据,每个数据包大小,由项目运行过程中的可用内存来定,本demo在uart_app.lua中单包处理的大小为16384字节,如果最后一包长度不足16384字节,返回最后一包实际长度的数据即可
- 例如:getData,1\r\n,表示返回开头位置开始的16384长度字节数据;getData,116385\r\n,表示返回第116385字节开始的16384长度字节数据;
7、上传成功后,在浏览器中打开https://iot.luatos.com/#/p8000/netlab_file_server,即可查看上传的文件
@@ -0,0 +1,126 @@
--[[
@module uart_app
@summary UART串口通信模块,负责与PC端串口交互获取文件数据
@version 1.0
@date 2025.07.28
@author 马梦阳
@usage
本文件为UART串口通信模块,核心业务逻辑为:
1. 接收http_upload_stream模块的GET_FILE_SIZE_REQ消息,向PC端串口请求文件大小
2. 接收http_upload_stream模块的GET_FILE_DATA_REQ消息,向PC端串口请求分片数据
3. 将串口接收到的数据通过GET_FILE_SIZE_RESP/GET_FILE_DATA_RESP消息返回给http_upload_stream
PC端串口协议:
- 模组发送 "getSize\r\n" → PC端返回文件大小(数字字符串)
- 模组发送 "getData,{index}\r\n" → PC端返回从index位置开始的文件数据(二进制)
本文件没有对外接口,直接在main.lua中require "uart_app"就可以加载运行
]]
local UART_ID = 1
-- 串口接收数据缓冲区大小,内核固件最大支持16384字节
local RX_BUFF_SIZE = 16384
-- 串口接收数据缓冲区模式(模拟器仅支持STRING模式)
-- "ZBUFF":使用zbuff缓冲区,大数据接收效率高
-- "STRING":使用字符串缓冲区,大数据接收效率低
local rx_mode = "ZBUFF"
if rtos.bsp() == "PC" then
rx_mode = "STRING"
end
local is_zbuff = (rx_mode == "ZBUFF")
local rxbuff = ""
if rx_mode == "ZBUFF" then
rxbuff = zbuff.create(RX_BUFF_SIZE)
end
-- 状态机: "IDLE"空闲 / "GET_FILE_SIZE"等待文件大小 / "GET_FILE_DATA"等待文件数据
local state = "IDLE"
-- 串口接收超时处理:将缓冲区数据通过消息发布给http_upload_stream模块
local function concat_timeout_func()
local data_len = is_zbuff and rxbuff:used() or rxbuff:len()
log.info("uart_app", "concat_timeout_func", data_len, state)
if data_len > 0 then
if state == "GET_FILE_SIZE" then
local size_str = is_zbuff and rxbuff:toStr(0, data_len) or rxbuff
sys.publish("GET_FILE_SIZE_RESP", tonumber(size_str))
elseif state == "GET_FILE_DATA" then
sys.publish("GET_FILE_DATA_RESP", rxbuff)
end
end
state = "IDLE"
end
-- 请求PC端返回文件大小
local function get_file_size_req()
if is_zbuff then
uart.rxClear(UART_ID)
end
uart.write(UART_ID, "getSize\r\n")
if is_zbuff then
rxbuff:seek(0)
else
rxbuff = ""
end
state = "GET_FILE_SIZE"
end
-- 请求PC端返回从index位置开始的文件数据
local function get_file_data_req(index)
if is_zbuff then
uart.rxClear(UART_ID)
end
uart.write(UART_ID, "getData," .. index .. "\r\n")
if is_zbuff then
rxbuff:seek(0)
else
rxbuff = ""
end
state = "GET_FILE_DATA"
end
-- UART数据接收中断处理函数
-- 采用50ms超时拼接策略:串口大数据包会拆成多次中断,等待50ms无新数据后视为一包完整数据
local function read(id)
local temp_buff
if is_zbuff then
temp_buff = zbuff.create(RX_BUFF_SIZE)
end
while true do
if is_zbuff then
local len = uart.rx(id, temp_buff)
log.info("uart_app", "read len", len, temp_buff:used())
if len <= 0 then
sys.timerStart(concat_timeout_func, 500)
break
end
rxbuff:copy(nil, temp_buff)
temp_buff:seek(0)
else
local s = uart.read(id, RX_BUFF_SIZE)
log.info("uart_app", "read len", id, len, s:len())
if s:len() <= 0 then
sys.timerStart(concat_timeout_func, 500)
break
end
rxbuff = rxbuff..s
end
end
if is_zbuff then
temp_buff:free()
end
end
-- 初始化UART
uart.setup(UART_ID, 115200, 8, 1, uart.NONE, uart.LSB, RX_BUFF_SIZE)
uart.on(UART_ID, "receive", read)
-- 订阅http_upload_stream模块的消息
sys.subscribe("GET_FILE_SIZE_REQ", get_file_size_req)
sys.subscribe("GET_FILE_DATA_REQ", get_file_data_req)