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,279 @@
--[[
@module ble_5101_file_fota
@summary Air5101蓝牙FOTA升级功能模块(文件写入方式)
@version 1.0
@date 2026.03.04
@author 王世豪
@usage
-- Air5101蓝牙FOTA升级功能(文件写入方式)
-- 提供通过Air5101接收升级包数据并写入文件进行固件升级的功能
本文件为FOTA业务逻辑处理模块(文件方式),核心业务逻辑为:
1. 处理开始升级命令
2. 将接收到的固件数据写入本地文件
3. 处理结束升级命令并执行FOTA升级
4. 管理升级状态和文件操作
本文件的对外接口有1个:
1. ble_file_fota.proc(data): 处理接收到的BLE数据
依赖模块:
- exril_5101: 用于Air5101蓝牙模块的AT指令封装
]]
local ble_5101_file_fota = {}
-- 升级状态管理
local upgrade_state = {
is_upgrading = false, -- 是否正在升级
total_size = 0, -- 总文件大小(字节)
received_size = 0, -- 已接收大小(字节)
upgrade_file = "/ble_fota.bin" -- 临时升级文件路径
}
-- 重启设备
local function ble_reboot()
-- 完成FOTA流程并重启
fota.finish(true)
log.info("FOTA_CMD", "正在重启设备...")
rtos.reboot()
end
-- 处理FOTA命令
-- @param cmd_data 命令数据,格式:[命令码(1字节)] 或 [命令码(1字节) + 文件大小(4字节)]
function ble_5101_file_fota.handle_command(cmd_data)
log.info("FOTA_CMD", "收到命令数据 (文件方式):", cmd_data:toHex(), "长度:", #cmd_data)
-- 检查命令数据是否有效
if #cmd_data < 1 then
log.error("FOTA_CMD", "命令数据为空")
return
end
-- 解析命令码(第一个字节)
local cmd = cmd_data:byte(1)
log.info("FOTA_CMD", "解析命令码:", cmd, string.format("(0x%02X)", cmd))
-- 命令0x01:开始升级
if cmd == 0x01 then
log.info("FOTA_CMD", "处理开始升级命令 (文件方式)")
-- 检查命令格式:需要至少5字节(1字节命令码 + 4字节文件大小)
if #cmd_data >= 5 then
-- 解析文件大小(小端序,从第2字节开始)
local total_size = string.unpack("<I4", cmd_data, 2)
log.info("FOTA_CMD", "文件总大小:", total_size, "字节")
-- 初始化FOTA子系统
log.info("FOTA_CMD", "初始化FOTA子系统...")
if fota.init() then
log.info("FOTA_CMD", "FOTA初始化成功")
-- 等待FOTA底层准备就绪
log.info("FOTA_CMD", "等待FOTA底层准备...")
-- 等待FOTA底层准备就绪,最多等待10秒
local wait_count = 0
local wait_ok = false
while wait_count < 100 do -- 最多轮询100次,每次100ms,共10秒
if fota.wait() then
wait_ok = true
break
end
sys.wait(100)
wait_count = wait_count + 1
end
if wait_ok then
log.info("FOTA_CMD", "FOTA底层准备就绪")
-- 删除旧的临时文件(如果存在)
if os.remove(upgrade_state.upgrade_file) then
log.info("FOTA_CMD", "已清理旧临时文件")
end
-- 更新升级状态
upgrade_state.is_upgrading = true
upgrade_state.total_size = total_size
upgrade_state.received_size = 0
log.info("FOTA_CMD", "升级状态已设置",
"总大小:", upgrade_state.total_size,
"升级方式: file")
log.info("FOTA_CMD", "临时文件:", upgrade_state.upgrade_file)
log.info("FOTA_CMD", "准备接收固件数据...")
else
log.error("FOTA_CMD", "FOTA底层准备超时")
fota.finish(false)
upgrade_state.is_upgrading = false
end
else
log.error("FOTA_CMD", "FOTA初始化失败")
end
else
log.error("FOTA_CMD", "开始命令格式错误,长度不足")
end
-- 命令0x02:结束升级
elseif cmd == 0x02 then
log.info("FOTA_CMD", "处理结束升级命令 (文件方式)")
-- 检查是否处于升级状态
if not upgrade_state.is_upgrading then
log.warn("FOTA_CMD", "未处于升级状态,忽略结束命令")
return
end
-- 验证文件完整性
log.info("FOTA_CMD", "验证文件完整性...")
log.info("FOTA_CMD", "已接收:", upgrade_state.received_size, "字节")
log.info("FOTA_CMD", "应接收:", upgrade_state.total_size, "字节")
if upgrade_state.received_size == upgrade_state.total_size then
log.info("FOTA_CMD", "文件完整性验证通过")
-- 执行FOTA升级
log.info("FOTA_CMD", "开始执行FOTA升级...")
local result, isDone = fota.file(upgrade_state.upgrade_file)
log.info("FOTA_CMD", "FOTA升级结果:", "result:", result, "isDone:", isDone)
-- 检查升级结果
if result and isDone then
log.info("FOTA_CMD", " FOTA升级成功!")
-- 延迟重启,给用户一些反应时间
log.info("FOTA_CMD", "2秒后设备将自动重启...,重启后通过日志判断最终是否升级成功")
-- 延迟2秒后重启设备
sys.timerStart(ble_reboot, 2000)
else
log.error("FOTA_CMD", "FOTA升级失败")
end
else
log.error("FOTA_CMD", "文件不完整,升级失败")
end
-- 清理升级状态(无论成功还是失败)
log.info("FOTA_CMD", "清理升级状态...")
upgrade_state.is_upgrading = false
-- 删除临时文件
if upgrade_state.upgrade_file then
if os.remove(upgrade_state.upgrade_file) then
log.info("FOTA_CMD", "已删除临时文件")
else
log.warn("FOTA_CMD", "删除临时文件失败")
end
end
-- 结束FOTA流程
fota.finish(false)
log.info("FOTA_CMD", "升级流程结束")
else
log.warn("FOTA_CMD", "未知命令码:", cmd, string.format("(0x%02X)", cmd))
end
end
-- 处理FOTA数据
-- @param data 固件数据块
function ble_5101_file_fota.handle_data(data)
log.info("FOTA_DATA", "收到数据包,长度:", #data, "字节 (文件方式)")
-- 检查是否处于升级状态
if not upgrade_state.is_upgrading then
log.warn("FOTA_DATA", "未处于升级状态,忽略数据")
return
end
-- 保存数据到临时文件
log.info("FOTA_DATA", "写入文件:", upgrade_state.upgrade_file)
local file = io.open(upgrade_state.upgrade_file, "ab")
if file then
-- 写入数据
file:write(data)
file:close()
-- 更新接收状态
upgrade_state.received_size = upgrade_state.received_size + #data
-- 计算并显示进度
local progress = math.floor((upgrade_state.received_size / upgrade_state.total_size) * 100)
log.info("FOTA_DATA", "接收进度:", progress, "%",
"已接收:", upgrade_state.received_size, "字节",
"总大小:", upgrade_state.total_size, "字节")
else
log.error("FOTA_DATA", "打开文件失败:", upgrade_state.upgrade_file)
-- 文件操作失败,终止升级
upgrade_state.is_upgrading = false
fota.finish(false)
end
end
-- 处理接收到的蓝牙数据
function ble_5101_file_fota.proc(data)
log.info("FOTA_FILE", "收到蓝牙数据:", data:toHex())
-- 简单的协议解析:第一个字节表示数据类型
-- 0x01: 命令数据
-- 0x02: 固件数据
if #data > 0 then
local data_type = data:byte(1)
if data_type == 0x01 then
-- 命令数据
ble_5101_file_fota.handle_command(data:sub(2))
elseif data_type == 0x02 then
-- 固件数据
ble_5101_file_fota.handle_data(data:sub(2))
else
log.warn("FOTA_FILE", "未知数据类型:", data_type)
end
end
end
-- 处理蓝牙连接成功事件
function ble_5101_file_fota.proc_connect()
log.info("FOTA_FILE", "蓝牙连接成功")
end
-- 处理蓝牙断开连接事件
function ble_5101_file_fota.proc_disconnect()
log.info("FOTA_FILE", "处理连接断开事件")
-- 如果正在升级,连接断开则终止升级
if upgrade_state.is_upgrading then
log.error("FOTA_FILE", "升级过程中连接断开,终止升级")
upgrade_state.is_upgrading = false
-- 删除临时文件
if upgrade_state.upgrade_file then
os.remove(upgrade_state.upgrade_file)
end
-- 结束FOTA流程
fota.finish(false)
end
end
-- 清理升级状态
function ble_5101_file_fota.cleanup()
-- 清理升级状态(无论成功还是失败)
log.info("FOTA_CMD", "清理升级状态...")
upgrade_state.is_upgrading = false
-- 删除临时文件
if upgrade_state.upgrade_file then
if os.remove(upgrade_state.upgrade_file) then
log.info("FOTA_CMD", "已删除临时文件")
else
log.warn("FOTA_CMD", "删除临时文件失败")
end
end
log.info("FOTA_CMD", "升级流程结束")
end
-- 获取升级状态
function ble_5101_file_fota.get_state()
return upgrade_state
end
return ble_5101_file_fota
@@ -0,0 +1,271 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Air5101 BLE FOTA 升级工具
用于通过 Air5101 蓝牙模块向设备发送固件升级包
"""
import asyncio
import argparse
import os
import struct
import time
from bleak import BleakScanner, BleakClient
# Air5101 服务和特征值 UUID
SERVICE_UUID = "0000ff00-f7e3-55b4-6c4c-9fd140100a16"
CHARACTERISTIC_UUID = "0000ff02-f7e3-55b4-6c4c-9fd140100a16" # write 和 write no response
# 命令定义
CMD_START = 0x01
CMD_END = 0x02
# 数据包大小(字节)
MAX_PACKET_SIZE = 500
# 数据包延迟(秒)
PACKET_DELAY = 0.05
class Air5101FotaTool:
def __init__(self, device_name, firmware_path):
"""初始化 FOTA 工具"""
self.device_name = device_name
self.firmware_path = firmware_path
self.client = None
self.firmware_data = None
self.firmware_size = 0
self.target_device = None
async def load_firmware(self):
"""加载固件文件到内存"""
print("\n1. 加载固件文件...")
try:
if not os.path.exists(self.firmware_path):
print(f" 错误: 固件文件不存在: {self.firmware_path}")
return False
with open(self.firmware_path, 'rb') as f:
self.firmware_data = f.read()
self.firmware_size = len(self.firmware_data)
print(f" 固件加载完成,大小: {self.firmware_size} 字节")
return True
except Exception as e:
print(f" 加载固件失败: {e}")
return False
async def scan_device(self):
"""扫描并返回指定名称的蓝牙设备"""
print("\n2. 扫描目标设备...")
print(f" 正在扫描设备: {self.device_name}...")
try:
devices = await BleakScanner.discover(timeout=10.0)
found_devices = []
for device in devices:
if device.name == self.device_name:
found_devices.append(device)
print(f" 找到设备: {device.name} ({device.address})")
if not found_devices:
print(f" 未找到设备: {self.device_name}")
return None
# 选择第一个匹配的设备
self.target_device = found_devices[0]
print(f" 选择设备: {self.target_device.name} ({self.target_device.address})")
return self.target_device
except Exception as e:
print(f" 扫描失败: {e}")
return None
async def connect_device(self, device):
"""连接到目标设备"""
print("\n3. 建立BLE连接...")
try:
self.client = BleakClient(device)
await self.client.connect(timeout=30.0)
print(f" 连接成功,状态: {self.client.is_connected}")
# 调试:打印所有服务和特征值
print("\n4. 发现服务和特征值...")
# 兼容不同版本的Bleak库
try:
# 新版本Bleak
services = self.client.services
except AttributeError:
# 旧版本Bleak
services = await self.client.get_services()
fota_service_found = False
for service in services:
if service.uuid.lower() == SERVICE_UUID.lower():
fota_service_found = True
print(f" 找到FOTA服务: {service.uuid}")
for char in service.characteristics:
print(f" 特征值: {char.uuid} - 属性: {char.properties}")
if char.uuid.lower() == CHARACTERISTIC_UUID.lower():
print(f" -> 写入特征值 (可写)")
if not fota_service_found:
print(" 警告: 未找到FOTA服务,但继续尝试...")
return True
except Exception as e:
print(f" 连接失败: {e}")
return False
async def write_characteristic(self, data):
"""写入特征值"""
try:
# 发送数据
await self.client.write_gatt_char(CHARACTERISTIC_UUID, data, response=True)
return True
except Exception as e:
print(f" 写入特征值失败: {e}")
return False
async def send_start_command(self):
"""发送开始升级命令"""
print("\n5. 发送开始升级命令...")
# 等待连接稳定
print(" 连接成功,等待1秒...")
await asyncio.sleep(1)
# 发送开始命令
start_cmd = struct.pack('B', CMD_START) + struct.pack('<I', self.firmware_size)
# 包装命令数据(添加数据类型前缀)
wrapped_start_cmd = struct.pack('B', 0x01) + start_cmd
print(f" 发送开始命令: {wrapped_start_cmd.hex()}")
if not await self.write_characteristic(wrapped_start_cmd):
return False
print(" 开始命令发送完成")
await asyncio.sleep(2) # 等待设备准备
return True
async def send_firmware_data(self):
"""分块发送固件数据"""
print("\n6. 分块传输固件数据...")
sent_size = 0
packet_count = 0
start_time = time.time()
while sent_size < self.firmware_size:
# 计算当前包大小
chunk_size = min(MAX_PACKET_SIZE, self.firmware_size - sent_size)
chunk = self.firmware_data[sent_size:sent_size + chunk_size]
# 包装数据(添加数据类型前缀)
wrapped_chunk = struct.pack('B', 0x02) + chunk
# 发送数据
if not await self.write_characteristic(wrapped_chunk):
return False
# 更新进度
sent_size += chunk_size
packet_count += 1
# 短暂延迟,避免数据包堆积
await asyncio.sleep(PACKET_DELAY)
# 每20个数据包显示一次进度
if packet_count % 20 == 0 or sent_size >= self.firmware_size:
progress = (sent_size / self.firmware_size) * 100
elapsed = time.time() - start_time
speed = sent_size / elapsed / 1024 if elapsed > 0 else 0
remaining_time = (self.firmware_size - sent_size) / (sent_size / elapsed) if sent_size > 0 else 0
print(f" 进度: {progress:.1f}% - {speed:.1f} KB/s - 已发送 {packet_count} 包 - 预计剩余: {remaining_time:.1f}s")
total_time = time.time() - start_time
avg_speed = self.firmware_size / total_time / 1024
print(f" 数据传输完成! 总时间: {total_time:.1f}s, 平均速度: {avg_speed:.1f} KB/s")
return True
async def send_end_command(self):
"""发送结束升级命令"""
print("\n7. 发送结束升级命令...")
end_cmd = struct.pack('B', CMD_END)
print(f" 发送结束命令: {end_cmd.hex()}")
# 包装命令数据(添加数据类型前缀)
wrapped_end_cmd = struct.pack('B', 0x01) + end_cmd
if not await self.write_characteristic(wrapped_end_cmd):
return False
print(" 结束命令发送完成")
return True
async def run(self):
"""主执行流程"""
# 1. 加载固件文件
if not await self.load_firmware():
return False
# 2. 扫描设备
device = await self.scan_device()
if not device:
return False
# 3. 连接设备
if not await self.connect_device(device):
return False
try:
# 4. 发送开始命令
if not await self.send_start_command():
return False
# 5. 发送固件数据
if not await self.send_firmware_data():
return False
# 6. 发送结束命令
if not await self.send_end_command():
return False
print("\n8. 等待设备处理升级...")
print(" 固件发送完成,等待设备处理升级...")
# 等待设备处理
await asyncio.sleep(5)
print("\n" + "="*50)
print("升级流程完成! 设备应该正在重启...")
print("请查看设备日志确认升级结果")
print("="*50)
return True
except Exception as e:
print(f" 升级过程中出现错误: {e}")
return False
finally:
# 断开连接
if self.client and self.client.is_connected:
await self.client.disconnect()
print(" 已断开连接")
async def main(args):
"""主函数"""
tool = Air5101FotaTool(args.device, args.firmware)
success = await tool.run()
return success
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Air5101 BLE FOTA 升级工具")
parser.add_argument("-d", "--device", default="Air5101_FOTA", help="目标设备名称")
parser.add_argument("-f", "--firmware", required=True, help="固件文件路径")
args = parser.parse_args()
success = asyncio.run(main(args))
exit(0 if success else 1)
@@ -0,0 +1,193 @@
--[[
@module ble_5101_main
@summary Air5101蓝牙服务主功能模块
@version 1.0
@date 2026.03.04
@author 王世豪
@usage
核心业务逻辑为:
1. 初始化Air5101蓝牙模块
2. 选择FOTA升级模式(file或packet
3. 注册并处理BLE事件(连接、断开连接、数据接收等)
4. 调用对应模式的处理函数
5. 管理异常处理和重新初始化
依赖模块:
- exril_5101: 用于Air5101蓝牙模块的AT指令封装
- ble_5101_file_fota: 用于处理FOTA相关业务逻辑(文件写入方式)
- ble_5101_packet_fota: 用于处理FOTA相关业务逻辑(分段写入方式)
]]
-- 加载exril_5101扩展库
local exril_5101 = require "exril_5101"
-- 选择FOTA升级方式:"file" 或 "packet"
-- 1. file方式:将升级包数据先写入本地文件,然后调用fota.file()进行升级
-- 2. packet方式:直接使用fota.packet()处理分段数据,不写入文件,适合差分升级
local fota_mode = "packet" -- 默认使用packet方式
-- 当前使用的FOTA处理器
local current_fota_handler = nil
-- 根据选择加载对应的FOTA模块
if fota_mode == "file" then
current_fota_handler = require "ble_5101_file_fota"
else
current_fota_handler = require "ble_5101_packet_fota"
end
-- 主任务名
local TASK_NAME = "BLE_5101_MAIN"
-- 配置参数
local config = {
device_name = "Air5101_FOTA", -- 设备广播名称
}
-- BLE 事件回调函数
local function ble_event_cb(event, payload)
-- 连接中心设备成功
if event == "connected" then
sys.sendMsg(TASK_NAME, "BLE_EVENT", "CONNECTED", payload)
-- 连接断开
elseif event == "disconnected" then
sys.sendMsg(TASK_NAME, "BLE_EVENT", "DISCONNECTED", payload)
-- 收到数据
elseif event == "data" then
sys.sendMsg(TASK_NAME, "BLE_EVENT", "DATA_RECV", payload)
-- 收到错误码(AE 或 UE
elseif event == "error" then
log.warn("ble_5101_main", "收到错误码:", payload.error_type, payload.error_code)
-- 系统事件(如设备重启)
elseif event == "system" then
if payload.type == "mac" then
-- 设备重启后重新初始化
log.info("ble_5101_main", "设备重启,开始重新初始化...")
-- 发送消息给主任务,触发重新初始化
sys.sendMsg(TASK_NAME, "BLE_EVENT", "REBOOT_DETECTED")
end
end
end
-- 初始化配置
local function init_config()
log.info("ble_5101_main", "========== 配置初始化 ==========")
-- 1. 获取当前工作模式
local success, mode = exril_5101.mode()
if success then
log.info("ble_5101_main", "当前工作模式:", mode)
else
log.error("ble_5101_main", "获取模式失败:", mode)
return false
end
-- 2. 切换到AT模式(确保可以配置参数)
log.info("ble_5101_main", "切换到AT指令模式...")
success, mode = exril_5101.mode(exril_5101.MODE_AT)
if not success then
log.error("ble_5101_main", "切换AT模式失败:", mode)
return false
end
log.info("ble_5101_main", "已切换到:", mode)
-- 3. 配置设备参数
log.info("ble_5101_main", "配置设备参数...")
local config_result, err = exril_5101.set({
name = config.device_name,
})
if config_result then
log.info("ble_5101_main", "参数配置成功")
else
log.error("ble_5101_main", "参数配置失败:", err)
end
-- 4. 切换到透传模式(用于接收蓝牙数据)
log.info("ble_5101_main", "切换到透传模式...")
success, mode = exril_5101.mode(exril_5101.MODE_UA)
if not success then
log.error("ble_5101_main", "切换透传模式失败:", mode)
return false
end
log.info("ble_5101_main", "已切换到透传模式:", mode)
return true
end
-- 主任务处理函数
local function ble_5101_main_task_func()
local msg
while true do
-- 初始化配置
local result = init_config()
if not result then
log.error("ble_5101_main", "配置初始化失败,5秒后重试...")
goto EXCEPTION_PROC
end
-- 注册BLE事件回调
exril_5101.on(ble_event_cb)
log.info("ble_5101_main", "BLE事件回调已注册")
-- 主模块初始化完成,通知其他模块
sys.publish("BLE_5101_MAIN_READY")
while true do
msg = sys.waitMsg(TASK_NAME, "BLE_EVENT")
log.info("ble_5101_main", "收到BLE事件:", msg[2], msg[3])
-- 设备重启
if msg[2] == "REBOOT_DETECTED" then
log.info("ble_5101_main", "检测到设备重启,重新初始化...")
goto EXCEPTION_PROC
end
-- 蓝牙连接成功
if msg[2] == "CONNECTED" then
log.info("ble_5101_main", "蓝牙连接成功")
-- 通知FOTA模块连接成功
if current_fota_handler and current_fota_handler.proc_connect then
current_fota_handler.proc_connect()
end
-- 蓝牙断开连接
elseif msg[2] == "DISCONNECTED" then
log.info("ble_5101_main", "蓝牙断开连接")
-- 通知FOTA模块连接断开
if current_fota_handler and current_fota_handler.proc_disconnect then
current_fota_handler.proc_disconnect()
end
-- 收到数据
elseif msg[2] == "DATA_RECV" then
local payload = msg[3]
if payload and payload.data then
-- 处理接收到的数据
if current_fota_handler and current_fota_handler.proc then
current_fota_handler.proc(payload.data)
end
end
end
end
-- 出现异常
::EXCEPTION_PROC::
-- 清空此task绑定的消息队列中的未处理的消息
sys.cleanMsg(TASK_NAME)
-- 5秒后跳转到循环体开始位置,自动重新初始化
log.info("ble_5101_main", "等待5秒后重新尝试...")
sys.wait(5000)
end
end
-- 启动主任务
sys.taskInitEx(ble_5101_main_task_func, TASK_NAME)
@@ -0,0 +1,268 @@
--[[
@module ble_5101_packet_fota
@summary Air5101蓝牙FOTA升级功能模块(分段写入方式)
@version 1.0
@date 2026.03.04
@author 王世豪
@usage
-- Air5101蓝牙FOTA升级功能(分段写入方式)
-- 提供通过Air5101接收升级包数据并直接处理分段数据进行固件升级的功能
本文件为FOTA业务逻辑处理模块(分段方式),核心业务逻辑为:
1. 处理开始升级命令
2. 直接处理接收到的固件数据,不写入文件
3. 处理结束升级命令并执行FOTA升级
4. 管理升级状态和分段数据操作
本文件的对外接口有1个:
1. ble_packet_fota.proc(data): 处理接收到的BLE数据
依赖模块:
- exril_5101: 用于Air5101蓝牙模块的AT指令封装
]]
local ble_5101_packet_fota = {}
-- 升级状态管理
local upgrade_state = {
is_upgrading = false, -- 是否正在升级
total_size = 0, -- 总文件大小(字节)
received_size = 0, -- 已接收大小(字节)
upgrade_packet = 0 -- 升级包计数器
}
-- 重启设备
local function ble_reboot()
-- 完成FOTA流程并重启
fota.finish(true)
log.info("FOTA_CMD", "正在重启设备...")
rtos.reboot()
end
-- 处理FOTA命令
-- @param cmd_data 命令数据,格式:[命令码(1字节)] 或 [命令码(1字节) + 文件大小(4字节)]
function ble_5101_packet_fota.handle_command(cmd_data)
log.info("FOTA_CMD", "收到命令数据 (分段方式):", cmd_data:toHex(), "长度:", #cmd_data)
-- 检查命令数据是否有效
if #cmd_data < 1 then
log.error("FOTA_CMD", "命令数据为空")
return
end
-- 解析命令码(第一个字节)
local cmd = cmd_data:byte(1)
log.info("FOTA_CMD", "解析命令码:", cmd, string.format("(0x%02X)", cmd))
-- 命令0x01:开始升级
if cmd == 0x01 then
log.info("FOTA_CMD", "处理开始升级命令 (分段方式)")
-- 检查命令格式:需要至少5字节(1字节命令码 + 4字节文件大小)
if #cmd_data >= 5 then
-- 解析文件大小(小端序,从第2字节开始)
local total_size = string.unpack("<I4", cmd_data, 2)
log.info("FOTA_CMD", "文件总大小:", total_size, "字节")
-- 初始化FOTA子系统
log.info("FOTA_CMD", "初始化FOTA子系统...")
if fota.init() then
log.info("FOTA_CMD", "FOTA初始化成功")
-- 等待FOTA底层准备就绪
log.info("FOTA_CMD", "等待FOTA底层准备...")
-- 等待FOTA底层准备就绪,最多等待10秒
local wait_count = 0
local wait_ok = false
while wait_count < 100 do -- 最多轮询100次,每次100ms,共10秒
if fota.wait() then
wait_ok = true
break
end
sys.wait(100)
wait_count = wait_count + 1
end
if wait_ok then
log.info("FOTA_CMD", "FOTA底层准备就绪")
-- 更新升级状态
upgrade_state.is_upgrading = true
upgrade_state.total_size = total_size
upgrade_state.received_size = 0
upgrade_state.upgrade_packet = 0
log.info("FOTA_CMD", "升级状态已设置",
"总大小:", upgrade_state.total_size,
"升级方式: packet")
log.info("FOTA_CMD", "准备接收固件数据...")
else
log.error("FOTA_CMD", "FOTA底层准备超时")
fota.finish(false)
upgrade_state.is_upgrading = false
end
else
log.error("FOTA_CMD", "FOTA初始化失败")
end
else
log.error("FOTA_CMD", "开始命令格式错误,长度不足")
end
-- 命令0x02:结束升级
elseif cmd == 0x02 then
log.info("FOTA_CMD", "处理结束升级命令 (分段方式)")
-- 检查是否处于升级状态
if not upgrade_state.is_upgrading then
log.warn("FOTA_CMD", "未处于升级状态,忽略结束命令")
return
end
-- 验证文件完整性
log.info("FOTA_CMD", "验证文件完整性...")
log.info("FOTA_CMD", "已接收:", upgrade_state.received_size, "字节")
log.info("FOTA_CMD", "应接收:", upgrade_state.total_size, "字节")
if upgrade_state.received_size == upgrade_state.total_size then
log.info("FOTA_CMD", "文件完整性验证通过")
log.info("FOTA_CMD", "升级数据已全部接收,等待升级完成...")
-- 等待底层校验结束
local success = false
for i = 1, 30 do -- 最多等待3秒
sys.wait(100)
local succ, fotaDone = fota.isDone()
if not succ then
log.error("FOTA_CMD", "校验过程出错")
fota.finish(false)
upgrade_state.is_upgrading = false
break
end
if fotaDone then
log.info("FOTA_CMD", "FOTA升级成功!")
-- 延迟重启,给用户一些反应时间
log.info("FOTA_CMD", "2秒后设备将自动重启...,重启后通过日志判断最终是否升级成功")
-- 延迟2秒后重启设备
sys.timerStart(ble_reboot, 2000)
success = true
break
end
end
if not success then
log.error("FOTA_CMD", "校验超时")
fota.finish(false)
upgrade_state.is_upgrading = false
end
else
log.error("FOTA_CMD", "文件不完整,升级失败")
-- 清理升级状态
upgrade_state.is_upgrading = false
fota.finish(false)
end
log.info("FOTA_CMD", "结束升级命令处理完成")
else
log.warn("FOTA_CMD", "未知命令码:", cmd, string.format("(0x%02X)", cmd))
end
end
-- 处理FOTA数据
-- @param data 固件数据块
function ble_5101_packet_fota.handle_data(data)
log.info("FOTA_DATA", "收到数据包,长度:", #data, "字节 (分段方式)")
-- 检查是否处于升级状态
if not upgrade_state.is_upgrading then
log.warn("FOTA_DATA", "未处于升级状态,忽略数据")
return
end
-- 直接使用fota.run()处理分段数据,不写入文件
log.info("FOTA_DATA", "处理分段数据,包序号:", upgrade_state.upgrade_packet)
local result, isDone = fota.run(data)
log.info("FOTA_DATA", "分段写入结果:", "result:", result, "isDone:", isDone)
if result then
-- 更新接收状态
upgrade_state.received_size = upgrade_state.received_size + #data
upgrade_state.upgrade_packet = upgrade_state.upgrade_packet + 1
-- 计算并显示进度
local progress = math.floor((upgrade_state.received_size / upgrade_state.total_size) * 100)
log.info("FOTA_DATA", "接收进度:", progress, "%",
"已接收:", upgrade_state.received_size, "字节",
"总大小:", upgrade_state.total_size, "字节")
-- 如果所有数据都已接收,检查升级是否完成
if upgrade_state.received_size >= upgrade_state.total_size then
log.info("FOTA_DATA", "所有数据已接收,等待升级完成...")
end
else
log.error("FOTA_DATA", "分段写入失败")
-- 分段写入失败,终止升级
upgrade_state.is_upgrading = false
fota.finish(false)
end
end
-- 处理接收到的蓝牙数据
function ble_5101_packet_fota.proc(data)
log.info("FOTA_PACKET", "收到蓝牙数据:", data:toHex())
-- 简单的协议解析:第一个字节表示数据类型
-- 0x01: 命令数据
-- 0x02: 固件数据
if #data > 0 then
local data_type = data:byte(1)
if data_type == 0x01 then
-- 命令数据
ble_5101_packet_fota.handle_command(data:sub(2))
elseif data_type == 0x02 then
-- 固件数据
ble_5101_packet_fota.handle_data(data:sub(2))
else
log.warn("FOTA_PACKET", "未知数据类型:", data_type)
end
end
end
-- 处理蓝牙连接成功事件
function ble_5101_packet_fota.proc_connect()
log.info("FOTA_PACKET", "蓝牙连接成功")
end
-- 处理蓝牙断开连接事件
function ble_5101_packet_fota.proc_disconnect()
log.info("FOTA_PACKET", "处理连接断开事件")
-- 如果正在升级,连接断开则终止升级
if upgrade_state.is_upgrading then
log.error("FOTA_PACKET", "升级过程中连接断开,终止升级")
upgrade_state.is_upgrading = false
-- 结束FOTA流程
fota.finish(false)
end
end
-- 清理升级状态
function ble_5101_packet_fota.cleanup()
-- 清理升级状态(无论成功还是失败)
log.info("FOTA_CMD", "清理升级状态...")
upgrade_state.is_upgrading = false
-- 结束FOTA流程
fota.finish(false)
log.info("FOTA_CMD", "升级流程结束")
end
-- 获取升级状态
function ble_5101_packet_fota.get_state()
return upgrade_state
end
return ble_5101_packet_fota
@@ -0,0 +1,681 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Air5101 BLE FOTA 升级工具</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 24px;
margin-bottom: 8px;
}
.header p {
opacity: 0.9;
font-size: 14px;
}
.content {
padding: 30px;
}
.section {
margin-bottom: 25px;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
border-left: 4px solid #667eea;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.section-title .icon {
width: 24px;
height: 24px;
background: #667eea;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: #e9ecef;
color: #495057;
}
.btn-secondary:hover:not(:disabled) {
background: #dee2e6;
}
.file-input-wrapper {
position: relative;
overflow: hidden;
display: inline-block;
width: 100%;
}
.file-input-wrapper input[type=file] {
position: absolute;
left: -9999px;
}
.file-label {
display: block;
padding: 40px 20px;
border: 2px dashed #ced4da;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.file-label:hover {
border-color: #667eea;
background: #f8f9ff;
}
.file-label.has-file {
border-color: #28a745;
background: #f0fff4;
}
.file-label .file-icon {
font-size: 48px;
margin-bottom: 10px;
}
.file-label .file-text {
color: #6c757d;
font-size: 14px;
}
.file-label .file-name {
color: #28a745;
font-weight: 600;
margin-top: 8px;
}
.status {
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
margin-top: 10px;
}
.status-info {
background: #e3f2fd;
color: #1976d2;
}
.status-success {
background: #e8f5e9;
color: #388e3c;
}
.status-error {
background: #ffebee;
color: #c62828;
}
.status-warning {
background: #fff3e0;
color: #f57c00;
}
.progress-container {
margin-top: 20px;
}
.progress-bar {
height: 12px;
background: #e9ecef;
border-radius: 6px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
border-radius: 6px;
transition: width 0.3s ease;
width: 0%;
}
.progress-text {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 13px;
color: #6c757d;
}
.log-container {
margin-top: 20px;
padding: 15px;
background: #1e1e1e;
border-radius: 8px;
max-height: 300px;
overflow-y: auto;
}
.log-entry {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
padding: 4px 0;
border-bottom: 1px solid #333;
}
.log-entry:last-child {
border-bottom: none;
}
.log-time {
color: #6c757d;
}
.log-info {
color: #4fc3f7;
}
.log-success {
color: #81c784;
}
.log-error {
color: #e57373;
}
.log-warning {
color: #ffb74d;
}
.device-info {
display: flex;
align-items: center;
gap: 12px;
padding: 15px;
background: white;
border-radius: 8px;
margin-top: 10px;
}
.device-icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
}
.device-details h4 {
font-size: 16px;
color: #333;
margin-bottom: 4px;
}
.device-details p {
font-size: 13px;
color: #6c757d;
}
.settings-row {
display: flex;
gap: 15px;
margin-top: 15px;
}
.settings-row .input-group {
flex: 1;
}
.settings-row label {
display: block;
font-size: 13px;
color: #6c757d;
margin-bottom: 5px;
}
.settings-row input, .settings-row select {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 6px;
font-size: 14px;
}
.hidden {
display: none !important;
}
@media (max-width: 480px) {
.container {
border-radius: 0;
}
body {
padding: 0;
}
.content {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔧 Air5101 BLE FOTA</h1>
<p>通过Air5101蓝牙模块给4G模组无线升级</p>
</div>
<div class="content">
<!-- 步骤1: 连接设备 -->
<div class="section">
<div class="section-title">
<span class="icon">1</span>
连接蓝牙设备
</div>
<button id="btnScan" class="btn btn-primary" onclick="scanDevice()">
🔍 扫描并连接设备
</button>
<div id="deviceInfo" class="device-info hidden">
<div class="device-icon">📱</div>
<div class="device-details">
<h4 id="deviceName">设备名称</h4>
<p id="deviceId">设备ID</p>
</div>
</div>
<div id="statusConnect" class="status hidden"></div>
</div>
<!-- 步骤2: 选择固件 -->
<div class="section">
<div class="section-title">
<span class="icon">2</span>
选择固件文件
</div>
<div class="file-input-wrapper">
<input type="file" id="firmwareFile" accept=".bin,.sota" onchange="handleFileSelect(event)">
<label for="firmwareFile" class="file-label" id="fileLabel">
<div class="file-icon">📁</div>
<div class="file-text">点击选择固件文件</div>
<div class="file-name hidden" id="fileName"></div>
</label>
</div>
<div id="statusFile" class="status hidden"></div>
</div>
<!-- 步骤3: 升级设置 -->
<div class="section">
<div class="section-title">
<span class="icon">3</span>
升级设置
</div>
<div class="settings-row">
<div class="input-group">
<label>数据包大小 (字节)</label>
<select id="packetSize">
<option value="100">100</option>
<option value="200">200</option>
<option value="300">300</option>
<option value="400">400</option>
<option value="500" selected>500</option>
</select>
</div>
<div class="input-group">
<label>发送间隔 (毫秒)</label>
<select id="packetDelay">
<option value="20">20</option>
<option value="30">30</option>
<option value="40">40</option>
<option value="50" selected>50</option>
<option value="60">60</option>
<option value="70">70</option>
<option value="80">80</option>
<option value="90">90</option>
<option value="100">100</option>
</select>
</div>
</div>
</div>
<!-- 步骤4: 开始升级 -->
<div class="section">
<div class="section-title">
<span class="icon">4</span>
开始升级
</div>
<button id="btnUpgrade" class="btn btn-primary" onclick="startUpgrade()" disabled>
🚀 开始升级
</button>
<div id="progressContainer" class="progress-container hidden">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text">
<span id="progressPercent">0%</span>
<span id="progressDetail">0 / 0 字节</span>
</div>
</div>
<div id="statusUpgrade" class="status hidden"></div>
</div>
<!-- 日志输出 -->
<div class="section">
<div class="section-title">
<span class="icon">📋</span>
运行日志
</div>
<div class="log-container" id="logContainer">
<div class="log-entry">
<span class="log-time">[系统]</span>
<span class="log-info">等待操作...</span>
</div>
</div>
<button class="btn btn-secondary" onclick="clearLog()" style="margin-top: 10px;">
🗑️ 清空日志
</button>
</div>
</div>
</div>
<script>
// Air5101 服务和特征值 UUID
const SERVICE_UUID = "0000ff00-f7e3-55b4-6c4c-9fd140100a16";
const CHARACTERISTIC_UUID = "0000ff02-f7e3-55b4-6c4c-9fd140100a16";
// 命令定义
const CMD_START = 0x01;
const CMD_END = 0x02;
// 全局变量
let bluetoothDevice = null;
let gattServer = null;
let writeCharacteristic = null;
let firmwareData = null;
let isUpgrading = false;
// 添加日志
function addLog(message, type = 'info') {
const container = document.getElementById('logContainer');
const time = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.innerHTML = `
<span class="log-time">[${time}]</span>
<span class="log-${type}">${message}</span>
`;
container.appendChild(entry);
container.scrollTop = container.scrollHeight;
}
// 清空日志
function clearLog() {
document.getElementById('logContainer').innerHTML = '';
addLog('日志已清空', 'info');
}
// 显示状态
function showStatus(elementId, message, type = 'info') {
const element = document.getElementById(elementId);
element.textContent = message;
element.className = `status status-${type}`;
element.classList.remove('hidden');
}
// 隐藏状态
function hideStatus(elementId) {
document.getElementById(elementId).classList.add('hidden');
}
// 扫描并连接设备
async function scanDevice() {
try {
addLog('开始扫描蓝牙设备...', 'info');
if (!navigator.bluetooth) {
showStatus('statusConnect', '您的浏览器不支持Web Bluetooth API,请使用Chrome或Edge浏览器', 'error');
addLog('浏览器不支持Web Bluetooth', 'error');
return;
}
const device = await navigator.bluetooth.requestDevice({
filters: [{ name: 'Air5101_FOTA' }],
optionalServices: [SERVICE_UUID]
});
bluetoothDevice = device;
addLog(`找到设备: ${device.name}`, 'success');
// 显示设备信息
document.getElementById('deviceName').textContent = device.name || '未知设备';
document.getElementById('deviceId').textContent = device.id || '未知ID';
document.getElementById('deviceInfo').classList.remove('hidden');
// 连接GATT服务器
addLog('正在连接GATT服务器...', 'info');
gattServer = await device.gatt.connect();
addLog('GATT连接成功', 'success');
// 获取服务和特征值
addLog('正在发现服务...', 'info');
const service = await gattServer.getPrimaryService(SERVICE_UUID);
addLog('服务发现成功', 'success');
addLog('正在获取写入特征值...', 'info');
writeCharacteristic = await service.getCharacteristic(CHARACTERISTIC_UUID);
addLog('特征值获取成功', 'success');
showStatus('statusConnect', '设备连接成功!', 'success');
// 监听断开事件
device.addEventListener('gattserverdisconnected', onDisconnected);
updateUpgradeButton();
} catch (error) {
console.error(error);
showStatus('statusConnect', `连接失败: ${error.message}`, 'error');
addLog(`连接失败: ${error.message}`, 'error');
}
}
// 断开连接处理
function onDisconnected(event) {
addLog('设备已断开连接', 'warning');
showStatus('statusConnect', '设备已断开连接', 'warning');
document.getElementById('deviceInfo').classList.add('hidden');
bluetoothDevice = null;
gattServer = null;
writeCharacteristic = null;
updateUpgradeButton();
}
// 处理文件选择
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
firmwareData = new Uint8Array(e.target.result);
document.getElementById('fileName').textContent = `${file.name} (${formatBytes(firmwareData.length)})`;
document.getElementById('fileName').classList.remove('hidden');
document.getElementById('fileLabel').classList.add('has-file');
showStatus('statusFile', `固件加载成功: ${file.name} (${formatBytes(firmwareData.length)})`, 'success');
addLog(`固件加载成功: ${file.name}, 大小: ${formatBytes(firmwareData.length)}`, 'success');
updateUpgradeButton();
};
reader.readAsArrayBuffer(file);
}
// 格式化字节大小
function formatBytes(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
}
// 更新升级按钮状态
function updateUpgradeButton() {
const btn = document.getElementById('btnUpgrade');
btn.disabled = !(bluetoothDevice && firmwareData && !isUpgrading);
}
// 写入特征值
async function writeCharacteristicData(data) {
if (!writeCharacteristic) {
throw new Error('未连接到设备');
}
// 不等待确认,直接发送
// await writeCharacteristic.writeValue(data);
// 如果需要等待设备确认,使用下面这行(进度更准确但速度较慢):
await writeCharacteristic.writeValueWithResponse(data);
}
// 延迟函数
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 开始升级
async function startUpgrade() {
if (!bluetoothDevice || !firmwareData) {
showStatus('statusUpgrade', '请先连接设备并选择固件', 'error');
return;
}
isUpgrading = true;
updateUpgradeButton();
const packetSize = parseInt(document.getElementById('packetSize').value);
const packetDelay = parseInt(document.getElementById('packetDelay').value);
document.getElementById('progressContainer').classList.remove('hidden');
showStatus('statusUpgrade', '正在升级...', 'info');
try {
// 1. 发送开始命令
addLog('发送开始升级命令...', 'info');
const startCmd = new Uint8Array(6);
startCmd[0] = 0x01; // 数据类型: 命令
startCmd[1] = CMD_START; // 命令码: 开始
// 文件大小 (4字节小端序)
const size = firmwareData.length;
startCmd[2] = size & 0xFF;
startCmd[3] = (size >> 8) & 0xFF;
startCmd[4] = (size >> 16) & 0xFF;
startCmd[5] = (size >> 24) & 0xFF;
await writeCharacteristicData(startCmd);
addLog(`开始命令发送成功: ${Array.from(startCmd).map(b => b.toString(16).padStart(2, '0')).join(' ')}`, 'success');
await delay(2000); // 等待设备准备
// 2. 发送固件数据
addLog('开始传输固件数据...', 'info');
const totalSize = firmwareData.length;
let sentSize = 0;
let packetCount = 0;
const startTime = Date.now();
while (sentSize < totalSize) {
const chunkSize = Math.min(packetSize, totalSize - sentSize);
const chunk = firmwareData.slice(sentSize, sentSize + chunkSize);
// 包装数据 (添加数据类型前缀)
const wrappedChunk = new Uint8Array(chunkSize + 1);
wrappedChunk[0] = 0x02; // 数据类型: 固件数据
wrappedChunk.set(chunk, 1);
await writeCharacteristicData(wrappedChunk);
sentSize += chunkSize;
packetCount++;
// 更新进度
const progress = (sentSize / totalSize) * 100;
document.getElementById('progressFill').style.width = progress + '%';
document.getElementById('progressPercent').textContent = progress.toFixed(1) + '%';
document.getElementById('progressDetail').textContent = `${formatBytes(sentSize)} / ${formatBytes(totalSize)}`;
// 每20包显示一次日志
if (packetCount % 20 === 0 || sentSize >= totalSize) {
const elapsed = (Date.now() - startTime) / 1000;
const speed = sentSize / elapsed / 1024;
addLog(`进度: ${progress.toFixed(1)}% - ${speed.toFixed(1)} KB/s - 已发送 ${packetCount}`, 'info');
}
// 延迟
await delay(packetDelay);
}
const totalTime = (Date.now() - startTime) / 1000;
const avgSpeed = totalSize / totalTime / 1024;
addLog(`数据传输完成! 总时间: ${totalTime.toFixed(1)}s, 平均速度: ${avgSpeed.toFixed(1)} KB/s`, 'success');
// 3. 发送结束命令
addLog('发送结束升级命令...', 'info');
const endCmd = new Uint8Array(2);
endCmd[0] = 0x01; // 数据类型: 命令
endCmd[1] = CMD_END; // 命令码: 结束
await writeCharacteristicData(endCmd);
addLog('结束命令发送成功', 'success');
showStatus('statusUpgrade', '升级数据发送完成!设备正在处理升级...', 'success');
addLog('升级数据发送完成,等待设备处理...', 'success');
} catch (error) {
console.error(error);
showStatus('statusUpgrade', `升级失败: ${error.message}`, 'error');
addLog(`升级失败: ${error.message}`, 'error');
} finally {
isUpgrading = false;
updateUpgradeButton();
}
}
// 页面加载完成
window.onload = function() {
addLog('页面加载完成,等待操作...', 'info');
// 检查浏览器支持
if (!navigator.bluetooth) {
addLog('警告: 当前浏览器不支持Web Bluetooth API', 'warning');
addLog('请使用Chrome、Edge或其他支持Web Bluetooth的浏览器', 'warning');
} else {
addLog('浏览器支持Web Bluetooth', 'success');
}
};
</script>
</body>
</html>
@@ -0,0 +1,75 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2026.03.03
@author 王世豪
@usage
Air780EPM模块通过Air5101 BLE模块进行FOTA升级功能演示;
通过Air5101蓝牙低功耗(BLE)方式实现固件升级的应用场景:
1、蓝牙分段升级:通过Air5101 BLE连接将升级包文件分多个片段发送,每个片段接收并写入,代码演示通过蓝牙特征值传输固件数据进行升级;
2、无需有线连接:利用BLE无线通信特性,实现设备固件的无线远程升级;
适用场景:
- 蓝牙设备固件升级 -> 智能硬件、可穿戴设备等蓝牙连接设备
- 自定义升级流程 -> 通过BLE特征值实现命令控制和数据传输
更多说明参考本目录下的readme.md文件
]]
--[[
必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
PROJECT:项目名,ascii string类型
可以随便定义,只要不使用,就行
VERSION:项目版本号,ascii string类型
如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
]]
PROJECT = "fota_test"
VERSION = "001.000.001" --不同于使用libfota2扩展库来升级必须是xxx.xxx.xxx的格式,这里可以自定义版本号格式。
-- 在日志中打印项目名和项目版本号
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)
-- 循环打印版本号, 方便看版本号变化, 非必须
function print_version()
while true do
sys.wait(5000)
log.info("fota", "version", VERSION)
end
end
sys.taskInit(print_version)
-- 加载Air5101蓝牙FOTA功能模块
require "ble_5101_main"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,70 @@
--[[
@module fota_file
@summary 文件系统FOTA升级功能模块
@version 1.0
@date 2025.10.24
@author 孟伟
@usage
-- 文件系统FOTA升级功能
-- 提供从文件系统直接读取升级包进行固件升级的功能
-- 可以使用luatools工具的烧录系统文件功能将升级包直接烧录到文件系统中,
本文件没有对外接口,直接在main.lua中require "fota_file"就可以加载运行;
]]
local function fileUpgradeTask()
-- 等待系统稳定后再开始升级
sys.wait(10000)
log.info("FOTA_FILE", "=== 开始文件系统升级 ===")
-- 步骤1: 初始化FOTA流程
log.info("FOTA_FILE", "初始化FOTA...")
if not fota.init() then
log.error("FOTA_FILE", "FOTA初始化失败")
return
end
-- 步骤2: 等待底层准备就绪
log.info("FOTA_FILE", "等待底层准备...")
while not fota.wait() do
sys.wait(100)
end
log.info("FOTA_FILE", "底层准备就绪")
-- 步骤3: 从文件系统读取升级包并启动升级
local filePath = "/update.bin"
log.info("FOTA_FILE", "开始读取升级文件:", filePath)
local result, isDone, cache = fota.file(filePath)
log.info("FOTA_FILE", "升级文件写入flash中的fota分区结果", result, isDone, cache)
-- 步骤4: 结束写入fota分区
log.info("FOTA_FILE", "结束写入fota分区...")
local result, isDone = fota.isDone()
log.info("FOTA_FILE", "写入fota分区状态", "结果:", result, "完成:", isDone)
if result then
-- 步骤5: 处理写入结果
if isDone then
-- 升级文件成功写入flash中的fota分区,准备重启设备;
-- 设备重启后,在初始化阶段的运行过程中会自动应用fota分区中的数据完成升级,最终升级结果可以通过观察日志中的版本号来区分。
log.info("FOTA_FILE", "升级成功,准备重启设备")
-- 调用fota.finish(true)结束升级流程,参数true表示正确走完流程。
fota.finish(true)
-- 可选:删除升级包文件
-- os.remove("/update.bin")
sys.wait(2000)
rtos.reboot()
else
log.error("FOTA_FILE", "升级失败")
-- -- 调用fota.finish(false)结束升级流程,参数false表示升级流程失败。
fota.finish(false)
end
else
log.error("FOTA_FILE", "升级失败:检查写入状态失败")
-- -- 调用fota.finish(false)结束升级流程,参数false表示升级流程失败。
fota.finish(false)
end
end
-- 启动文件升级任务
sys.taskInit(fileUpgradeTask)
@@ -0,0 +1,217 @@
--[[
@module fota_uart
@summary 串口FOTA升级功能模块
@version 1.0
@date 2025.10.24
@author 孟伟
@usage
-- 串口FOTA升级功能
-- 提供通过串口分段接收升级包数据进行固件升级的功能
用法:
1. 先把脚本和固件烧录到模块里, 并确认开机
2. 按下Power键启动串口升级模式(设备会等待升级数据)
3. 在电脑端操作:进入命令行程序,执行 `python main.py` 进行升级,需要保证升级文件名字为 `fota_uart.bin`,并且和 `main.py` 在同一目录下
注意:运行`python main.py`需要确保电脑安装了Python环境。
4. 观察luatools的输出和main.py的输出
5. 模块接收正确的升级数据后,会提示1秒后重启
6. 本demo自带的脚本升级包,仅加了一条打印和修改版本号
串口通讯过程说明
串口升级采用简单的文本协议进行握手和数据传输控制:
协议流程:
1. 上位机发送:#FOTA\n
2. 设备回复:#FOTA RDY\n
3. 上位机发送:256字节数据包
4. 设备回复:#FOTA NEXT\n(请求下一包)
5. 重复步骤3-4直到所有数据发送完成
6. 设备回复:#FOTA OK\n(升级成功)
7. 设备自动重启
注意:
- 本demo默认是走虚拟串口进行交互, 如需改成物理串口, 修改uart_id和main.py
- 升级过程中如果发生错误,串口会自动关闭,需要重新按Power键开启
- 升级成功设备会自动重启
本文件没有对外接口,直接在main.lua中require "fota_uart"就可以加载运行;
]]
-- 定义所需要的UART编号
-- uart_id = 1 -- UART1, 通常也是MAIN_UART
local uart_id = uart.VUART_0 -- 虚拟USB串口
-- 全局变量
local uart_zbuff = nil
local uart_fota_state = 0
local uart_rx_counter = 0
local uart_fota_writed = 0
local upgrade_active = false -- 升级是否激活标志
-- 按键回调函数 - Power键
local function power_key_callback()
if not upgrade_active then
log.info("FOTA_UART", "Power键按下,启动串口升级模式")
-- 初始化串口和缓冲区
uart_zbuff = zbuff.create(1024)
uart.setup(uart_id, 115200)
uart.on(uart_id, "receive", uart_cbfun)
upgrade_active = true
uart_fota_state = 0
uart_rx_counter = 0
uart_fota_writed = 0
-- 发布事件,唤醒升级任务
sys.publish("UART_UPGRADE_START")
else
log.info("FOTA_UART", "升级模式已激活,请等待当前升级完成")
end
end
-- 配置Power键
gpio.setup(gpio.PWR_KEY, power_key_callback, gpio.PULLUP, gpio.FALLING)
gpio.debounce(gpio.PWR_KEY, 200, 1) -- 200ms去抖
-- 清理资源的函数
local function cleanup_resources()
if uart_zbuff then
uart_zbuff:del()
uart_zbuff = nil
end
uart.close(uart_id) -- 关闭串口
upgrade_active = false
uart_fota_state = 0
log.info("FOTA_UART", "资源已清理,串口已关闭")
end
-- 串口接收回调函数
function uart_cbfun(id, len)
if not upgrade_active or not uart_zbuff then
return
end
-- 防御缓冲区超标的情况
if uart_zbuff:used() > 8192 then
log.warn("fota", "uart_zbuff待处理的数据太多了,强制清空")
uart_zbuff:del()
end
while true do
local len = uart.rx(id, uart_zbuff)
if len <= 0 then
break
end
uart_rx_counter = uart_rx_counter + len
log.info("uart", "收到数据", len, "累计", uart_rx_counter)
-- 首次收到数据即发布事件,唤醒升级任务
if uart_fota_state == 0 then
sys.publish("UART_FOTA")
end
end
end
-- 串口升级任务
local function uartUpgradeTask()
local fota_state = 0 -- 0还没开始, 1进行中
while true do
-- 等待升级启动信号
sys.waitUntil("UART_UPGRADE_START")
log.info("FOTA_UART", "升级任务已启动,等待数据...")
while upgrade_active do
-- 等待升级数据到来
sys.waitUntil("UART_FOTA", 1000)
if not upgrade_active then break end
local used = uart_zbuff and uart_zbuff:used() or 0
if used > 0 then
if fota_state == 0 then
-- 等待FOTA的状态
if used > 5 then
local data = uart_zbuff:query()
uart_zbuff:del()
-- 如果接受到 #FOTA\n 代表数据要来了
if data:startsWith("#FOTA") and data:endsWith("\n") then
fota_state = 1
log.info("fota", "检测到fota起始标记,进入FOTA状态", data)
if fota.init() then
-- 固件数据发送端应该在收到#FOTA RDY\n之后才开始发送数据
uart.write(uart_id, "#FOTA RDY\n")
else
log.error("FOTA_UART", "FOTA初始化失败")
cleanup_resources()
break
end
end
end
else
-- 已进入升级状态:把收到的数据喂给fota.run
uart_fota_writed = uart_fota_writed + used
log.info("准备写入fota包", used, "累计写入", uart_fota_writed)
local result, isDone, cache = fota.run(uart_zbuff)
log.debug("fota.run", result, isDone, cache)
uart_zbuff:del() -- 清空缓冲区
if not result then
-- 写入失败,退出升级状态并通知上位机
log.error("fota", "出错了", result, isDone, cache)
uart.write(uart_id, "#FOTA ERR\n")
-- 调用fota.finish(false)结束升级流程,参数false表示升级流程失败。
fota.finish(false)
cleanup_resources()
fota_state = 0
break
elseif isDone then
-- 全部数据写入完成,等待底层校验结束
local success = false
for i = 1, 30 do -- 最多等待3秒
sys.wait(100)
local succ, fotaDone = fota.isDone()
if not succ then
log.error("fota", "校验过程出错")
uart.write(uart_id, "#FOTA ERR\n")
fota.finish(false)
cleanup_resources()
fota_state = 0
break
end
if fotaDone then
uart_fota_state = 1
-- 升级文件成功写入flash中的fota分区,准备重启设备;
log.info("fota", "已完成,1s后重启")
-- 调用fota.finish(true)结束升级流程,参数true表示正确走完流程。
fota.finish(true)
-- 反馈给上位机
uart.write(uart_id, "#FOTA OK\n")
sys.wait(1000)
success = true
rtos.reboot()
break
end
end
if not success then
log.error("fota", "校验超时")
uart.write(uart_id, "#FOTA ERR\n")
fota.finish(false)
cleanup_resources()
fota_state = 0
end
break
else
-- 单包写入成功,通知上位机继续下发
log.info("fota", "单包写入完成", used, "等待下一个包")
uart.write(uart_id, "#FOTA NEXT\n")
end
end
end
end
-- 重置状态,等待下次升级
fota_state = 0
uart_fota_state = 0
uart_rx_counter = 0
uart_fota_writed = 0
end
end
-- 启动串口升级任务
sys.taskInit(uartUpgradeTask)
@@ -0,0 +1,88 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.10.24
@author 孟伟
@usage
Air780EPM模块的两种FOTA升级方式:文件系统直接升级和串口分段升级;
分两种不同的应用场景来演示固件升级的实现方法:
1、文件系统直接升级:通过模组文件系统中的文件直接升级,代码演示通过luatools的烧录文件系统功能将升级包文件直接烧录到文件系统然后升级;
2、分段升级:通过串口将升级包文件分多个片段发送,每个片段接收并写入,代码演示使用usb虚拟串口分段写入升级包升级;
适用场景:
- 非标准数据传输 -> 串口、TCP、MQTT等自定义通道升级
- 流程精细控制 -> 需要自定义升级前后处理逻辑
更多说明参考本目录下的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 = "fota_test"
VERSION = "1.0.0" --不同于使用libfota2扩展库来升级必须是xxx.xxx.xxx的格式,这里可以自定义版本号格式。
-- 在日志中打印项目名和项目版本号
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)
-- 循环打印版本号, 方便看版本号变化, 非必须
function print_version()
while true do
sys.wait(1000)
log.info("fota", "version", VERSION)
-- log.info("fota1111122222222222")
end
end
sys.taskInit(print_version)
-- 方式1: 文件系统直接升级
-- require("fota_file")
-- 方式2: 分段写入升级,以串口来分段写入升级包
require("fota_uart")
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
@@ -0,0 +1,42 @@
#!/usr/bin/python3
# -*- coding: UTF-8 -*-
import serial, serial.tools.list_ports, sys, argparse
def find_virtual_port():
for port in serial.tools.list_ports.comports():
if port.vid == 0x19d1 and port.pid == 0x0001 and port.location and "x.6" in port.location:
return port.device
return None
# 解析命令行参数
parser = argparse.ArgumentParser()
parser.add_argument('-p', '--port', help='串口名称,如COM1或/dev/ttyS0')
args = parser.parse_args()
# 确定使用的串口
port = args.port or find_virtual_port()
if not port:
print("错误: 未找到虚拟串口且未指定串口")
sys.exit(1)
print(f"使用串口: {port}")
try:
with serial.Serial(port, 115200, timeout=1) as ser:
ser.write(b"#FOTA\n")
data = ser.read(128)
if data and data.startswith(b"#FOTA"):
print("设备响应", data)
with open("fota_uart.bin", "rb") as f:
while fdata := f.read(256):
print("发送升级包数据", len(fdata))
ser.write(fdata)
if resp := ser.read(128):
print("设备响应", resp)
print("发送完毕,退出")
else:
print("设备没响应", data)
except Exception as e:
print(f"错误: {e}")
sys.exit(1)
@@ -0,0 +1,375 @@
## 功能模块介绍
1、main.lua:主程序入口;
2、fota_file.lua:介绍了使用文件系统进行FOTA升级功能的实现模块,包括完整的升级流程;
3、fota_uart.lua:介绍了使用串口分段进行FOTA升级功能的实现模块,包括完整的升级流程;
4、main.py:Python脚本工具,用于通过串口分段发送升级包,演示分段升级的流程;
5、fota_uart.bin:演示串口分段升级的升级包文件,升级内容仅升级版本号以及添加几行打印;
## 演示功能概述
FOTA是固件远程升级的简称,用于设备固件的远程更新和维护;
本demo演示的核心功能为:
Air780EPM模块的两种FOTA升级方式:文件系统直接升级和串口分段升级;
分两种不同的应用场景来演示固件升级的实现方法:
1、文件系统直接升级:通过模组文件系统中的文件直接升级,代码演示通过luatools的烧录文件系统功能将升级包文件直接烧录到文件系统然后升级;
2、分段升级:通过串口将升级包文件分多个片段发送,每个片段接收并写入,代码演示使用usb虚拟串口分段写入升级包升级;
适用场景:
非标准数据传输 -> 串口、TCP、MQTT等自定义通道升级
流程精细控制 -> 需要自定义升级前后处理逻辑
## 演示硬件环境
![](https://docs.openluat.com/air780epm/luatos/app/driver/eth/image/RFSvb75NRoEWqYxfCRVcVrOKnsf.jpg)
1、Air780EPM V1.3版本开发板一块
2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air780EPM V1.3版本开发板和数据线的硬件接线方式为:
- Air780EPM V1.3版本开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
## 演示软件环境
1、Luatools下载调试工具
2、[Air780EPM V2016版本固件)](https://docs.openluat.com/air780epm/luatos/firmware/version/)
3、Python 3 环境(用于运行main.py发送升级包)
## 演示操作步骤
### 方式1:文件系统直接升级
1、搭建好演示硬件环境
2、修改demo脚本代码,取消`main.lua``require("fota_file")`的注释,注释掉`require("fota_uart")`
3、使用Luatools制作升级包,先把新旧版本分别生成量产文件,然后再制作升级包,工具上栏 luatOS->固件工具->差分包/整包升级包制作,将制作好的升级包放到luatools工具上面的烧录文件系统功能。
4、Luatools烧录内核固件和修改前的demo脚本代码,以及升级包文件。烧录成功后,自动开机运行。
5、可以看到如下日志:
日志分析如下:
1. 开始升级,读取文件系统目录下的升级包文件/update.bin
2. FOTA初始化 → 底层就绪 → 文件写入 → MD5校验通过
3. 升级完成,版本验证成功
4. 设备自动重启
5. 新版本 1.0.1 正常运行,新增日志确认升级成功
结果:文件系统FOTA升级完全成功,版本从1.0.0升级到1.0.1
```lua
[2025-10-24 17:56:47.892][000000005.840] D/mobile bearer act 0, result 0
[2025-10-24 17:56:47.902][000000005.841] D/mobile NETIF_LINK_ON -> IP_READY
[2025-10-24 17:56:47.911][000000005.890] D/mobile TIME_SYNC 0
[2025-10-24 17:56:48.203][000000006.204] I/user.fota version 1.0.0
[2025-10-24 17:56:49.208][000000007.204] I/user.fota version 1.0.0
[2025-10-24 17:56:50.205][000000008.204] I/user.fota version 1.0.0
[2025-10-24 17:56:51.205][000000009.205] I/user.fota version 1.0.0
[2025-10-24 17:56:52.199][000000010.205] I/user.fota version 1.0.0
[2025-10-24 17:56:52.207][000000010.210] I/user.FOTA_FILE === ===
[2025-10-24 17:56:52.217][000000010.210] I/user.FOTA_FILE FOTA...
[2025-10-24 17:56:52.224][000000010.226] I/user.FOTA_FILE ...
[2025-10-24 17:56:52.234][000000010.226] I/user.FOTA_FILE
[2025-10-24 17:56:52.242][000000010.227] I/user.FOTA_FILE /update.bin
[2025-10-24 17:56:52.251][000000010.230] I/fota write common data
[2025-10-24 17:56:52.309][000000010.311] I/fota common data done, now checking 0
[2025-10-24 17:56:52.317][000000010.312] I/fota common data md5 ok
[2025-10-24 17:56:52.325][000000010.312] I/fota only common data
[2025-10-24 17:56:52.358][000000010.362] I/fota fota type 0 ok!, wait reboot
[2025-10-24 17:56:52.370][000000010.362] I/user.FOTA_FILE flash中的fota分区结果 true true 0
[2025-10-24 17:56:52.380][000000010.363] I/user.FOTA_FILE fota分区...
[2025-10-24 17:56:52.389][000000010.363] I/user.FOTA_FILE fota分区状态 : true : true
[2025-10-24 17:56:52.402][000000010.363] I/user.FOTA_FILE
[2025-10-24 17:56:53.199][000000011.205] I/user.fota version 1.0.0
[2025-10-24 17:56:54.197][000000012.205] I/user.fota version 1.0.0
[2025-10-24 17:56:55.575][000000000.000] main_entry 708:SDK base line V017_pb18.002
[2025-10-24 17:56:55.589][000000000.008] am_service_init 1154:Air780EPM_A11
[2025-10-24 17:56:55.604][000000000.008] am_get_chip_type 635:6bef6,19,24,c7,0,EC718
[2025-10-24 17:56:55.618][000000000.049] bsp_user_init_io 312:io volt 3.3v 21
[2025-10-24 17:56:55.630][000000000.050] BSP_CustomInit 558:hardfault mode init 4
[2025-10-24 17:56:55.643][000000000.050] Uart_ChangeBR 1338:uart0, 6000000 6028985 26000000 69
[2025-10-24 17:56:55.654][000000000.072] I/pm poweron: Power/Reset
[2025-10-24 17:56:55.668][000000000.181] self_info 127:model Air780EPM_A11 imei 862419074066563
[2025-10-24 17:56:55.682][000000000.181] self_info 129:firmware[1] BASIC
[2025-10-24 17:56:55.692][000000000.181] self_info 131:zone(kbytes) fs 168 script 256
[2025-10-24 17:56:55.704][000000000.181] I/main LuatOS@Air780EPM base 25.03 bsp V2016 32bit
[2025-10-24 17:56:55.720][000000000.181] I/main ROM Build: Oct 9 2025 21:32:15
[2025-10-24 17:56:55.734][000000000.183] W/pins /luadb/pins_AIR780EPM.json not exist!!
[2025-10-24 17:56:55.749][000000000.186] D/main loadlibs luavm 1048568 14888 14888
[2025-10-24 17:56:55.762][000000000.186] D/main loadlibs sys 2375432 53100 58844
[2025-10-24 17:56:55.772][000000000.186] D/main loadlibs psram 2375432 53184 58844
[2025-10-24 17:56:56.105][000000001.206] I/user.fota version 1.0.1
[2025-10-24 17:56:56.118][000000001.206] I/user.fota123456789
[2025-10-24 17:56:57.105][000000002.206] I/user.fota version 1.0.1
[2025-10-24 17:56:57.111][000000002.207] I/user.fota123456789
[2025-10-24 17:56:58.108][000000003.207] I/user.fota version 1.0.1
[2025-10-24 17:56:58.116][000000003.208] I/user.fota123456789
```
### 方式2:串口分段升级
1、搭建好演示硬件环境
2、修改demo脚本代码,确保`main.lua`中已注释`require("fota_file")`,取消`require("fota_uart")`的注释
3、使用Luatools制作升级包,先把新旧版本分别生成量产文件,然后再制作升级包,工具上栏 luatOS->固件工具->差分包/整包升级包制作,将制作好的升级包放在main.py同级目录下
4、Luatools烧录内核固件和修改前的demo脚本代码,烧录成功后,自动开机运行。
5、确认设备连接到电脑的串口(虚拟USB串口)
6、按一下核心板上的Powerkey键,然后运行Python脚本发送升级包:
```lua
python main.py
```
7、脚本会自动寻找设备虚拟串口,发送升级命令并传输`fota_uart.bin`文件
8、设备接收并验证升级包,升级成功后会自动重启
9、可以看到如下日志:
串口分段升级日志解读:
1. USB虚拟串口连接,收到#FOTA起始指令
2. 开始分段接收升级包,每次256字节,累计5751字节
3. 所有数据包写入成功,MD5校验通过
4. 升级完成,重启
5. 重启后新版本1.0.2运行,新增日志确认升级成功
结果:串口FOTA升级完全成功,版本从1.0.0升级到1.0.2。
```lua
[2025-10-24 19:04:42.229][000000130.217] I/user.fota version 1.0.0
[2025-10-24 19:04:43.235][000000131.217] I/user.fota version 1.0.0
[2025-10-24 19:04:44.239][000000132.217] I/user.fota version 1.0.0
[2025-10-24 19:04:44.896][000000132.888] luat_usb_recv_cb 447:usb serial connected
[2025-10-24 19:04:44.909][000000132.889] I/user.uart 6 6
[2025-10-24 19:04:44.921][000000132.891] I/user.fota fota起始标记,FOTA状态 #FOTA
[2025-10-24 19:04:45.238][000000133.217] I/user.fota version 1.0.0
[2025-10-24 19:04:45.916][000000133.894] I/user.uart 256 262
[2025-10-24 19:04:45.931][000000133.895] I/user.fota包 256 256
[2025-10-24 19:04:45.939][000000133.896] I/fota write common data
[2025-10-24 19:04:45.957][000000133.896] D/user.fota.run true false 1
[2025-10-24 19:04:45.968][000000133.896] I/user.fota 256
[2025-10-24 19:04:46.227][000000134.217] I/user.fota version 1.0.0
[2025-10-24 19:04:46.929][000000134.906] I/user.uart 256 518
[2025-10-24 19:04:46.942][000000134.907] I/user.fota包 256 512
[2025-10-24 19:04:46.953][000000134.908] D/user.fota.run true false 1
[2025-10-24 19:04:46.964][000000134.908] I/user.fota 256
[2025-10-24 19:04:47.227][000000135.217] I/user.fota version 1.0.0
[2025-10-24 19:04:47.940][000000135.917] I/user.uart 256 774
[2025-10-24 19:04:47.950][000000135.918] I/user.fota包 256 768
[2025-10-24 19:04:47.963][000000135.919] D/user.fota.run true false 1
[2025-10-24 19:04:47.971][000000135.919] I/user.fota 256
[2025-10-24 19:04:48.238][000000136.217] I/user.fota version 1.0.0
[2025-10-24 19:04:48.956][000000136.933] I/user.uart 256 1030
[2025-10-24 19:04:48.965][000000136.933] I/user.fota包 256 1024
[2025-10-24 19:04:48.976][000000136.934] D/user.fota.run true false 1
[2025-10-24 19:04:48.986][000000136.934] I/user.fota 256
[2025-10-24 19:04:49.237][000000137.217] I/user.fota version 1.0.0
[2025-10-24 19:04:49.968][000000137.945] I/user.uart 256 1286
[2025-10-24 19:04:49.977][000000137.946] I/user.fota包 256 1280
[2025-10-24 19:04:49.987][000000137.946] D/user.fota.run true false 1
[2025-10-24 19:04:49.995][000000137.947] I/user.fota 256
[2025-10-24 19:04:50.231][000000138.217] I/user.fota version 1.0.0
[2025-10-24 19:04:50.981][000000138.957] I/user.uart 256 1542
[2025-10-24 19:04:50.990][000000138.958] I/user.fota包 256 1536
[2025-10-24 19:04:51.008][000000138.958] D/user.fota.run true false 1
[2025-10-24 19:04:51.016][000000138.959] I/user.fota 256
[2025-10-24 19:04:51.229][000000139.217] I/user.fota version 1.0.0
[2025-10-24 19:04:51.992][000000139.969] I/user.uart 256 1798
[2025-10-24 19:04:52.002][000000139.970] I/user.fota包 256 1792
[2025-10-24 19:04:52.013][000000139.970] D/user.fota.run true false 1
[2025-10-24 19:04:52.020][000000139.971] I/user.fota 256
[2025-10-24 19:04:52.228][000000140.217] I/user.fota version 1.0.0
[2025-10-24 19:04:53.007][000000140.984] I/user.uart 256 2054
[2025-10-24 19:04:53.019][000000140.985] I/user.fota包 256 2048
[2025-10-24 19:04:53.029][000000140.985] D/user.fota.run true false 1
[2025-10-24 19:04:53.037][000000140.985] I/user.fota 256
[2025-10-24 19:04:53.226][000000141.217] I/user.fota version 1.0.0
[2025-10-24 19:04:54.005][000000141.997] I/user.uart 256 2310
[2025-10-24 19:04:54.019][000000141.998] I/user.fota包 256 2304
[2025-10-24 19:04:54.034][000000141.998] D/user.fota.run true false 1
[2025-10-24 19:04:54.044][000000141.999] I/user.fota 256
[2025-10-24 19:04:54.225][000000142.217] I/user.fota version 1.0.0
[2025-10-24 19:04:55.022][000000142.998] I/user.uart 256 2566
[2025-10-24 19:04:55.032][000000142.999] I/user.fota包 256 2560
[2025-10-24 19:04:55.042][000000143.000] D/user.fota.run true false 1
[2025-10-24 19:04:55.050][000000143.000] I/user.fota 256
[2025-10-24 19:04:55.240][000000143.217] I/user.fota version 1.0.0
[2025-10-24 19:04:56.035][000000144.012] I/user.uart 256 2822
[2025-10-24 19:04:56.044][000000144.013] I/user.fota包 256 2816
[2025-10-24 19:04:56.054][000000144.013] D/user.fota.run true false 1
[2025-10-24 19:04:56.060][000000144.014] I/user.fota 256
[2025-10-24 19:04:56.237][000000144.217] I/user.fota version 1.0.0
[2025-10-24 19:04:57.048][000000145.024] I/user.uart 256 3078
[2025-10-24 19:04:57.063][000000145.025] I/user.fota包 256 3072
[2025-10-24 19:04:57.076][000000145.026] D/user.fota.run true false 1
[2025-10-24 19:04:57.088][000000145.026] I/user.fota 256
[2025-10-24 19:04:57.235][000000145.217] I/user.fota version 1.0.0
[2025-10-24 19:04:58.057][000000146.034] I/user.uart 256 3334
[2025-10-24 19:04:58.065][000000146.035] I/user.fota包 256 3328
[2025-10-24 19:04:58.076][000000146.035] D/user.fota.run true false 1
[2025-10-24 19:04:58.085][000000146.035] I/user.fota 256
[2025-10-24 19:04:58.228][000000146.217] I/user.fota version 1.0.0
[2025-10-24 19:04:59.070][000000147.048] I/user.uart 256 3590
[2025-10-24 19:04:59.079][000000147.049] I/user.fota包 256 3584
[2025-10-24 19:04:59.085][000000147.049] D/user.fota.run true false 1
[2025-10-24 19:04:59.094][000000147.049] I/user.fota 256
[2025-10-24 19:04:59.227][000000147.217] I/user.fota version 1.0.0
[2025-10-24 19:05:00.083][000000148.061] I/user.uart 256 3846
[2025-10-24 19:05:00.096][000000148.062] I/user.fota包 256 3840
[2025-10-24 19:05:00.109][000000148.062] D/user.fota.run true false 1
[2025-10-24 19:05:00.121][000000148.062] I/user.fota 256
[2025-10-24 19:05:00.240][000000148.217] I/user.fota version 1.0.0
[2025-10-24 19:05:01.093][000000149.070] I/user.uart 256 4102
[2025-10-24 19:05:01.102][000000149.071] I/user.fota包 256 4096
[2025-10-24 19:05:01.112][000000149.071] D/user.fota.run true false 1
[2025-10-24 19:05:01.119][000000149.071] I/user.fota 256
[2025-10-24 19:05:01.233][000000149.217] I/user.fota version 1.0.0
[2025-10-24 19:05:02.090][000000150.082] I/user.uart 256 4358
[2025-10-24 19:05:02.102][000000150.083] I/user.fota包 256 4352
[2025-10-24 19:05:02.136][000000150.116] D/user.fota.run true false 1
[2025-10-24 19:05:02.144][000000150.116] I/user.fota 256
[2025-10-24 19:05:02.229][000000150.217] I/user.fota version 1.0.0
[2025-10-24 19:05:03.117][000000151.094] I/user.uart 256 4614
[2025-10-24 19:05:03.144][000000151.095] I/user.fota包 256 4608
[2025-10-24 19:05:03.153][000000151.095] D/user.fota.run true false 1
[2025-10-24 19:05:03.162][000000151.096] I/user.fota 256
[2025-10-24 19:05:03.226][000000151.216] I/user.fota version 1.0.0
[2025-10-24 19:05:04.118][000000152.095] I/user.uart 256 4870
[2025-10-24 19:05:04.128][000000152.096] I/user.fota包 256 4864
[2025-10-24 19:05:04.138][000000152.097] D/user.fota.run true false 1
[2025-10-24 19:05:04.152][000000152.097] I/user.fota 256
[2025-10-24 19:05:04.227][000000152.216] I/user.fota version 1.0.0
[2025-10-24 19:05:05.133][000000153.108] I/user.uart 256 5126
[2025-10-24 19:05:05.147][000000153.109] I/user.fota包 256 5120
[2025-10-24 19:05:05.167][000000153.109] D/user.fota.run true false 1
[2025-10-24 19:05:05.179][000000153.110] I/user.fota 256
[2025-10-24 19:05:05.226][000000153.217] I/user.fota version 1.0.0
[2025-10-24 19:05:06.126][000000154.117] I/user.uart 256 5382
[2025-10-24 19:05:06.138][000000154.118] I/user.fota包 256 5376
[2025-10-24 19:05:06.150][000000154.119] D/user.fota.run true false 1
[2025-10-24 19:05:06.161][000000154.119] I/user.fota 256
[2025-10-24 19:05:06.233][000000154.216] I/user.fota version 1.0.0
[2025-10-24 19:05:07.155][000000155.132] I/user.uart 256 5638
[2025-10-24 19:05:07.167][000000155.133] I/user.fota包 256 5632
[2025-10-24 19:05:07.181][000000155.133] D/user.fota.run true false 1
[2025-10-24 19:05:07.192][000000155.133] I/user.fota 256
[2025-10-24 19:05:07.234][000000155.216] I/user.fota version 1.0.0
[2025-10-24 19:05:08.170][000000156.147] I/user.uart 113 5751
[2025-10-24 19:05:08.189][000000156.148] I/user.fota包 113 5745
[2025-10-24 19:05:08.209][000000156.177] I/fota common data done, now checking 0
[2025-10-24 19:05:08.232][000000156.179] I/fota common data md5 ok
[2025-10-24 19:05:08.256][000000156.179] I/fota only common data
[2025-10-24 19:05:08.268][000000156.213] I/fota fota type 0 ok!, wait reboot
[2025-10-24 19:05:08.278][000000156.213] D/user.fota.run true true 0
[2025-10-24 19:05:08.287][000000156.217] I/user.fota version 1.0.0
[2025-10-24 19:05:08.324][000000156.314] I/user.fota ,1s后重启
[2025-10-24 19:05:09.227][000000157.217] I/user.fota version 1.0.0
[2025-10-24 19:05:10.822][000000000.000] main_entry 708:SDK base line V017_pb18.002
[2025-10-24 19:05:10.835][000000000.008] am_service_init 1154:Air780EPM_A11
[2025-10-24 19:05:10.847][000000000.008] am_get_chip_type 635:6bef6,19,24,c7,0,EC718
[2025-10-24 19:05:10.858][000000000.051] bsp_user_init_io 312:io volt 3.3v 21
[2025-10-24 19:05:10.870][000000000.051] BSP_CustomInit 558:hardfault mode init 4
[2025-10-24 19:05:10.879][000000000.052] Uart_ChangeBR 1338:uart0, 6000000 6028985 26000000 69
[2025-10-24 19:05:10.894][000000000.073] I/pm poweron: Power/Reset
[2025-10-24 19:05:10.908][000000000.185] self_info 127:model Air780EPM_A11 imei 862419074066563
[2025-10-24 19:05:10.920][000000000.186] self_info 129:firmware[1] BASIC
[2025-10-24 19:05:10.935][000000000.186] self_info 131:zone(kbytes) fs 168 script 256
[2025-10-24 19:05:10.946][000000000.186] I/main LuatOS@Air780EPM base 25.03 bsp V2016 32bit
[2025-10-24 19:05:10.956][000000000.186] I/main ROM Build: Oct 9 2025 21:32:15
[2025-10-24 19:05:10.972][000000000.188] W/pins /luadb/pins_AIR780EPM.json not exist!!
[2025-10-24 19:05:10.987][000000000.191] D/main loadlibs luavm 1048568 14888 14888
[2025-10-24 19:05:11.002][000000000.191] D/main loadlibs sys 2375432 53100 58844
[2025-10-24 19:05:11.018][000000000.191] D/main loadlibs psram 2375432 53184 58844
[2025-10-24 19:05:11.257][000000001.213] I/user.fota version 1.0.2
[2025-10-24 19:05:11.273][000000001.213] I/user.fota1111122222222222
[2025-10-24 19:05:12.083][000000002.214] I/user.fota version 1.0.2
[2025-10-24 19:05:12.094][000000002.214] I/user.fota1111122222222222
[2025-10-24 19:05:13.079][000000003.215] I/user.fota version 1.0.2
[2025-10-24 19:05:13.088][000000003.215] I/user.fota1111122222222222
```
main.py 日志:
```lua
D:\gitee_hz\fota>python main.py
COM59
b'#FOTA RDY\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
256
b'#FOTA NEXT\n'
113
b'#FOTA OK\n'
,退
```
@@ -0,0 +1,130 @@
## 功能模块介绍
### iot服务器fota功能演示
1、main.lua:主程序入口;
2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的四种网卡(单4g网卡,单wifi网卡,单spi以太网卡,多网卡)中的任何一种网卡;
3、update.lua:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
4、tcp_iot文件夹:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备启动air_srv_fota功能模块,使用合宙iot服务器进行升级;
5、air_srv_fota.lua:合宙服务器升级功能模块;
6、psm_power_fota.lua:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。需要注意的是此场景与上面两种场景不能同时使用;
## 系统消息介绍
1、"IP_READY":某种网卡已经获取到ip信息,仅仅获取到了ip信息,能否和外网连通还不确认;
2、"IP_LOSE":某种网卡已经掉网;
## 用户消息介绍
1、"RECV_DATA_FROM_SERVER"socket client收到服务器下发的数据后,通过此消息发布出去,给其他应用模块处理;
2、"SEND_DATA_REQ":其他应用模块发布此消息,通知socket client发送数据给服务器;
## 演示功能概述
1、此demo演示了三种场景:
(1) fota升级简单演示:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
(2) tcp服务器下发升级指令:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块;
(3) psm低功耗fota:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子;
2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
(1) netdrv_4g4G网卡
(2) netdrv_wifiWIFI STA网卡
(3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
(4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
## 演示硬件环境
![](https://docs.openluat.com/air780epm/luatos/app/driver/eth/image/RFSvb75NRoEWqYxfCRVcVrOKnsf.jpg)
1、Air780EPM V1.3版本开发板一块+可上网的sim卡一张+4g天线一根+网线一根:
- sim卡插入开发板的sim卡槽
- 天线装到开发板上
- 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口
2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air780EPM V1.3版本开发板和数据线的硬件接线方式为:
- Air780EPM V1.3版本开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
## 演示软件环境
1、Luatools下载调试工具
2、[Air780EPM V2012版本固件)](https://docs.openluat.com/air780epm/luatos/firmware/version/)
3、PC端浏览器访问[合宙TCP/UDP web测试工具](https://iot.luatos.com/#/page6/netlab)
## 演示核心步骤
1、搭建好硬件环境
2、PC端浏览器访问[合宙TCP/UDP web测试工具](https://iot.luatos.com/#/page6/netlab),点击 打开TCP 按钮,会创建一个TCP server,将server的地址和端口赋值给tcp_iot_main.lua中的SERVER_ADDR和SERVER_PORT两个变量
详细使用说明参考:[合宙 TCP/UDP web 测试工具使用说明](https://iot.luatos.com/#/p8000/netlab) 。
4、demo脚本代码netdrv_device.lua中,按照自己的网卡需求启用对应的Lua文件
- 如果需要单4G网卡,打开require "netdrv_4g",其余注释掉
- 如果需要单WIFI STA网卡,打开require "netdrv_wifi",其余注释掉;同时netdrv_wifi.lua中的wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1),前两个参数,修改为自己测试时wifi热点的名称和密码;注意:仅支持2.4G的wifi,不支持5G的wifi
- 如果需要以太网卡,打开require "netdrv_eth_spi",其余注释掉
- 如果需要多网卡,打开require "netdrv_multiple",其余注释掉;同时netdrv_multiple.lua中的ssid = "茶室-降功耗,找合宙!", password = "Air123456", 修改为自己测试时wifi热点的名称和密码;注意:仅支持2.4G的wifi,不支持5G的wifi
5、Luatools烧录内核固件和修改后的demo脚本代码
6、使用Luatools制作升级包,先把新旧版本分别生成量产文件,然后再制作升级包,工具上栏 luatOS->固件工具->差分包/整包升级包制作,将制作好的升级包配置到合宙iot服务器自己项目下,或上传到自建服务器上面;
7、烧录成功后,自动开机运行
8、可以看到升级过程如下,不管是什么场景下升级,基本都是如下日志情况:
``` lua
--没有升级之前可以看到如下打印
I/user.fota 脚本版本号 001.000.000 core版本号 V2010
I/user.fota_task_func recv IP_READY 1
I/user.开始检查升级
I/user.升级包下载成功,重启模块
--升级之后可以看到如下打印
I/user.fota 脚本版本号 001.000.001 core版本号 V2012
--升级重启之后还是会检查升级,所以会有如下打印属于正常情况,其中"code": 27 是合宙iot服务器返回的状态码,意思是已经是最新版本了。
I/user.fota -9 {"code": 27, "msg": "\u5df2\u662f\u6700\u65b0\u7248\u672c"}
I/user.使用合宙服务器,接下来解析body里的code
I/user.已是最新版本 1.设备的固件/脚本版本高于或等于云平台上的版本号 2.用户项目升级配置中未添加该设备 3.云平台升级配置中,是否升级配置为否
I/user.fota 4
```
9、对于psm休眠状态下的升级的场景,可以通过iot平台查看是否成功,在iot平台的升级日志页面搜索模组的imei,可以看到有两条升级结果“成功”和“已是最新版本”。模组升级成功后会自动进入psm休眠状态。可以通过电流查看休眠情况。
@@ -0,0 +1,119 @@
--[[
@module air_srv_fota
@summary 使用合宙iot平台远程升级功能模块
@version 1.0
@date 2025.08.12
@author 孟伟
@usage
实现远程升级功能,具体流程如下:
1、接收 AIR_SRV_FOTA 系统消息,触发升级;
2、判断网卡是否连接成功;
3、初始化fota2模块;
4、调用fota2模块的升级函数;
5、根据升级结果进行处理;
]]
-- 使用合宙iot平台时需要这个参数
PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
--加在libfota2扩展库
libfota2 = require "libfota2"
-- 循环打印版本号, 方便看版本号变化, 非必须
function get_version()
log.info("降功耗 找合宙")
log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
end
sys.timerLoopStart(get_version, 3000)
-- fota升级标志:true 表示当前正有 FOTA 流程在跑
local fota_running = false
-- 升级结果的回调函数
-- 功能:获取fota的回调函数
-- 参数:
-- result:number类型
-- 0表示成功
-- 1表示连接失败
-- 2表示url错误
-- 3表示服务器断开
-- 4表示接收报文错误
-- 5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
local function fota_cb(ret)
log.info("fota", ret)
-- fota结束,无论成功还是失败,都释放fota_running标志
fota_running = false
if ret == 0 then
log.info("升级包下载成功,重启模块")
rtos.reboot()
elseif ret == 1 then
log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
elseif ret == 2 then
log.info("url错误", "检查url拼写")
elseif ret == 3 then
log.info("服务器断开", "检查服务器白名单配置")
elseif ret == 4 then
log.error("FOTA 失败",
"原因可能有:\n" ..
"1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
"2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
"3) 已经是最新版本,无需升级")
elseif ret == 5 then
log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
else
log.info("不是上面几种情况 ret为", ret)
end
end
-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
local opts = {}
local function air_fota_func(data)
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("fota_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 检测到了IP_READY消息
log.info("fota_task_func", "recv IP_READY", socket.dft())
while true do
-- 阻塞等待外部事件:"AIR_SRV_FOTA"
sys.waitUntil("AIR_SRV_FOTA")
-- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
if "123" == _G.PRODUCT_KEY then
while true do
sys.wait(1000)
log.info("fota", "请修改正确的PRODUCT_KEY")
end
end
if fota_running then
log.warn("fota_task", "FOTA 正在运行,跳过本次请求")
else
-- 标记FOTA正在运行
-- 注意:这里只是标记,实际的FOTA流程还没有开始
fota_running = true
log.info("开始检查升级")
libfota2.request(fota_cb, opts)
end
end
end
-- 初始化FOTA任务
sys.taskInit(air_fota_func)
-- 演示定时自动升级, 每隔4小时自动检查一次,可以根据需求打开
-- sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, opts)
@@ -0,0 +1,83 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.08.12
@author 孟伟
@usage
本demo演示的核心功能为:
1、此demo演示了三种场景:
(1)fota升级简单演示:使用合宙iot服务器进行远程升级功能模块,简单升级演示;
(2)tcp服务器下发升级指令:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块;
(3)psm低功耗fota:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子;
2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
(1) netdrv_4g4G网卡
(2) netdrv_wifiWIFI STA网卡
(3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
(4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
更多说明参考本目录下的readme.md文件
]]
--[[
必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
PROJECT:项目名,ascii string类型
可以随便定义,只要不使用,就行
VERSION:项目版本号,ascii string类型
如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为999
如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
]]
PROJECT = "FOTA2_DEMO"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 加载网络驱动设备功能模块
require "netdrv_device"
-- 加载远程升级功能模块,场景1
require "update"
---------------------------------------------------------------------------
-- 加载tcp client iot socket主应用功能模块,通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备使用fota功能模块,场景二
-- require "tcp_iot_main"
-- 加载合宙iot平台远程升级功能模块
-- require "air_srv_fota"
---------------------------------------------------------------------------
-- 加载psm+低功耗模式升级功能模块,场景三
-- require "psm_power_fota"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -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,30 @@
--[[
@module netdrv_device
@summary 网络驱动设备功能模块
@version 1.0
@date 2025.08.12
@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"
-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
-- require "netdrv_eth_spi"
-- 加载“可以配置优先级的多种网卡”驱动模块
-- require "netdrv_multiple"
@@ -0,0 +1,121 @@
--[[
@module psm+_power_fota
@summary psm+超低功耗模式下升级功能模块
@version 1.0
@date 2025.08.12
@author 孟伟
@usage
本文件为psm+超低功耗模式下升级功能模块,核心设计思路
1.升级触发机制
- 定时器唤醒升级 :设备定期从PSM模式唤醒,主动检查是否有新固件版本。
- 外部中断唤醒升级 :通过特定GPIO中断或网络消息唤醒设备进行升级。
2.防止升级过程中进入休眠
- 在开始FOTA升级前,禁用PSM模式进入。
- 升级完成后,根据结果决定是否重启设备或重新进入PSM模式。
]]
-- 使用合宙iot平台时需要这个参数
PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
--加在libfota2扩展库
libfota2 = require "libfota2"
-- 升级结果的回调函数
-- 功能:获取fota的回调函数
-- 参数:
-- result:number类型
-- 0表示成功
-- 1表示连接失败
-- 2表示url错误
-- 3表示服务器断开
-- 4表示接收报文错误
-- 5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
local function fota_cb(ret)
log.info("fota", ret)
--升级结束,触发升级回调,发布消息升级结束,可以进入休眠模式
sys.publish("FOTA_END")
if ret == 0 then
log.info("升级包下载成功,重启模块")
rtos.reboot()
elseif ret == 1 then
log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
elseif ret == 2 then
log.info("url错误", "检查url拼写")
elseif ret == 3 then
log.info("服务器断开", "检查服务器白名单配置")
elseif ret == 4 then
log.error("FOTA 失败",
"原因可能有:\n" ..
"1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
"2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
"3) 已经是最新版本,无需升级")
elseif ret == 5 then
log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
else
log.info("不是上面几种情况 ret为", ret)
end
end
-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
local ota_opts = {}
function psm_fota_task_func()
-- 如果是被定时器唤醒,因为上次进入PSM+时是开启了飞行模式,所以在唤醒后第一时间关闭飞行模式。
mobile.flymode(0, false)
log.info("开始测试PSM+模式功耗。")
-- 打印版本号, 方便看版本号变化, 非必须
log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("fota_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 检测到了IP_READY消息
log.info("fota_task_func", "recv IP_READY", socket.dft())
log.info("开始检查升级")
libfota2.request(fota_cb, ota_opts)
-- 等待下载升级包结束, 发布消息"FOTA_END",
-- 如果15秒内没有收到消息,则15秒的时长到达后进入PSM+模式。
-- 需要注意的是在fota_cb回调函数中,升级包下载成功后,会立马重启并升级模组。如果还有其他事情要做不想立马重启升级,需自行决定reboot的时机
-- 升级包下载成功后,本demo默认是立即自动重启并且将升级包更新到模组中,更新成功后,会再次走到这里
-- 再次走到这里后,合宙iot平台会返回“已经是最新版本,不需要升级”,fota_cb回调函数中会发布消息"FOTA_END"
-- 至此,才会继续向下执行代码,进入PSM+模式
sys.waitUntil("FOTA_END", 15000)
log.info("升级结束,进入PSM模式")
-- 定时检查升级 (每4小时唤醒一次)
pm.dtimerStart(2, 4 * 3600000)
-- 启动飞行模式,规避可能会出现的网络问题
mobile.flymode(0, true)
-- 进入PSM模式
pm.power(pm.WORK_MODE, 3)
-- 防御机制:15秒后如果未进入PSM则重启
sys.wait(15000)
log.info("进入PSM+失败,重启")
rtos.reboot()
end
sys.taskInit(psm_fota_task_func)
@@ -0,0 +1,141 @@
--[[
@module tcp_client_main
@summary tcp client socket主应用功能模块
@version 1.0
@date 2025.08.12
@author 孟伟
@usage
本文件为tcp client socket主应用功能模块,核心业务逻辑为:
1、创建一个tcp client socket,连接server
2、处理连接异常,出现异常后执行重连动作;
3、调用tcp_client_receiver和tcp_client_sender中的外部接口,进行数据收发处理;
本文件没有对外接口,直接在main.lua中require "tcp_client_main"就可以加载运行;
]]
local libnet = require "libnet"
-- 加载tcp client socket数据接收功能模块
local tcp_client_receiver = require "tcp_iot_receiver"
-- 加载tcp client socket数据发送功能模块
local tcp_client_sender = require "tcp_iot_sender"
-- 电脑访问:https://iot.luatos.com/#/page6/netlab
-- 本工具使用方法可以参考:https://docs.openluat.com/common/TCPUDP_Test/
-- 点击 打开TCP 按钮,会创建一个TCP server
-- 将server的地址和端口赋值给下面这两个变量
local SERVER_ADDR = "112.125.89.8"
local SERVER_PORT = 42662
-- tcp_client_main的任务名
local TASK_NAME = tcp_client_sender.TASK_NAME
-- 处理未识别的消息
local function tcp_client_main_cbfunc(msg)
log.info("tcp_client_main_cbfunc", msg[1], msg[2], msg[3], msg[4])
end
-- tcp client socket的任务处理函数
local function tcp_client_main_task_func()
local socket_client
local result, para1, para2
while true do
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("sntp_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 检测到了IP_READY消息
log.info("tcp_client_main_task_func", "recv IP_READY", socket.dft())
-- 创建socket client对象
socket_client = socket.create(nil, TASK_NAME)
-- 如果创建socket client对象失败
if not socket_client then
log.error("tcp_client_main_task_func", "socket.create error")
goto EXCEPTION_PROC
end
-- 配置socket client对象为tcp client
result = socket.config(socket_client, nil, nil, nil, 300, 10, 3)
-- 如果配置失败
if not result then
log.error("tcp_client_main_task_func", "socket.config error")
goto EXCEPTION_PROC
end
-- 连接server
result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
-- 如果连接server失败
if not result then
log.error("tcp_client_main_task_func", "libnet.connect error")
goto EXCEPTION_PROC
end
log.info("tcp_client_main_task_func", "libnet.connect success")
-- 数据收发以及网络连接异常事件总处理逻辑
while true do
-- 数据接收处理(接收处理必须写在libnet.wait之前,因为老版本的内核固件要求必须这样,新版本的内核固件没这个要求,为了不出问题,写在libnet.wait之前就行了)
-- 如果处理失败,则退出循环
if not tcp_client_receiver.proc(socket_client) then
log.error("tcp_client_main_task_func", "tcp_client_receiver.proc error")
break
end
-- 数据发送处理
-- 如果处理失败,则退出循环
if not tcp_client_sender.proc(TASK_NAME, socket_client) then
log.error("tcp_client_main_task_func", "tcp_client_sender.proc error")
break
end
-- 阻塞等待socket.EVENT事件或者15秒钟超时
-- 以下三种业务逻辑会发布事件:
-- 1、socket client和server之间的连接出现异常(例如server主动断开,网络环境出现异常等),此时在内核固件中会发布事件socket.EVENT
-- 2、socket client接收到server发送过来的数据,此时在内核固件中会发布事件socket.EVENT
-- 3、socket client需要发送数据到server, 在tcp_client_sender.lua中会发布事件socket.EVENT
result, para1, para2 = libnet.wait(TASK_NAME, 15000, socket_client)
log.info("tcp_client_main_task_func", "libnet.wait", result, para1, para2)
-- 如果连接异常,则退出循环
if not result then
log.warn("tcp_client_main_task_func", "connection exception")
break
end
end
-- 出现异常
::EXCEPTION_PROC::
-- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
tcp_client_sender.exception_proc()
-- 如果存在socket client对象
if socket_client then
-- 关闭socket client连接
libnet.close(TASK_NAME, 5000, socket_client)
-- 释放socket client对象
socket.release(socket_client)
socket_client = nil
end
-- 5秒后跳转到循环体开始位置,自动发起重连
sys.wait(5000)
end
end
--创建并且启动一个task
--运行这个task的主函数tcp_client_main_task_func
sys.taskInitEx(tcp_client_main_task_func, TASK_NAME, tcp_client_main_cbfunc)
@@ -0,0 +1,111 @@
--[[
@module tcp_client_receiver
@summary tcp client socket数据接收应用功能模块
@version 1.0
@date 2025.08.12
@author 孟伟
@usage
本文件为tcp client socket数据接收应用功能模块,核心业务逻辑为:
从内核读取接收到的数据,然后将数据处理后发送给其他应用功能模块做进一步处理;
本文件的对外接口有2个:
1、tcp_client_receiver.proc(socket_client):数据接收应用逻辑处理入口,在tcp_client_main.lua中调用;
2、sys.publish("RECV_DATA_FROM_SERVER", data)
将接收到的数据通过消息"RECV_DATA_FROM_SERVER"发布出去处理;
处理后通过消息"AIR_SRV_FOTA"发布到air_srv_fota.lua文件中去通知升级
]]
local tcp_client_receiver = {}
-- socket数据接收缓冲区
local recv_buff = nil
--[[
检查socket client是否收到数据,如果收到数据,读取并且处理完所有数据
@api tcp_client_receiver.proc(socket_client)
@param1 socket_client userdata
表示由socket.create接口创建的socket client对象;
必须传入,不允许为空或者nil
@return1 result bool
表示处理结果,成功为true,失败为false
@usage
--
tcp_client_receiver.proc(socket_client)
]]
function tcp_client_receiver.proc(socket_client)
-- 如果socket数据接收缓冲区还没有申请过空间,则先申请内存空间
if recv_buff == nil then
recv_buff = zbuff.create(1024)
-- 当recv_buff不再使用时,不需要主动调用recv_buff:free()去释放
-- 因为Lua的垃圾处理器会自动释放recv_buff所申请的内存空间
-- 如果等不及垃圾处理器自动处理,在确定以后不会再使用recv_buff时,则可以主动调用recv_buff:free()释放内存空间
end
-- 循环从内核的缓冲区读取接收到的数据
-- 如果读取失败,返回false,退出
-- 如果读取成功,处理数据,并且继续循环读取
-- 如果读取成功,并且读出来的数据为空,表示已经没有数据可读,返回true,退出
while true do
-- 从内核的缓冲区中读取数据到recv_buff中
-- 如果recv_buff的存储空间不足,会自动扩容
local result = socket.rx(socket_client, recv_buff)
-- 读取数据失败
-- 有两种情况:
-- 1、recv_buff扩容失败
-- 2、socket client和server之间的连接断开
if not result then
log.error("tcp_client_receiver.proc", "socket.rx error")
return false
end
-- 如果读取到了数据, used()就必然大于0, 进行处理
if recv_buff:used() > 0 then
log.info("tcp_client_receiver.proc", "recv data len", recv_buff:used())
-- 读取socket数据接收缓冲区中的数据,赋值给data
local data = recv_buff:query()
-- 将数据data通过"RECV_IOT_DATA_SERVER"消息publish出去,给其他应用模块处理
sys.publish("RECV_IOT_DATA_SERVER", data)
-- 清空socket数据接收缓冲区中的数据
recv_buff:del()
-- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
else
break
end
end
return true
end
--解析接受到的json参数并发布到消息"AIR_SRV_FOTA"中
--定义一个json格式如下,具体可以根据实际情况定义,使用iot升级的话只定义了一个参数即可:
--{
-- "fota": "true"
--}
local function data_processing(data)
-- 解析json数据
local json_data = json.decode(data)
-- 如果解析失败
if not json_data then
log.error("data_processing", "json.decode error")
return
end
-- 如果fota为true
if json_data.fota == "true" then
-- 发布消息"AIR_SRV_FOTA"
sys.publish("AIR_SRV_FOTA")
end
end
-- 订阅"RECV_IOT_DATA_SERVER"消息
sys.subscribe("RECV_IOT_DATA_SERVER", data_processing)
return tcp_client_receiver
@@ -0,0 +1,139 @@
--[[
@module tcp_client_sender
@summary tcp client socket数据发送应用功能模块
@version 1.0
@date 2025.07.31
@author 孟伟
@usage
本文件为tcp client socket数据发送应用功能模块,核心业务逻辑为:
1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列iot_send_queue中;
2、tcp_client_main主任务调用tcp_client_sender.proc接口,遍历队列iot_send_queue,逐条发送数据到server
3、tcp client socket和server之间的连接如果出现异常,tcp_client_main主任务调用tcp_client_sender.exception_proc接口,丢弃掉队列iot_send_queue中未发送的数据;
4、任何一条数据无论发送成功还是失败,只要这条数据有回调函数,都会通过回调函数通知数据发送方;
本文件的对外接口有3个:
1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func):订阅"SEND_DATA_REQ"消息;
其他应用模块如果需要发送数据,直接sys.publish这个消息即可,将需要发送的数据以及回调函数和回调参数一起publish出去;
本demo项目中uart_app.lua和timer_app.lua中publish了这个消息;
2、tcp_client_sender.proc:数据发送应用逻辑处理入口,在tcp_client_main.lua中调用;
3、tcp_client_sender.exception_proc:数据发送应用逻辑异常处理入口,在tcp_client_main.lua中调用;
]]
local tcp_client_sender = {}
local libnet = require "libnet"
--[[
数据发送队列,数据结构为:
{
[1] = {data="data1", cb={func=callback_function1, para=callback_para1}},
[2] = {data="data2", cb={func=callback_function2, para=callback_para2}},
}
data的内容为真正要发送的数据,必须存在;
func的内容为数据发送结果的用户回调函数,可以不存在
para的内容为数据发送结果的用户回调函数的回调参数,可以不存在;
]]
local iot_send_queue = {}
-- tcp_client_main的任务名
tcp_client_sender.TASK_NAME = "iot_tcp_client_main"
-- "SEND_DATA_REQ"消息的处理函数
local function send_data_req_proc_func(data, cb)
-- 将原始数据增加前缀,然后插入到发送队列iot_send_queue中
table.insert(iot_send_queue, { data = data, cb = cb })
-- 通知tcp_client_main主任务有数据需要发送
-- tcp_client_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
sys.sendMsg(tcp_client_sender.TASK_NAME, socket.EVENT, 0)
end
--[[
检查socket client是否需要发送数据,如果需要发送数据,读取并且发送完发送队列中的所有数据
@api tcp_client_sender.proc(task_name, socket_client)
@param1 task_name string
表示socket.create接口创建socket client对象时所处的task的name
必须传入,不允许为空或者nil
@param2 socket_client userdata
表示由socket.create接口创建的socket client对象;
必须传入,不允许为空或者nil
@return1 result bool
表示处理结果,成功为true,失败为false
@usage
tcp_client_sender.proc("tcp_client_main", socket_client)
]]
function tcp_client_sender.proc(task_name, socket_client)
local send_item
local result, buff_full
-- 遍历数据发送队列iot_send_queue
while #iot_send_queue > 0 do
-- 取出来第一条数据赋值给send_item
-- 同时从队列iot_send_queue中删除这一条数据
send_item = table.remove(iot_send_queue, 1)
-- 发送这条数据,超时时间15秒钟
result, buff_full = libnet.tx(task_name, 15000, socket_client, send_item.data)
-- 发送失败
if not result then
log.error("tcp_client_sender.proc", "libnet.tx error")
-- 如果当前发送的数据有用户回调函数,则执行用户回调函数
if send_item.cb and send_item.cb.func then
send_item.cb.func(false, send_item.cb.para)
end
return false
end
-- 如果内核固件中缓冲区满了,则将send_item再次插入到iot_send_queue的队首位置,等待下次尝试发送
if buff_full then
log.error("tcp_client_sender.proc", "buffer is full, wait for the next time")
table.insert(iot_send_queue, 1, send_item)
return true
end
log.info("tcp_client_sender.proc", "send success")
-- 发送成功,如果当前发送的数据有用户回调函数,则执行用户回调函数
if send_item.cb and send_item.cb.func then
send_item.cb.func(true, send_item.cb.para)
end
-- 发送成功,通知网络环境检测看门狗功能模块进行喂狗
sys.publish("FEED_NETWORK_WATCHDOG")
end
return true
end
--[[
socket client连接出现异常时,清空等待发送的数据,并且执行发送方的回调函数
@api tcp_client_sender.exception_proc()
@usage
tcp_client_sender.exception_proc()
]]
function tcp_client_sender.exception_proc()
-- 遍历数据发送队列iot_send_queue
while #iot_send_queue > 0 do
local send_item = table.remove(iot_send_queue, 1)
-- 发送失败,如果当前发送的数据有用户回调函数,则执行用户回调函数
if send_item.cb and send_item.cb.func then
send_item.cb.func(false, send_item.cb.para)
end
end
end
-- 订阅"SEND_DATA_REQ"消息;
-- 其他应用模块如果需要发送数据,直接sys.publish这个消息即可,将需要发送的数据以及回调函数和回调参数一起publish出去;
-- 本demo项目中uart_app.lua和timer_app.lua中publish了这个消息;
sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)
return tcp_client_sender
@@ -0,0 +1,101 @@
--[[
@module update
@summary 远程升级功能模块
@version 1.0
@date 2025.08.12
@author 孟伟
@usage
实现远程升级功能,具体流程如下:
1、判断网卡是否连接成功;
2、初始化fota2模块;
3、配置fota2模块的参数;
4、调用fota2模块的升级函数;
5、在升级结果的回调函数中,根据升级结果进行处理;
]]
-- 使用合宙iot平台时需要这个参数
PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
libfota2 = require "libfota2"
-- 循环打印版本号, 方便看版本号变化, 非必须
function get_version()
log.info("降功耗 找合宙")
log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
end
sys.timerLoopStart(get_version, 3000)
-- 升级结果的回调函数
-- 功能:获取fota的回调函数
-- 参数:
-- result:number类型
-- 0表示成功
-- 1表示连接失败
-- 2表示url错误
-- 3表示服务器断开
-- 4表示接收报文错误
-- 5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
local function fota_cb(ret)
log.info("fota", ret)
if ret == 0 then
log.info("升级包下载成功,重启模块")
rtos.reboot()
elseif ret == 1 then
log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
elseif ret == 2 then
log.info("url错误", "检查url拼写")
elseif ret == 3 then
log.info("服务器断开", "检查服务器白名单配置")
elseif ret == 4 then
log.error("FOTA 失败",
"原因可能有:\n" ..
"1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
"2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
"3) 已经是最新版本,无需升级" )
elseif ret == 5 then
log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
else
log.info("不是上面几种情况 ret为", ret)
end
end
-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
local ota_opts = {}
function fota_task_func()
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("fota_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 检测到了IP_READY消息
log.info("fota_task_func", "recv IP_READY", socket.dft())
-- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
if "123" == _G.PRODUCT_KEY then
while true do
sys.wait(1000)
log.info("fota", "请修改正确的PRODUCT_KEY")
end
end
log.info("开始检查升级")
libfota2.request(fota_cb, ota_opts)
end
--创建并且启动一个task
--运行这个task的主函数fota_task_func
sys.taskInit(fota_task_func)
-- 演示定时自动升级, 每隔4小时自动检查一次
sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, ota_opts)
@@ -0,0 +1,143 @@
## 功能模块介绍
### 自建服务器fota功能演示
1、main.lua:主程序入口;
2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的四种网卡(单4g网卡,单wifi网卡,单spi以太网卡,多网卡)中的任何一种网卡;
3、update.lua:使用自建服务器进行远程升级功能模块,简单升级演示;
4、tcp_self_server文件夹:通过tcp服务器下发升级指令(指令格式使用json字符串,包含是否升级参数),控制设备启动customer_srv_fota功能模块,使用自建服务器进行升级;
5、customer_srv_fota.lua:自建服务器升级功能模块;
6、psm_power_fota.lua:低功耗fota功能模块,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。需要注意的是此场景与上面两种场景不能同时使用;
## 系统消息介绍
1、"IP_READY":某种网卡已经获取到ip信息,仅仅获取到了ip信息,能否和外网连通还不确认;
2、"IP_LOSE":某种网卡已经掉网;
## 用户消息介绍
1、"RECV_DATA_FROM_SERVER"socket client收到服务器下发的数据后,通过此消息发布出去,给其他应用模块处理;
2、"SEND_DATA_REQ":其他应用模块发布此消息,通知socket client发送数据给服务器;
## 演示功能概述
1、combination文件夹下的demo会有三个演示场景,在main.lua中选择要使用的场景:
(1) 使用自建服务器升级,演示最简单的升级逻辑。
(2) 使用自建服务器升级,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。
(3) 休眠状态下升级,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。
2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
(1) netdrv_4g4G网卡
(2) netdrv_wifiWIFI STA网卡
(3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
(4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
## 演示硬件环境
![](https://docs.openluat.com/air780epm/luatos/app/driver/eth/image/RFSvb75NRoEWqYxfCRVcVrOKnsf.jpg)
1、Air780EPM V1.3版本开发板一块+可上网的sim卡一张+4g天线一根+网线一根:
- sim卡插入开发板的sim卡槽
- 天线装到开发板上
- 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口
2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air780EPM V1.3版本开发板和数据线的硬件接线方式为:
- Air780EPM V1.3版本开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
## 演示软件环境
1、Luatools下载调试工具
2、[Air780EPM V2012版本固件)](https://docs.openluat.com/air780epm/luatos/firmware/version/)
3、PC端浏览器访问[合宙TCP/UDP web测试工具](https://iot.luatos.com/#/page6/netlab)
## 演示核心步骤
1、搭建好硬件环境
2、PC端浏览器访问[合宙TCP/UDP web测试工具](https://iot.luatos.com/#/page6/netlab),点击 打开TCP 按钮,会创建一个TCP server,将server的地址和端口赋值给tcp_client_self_main.lua中的SERVER_ADDR和SERVER_PORT两个变量
详细使用说明参考:[合宙 TCP/UDP web 测试工具使用说明](https://iot.luatos.com/#/p8000/netlab) 。
3、demo脚本代码netdrv_device.lua中,按照自己的网卡需求启用对应的Lua文件
- 如果需要单4G网卡,打开require "netdrv_4g",其余注释掉
- 如果需要单WIFI STA网卡,打开require "netdrv_wifi",其余注释掉;同时netdrv_wifi.lua中的wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1),前两个参数,修改为自己测试时wifi热点的名称和密码;注意:仅支持2.4G的wifi,不支持5G的wifi
- 如果需要以太网卡,打开require "netdrv_eth_spi",其余注释掉
- 如果需要多网卡,打开require "netdrv_multiple",其余注释掉;同时netdrv_multiple.lua中的ssid = "茶室-降功耗,找合宙!", password = "Air123456", 修改为自己测试时wifi热点的名称和密码;注意:仅支持2.4G的wifi,不支持5G的wifi
4、Luatools烧录内核固件和修改后的demo脚本代码
5、使用Luatools制作升级包,先把新旧版本分别生成量产文件,然后再制作升级包,工具上栏 luatOS->固件工具->差分包/整包升级包制作,将制作好的升级包配置到合宙iot服务器自己项目下,或上传到自建服务器上面;
6、烧录成功后,自动开机运行
7、[合宙TCP/UDP web测试工具](https://iot.luatos.com/#/page6/netlab)上创建的两个TCP server可以看到有设备连接上来,然后可以下发下面字符串触发升级:
``` lua
--自建服务器下发这个指令,下发之前需要在服务器上面配置好升级包,然后吧url给到字符串
--定义一个json格式如下,具体可以根据实际情况定义:
-- {"fota": "true","url": "http://airtest.openluat.com:2900/download/FOTA2_DEMO_2012.001.001_LuatOS-SoC_Air780EPM.bin"}
```
8、可以看到升级过程如下,不管是什么场景下升级,基本都是如下情况:
``` lua
--没有升级之前可以看到如下打印
I/user.fota 脚本版本号 001.000.000 core版本号 V2010
I/user.fota_task_func recv IP_READY 1
I/user.开始检查升级
I/user.升级包下载成功,重启模块
--升级之后可以看到如下打印
I/user.fota 脚本版本号 001.000.001 core版本号 V2012
--升级重启之后还是会检查升级,所以会有如下打印属于正常情况,其中"code": 27 是合宙iot服务器返回的状态码,意思是已经是最新版本了。自建服务器如果没做设置会再下载一遍升级包。
I/user.fota -9 {"code": 27, "msg": "\u5df2\u662f\u6700\u65b0\u7248\u672c"}
I/user.使用合宙服务器,接下来解析body里的code
I/user.已是最新版本 1.设备的固件/脚本版本高于或等于云平台上的版本号 2.用户项目升级配置中未添加该设备 3.云平台升级配置中,是否升级配置为否
I/user.fota 4
```
9、对于psm休眠状态下的升级的场景,打印的版本来判断是否是最新版本。模组升级成功后会自动进入psm休眠状态。可以通过电流查看休眠情况。
@@ -0,0 +1,139 @@
--[[
@module air_srv_fota
@summary 使用自建服务器远程升级功能模块
@version 1.0
@date 2025.08.12
@author 孟伟
@usage
实现远程升级功能,具体流程如下:
1、接收 CUSTOMER_SRV_FOTA 系统消息,触发升级;
2、判断网卡是否连接成功;
3、初始化fota2模块;
4、调用fota2模块的升级函数;
5、根据升级结果进行处理;
]]
--加在libfota2扩展库
libfota2 = require "libfota2"
-- 循环打印版本号, 方便看版本号变化, 非必须
function get_version()
log.info("降功耗 找合宙")
log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
end
sys.timerLoopStart(get_version, 3000)
-- fota升级标志:true 表示当前正有 FOTA 流程在跑
local fota_running = false
-- 升级结果的回调函数
-- 功能:获取fota的回调函数
-- 参数:
-- result:number类型
-- 0表示成功
-- 1表示连接失败
-- 2表示url错误
-- 3表示服务器断开
-- 4表示接收报文错误
-- 5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
local function fota_cb(ret)
log.info("fota", ret)
-- fota结束,无论成功还是失败,都释放fota_running标志
fota_running = false
if ret == 0 then
log.info("升级包下载成功,重启模块")
rtos.reboot()
elseif ret == 1 then
log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
elseif ret == 2 then
log.info("url错误", "检查url拼写")
elseif ret == 3 then
log.info("服务器断开", "检查服务器白名单配置")
elseif ret == 4 then
log.error("FOTA 失败",
"原因可能有:\n" ..
"1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
"2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n" ..
"3) 已经是最新版本,无需升级")
elseif ret == 5 then
log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
else
log.info("不是上面几种情况 ret为", ret)
end
end
-- 使用第三方服务器,配置ota_opts参数
--[[
-- opts参数说明, 所有参数都是可选的
-- 1. opts.url string 升级所需要的URL, 若使用合宙iot平台,则不需要填
-- 2. opts.version string 版本号, 默认是 BSP版本号.x.z格式
-- 3. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
-- 4. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
-- 5. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
-- 6. opts.firmware_name string 固件名称,默认是 _G.PROJECT.. "_LuatOS-SoC_" .. rtos.bsp()
-- 7. opts.server_cert string 服务器证书, 默认不使用
-- 8. opts.client_cert string 客户端证书, 默认不使用
-- 9. opts.client_key string 客户端私钥, 默认不使用
-- 10. opts.client_password string 客户端私钥口令, 默认不使用
-- 11. opts.method string 请求方法, 默认是GET
-- 12. opts.headers table 额外添加的请求头,默认不需要
-- 13. opts.body string 额外添加的请求body,默认不需要
]]
local opts = {
url = "",
-- 合宙IOT平台的默认升级URL, 不填就是这个默认值
-- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
-- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
-- 如果不加###,则默认会上传如下参数
-- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
-- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
-- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
-- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
-- 5. opts.firmware_name string 底层版本号
-- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
version = ""
-- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
}
local function air_fota_func(data)
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("fota_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 检测到了IP_READY消息
log.info("fota_task_func", "recv IP_READY", socket.dft())
while true do
-- 阻塞等待外部事件:"CUSTOMER_SRV_FOTA"
local result, data = sys.waitUntil("CUSTOMER_SRV_FOTA")
if result then
log.info("接收到数据", "date", #data)
if fota_running then
log.warn("fota_task", "FOTA 正在运行,跳过本次请求")
else
-- 标记FOTA正在运行
-- 注意:这里只是标记,实际的FOTA流程还没有开始
opts.url = data.url
opts.version = data.version
fota_running = true
log.info("开始检查升级")
libfota2.request(fota_cb, opts)
end
end
end
end
-- 初始化FOTA任务
sys.taskInit(air_fota_func)
@@ -0,0 +1,100 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
@version 1.0
@date 2025.08.12
@author 孟伟
@usage
本demo演示的核心功能为:
1、这个demo会有三个演示场景:
(1)使用自建服务器升级,演示最简单的升级逻辑。
(2)使用自建服务器升级,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。
(3)休眠状态下升级,此场景是针对psm状态下升级没完成就进入休眠导致升级失败的情况写的一个例子。
2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
(1) netdrv_4g4G网卡
(2) netdrv_wifiWIFI STA网卡
(3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
(4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
更多说明参考本目录下的readme.md文件
]]
--[[
必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
PROJECT:项目名,ascii string类型
可以随便定义,只要不使用,就行
VERSION:项目版本号,ascii string类型
如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为999
如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
]]
PROJECT = "FOTA2_DEMO"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 循环打印版本号, 方便看版本号变化, 非必须
function get_version()
log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
end
sys.timerLoopStart(get_version, 3000)
-- 加载网络驱动设备功能模块
require "netdrv_device"
--两种tcp下发指令升级的场景和psm+低功耗模式升级不能同时使用,需要根据自己场景选择其中一种
--两种tcp下发指令升级的场景可以启用一种,也可以启用两种,启用两种时,注意控制不要一个在fota的过程中,另外一个再fota。
-- 加载远程升级功能模块,场景一
require "update"
---------------------------------------------------------------------------
-- 加载tcp client self socket主应用功能模块,通过tcp下发升级指令控制设备升级,指令格式使用json字符串,包含版本、url、是否升级参数,演示如何通过服务器控制下发指令去升级。场景二
-- require "tcp_self_main"
-- 加载自建服务器远程升级功能模块
-- require "customer_srv_fota"
---------------------------------------------------------------------------
-- 加载psm+低功耗模式升级功能模块,场景三
-- require "psm_power_fota"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -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芯片的以太网卡
1CH390H芯片供电开关
2spi0DHCP()
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
1exnetif.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,30 @@
--[[
@module netdrv_device
@summary
@version 1.0
@date 2025.08.12
@author
@usage
()
1netdrv_4gsocket.LWIP_GP4G网卡
2netdrv_wifisocket.LWIP_STAWIFI STA网卡
3netdrv_eth_spisocket.LWIP_USER1SPI外挂CH390H芯片的以太网卡
4netdrv_multiple使
require以上四种中的一种即可
main.lua中require "netdrv_device"
]]
-- 根据自己的项目需求,只需要require以下四种中的一种即可;
-- 加载“4G网卡”驱动模块
require "netdrv_4g"
-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
-- require "netdrv_eth_spi"
-- 加载“可以配置优先级的多种网卡”驱动模块
-- require "netdrv_multiple"
@@ -0,0 +1,131 @@
--[[
@module psm+_power_fota
@summary psm+
@version 1.0
@date 2025.08.12
@author
@usage
psm+
1.
- PSM模式唤醒
- GPIO中断或网络消息唤醒设备进行升级
2.
- FOTA升级前PSM模式进入
- PSM模式
]]
-- 使用合宙iot平台时需要这个参数
PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
--加在libfota2扩展库
libfota2 = require "libfota2"
-- 升级结果的回调函数
-- 功能:获取fota的回调函数
-- 参数:
-- result:number类型
-- 0表示成功
-- 1表示连接失败
-- 2表示url错误
-- 3表示服务器断开
-- 4表示接收报文错误
-- 5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
local function fota_cb(ret)
log.info("fota", ret)
--升级结束,触发升级回调,发布消息升级结束,可以进入休眠模式
sys.publish("FOTA_END")
if ret == 0 then
log.info("升级包下载成功,重启模块")
rtos.reboot()
elseif ret == 1 then
log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
elseif ret == 2 then
log.info("url错误", "检查url拼写")
elseif ret == 3 then
log.info("服务器断开", "检查服务器白名单配置")
elseif ret == 4 then
log.error("FOTA 失败",
"原因可能有:\n" ..
"1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
"2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
"3) 已经是最新版本,无需升级" )
elseif ret == 5 then
log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
else
log.info("不是上面几种情况 ret为", ret)
end
end
-- 使用合宙iot平台进行升级, 支持自定义参数, 也可以不配置,如果要配置参数可以参考此链接https://docs.openluat.com/osapi/ext/libfota2/
local opts = {
-- 使用的是自定义的URL,此处写的URL是不存在的一个无效URL,自己测试时,需要替换为自己的有效URL
url = "###http://your_fota_packet_url.bin",
-- 合宙IOT平台的默认升级URL, 不填就是这个默认值
-- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
-- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
-- 如果不加###,则默认会上传如下参数
-- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
-- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
-- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
-- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
-- 5. opts.firmware_name string 底层版本号
-- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
-- version = ""
-- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
}
function psm_fota_task_func()
-- 如果是被定时器唤醒,因为上次进入PSM+时是开启了飞行模式,所以在唤醒后第一时间关闭飞行模式。
mobile.flymode(0, false)
log.info("开始测试PSM+模式功耗。")
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("fota_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 检测到了IP_READY消息
log.info("fota_task_func", "recv IP_READY", socket.dft())
log.info("开始检查升级")
libfota2.request(fota_cb, opts)
-- 打印版本号, 方便看版本号变化, 非必须
log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
-- 等待下载升级包结束, 发布消息"FOTA_END",
-- 如果15秒内没有收到消息,则15秒的时长到达后进入PSM+模式。
-- 需要注意的是在fota_cb回调函数中,升级包下载成功后,会立马重启并升级模组。如果还有其他事情要做不想立马重启升级,需自行决定reboot的时机
-- 升级包下载成功后,本demo默认是立即自动重启并且将升级包更新到模组中,更新成功后,会再次走到这里
-- 再次走到这里后,合宙iot平台会返回“已经是最新版本,不需要升级”,fota_cb回调函数中会发布消息"FOTA_END"
-- 至此,才会继续向下执行代码,进入PSM+模式
sys.waitUntil("FOTA_END", 15000)
log.info("升级结束,进入PSM模式")
-- 定时检查升级 (每4小时唤醒一次)
pm.dtimerStart(2, 4 * 3600000)
-- 启动飞行模式,规避可能会出现的网络问题
mobile.flymode(0, true)
-- 进入PSM模式
pm.power(pm.WORK_MODE, 3)
-- 防御机制:15秒后如果未进入PSM则重启
sys.wait(15000)
log.info("进入PSM+失败,重启")
rtos.reboot()
end
sys.taskInit(psm_fota_task_func)
@@ -0,0 +1,141 @@
--[[
@module tcp_client_main
@summary tcp client socket主应用功能模块
@version 1.0
@date 2025.08.12
@author
@usage
tcp client socket主应用功能模块
1tcp client socketserver
2
3tcp_client_receiver和tcp_client_sender中的外部接口
main.lua中require "tcp_client_main"
]]
local libnet = require "libnet"
-- 加载tcp client socket数据接收功能模块
local tcp_client_receiver = require "tcp_self_receiver"
-- 加载tcp client socket数据发送功能模块
local tcp_client_sender = require "tcp_self_sender"
-- 电脑访问:https://iot.luatos.com/#/page6/netlab
-- 本工具使用方法可以参考:https://docs.openluat.com/common/TCPUDP_Test/
-- 点击 打开TCP 按钮,会创建一个TCP server
-- 将server的地址和端口赋值给下面这两个变量
local SERVER_ADDR = "112.125.89.8"
local SERVER_PORT = 45433
-- tcp_client_main的任务名
local TASK_NAME = tcp_client_sender.TASK_NAME
-- 处理未识别的消息
local function tcp_client_main_cbfunc(msg)
log.info("tcp_client_main_cbfunc", msg[1], msg[2], msg[3], msg[4])
end
-- tcp client socket的任务处理函数
local function tcp_client_main_task_func()
local socket_client
local result, para1, para2
while true do
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("sntp_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 检测到了IP_READY消息
log.info("tcp_client_main_task_func", "recv IP_READY", socket.dft())
-- 创建socket client对象
socket_client = socket.create(nil, TASK_NAME)
-- 如果创建socket client对象失败
if not socket_client then
log.error("tcp_client_main_task_func", "socket.create error")
goto EXCEPTION_PROC
end
-- 配置socket client对象为tcp client
result = socket.config(socket_client, nil, nil, nil, 300, 10, 3)
-- 如果配置失败
if not result then
log.error("tcp_client_main_task_func", "socket.config error")
goto EXCEPTION_PROC
end
-- 连接server
result = libnet.connect(TASK_NAME, 15000, socket_client, SERVER_ADDR, SERVER_PORT)
-- 如果连接server失败
if not result then
log.error("tcp_client_main_task_func", "libnet.connect error")
goto EXCEPTION_PROC
end
log.info("tcp_client_main_task_func", "libnet.connect success")
-- 数据收发以及网络连接异常事件总处理逻辑
while true do
-- 数据接收处理(接收处理必须写在libnet.wait之前,因为老版本的内核固件要求必须这样,新版本的内核固件没这个要求,为了不出问题,写在libnet.wait之前就行了)
-- 如果处理失败,则退出循环
if not tcp_client_receiver.proc(socket_client) then
log.error("tcp_client_main_task_func", "tcp_client_receiver.proc error")
break
end
-- 数据发送处理
-- 如果处理失败,则退出循环
if not tcp_client_sender.proc(TASK_NAME, socket_client) then
log.error("tcp_client_main_task_func", "tcp_client_sender.proc error")
break
end
-- 阻塞等待socket.EVENT事件或者15秒钟超时
-- 以下三种业务逻辑会发布事件:
-- 1、socket client和server之间的连接出现异常(例如server主动断开,网络环境出现异常等),此时在内核固件中会发布事件socket.EVENT
-- 2、socket client接收到server发送过来的数据,此时在内核固件中会发布事件socket.EVENT
-- 3、socket client需要发送数据到server, 在tcp_client_sender.lua中会发布事件socket.EVENT
result, para1, para2 = libnet.wait(TASK_NAME, 15000, socket_client)
log.info("tcp_client_main_task_func", "libnet.wait", result, para1, para2)
-- 如果连接异常,则退出循环
if not result then
log.warn("tcp_client_main_task_func", "connection exception")
break
end
end
-- 出现异常
::EXCEPTION_PROC::
-- 数据发送应用模块对来不及发送的数据做清空和通知失败处理
tcp_client_sender.exception_proc()
-- 如果存在socket client对象
if socket_client then
-- 关闭socket client连接
libnet.close(TASK_NAME, 5000, socket_client)
-- 释放socket client对象
socket.release(socket_client)
socket_client = nil
end
-- 5秒后跳转到循环体开始位置,自动发起重连
sys.wait(5000)
end
end
--创建并且启动一个task
--运行这个task的主函数tcp_client_main_task_func
sys.taskInitEx(tcp_client_main_task_func, TASK_NAME, tcp_client_main_cbfunc)
@@ -0,0 +1,126 @@
--[[
@module tcp_client_receiver
@summary tcp client socket数据接收应用功能模块
@version 1.0
@date 2025.08.12
@author
@usage
tcp client socket数据接收应用功能模块
2
1tcp_client_receiver.proc(socket_client)tcp_client_main.lua中调用
2sys.publish("RECV_DATA_FROM_SERVER", data)tcp_client_main
"RECV_DATA_FROM_SERVER"
"CUSTOMER_SRV_FOTA"customer_srv_fota.lua文件中去通知升级
]]
local tcp_client_receiver = {}
-- socket数据接收缓冲区
local recv_buff = nil
--[[
socket client是否收到数据
@api tcp_client_receiver.proc(socket_client)
@param1 socket_client userdata
socket.create接口创建的socket client对象
nil
@return1 result bool
truefalse
@usage
--
tcp_client_receiver.proc(socket_client)
]]
function tcp_client_receiver.proc(socket_client)
-- 如果socket数据接收缓冲区还没有申请过空间,则先申请内存空间
if recv_buff==nil then
recv_buff = zbuff.create(1024)
-- 当recv_buff不再使用时,不需要主动调用recv_buff:free()去释放
-- 因为Lua的垃圾处理器会自动释放recv_buff所申请的内存空间
-- 如果等不及垃圾处理器自动处理,在确定以后不会再使用recv_buff时,则可以主动调用recv_buff:free()释放内存空间
end
-- 循环从内核的缓冲区读取接收到的数据
-- 如果读取失败,返回false,退出
-- 如果读取成功,处理数据,并且继续循环读取
-- 如果读取成功,并且读出来的数据为空,表示已经没有数据可读,返回true,退出
while true do
-- 从内核的缓冲区中读取数据到recv_buff中
-- 如果recv_buff的存储空间不足,会自动扩容
local result = socket.rx(socket_client, recv_buff)
-- 读取数据失败
-- 有两种情况:
-- 1、recv_buff扩容失败
-- 2、socket client和server之间的连接断开
if not result then
log.error("tcp_client_receiver.proc", "socket.rx error")
return false
end
-- 如果读取到了数据, used()就必然大于0, 进行处理
if recv_buff:used() > 0 then
log.info("tcp_client_receiver.proc", "recv data len", recv_buff:used())
-- 读取socket数据接收缓冲区中的数据,赋值给data
local data = recv_buff:query()
-- 将数据data通过"RECV_SELF_DATA_SERVER"消息publish出去,给其他应用模块处理
sys.publish("RECV_SELF_DATA_SERVER", data)
-- 清空socket数据接收缓冲区中的数据
recv_buff:del()
-- 读取成功,但是读出来的数据为空,表示已经没有数据可读,可以退出循环了
else
break
end
end
return true
end
--解析接受到的json参数并发布到消息"AIR_SRV_FOTA"中
--定义一个json格式如下,具体可以根据实际情况定义:
--{
-- "fota": "true",
-- "url": "http://192.168.1.100:8080/firmware.bin"
--}
local function data_processing(data)
-- 解析json数据
local json_data = json.decode(data)
-- 如果解析失败
if not json_data then
log.error("data_processing", "json.decode error")
return
end
-- 解析fota参数
local fota = json_data.fota
-- 如果fota参数不存在
if not fota then
log.error("data_processing", "fota error")
return
end
-- 解析url参数
local url = json_data.url
-- 如果url参数不存在
if not url then
log.error("data_processing", "url error")
return
end
-- 如果fota为true
if fota == "true" then
-- -- 发布消息"CUSTOMER_SRV_FOTA"
sys.publish("CUSTOMER_SRV_FOTA",json_data)
end
end
-- 订阅"RECV_SELF_DATA_SERVER"消息
sys.subscribe("RECV_SELF_DATA_SERVER", data_processing)
return tcp_client_receiver
@@ -0,0 +1,137 @@
--[[
@module tcp_client_sender
@summary tcp client socket数据发送应用功能模块
@version 1.0
@date 2025.07.31
@author
@usage
tcp client socket数据发送应用功能模块
1sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)"SEND_DATA_REQ"self_send_queue中
2tcp_client_main主任务调用tcp_client_sender.proc接口self_send_queueserver
3tcp client socket和server之间的连接如果出现异常tcp_client_main主任务调用tcp_client_sender.exception_proc接口self_send_queue中未发送的数据
4
3
1sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)"SEND_DATA_REQ"
sys.publish这个消息即可publish出去
demo项目中uart_app.lua和timer_app.lua中publish了这个消息
2tcp_client_sender.proctcp_client_main.lua中调用
3tcp_client_sender.exception_proctcp_client_main.lua中调用
]]
local tcp_client_sender = {}
local libnet = require "libnet"
--[[
{
[1] = {data="data1", cb={func=callback_function1, para=callback_para1}},
[2] = {data="data2", cb={func=callback_function2, para=callback_para2}},
}
data的内容为真正要发送的数据
func的内容为数据发送结果的用户回调函数
para的内容为数据发送结果的用户回调函数的回调参数
]]
local self_send_queue = {}
-- tcp_client_main的任务名
tcp_client_sender.TASK_NAME = "self_tcp_client_main"
-- "SEND_DATA_REQ"消息的处理函数
local function send_data_req_proc_func(data, cb)
-- 将原始数据增加前缀,然后插入到发送队列self_send_queue中
table.insert(self_send_queue, {data=data, cb=cb})
-- 通知tcp_client_main主任务有数据需要发送
-- tcp_client_main主任务如果处在libnet.wait调用的阻塞等待状态,就会退出阻塞状态
sys.sendMsg(tcp_client_sender.TASK_NAME, socket.EVENT, 0)
end
--[[
socket client是否需要发送数据
@api tcp_client_sender.proc(task_name, socket_client)
@param1 task_name string
socket.create接口创建socket client对象时所处的task的name
nil
@param2 socket_client userdata
socket.create接口创建的socket client对象
nil
@return1 result bool
truefalse
@usage
tcp_client_sender.proc("tcp_client_main", socket_client)
]]
function tcp_client_sender.proc(task_name, socket_client)
local send_item
local result, buff_full
-- 遍历数据发送队列self_send_queue
while #self_send_queue>0 do
-- 取出来第一条数据赋值给send_item
-- 同时从队列self_send_queue中删除这一条数据
send_item = table.remove(self_send_queue,1)
-- 发送这条数据,超时时间15秒钟
result, buff_full = libnet.tx(task_name, 15000, socket_client, send_item.data)
-- 发送失败
if not result then
log.error("tcp_client_sender.proc", "libnet.tx error")
-- 如果当前发送的数据有用户回调函数,则执行用户回调函数
if send_item.cb and send_item.cb.func then
send_item.cb.func(false, send_item.cb.para)
end
return false
end
-- 如果内核固件中缓冲区满了,则将send_item再次插入到self_send_queue的队首位置,等待下次尝试发送
if buff_full then
log.error("tcp_client_sender.proc", "buffer is full, wait for the next time")
table.insert(self_send_queue, 1, send_item)
return true
end
log.info("tcp_client_sender.proc", "send success")
-- 发送成功,如果当前发送的数据有用户回调函数,则执行用户回调函数
if send_item.cb and send_item.cb.func then
send_item.cb.func(true, send_item.cb.para)
end
-- 发送成功,通知网络环境检测看门狗功能模块进行喂狗
sys.publish("FEED_NETWORK_WATCHDOG")
end
return true
end
--[[
socket client连接出现异常时
@api tcp_client_sender.exception_proc()
@usage
tcp_client_sender.exception_proc()
]]
function tcp_client_sender.exception_proc()
-- 遍历数据发送队列self_send_queue
while #self_send_queue>0 do
local send_item = table.remove(self_send_queue,1)
-- 发送失败,如果当前发送的数据有用户回调函数,则执行用户回调函数
if send_item.cb and send_item.cb.func then
send_item.cb.func(false, send_item.cb.para)
end
end
end
-- 订阅"SEND_DATA_REQ"消息;
-- 其他应用模块如果需要发送数据,直接sys.publish这个消息即可,将需要发送的数据以及回调函数和回调参数一起publish出去;
-- 本demo项目中uart_app.lua和timer_app.lua中publish了这个消息;
sys.subscribe("SELF_SEND_DATA_REQ", send_data_req_proc_func)
return tcp_client_sender
@@ -0,0 +1,129 @@
--[[
@module update
@summary
@version 1.0
@date 2025.08.12
@author
@usage
1
2fota2模块
3fota2模块的参数
4fota2模块的升级函数
5
]]
libfota2 = require "libfota2"
-- 循环打印版本号, 方便看版本号变化, 非必须
function get_version()
log.info("fota", "脚本版本号", VERSION, "core版本号", rtos.version())
end
sys.timerLoopStart(get_version, 3000)
-- 升级结果的回调函数
-- 功能:获取fota的回调函数
-- 参数:
-- result:number类型
-- 0表示成功
-- 1表示连接失败
-- 2表示url错误
-- 3表示服务器断开
-- 4表示接收报文错误
-- 5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
local function fota_cb(ret)
log.info("fota", ret)
if ret == 0 then
log.info("升级包下载成功,重启模块")
rtos.reboot()
elseif ret == 1 then
log.info("连接失败", "请检查url拼写或服务器配置(是否为内网)")
elseif ret == 2 then
log.info("url错误", "检查url拼写")
elseif ret == 3 then
log.info("服务器断开", "检查服务器白名单配置")
elseif ret == 4 then
log.error("FOTA 失败",
"原因可能有:\n" ..
"1) 服务器返回 200/206 但报文体为空(0 字节)—— 通常是升级包文件缺失或 URL 指向空文件;\n" ..
"2) 服务器返回 4xx/5xx 等异常状态码 —— 请确认升级包已上传、URL 正确、鉴权信息有效;\n"..
"3) 已经是最新版本,无需升级" )
elseif ret == 5 then
log.info("版本号书写错误", "iot平台版本号需要使用xxx.yyy.zzz形式")
else
log.info("不是上面几种情况 ret为", ret)
end
end
-- 使用第三方服务器,配置ota_opts参数
--[[
-- opts参数说明, 所有参数都是可选的
-- 1. opts.url string 升级所需要的URL, 若使用合宙iot平台,则不需要填
-- 2. opts.version string 版本号, 默认是 BSP版本号.x.z格式
-- 3. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
-- 4. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
-- 5. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
-- 6. opts.firmware_name string 固件名称,默认是 _G.PROJECT.. "_LuatOS-SoC_" .. rtos.bsp()
-- 7. opts.server_cert string 服务器证书, 默认不使用
-- 8. opts.client_cert string 客户端证书, 默认不使用
-- 9. opts.client_key string 客户端私钥, 默认不使用
-- 10. opts.client_password string 客户端私钥口令, 默认不使用
-- 11. opts.method string 请求方法, 默认是GET
-- 12. opts.headers table 额外添加的请求头,默认不需要
-- 13. opts.body string 额外添加的请求body,默认不需要
]]
local opts = {
-- 使用的是自定义的URL,此处写的URL是不存在的一个无效URL,自己测试时,需要替换为自己的有效URL
url = "###http://your_fota_packet_url.bin",
-- 合宙IOT平台的默认升级URL, 不填就是这个默认值
-- 如果是自建的OTA服务器, 则需要填写正确的URL, 例如 http://192.168.1.5:8000/update
-- 如果自建OTA服务器,且url包含全部参数,不需要额外添加参数, 请在url前面添加 ###
-- 如果不加###,则默认会上传如下参数
-- 1. opts.version string 版本号, 默认是 BSP版本号.x.z格式
-- 2. opts.timeout int 请求超时时间, 默认300000毫秒,单位毫秒
-- 3. opts.project_key string 合宙IOT平台的项目key, 默认取全局变量PRODUCT_KEY. 自建服务器不用填
-- 4. opts.imei string 设备识别码, 默认取IMEI(Cat.1模块)或WLAN MAC地址(wifi模块)或MCU唯一ID
-- 5. opts.firmware_name string 底层版本号
-- 请求的版本号, 合宙IOT有一套版本号体系,不传就是合宙规则, 自建服务器的话当然是自行约定版本号了
-- version = ""
-- 其他更多参数, 请查阅libfota2的文档 https://wiki.luatos.com/api/libs/libfota2.html
}
function fota_task_func()
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("fota_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 检测到了IP_READY消息
log.info("fota_task_func", "recv IP_READY", socket.dft())
----这个判断是提醒要设置url的,且不要使用本文中的测试服务器,实际生产请删除
if not opts.url or string.find(opts.url,"airtest.openluat.com") then
while true do
sys.wait(1000)
log.info("fota", "当前URL",opts.url,"请修改正确的url")
end
end
log.info("开始检查升级")
libfota2.request(fota_cb, opts)
end
--创建并且启动一个task
--运行这个task的主函数fota_task_func
sys.taskInit(fota_task_func)
-- 演示定时自动升级, 每隔4小时自动检查一次
sys.timerLoopStart(libfota2.request, 4 * 3600000, fota_cb, opts)
@@ -0,0 +1,104 @@
--[[
@module fota3_auto
@summary libfota3 UI自动升级功能模块
@version 1.0
@date 2026.08.24
@author
@ 使
@usage
使libfota3扩展库实现无UI设备的自动FOTA升级功能
1使 on_confirm
2使 on_status
324
- DTU
-
]]
-- 项目密钥:在 iot.openluat.com 平台主页面上方导航栏 Turnkey 页面中获取,仅支持合宙内部使用
local PRODUCT_KEY = "your_project_key_here"
-- 加载libfota3扩展库
local libfota3 = require "libfota3"
-- 循环打印版本号, 方便看版本号变化, 非必须
local function print_version()
log.info("fota3", "脚本版本号", VERSION, "core版本号", rtos.version())
end
sys.timerLoopStart(print_version, 3000)
-- 启动FOTA自动升级任务
local function fota3_auto_task()
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("fota3_auto", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 检测到了IP_READY消息
log.info("fota3_auto", "recv IP_READY", socket.dft())
-- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
if "your_project_key_here" == _G.PRODUCT_KEY then
while true do
sys.wait(1000)
log.info("fota3", "请修改正确的PRODUCT_KEY")
end
end
-- 启动 FOTA 自动升级
libfota3.request({
-- 项目密钥:在 iot.openluat.com 平台主页面上方导航栏 Turnkey 页面中获取,仅支持合宙内部使用
project_key = PRODUCT_KEY,
-- 脚本名称:与项目名保持一致即可
script_name = PROJECT,
-- 脚本版本:与 VERSION 保持一致
script_version = VERSION,
-- 启用自动定时检测
auto = true,
-- 自动检测间隔:24小时(86400秒)
-- 可根据需求调整,建议不要太频繁
interval = 86400,
-- 状态回调:记录升级状态日志
on_status = function(status, msg, percent)
if status == "downloading" then
-- 下载进度:percent 为 0-100
log.info("fota3", string.format("[%s] %s %d%%", status, msg, percent or 0))
else
-- 其他状态
log.info("fota3", string.format("[%s] %s", status, msg))
end
end,
-- 注意:无UI设备不需要 on_confirm 回调
-- libfota3 检测到新版本后会自动下载并重启
})
end
-- 初始化FOTA任务
sys.taskInit(fota3_auto_task)
-- 演示手动触发升级检测,可以根据需求打开
-- sys.timerLoopStart(function()
-- libfota3.check_update()
-- end, 4 * 3600000) -- 每4小时手动检测一次
@@ -0,0 +1,69 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口
@version 1.0
@date 2026.08.24
@author
@ 使
@usage
demo演示的核心功能为
使libfota3扩展库进行FOTA远程升级DTU
1使 on_confirm
24G//
readme.md文件
]]
--[[
PROJECT和VERSION变量Luatools工具会用到这两个变量
PROJECTascii string类型
便使,
VERSIONascii string类型
使iot.openluat.com进行远程升级"XXX.YYY.ZZZ"
XYZ各表示1位数字X表示的数字可以相同Y和三个Z表示的数字也是可以相同
YYY这三位数字必须存在999
使iot.openluat.com进行远程升级
]]
PROJECT = "FOTA3_NO_UI"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 加载网络驱动设备功能模块
require "netdrv_device"
-- 加载无UI自动升级功能模块
require "fota3_auto"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,43 @@
--[[
@module netdrv_4g
@summary "4G网卡"
@version 1.0
@date 2026.08.24
@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,78 @@
--[[
@module netdrv_eth_spi
@summary "通过SPI外挂CH390H芯片的以太网卡"
@version 1.0
@date 2026.08.24
@author
@usage
"通过SPI外挂CH390H芯片的以太网卡"
1CH390H芯片供电开关
2spi0DHCP()
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,89 @@
--[[
@module netdrv_multiple
@summary 4G网卡SPI外挂CH390H芯片的以太网卡
@version 1.0
@date 2026.08.24
@author
@usage
1exnetif.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,34 @@
--[[
@module netdrv_device
@summary
@version 1.0
@date 2026.08.24
@author
@usage
()
1netdrv_4gsocket.LWIP_GP4G网卡
2netdrv_eth_spisocket.LWIP_USER1SPI外挂CH390H芯片的以太网卡
3netdrv_multiple使
4netdrv_pcsocket.ETH0PC模拟器网卡
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,211 @@
--[[
@module fota3_ui
@summary libfota3 UI确认升级功能模块
@version 1.0
@date 2026.08.24
@author
@ 使
@usage
使libfota3扩展库实现有UI设备的FOTA升级功能
1使 on_confirm
2
3
4
-
-
]]
-- 项目密钥:在 iot.openluat.com 平台主页面上方导航栏 Turnkey 页面中获取,仅支持合宙内部使用
local PRODUCT_KEY = "your_project_key_here"
-- 加载libfota3扩展库
local libfota3 = require "libfota3"
-- 循环打印版本号, 方便看版本号变化, 非必须
local function print_version()
log.info("fota3", "脚本版本号", VERSION, "core版本号", rtos.version())
end
sys.timerLoopStart(print_version, 3000)
-- ============================================================
-- UI 相关函数(需要根据实际硬件平台实现)
-- ============================================================
-- 存储用户确认结果
local user_confirm_result = nil
--[[
@function show_confirm_dialog
@summary UI框架实现
@param title string
@param message string
@param on_confirm function 确认回调 true/false
@return
@description
UI框架实现
使
- AirUI
- GUI框架的对话框API
1. "确认""取消"
2. on_confirm(true) on_confirm(false)
]]
local function show_confirm_dialog(title, message, on_confirm)
-- ========================================
-- 以下是模拟实现,实际项目请替换为真实UI代码
-- ========================================
log.info("ui", "========================================")
log.info("ui", title)
log.info("ui", message)
log.info("ui", "========================================")
-- 模拟用户确认(3秒后自动确认)
-- 实际项目中应等待用户点击按钮
sys.taskInit(function()
log.info("ui", "模拟用户确认:3秒后自动确认...")
sys.wait(3000)
-- 模拟用户点击"确认"按钮
on_confirm(true)
end)
end
--[[
@function show_progress
@summary UI框架实现
@param percent number 0-100
@param message string
@return
@description
UI框架实现
使UI
]]
local function show_progress(percent, message)
-- ========================================
-- 以下是模拟实现,实际项目请替换为真实UI代码
-- ========================================
log.info("ui", string.format("进度: %d%% - %s", percent, message))
end
-- 启动FOTA带UI确认升级任务
local function fota3_ui_task()
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("fota3_ui", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 检测到了IP_READY消息
log.info("fota3_ui", "recv IP_READY", socket.dft())
-- 这个判断是提醒要设置PRODUCT_KEY的,实际生产请删除
if "your_project_key_here" == _G.PRODUCT_KEY then
while true do
sys.wait(1000)
log.info("fota3", "请修改正确的PRODUCT_KEY")
end
end
-- 启动 FOTA 升级(带UI确认)
libfota3.request({
-- 项目密钥:在 iot.openluat.com 平台主页面上方导航栏 Turnkey 页面中获取,仅支持合宙内部使用
project_key = PRODUCT_KEY,
-- 脚本名称:与项目名保持一致即可
script_name = PROJECT,
-- 脚本版本:与 VERSION 保持一致
script_version = VERSION,
-- 启用自动定时检测
auto = true,
-- 自动检测间隔:24小时(86400秒)
interval = 86400,
-- 状态回调:更新UI显示
on_status = function(status, msg, percent)
if status == "checking" then
-- 正在检测更新
show_progress(0, "正在检测更新...")
elseif status == "new_version" then
-- 发现新版本:msg 包含版本信息
log.info("fota3", "发现新版本:", msg)
elseif status == "downloading" then
-- 下载进度:percent 为 0-100
show_progress(percent or 0, msg)
elseif status == "download_done" then
-- 下载完成
show_progress(100, "下载完成")
elseif status == "download_fail" then
-- 下载失败
log.error("fota3", "下载失败:", msg)
elseif status == "rebooting" then
-- 正在重启
show_progress(100, "正在重启升级...")
else
-- 其他状态
log.info("fota3", string.format("[%s] %s", status, msg))
end
end,
-- 确认回调:显示UI让用户确认
on_confirm = function(action, info, callback)
if action == "download" then
-- 确认下载:info 包含 version, size, fota_sn
local version = info and info.version or "未知"
local size = info and info.size or 0
local size_kb = math.floor(size / 1024)
local message = string.format(
"发现新版本: %s\n大小: %d KB\n\n是否下载?",
version, size_kb
)
-- 显示确认对话框
show_confirm_dialog("固件更新", message, function(ok)
log.info("fota3", "用户" .. (ok and "确认" or "取消") .. "下载")
callback(ok)
end)
elseif action == "reboot" then
-- 确认重启
show_confirm_dialog("升级确认", "下载完成,是否立即重启升级?", function(ok)
log.info("fota3", "用户" .. (ok and "确认" or "取消") .. "重启")
callback(ok)
end)
end
end,
})
end
-- 初始化FOTA任务
sys.taskInit(fota3_ui_task)
-- 演示手动触发升级检测,可以根据需求打开
-- sys.timerLoopStart(function()
-- libfota3.check_update()
-- end, 4 * 3600000) -- 每4小时手动检测一次
@@ -0,0 +1,72 @@
--[[
@module main
@summary LuatOS用户应用脚本文件入口
@version 1.0
@date 2026.08.24
@author
@ 使
@usage
demo演示的核心功能为
使libfota3扩展库进行FOTA远程升级UI确认升级场景
1使 on_confirm
2
3
4
54G//
readme.md文件
]]
--[[
PROJECT和VERSION变量Luatools工具会用到这两个变量
PROJECTascii string类型
便使,
VERSIONascii string类型
使iot.openluat.com进行远程升级"XXX.YYY.ZZZ"
XYZ各表示1位数字X表示的数字可以相同Y和三个Z表示的数字也是可以相同
YYY这三位数字必须存在999
使iot.openluat.com进行远程升级
]]
PROJECT = "FOTA3_WITH_UI"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
-- errDump.config(true, 600)
-- end
-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
-- log.info("mem.lua", rtos.meminfo())
-- log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)
-- 加载网络驱动设备功能模块
require "netdrv_device"
-- 加载有UI确认升级功能模块
require "fota3_ui"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,43 @@
--[[
@module netdrv_4g
@summary "4G网卡"
@version 1.0
@date 2026.08.24
@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,78 @@
--[[
@module netdrv_eth_spi
@summary "通过SPI外挂CH390H芯片的以太网卡"
@version 1.0
@date 2026.08.24
@author
@usage
"通过SPI外挂CH390H芯片的以太网卡"
1CH390H芯片供电开关
2spi0DHCP()
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,89 @@
--[[
@module netdrv_multiple
@summary 4G网卡SPI外挂CH390H芯片的以太网卡
@version 1.0
@date 2026.08.24
@author
@usage
1exnetif.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,34 @@
--[[
@module netdrv_device
@summary
@version 1.0
@date 2026.08.24
@author
@usage
()
1netdrv_4gsocket.LWIP_GP4G网卡
2netdrv_eth_spisocket.LWIP_USER1SPI外挂CH390H芯片的以太网卡
3netdrv_multiple使
4netdrv_pcsocket.ETH0PC模拟器网卡
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,127 @@
# FOTA 与 FOTA2 选择指南
## 核心区别总结
### fota(底层核心库)
定位: 基础升级,提供最核心的固件写入能力
**核心能力:**
支持两种写入方式:fota.run() 分段写入 和 fota.file() 文件直接升级
支持内部存储和外部SPI Flash
提供完整的升级流程控制:init → wait → run/file → isDone → finish
代码特点:
```lua
-- 需要手动控制每个步骤
fota.init()
while not fota.wait() do sys.wait(100) end
fota.run(buf) -- 或 fota.file("/update.bin")
-- 自行检查状态和重启
```
### fota2libfota2扩展库)
定位: 完整的远程升级解决方案,开箱即用
**核心能力:**
自动处理HTTP/HTTPS网络下载
支持合宙IoT平台和自建服务器
内置版本检查、下载、验证全流程
提供详细错误码和回调函数
代码特点:
```lua
-- 一行代码完成升级
local function fota_cb(ret)
if ret == 0 then
log.info("升级包下载成功,重启模块")
rtos.reboot()
end
end
libfota2.request(fota_cb, opts)
```
## 适用场景推荐
### 选择 fota 的情况:
#### 需要自定义升级数据源
通过串口接收升级包
通过MQTT、TCP等自定义协议传输
从SD卡、U盘等外部存储读取
#### 对升级流程有特殊控制需求
需要在升级前后执行特定操作
需要精细控制数据写入时机
需要自定义进度监控逻辑
#### 资源极度受限环境
设备存储空间极小,内存紧张,无法加载额外库
#### 开发测试阶段
需要调试升级过程的每个环节
需要验证自定义升级方案
### 选择 fota2 的情况:
#### 标准的HTTP远程升级
从服务器下载升级包
使用合宙IoT平台服务
需要HTTPS安全下载
#### 希望快速实现升级功能
不想处理网络下载细节
需要自动版本检查
希望简单的错误处理
#### 生产环境部署
需要稳定的远程升级方案
需要详细的升级状态反馈
支持定时自动检查更新
## 实际选择建议
### 新手用户 → 直接选择 fota2
接口简单,学习成本低
内置完整错误处理
适合大多数物联网应用场景
### 高级用户 → 根据需求选择
标准网络升级 → fota2
自定义数据传输 → fota + 自定义逻辑
一句话总结:**用 fota2 省心省力,用 fota 自由灵活**