工程提交

This commit is contained in:
2026-03-31 15:46:04 +08:00
parent 75f512a5b4
commit da4e944bca
2841 changed files with 4822938 additions and 1 deletions

Binary file not shown.

View File

@@ -0,0 +1,60 @@
--[[
@module air153C_wtd
@summary 添加软件看门狗功能,防止死机
@data 2023.5.23
@author 翟科研
@usage
--local air153C_wtd = require ("air153C_wtd")
-- 用法实例
-- sys.taskInit(function ()
-- air153C_wtd.init(28)
-- air153C_wtd.feed_dog(28,10)--28为看门狗引脚10为设置喂狗时间
-- --air153C_wtd.set_time(1)--开启定时模式再打开此代码,否则无效
-- end)
]]
local sys = require "sys"
_G.sysplus = require("sysplus")
air153C_wtd={}
--[[
初始化引脚
@api air153C_wtd.init(watchdogPin)
@int 看门狗控制引脚
@return nil 无返回值
@usage
air153C_wtd.init(28)
]]
function air153C_wtd.init(watchdogPin)
gpio.setup(watchdogPin,0,gpio.PULLDOWN)
gpio.set(watchdogPin,0)
end
function air153C_wtd.callback(watchdogPin)
gpio.set(watchdogPin,0)
end
--[[
调用此函数进行喂狗
@api air153C_wtd.feed_dog(watchdogPin)
@int watchdogPin设置看门狗控制引脚
@return nil 无返回值
@usage
air153C_wtd.feed_dog(28)
]]
function air153C_wtd.feed_dog(watchdogPin)
local watchdogFeedDuration = 400
gpio.set(watchdogPin,1)
sys.timerStart(air153C_wtd.callback,watchdogFeedDuration,watchdogPin)
end
--[[
调用此函数关闭喂狗,谨慎使用!
@api air153C_wtd.close_watch_dog(watchdogPin)
@int watchdogPin设置看门狗控制引脚
@return nil 无返回值
@usage
air153C_wtd.close_watch_dog(28)
]]
function air153C_wtd.close_watch_dog(watchdogPin)
local watchdogStopDuration = 700
gpio.set(watchdogPin,1)
sys.timerStart(air153C_wtd.callback,watchdogStopDuration,watchdogPin)
end
return air153C_wtd

View File

@@ -0,0 +1,239 @@
--[[
@module airlbs
@summary airlbs 定位服务(收费服务,需自行联系销售申请)
@version 1.1
@date 2024.12.30
@author Dozingfiretruck
@usage
-- lbsloc 是异步回调接口,
-- lbsloc2 是是同步接口。
-- lbsloc比lbsloc2多了一个请求地址文本的功能。
-- lbsloc 和 lbsloc2 都是免费LBS定位的实现方式
-- airlbs 扩展库是收费 LBS 的实现方式。
]]
sys = require("sys")
sysplus = require("sysplus")
libnet = require "libnet"
local airlbs_host = "airlbs.openluat.com"
local airlbs_port = 12413
local lib_name = "airlbs"
local lib_topic = lib_name .. "topic"
local location_data = 0
local disconnect = -1
local airlbs_timeout = 15000
local airlbs = {}
local function airlbs_task(task_name, buff, timeout, adapter)
local netc = socket.create(nil, lib_name)
socket.config(netc, nil, true) -- udp
sysplus.cleanMsg(lib_name)
local result = libnet.connect(lib_name, 15000, netc, airlbs_host, airlbs_port)
if result then
log.info(lib_name, "服务器连上了")
libnet.tx(lib_name, 0, netc, buff)
else
log.info(lib_name, "服务器没连上了!!!")
sys.publish(lib_topic, disconnect)
libnet.close(lib_name, 5000, netc)
return
end
buff:del()
while result do
local succ, param = socket.rx(netc, buff)
if not succ then
log.error(lib_name, "服务器断开了", succ, param)
sys.publish(lib_topic, disconnect)
break
end
if buff:used() > 0 then
local location = nil
local data = buff:query(0, 1) -- 获取数据
if data:toHex() == '00' then
location = json.decode(buff:query(1))
else
log.error(lib_name, "not json data")
end
sys.publish(lib_topic, location_data, location)
buff:del()
break
end
result, param, param2 = libnet.wait(lib_name, timeout, netc)
log.info(lib_name, "wait", result, param, param2)
if param == false then
log.error(lib_name, "服务器断开了", succ, param)
sys.publish(lib_topic, disconnect)
break
end
end
libnet.close(lib_name, 5000, netc)
end
-- 处理未识别的网络消息
local function netCB(msg)
log.info("未处理消息", msg[1], msg[2], msg[3], msg[4])
end
--[[
获取定位数据
@api airlbs.request(param)
@param table 参数(联系销售获取id与key) project_id:项目ID project_key:项目密钥 timeout:超时时间,单位毫秒 默认15000 adapter: 网络适配器id,可选,默认是平台自带的网络协议栈
@return bool 成功返回true,失败会返回false
@return table 定位成功生效,成功返回定位数据
@usage
--注意:函数内因使用了sys.waitUntil阻塞接口所以api需要在协程中使用
--注意:使用前需同步时间
local airlbs = require "airlbs"
sys.taskInit(function()
-- 等待网络就绪
sys.waitUntil("IP_READY")
-- 执行时间同步
socket.sntp()
sys.waitUntil("NTP_UPDATE", 10000)
while 1 do
-- airlbs请求定位
local result ,data = airlbs.request({
project_id = airlbs_project_id,
project_key = airlbs_project_key,
timeout = 10000,
adapter = socket.LWIP_STA
})
if result then
log.info("airlbs", json.encode(data))
end
sys.wait(20000)
end
end)
]]
function airlbs.request(param)
if not param or param.project_id == nil or param.project_key == nil then
log.error(lib_name, "param error")
return false
end
if not mobile and not param.wifi_info then
log.error(lib_name, "no mobile and no wifi_info")
return false
end
local udp_buff = zbuff.create(1500)
local auth_type = 0x01
local lbs_data_type = 0x00
local project_id = param.project_id
if project_id:len() ~= 6 then
log.error("airlbs", "project_id len not 6")
end
local mac1 = netdrv.mac(socket.LWIP_STA)
local mac = "MAC" .. mac1
log.info("mac", mac)
local timestamp = os.time()
local project_key = param.project_key
local nonce = crypto.trng(6)
local hmac_data
local bsp = rtos.bsp()
log.info("硬件型号", rtos.bsp())
if bsp == "Air8101" then
-- 此处由于目前属于测试阶段先将muid写死后续会进行修改
-- local muid = mcu.muid() or ""
local muid = "12345678901234567890123456789012"
log.info("muid", muid)
hmac_data = crypto.hmac_sha1(project_id .. mac .. muid .. timestamp .. nonce, project_key)
else
local imei = mobile and mobile.imei() or ""
local muid = mobile and mobile.muid() or ""
hmac_data = crypto.hmac_sha1(project_id .. imei .. muid .. timestamp .. nonce, project_key)
end
-- log.debug(lib_name,"hmac_sha1", hmac_data)
local lbs_data = {}
if mobile then
mobile.reqCellInfo(60)
sys.waitUntil("CELL_INFO_UPDATE", param.timeout or airlbs_timeout)
lbs_data.cells = {}
-- log.info("cell", json.encode(mobile.getCellInfo()))
for k, v in pairs(mobile.getCellInfo()) do
lbs_data.cells[k] = {}
lbs_data.cells[k][1] = v.mcc
lbs_data.cells[k][2] = v.mnc
lbs_data.cells[k][3] = v.tac
lbs_data.cells[k][4] = v.cid
lbs_data.cells[k][5] = v.rssi or v.rsrp
lbs_data.cells[k][6] = v.snr
lbs_data.cells[k][7] = v.pci
lbs_data.cells[k][8] = v.rsrp
lbs_data.cells[k][9] = v.rsrq
lbs_data.cells[k][10] = v.earfcn
end
end
if param.wifi_info and #param.wifi_info > 0 then
lbs_data.macs = {}
for k, v in pairs(param.wifi_info) do
lbs_data.macs[k] = {}
lbs_data.macs[k][1] = v.bssid:toHex():gsub("(%x%x)", "%1:"):sub(1, -2)
lbs_data.macs[k][2] = v.rssi
end
end
local lbs_jdata = json.encode(lbs_data)
log.info("扫描出的数据",lbs_jdata)
local bsp = rtos.bsp()
if bsp == "Air8101" then
-- 此处由于目前属于测试阶段先将muid写死后续会进行修改
-- local muid = mcu.muid() or ""
local muid = "12345678901234567890123456789012"
udp_buff:write(string.char(auth_type) .. project_id .. mac .. muid .. timestamp .. nonce .. hmac_data:fromHex() .. string.char(lbs_data_type) .. lbs_jdata)
else
local imei = mobile and mobile.imei() or ""
local muid = mobile and mobile.muid() or ""
udp_buff:write(string.char(auth_type) .. project_id .. imei .. muid .. timestamp .. nonce .. hmac_data:fromHex() .. string.char(lbs_data_type) .. lbs_jdata)
end
sysplus.taskInitEx(airlbs_task, lib_name, netCB, lib_name, udp_buff, param.timeout or airlbs_timeout, param.adapter)
while 1 do
local result, tp, data = sys.waitUntil(lib_topic, param.timeout or airlbs_timeout)
log.info("定位请求的结果", result, "超时时间", tp, data)
if not result then
return false, "timeout"
elseif tp == location_data then
if not data then
log.error(lib_name, "无数据, 请检查project_id和project_key")
return false
-- data.result 0-找不到 1-成功 2-qps超限 3-欠费 4-其他错误
elseif data.result == 0 then
log.error(lib_name, "no location(基站定位服务器查询当前地址失败)")
return false
elseif data.result == 1 then
log.info("多基站请求成功,服务器返回的原始数据", data)
return true, {
lng = data.lng,
lat = data.lat
}
elseif data.result == 2 then
log.error(lib_name, "qps limit(当前请求已到达限制,请检查当前请求是否过于频繁))")
return false
elseif data.result == 3 then
log.error(lib_name, "当前设备已欠费,请联系销售充值")
return false
elseif data.result == 4 then
log.error(lib_name, "other error")
return false
else
log.error("其他错误,错误码", data.result, lib_name)
end
else
log.error(lib_name, "net error")
return false
end
end
end
return airlbs

View File

@@ -0,0 +1,26 @@
local config = {
mode = 0,
is_msb = 0,
rx_bit = 1,
seq_type = 0,
is_ddr = 0,
i2c_slave_addr = 0x6e,
width = 240,
height = 320,
init_cmds = {{0xf2, 0x01}, {0xcf, 0xb0}, {0x12, 0x20}, {0x15, 0x80}, {0x6b, 0x71}, {0x00, 0x40}, {0x04, 0x00},
{0x06, 0x26}, {0x08, 0x07}, {0x1c, 0x12}, {0x20, 0x20}, {0x21, 0x20}, {0x34, 0x02}, {0x35, 0x02},
{0x36, 0x21}, {0x37, 0x13}, {0xca, 0x23}, {0xcb, 0x22}, {0xcc, 0x89}, {0xcd, 0x4c}, {0xce, 0x6b},
{0xa0, 0x8e}, {0x01, 0x1b}, {0x02, 0x1d}, {0x13, 0x08}, {0x87, 0x13}, {0x8b, 0x08}, {0x70, 0x17},
{0x71, 0x43}, {0x72, 0x0a}, {0x73, 0x62}, {0x74, 0xa2}, {0x75, 0xbf}, {0x76, 0x00}, {0x77, 0xcc},
{0x40, 0x32}, {0x41, 0x28}, {0x42, 0x26}, {0x43, 0x1d}, {0x44, 0x1a}, {0x45, 0x14}, {0x46, 0x11},
{0x47, 0x0f}, {0x48, 0x0e}, {0x49, 0x0d}, {0x4B, 0x0c}, {0x4C, 0x0b}, {0x4E, 0x0a}, {0x4F, 0x09},
{0x50, 0x09}, {0x24, 0x30}, {0x25, 0x36}, {0x80, 0x00}, {0x81, 0x20}, {0x82, 0x40}, {0x83, 0x30},
{0x84, 0x50}, {0x85, 0x30}, {0x86, 0xd8}, {0x89, 0x45}, {0x8a, 0x33}, {0x8f, 0x81}, {0x91, 0xff},
{0x92, 0x08}, {0x94, 0x82}, {0x95, 0xfd}, {0x9a, 0x20}, {0x9e, 0xbc}, {0xf0, 0x87}, {0x51, 0x06},
{0x52, 0x25}, {0x53, 0x2b}, {0x54, 0x0f}, {0x57, 0x2a}, {0x58, 0x22}, {0x59, 0x2c}, {0x23, 0x33},
{0xa1, 0x93}, {0xa2, 0x0f}, {0xa3, 0x2a}, {0xa4, 0x08}, {0xa5, 0x26}, {0xa7, 0x80}, {0xa8, 0x80},
{0xa9, 0x1e}, {0xaa, 0x19}, {0xab, 0x18}, {0xae, 0x50}, {0xaf, 0x04}, {0xc8, 0x10}, {0xc9, 0x15},
{0xd3, 0x0c}, {0xd4, 0x16}, {0xee, 0x06}, {0xef, 0x04}, {0x55, 0x34}, {0x56, 0x9c}, {0xb1, 0x98},
{0xb2, 0x98}, {0xb3, 0xc4}, {0xb4, 0x0c}, {0xa0, 0x8f}, {0x13, 0x07}}
}
return config

View File

@@ -0,0 +1,395 @@
--[[
@module dhcpsrv
@summary DHCP服务器端
@version 1.0.0
@date 2025.04.15
@author wendal
@usage
-- 参考dhcpsrv.create函数
]]
local dhcpsrv = {}
local udpsrv = require("udpsrv")
local TAG = "dhcpsrv"
----
-- 参考地址
-- https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol
local function dhcp_decode(buff)
-- buff:seek(0)
local dst = {}
-- 开始解析dhcp
dst.op = buff[0]
dst.htype = buff[1]
dst.hlen = buff[2]
dst.hops = buff[3]
buff:seek(4)
dst.xid = buff:read(4)
_, dst.secs = buff:unpack(">H")
_, dst.flags = buff:unpack(">H")
dst.ciaddr = buff:read(4)
dst.yiaddr = buff:read(4)
dst.siaddr = buff:read(4)
dst.giaddr = buff:read(4)
dst.chaddr = buff:read(16)
-- 跳过192字节
buff:seek(192, zbuff.SEEK_CUR)
-- 解析magic
_, dst.magic = buff:unpack(">I")
-- 解析option
local opt = {}
while buff:len() > buff:used() do
local tag = buff:read(1):byte()
if tag ~= 0 then
local len = buff:read(1):byte()
if tag == 0xFF or len == 0 then
break
end
local data = buff:read(len)
if tag == 53 then
-- 53: DHCP Message Type
dst.msgtype = data:byte()
end
table.insert(opt, {tag, data})
-- log.info(TAG, "tag", tag, "data", data:toHex())
end
end
if dst.msgtype == nil then
return -- 没有解析到msgtype直接返回
end
dst.opts = opt
return dst
end
local function dhcp_buff2ip(buff)
return string.format("%d.%d.%d.%d", buff:byte(1), buff:byte(2), buff:byte(3), buff:byte(4))
end
local function dhcp_print_pkg(pkg)
log.info(TAG, "XID", pkg.xid:toHex())
log.info(TAG, "secs", pkg.secs)
log.info(TAG, "flags", pkg.flags)
log.info(TAG, "chaddr", pkg.chaddr:sub(1, pkg.hlen):toHex())
log.info(TAG, "yiaddr", dhcp_buff2ip(pkg.yiaddr))
log.info(TAG, "siaddr", dhcp_buff2ip(pkg.siaddr))
log.info(TAG, "giaddr", dhcp_buff2ip(pkg.giaddr))
log.info(TAG, "ciaddr", dhcp_buff2ip(pkg.ciaddr))
log.info(TAG, "magic", string.format("%08X", pkg.magic))
for _, opt in pairs(pkg.opts) do
if opt[1] == 53 then
log.info(TAG, "msgtype", opt[2]:byte())
elseif opt[1] == 60 then
log.info(TAG, "auth", opt[2])
elseif opt[1] == 57 then
log.info(TAG, "Maximum DHCP message size", opt[2]:byte() * 256 + opt[2]:byte(2))
elseif opt[1] == 61 then
log.info(TAG, "Client-identifier", opt[2]:toHex())
elseif opt[1] == 55 then
log.info(TAG, "Parameter request list", opt[2]:toHex())
elseif opt[1] == 12 then
log.info(TAG, "Host name", opt[2])
-- elseif opt[1] == 58 then
-- log.info(TAG, "Renewal (T1) time value", opt[2]:unpack(">I"))
end
end
end
local function dhcp_encode(pkg, buff)
-- 合成DHCP包
buff:seek(0)
buff[0] = pkg.op
buff[1] = pkg.htype
buff[2] = pkg.hlen
buff[3] = pkg.hops
buff:seek(4)
-- 写入XID
buff:write(pkg.xid)
-- 几个重要的参数
buff:pack(">H", pkg.secs)
buff:pack(">H", pkg.flags)
buff:write(pkg.ciaddr)
buff:write(pkg.yiaddr)
buff:write(pkg.siaddr)
buff:write(pkg.giaddr)
-- 写入MAC地址
buff:write(pkg.chaddr)
-- 跳过192字节
buff:seek(192, zbuff.SEEK_CUR)
-- 写入magic
buff:pack(">I", pkg.magic)
-- 写入option
for _, opt in pairs(pkg.opts) do
buff:write(opt[1])
buff:write(#opt[2])
buff:write(opt[2])
end
buff:write(0xFF, 0x00)
end
----
local function dhcp_send_x(srv, pkg, client, msgtype)
local buff = zbuff.create(300)
pkg.op = 2
pkg.ciaddr = "\0\0\0\0"
pkg.yiaddr = string.char(srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], client.ip)
pkg.siaddr = string.char(srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], srv.opts.gw[4])
pkg.giaddr = "\0\0\0\0"
pkg.secs = 0
pkg.opts = {} -- 复位option
table.insert(pkg.opts, {53, string.char(msgtype)})
table.insert(pkg.opts, {1, string.char(srv.opts.mark[1], srv.opts.mark[2], srv.opts.mark[3], srv.opts.mark[4])})
table.insert(pkg.opts, {3, string.char(srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], srv.opts.gw[4])})
table.insert(pkg.opts, {51, "\x00\x00\x1E\x00"}) -- 7200秒, 大概
table.insert(pkg.opts, {54, string.char(srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], srv.opts.gw[4])})
table.insert(pkg.opts, {6, string.char(223, 5, 5, 5)})
table.insert(pkg.opts, {6, string.char(119, 29, 29, 29)})
table.insert(pkg.opts, {6, string.char(srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], srv.opts.gw[4])})
dhcp_encode(pkg, buff)
local dst = "255.255.255.255"
if 4 == msgtype then
dst = string.format("%d.%d.%d.%d", srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], client.ip)
end
-- log.info(TAG, "发送", msgtype, dst, buff:query():toHex())
srv.udp:send(buff, dst, 68)
end
local function dhcp_send_offer(srv, pkg, client)
dhcp_send_x(srv, pkg, client, 2)
end
local function dhcp_send_ack(srv, pkg, client)
dhcp_send_x(srv, pkg, client, 5)
end
local function dhcp_send_nack(srv, pkg, client)
dhcp_send_x(srv, pkg, client, 6)
end
local function dhcp_handle_discover(srv, pkg)
local mac = pkg.chaddr:sub(1, pkg.hlen)
-- 看看是不是已经分配了ip
for _, client in pairs(srv.clients) do
if client.mac == mac then
log.info(TAG, "发现已经分配的mac地址, send offer")
dhcp_send_offer(srv, pkg, client)
return
end
end
-- TODO 清理已经过期的IP分配记录
-- 分配一个新的ip
if #srv.clients >= (srv.opts.ip_end - srv.opts.ip_start) then
log.info(TAG, "没有可分配的ip了")
return
end
local ip = nil
for i = srv.opts.ip_start, srv.opts.ip_end, 1 do
if srv.clients[i] == nil then
ip = i
break
end
end
if ip == nil then
log.info(TAG, "没有可分配的ip了")
return
end
log.info(TAG, "分配ip", mac:toHex(), string.format("%d.%d.%d.%d", srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], ip))
local client = {
mac = mac,
ip = ip,
tm = mcu.ticks() // mcu.hz(),
stat = 1
}
srv.clients[ip] = client
log.info(TAG, "send offer")
dhcp_send_offer(srv, pkg, client)
end
local function dhcp_handle_request(srv, pkg)
local mac = pkg.chaddr:sub(1, pkg.hlen)
-- 看看是不是已经分配了ip
for _, client in pairs(srv.clients) do
if client.mac == mac then
log.info(TAG, "request,发现已经分配的mac地址, send ack", mac:toHex())
client.tm = mcu.ticks() // mcu.hz()
stat = 3
dhcp_send_ack(srv, pkg, client)
if srv.opts.ack_cb then
local cip = string.format("%d.%d.%d.%d", srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], client.ip)
srv.opts.ack_cb(cip, mac:toHex())
end
return
end
end
-- 没有找到, 那应该返回NACK
log.info(TAG, "request,对应mac地址没有分配ip, send nack")
dhcp_send_nack(srv, pkg, {ip=pkg.yiaddr:byte(1)})
end
local function dhcp_pkg_handle(srv, pkg)
-- 进行基本的检查
if pkg.magic ~= 0x63825363 then
log.warn(TAG, "dhcp数据包的magic不对劲,忽略该数据包", pkg.magic)
return
end
if pkg.op ~= 1 then
log.info(TAG, "op不对,忽略该数据包", pkg.op)
return
end
if pkg.htype ~= 1 or pkg.hlen ~= 6 then
log.warn(TAG, "htype/hlen 不认识, 忽略该数据包")
return
end
-- 看看是不是能处理的类型, 当前只处理discover/request
if pkg.msgtype == 1 or pkg.msgtype == 3 then
else
log.warn(TAG, "msgtype不是discover/request, 忽略该数据包", pkg.msgtype)
return
end
-- 检查一下mac地址是否合法
local mac = pkg.chaddr:sub(1, pkg.hlen)
if mac == "\0\0\0\0\0\0" or mac == "\xFF\xFF\xFF\xFF\xFF\xFF" then
log.warn(TAG, "mac地址为空, 忽略该数据包")
return
end
-- 处理discover包
if pkg.msgtype == 1 then
log.info(TAG, "是discover包", mac:toHex())
dhcp_handle_discover(srv, pkg)
elseif pkg.msgtype == 3 then
log.info(TAG, "是request包", mac:toHex())
dhcp_handle_request(srv, pkg)
end
-- TODO 处理结束, 打印一下客户的列表?
end
local function dhcp_task(srv)
while 1 do
-- log.info("ulwip", "等待DHCP数据")
local result, data = sys.waitUntil(srv.udp_topic, 1000)
if result then
-- log.info("ulwip", "收到dhcp数据包", data:toHex())
-- 解析DHCP数据包
local pkg = dhcp_decode(zbuff.create(#data, data))
if pkg then
-- dhcp_print_pkg(pkg)
dhcp_pkg_handle(srv, pkg)
end
end
end
end
--[[
创建一个dhcp服务器
@api dhcpsrv.create(opts)
@table 选项,参考库的说明, 及demo的用法
@return table 服务器对象
@usage
-- 创建一个dhcp服务器, 最简介的版本
dhcpsrv.create({adapter=socket.LWIP_AP})
-- 详细的版本
-- 创建一个dhcp服务器
local dhcpsrv_opts = {
adapter=socket.LWIP_AP, -- 监听哪个网卡, 必须填写
mark = {255, 255, 255, 0}, -- 网络掩码, 默认 255.255.255.0
gw = {192, 168, 4, 1}, -- 网关, 默认自动获取网卡IP如果获取失败则使用 192.168.4.1
ip_start = 100, -- ip起始地址, 默认100
ip_end = 200, -- ip结束地址, 默认200
ack_cb = function(ip, mac) end, -- ack回调, 有客户端连接上来时触发, ip和mac地址会传进来
}
local mydhcpsrv = dhcpsrv.create(dhcpsrv_opts)
-- 以下是一个打印客户端列表的例子, 非必选, 仅供参考
-- clients是一个table, 包含MAC和IP的对应关系, 注意, IP只记录了最后一段数字, 非完整IP
-- 注意, clients是动态变化的过程, mydhcpsrv对象的其他属性切勿修改, 仅提供clients的只读功能
sys.taskInit(function()
while true do
sys.wait(10000)
-- 这里可以打印一下当前的客户端列表
for ip, client in pairs(mydhcpsrv.clients) do
log.info(TAG, "client", ip, client.mac:toHex(), client.tm, client.stat)
end
end
end)
-- 自动分配网段功能说明:
-- 如果不指定gw参数系统会自动获取网卡IP作为网关地址
-- 这样可以确保DHCP分配的IP与网卡IP在同一网段
]]
function dhcpsrv.create(opts)
local srv = {}
if not opts then
opts = {}
end
srv.udp_topic = "dhcpd_inc"
-- 自动获取网卡IP地址的函数
local function get_adapter_ip()
if not opts.adapter then
return nil
end
-- 获取网卡IP地址
local ip = netdrv.ipv4(opts.adapter)
if not ip or ip == "0.0.0.0" then
return nil
end
-- 简单解析IP地址192.168.4.1 -> {192, 168, 4, 1}
local a, b, c, d = ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")
if a and b and c and d then
return {tonumber(a), tonumber(b), tonumber(c), tonumber(d)}
end
return nil
end
-- 补充参数
if not opts.mark then
opts.mark = {255, 255, 255, 0}
end
-- 如果没有指定网关则自动获取网卡IP作为网关
if not opts.gw then
local adapter_ip = get_adapter_ip()
if adapter_ip then
opts.gw = adapter_ip
log.info(TAG, "自动获取网卡IP作为网关", string.format("%d.%d.%d.%d", adapter_ip[1], adapter_ip[2], adapter_ip[3], adapter_ip[4]))
else
opts.gw = {192, 168, 4, 1}
log.warn(TAG, "无法获取网卡IP使用默认网关", string.format("%d.%d.%d.%d", opts.gw[1], opts.gw[2], opts.gw[3], opts.gw[4]))
end
end
if not opts.dns then
opts.dns = opts.gw
end
-- 根据网关IP自动设置IP分配范围
if not opts.ip_start then
opts.ip_start = 100
end
if not opts.ip_end then
opts.ip_end = 200
end
srv.clients = {}
srv.opts = opts
srv.udp = udpsrv.create(67, srv.udp_topic, opts.adapter)
srv.task = sys.taskInit(dhcp_task, srv)
return srv
end
return dhcpsrv

View File

@@ -0,0 +1,125 @@
--[[
@module dnsproxy
@summary DNS代理转发
@version 1.0
@date 2024.4.20
@author wendal
@demo socket
@tag LUAT_USE_NETWORK
@usage
-- 具体用法请查阅demo
]]
local sys = require "sys"
local dnsproxy = {
server = "119.29.29.29",
srvs = {},
map = {},
txid = 0x123,
rxbuff = zbuff.create(1500)
}
function dnsproxy.on_request(sc, event, adapter)
if event == socket.EVENT then
local rxbuff = dnsproxy.rxbuff
while 1 do
rxbuff:seek(0)
local succ, data_len, remote_ip, remote_port = socket.rx(sc, rxbuff)
if succ and data_len and data_len > 0 then
-- log.info("dnsproxy", "收到DNS查询数据", rxbuff:query():toHex())
if remote_ip and #remote_ip == 5 then
local ip1,ip2,ip3,ip4 = remote_ip:byte(2),remote_ip:byte(3),remote_ip:byte(4),remote_ip:byte(5)
remote_ip = string.format("%d.%d.%d.%d", ip1, ip2, ip3, ip4)
local txid_request = rxbuff[0] + rxbuff[1] * 256
local txid_map = dnsproxy.txid
dnsproxy.txid = dnsproxy.txid + 1
if dnsproxy.txid > 65000 then
dnsproxy.txid = 0x123
end
table.insert(dnsproxy.map, {txid_request, txid_map, remote_ip, remote_port, adapter})
rxbuff[0] = txid_map % 256
rxbuff[1] = txid_map // 256
socket.tx(dnsproxy.main_sc, rxbuff, dnsproxy.server or "223.5.5.5", 53)
end
else
break
end
end
end
end
function dnsproxy.on_response(sc, event)
if event == socket.EVENT then
local rxbuff = dnsproxy.rxbuff
while 1 do
rxbuff:seek(0)
local succ, data_len = socket.rx(sc, rxbuff)
if succ and data_len and data_len > 0 then
if true then
-- local ip1,ip2,ip3,ip4 = remote_ip:byte(2),remote_ip:byte(3),remote_ip:byte(4),remote_ip:byte(5)
-- remote_ip = string.format("%d.%d.%d.%d", ip1, ip2, ip3, ip4)
local txid_resp = rxbuff[0] + rxbuff[1] * 256
local index = -1
for i, mapit in pairs(dnsproxy.map) do
if mapit[2] == txid_resp then
local txid_request = mapit[1]
local remote_ip = mapit[3]
local remote_port = mapit[4]
rxbuff[0] = txid_request % 256
rxbuff[1] = txid_request // 256
local adapter = mapit[5]
-- log.info("dnsproxy", "转发DNS响应数据", adapter, dnsproxy.srvs[adapter])
socket.tx(dnsproxy.srvs[adapter], rxbuff, remote_ip, remote_port)
index = i
break
end
end
if index > 0 then
table.remove(dnsproxy.map, index)
end
end
else
break
end
end
end
end
--[[
创建UDP服务器
@api dnsproxy.setup(adapter, main_adapter)
@int 监听的网络适配器id
@int 网络适配编号, 默认为nil,可选
@return table UDP服务的实体, 若创建失败会返回nil
]]
function dnsproxy.setup(adapter, main_adapter)
log.info("dnsproxy", adapter, main_adapter)
if dnsproxy.main_sc == nil then
dnsproxy.main_sc = socket.create(main_adapter, dnsproxy.on_response)
socket.config(dnsproxy.main_sc, 1053, true)
end
if dnsproxy.srvs[adapter] == nil then
dnsproxy.srvs[adapter] = socket.create(adapter, function(sc, event)
dnsproxy.on_request(sc, event, adapter)
end)
socket.config(dnsproxy.srvs[adapter], 53, true)
end
dnsproxy.on_ip_ready()
return true
end
function dnsproxy.on_ip_ready()
log.info("dnsproxy", "开始监听")
if not dnsproxy.main_sc then return end
socket.close(dnsproxy.main_sc)
for k, v in pairs(dnsproxy.srvs) do
socket.close(v)
socket.connect(v, "255.255.255.255", 0)
end
socket.connect(dnsproxy.main_sc, dnsproxy.server or "223.5.5.5", 53)
end
sys.subscribe("IP_READY", dnsproxy.on_ip_ready)
return dnsproxy

View File

@@ -0,0 +1,544 @@
--[[
@module exaudio
@summary exaudio扩展库
@version 1.1
@date 2025.09.01
@author 梁健
@usage
]]
local exaudio = {}
-- 常量定义
local I2S_ID = 0
local I2S_MODE = 0 -- 0:主机 1:从机
local I2S_SAMPLE_RATE = 16000
local I2S_CHANNEL_FORMAT = i2s.MONO_R
local I2S_COMM_FORMAT = i2s.MODE_LSB -- 可选MODE_I2S, MODE_LSB, MODE_MSB
local I2S_CHANNEL_BITS = 16
local MULTIMEDIA_ID = 0
local EX_MSG_PLAY_DONE = "playDone"
local ES8311_ADDR = 0x18 -- 7位地址
local CHIP_ID_REG = 0x00 -- 芯片ID寄存器地址
-- 模块常量
exaudio.PLAY_DONE = 1 -- 音频播放完毕的事件之一
exaudio.RECORD_DONE = 1 -- 音频录音完毕的事件之一
exaudio.AMR_NB = 0
exaudio.AMR_WB = 1
exaudio.PCM_8000 = 2
exaudio.PCM_16000 = 3
exaudio.PCM_24000 = 4
exaudio.PCM_32000 = 5
exaudio.PCM_48000 = 6
-- 默认配置参数
local audio_setup_param = {
model = "es8311", -- dac类型: "es8311","es8211"
i2c_id = 0, -- i2c_id: 0,1
pa_ctrl = 0, -- 音频放大器电源控制管脚
dac_ctrl = 0, -- 音频编解码芯片电源控制管脚
dac_delay = 3, -- DAC启动前冗余时间(100ms)
pa_delay = 100, -- DAC启动后延迟打开PA的时间(ms)
dac_time_delay = 600, -- 播放完毕后PA与DAC关闭间隔(ms)
bits_per_sample = 16, -- 采样位数
pa_on_level = 1 -- PA打开电平 1:高 0:低
}
local audio_play_param = {
type = 0, -- 0:文件 1:TTS 2:流式
content = nil, -- 播放内容
cbfnc = nil, -- 播放完毕回调
priority = 0, -- 优先级(数值越大越高)
sampling_rate = 16000, -- 采样率(仅流式)
sampling_depth = 16, -- 采样位深(仅流式)
signed_or_unsigned = true -- PCM是否有符号(仅流式)
}
local audio_record_param = {
format = 0, -- 录制格式支持exaudio.AMR_NBexaudio.AMR_WB,exaudio.PCM_8000,exaudio.PCM_16000,exaudio.PCM_24000,exaudio.PCM_32000,exaudio.PCM_48000
time = 5, -- 录制时间(秒)
path = nil, -- 文件路径或流式回调
cbfnc = nil -- 录音完毕回调
}
-- 内部变量
local pcm_buff0 = nil
local pcm_buff1 = nil
local voice_vol = 55
local mic_vol = 80
-- 定义全局队列表
local audio_play_queue = {
data = {}, -- 存储字符串的数组
sequenceIndex = 1 -- 用于跟踪插入顺序的索引
}
-- 向队列中添加字符串(按调用顺序插入)
local function audio_play_queue_push(str)
if type(str) == "string" then
-- 存储格式: {index = 顺序索引, value = 字符串值}
table.insert(audio_play_queue.data, {
index = audio_play_queue.sequenceIndex,
value = str
})
audio_play_queue.sequenceIndex = audio_play_queue.sequenceIndex + 1
return true
end
return false
end
-- 从队列中取出最早插入的字符串(按顺序取出)
local function audio_play_queue_pop()
if #audio_play_queue.data > 0 then
-- 取出并移除第一个元素
local item = table.remove(audio_play_queue.data, 1)
return item.value -- 返回值
end
return nil
end
-- 清空队列中所有数据
function audio_queue_clear()
-- 清空数组
audio_play_queue.data = {}
-- 重置顺序索引
audio_play_queue.sequenceIndex = 1
return true
end
-- 工具函数:参数检查
local function check_param(param, expected_type, name)
if type(param) ~= expected_type then
log.error(string.format("参数错误: %s 应为 %s 类型", name, expected_type))
return false
end
return true
end
-- 音频回调处理
local function audio_callback(id, event, point)
-- log.info("audio_callback", "event:", event,
-- "MORE_DATA:", audio.MORE_DATA,
-- "DONE:", audio.DONE,
-- "RECORD_DATA:", audio.RECORD_DATA,
-- "RECORD_DONE:", audio.RECORD_DONE)
if event == audio.MORE_DATA then
audio.write(MULTIMEDIA_ID,audio_play_queue_pop())
elseif event == audio.DONE then
if type(audio_play_param.cbfnc) == "function" then
audio_play_param.cbfnc(exaudio.PLAY_DONE)
end
audio_queue_clear() -- 清空流式播放数据队列
sys.publish(EX_MSG_PLAY_DONE)
elseif event == audio.RECORD_DATA then
if type(audio_record_param.path) == "function" then
local buff, len = point == 0 and pcm_buff0 or pcm_buff1,
point == 0 and pcm_buff0:used() or pcm_buff1:used()
audio_record_param.path(buff, len)
end
elseif event == audio.RECORD_DONE then
if type(audio_record_param.cbfnc) == "function" then
audio_record_param.cbfnc(exaudio.RECORD_DONE)
end
end
end
-- 读取ES8311芯片ID
local function read_es8311_id()
-- 发送读取请求
local send_ok = i2c.send(audio_setup_param.i2c_id, ES8311_ADDR, CHIP_ID_REG)
if not send_ok then
log.error("发送芯片ID读取请求失败")
return false
end
-- 读取数据
local data = i2c.recv(audio_setup_param.i2c_id, ES8311_ADDR, 1)
if data and #data == 1 then
return true
end
log.error("读取ES8311芯片ID失败")
return false
end
-- 音频硬件初始化
local function audio_setup()
-- I2C配置
if not i2c.setup(audio_setup_param.i2c_id, i2c.FAST) then
log.error("I2C初始化失败")
return false
end
-- 初始化I2S
local result, data = i2s.setup(
I2S_ID,
I2S_MODE,
I2S_SAMPLE_RATE,
audio_setup_param.bits_per_sample,
I2S_CHANNEL_FORMAT,
I2S_COMM_FORMAT,
I2S_CHANNEL_BITS
)
if not result then
log.error("I2S设置失败")
return false
end
-- 配置音频通道
audio.config(
MULTIMEDIA_ID,
audio_setup_param.pa_ctrl,
audio_setup_param.pa_on_level,
audio_setup_param.dac_delay,
audio_setup_param.pa_delay,
audio_setup_param.dac_ctrl,
1, -- power_on_level
audio_setup_param.dac_time_delay
)
-- 设置总线
audio.setBus(
MULTIMEDIA_ID,
audio.BUS_I2S,
{
chip = audio_setup_param.model,
i2cid = audio_setup_param.i2c_id,
i2sid = I2S_ID,
voltage = audio.VOLTAGE_1800
}
)
-- 设置音量
audio.vol(MULTIMEDIA_ID, voice_vol)
audio.micVol(MULTIMEDIA_ID, mic_vol)
audio.pm(MULTIMEDIA_ID, audio.RESUME)
-- 检查芯片连接
if audio_setup_param.model == "es8311" and not read_es8311_id() then
log.error("ES8311通讯失败请检查硬件")
return false
end
-- 注册回调
audio.on(MULTIMEDIA_ID, audio_callback)
return true
end
-- 模块接口:初始化
function exaudio.setup(audioConfigs)
-- 检查必要参数
if not audio then
log.error("不支持audio 库,请选择支持audio 的core")
return false
end
if not audioConfigs or type(audioConfigs) ~= "table" then
log.error("配置参数必须为table类型")
return false
end
-- 检查codec型号
if not audioConfigs.model or
(audioConfigs.model ~= "es8311" and audioConfigs.model ~= "es8211") then
log.error("请指定正确的codec型号(es8311或es8211)")
return false
end
audio_setup_param.model = audioConfigs.model
-- 针对ES8311的特殊检查
if audioConfigs.model == "es8311" then
if not check_param(audioConfigs.i2c_id, "number", "i2c_id") then
return false
end
audio_setup_param.i2c_id = audioConfigs.i2c_id
end
-- 检查功率放大器控制管脚
if audioConfigs.pa_ctrl == nil then
log.warn("pa_ctrl(功率放大器控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
end
audio_setup_param.pa_ctrl = audioConfigs.pa_ctrl
-- 检查功率放大器控制管脚
if audioConfigs.dac_ctrl == nil then
log.warn("dac_ctrl(音频编解码控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
end
audio_setup_param.dac_ctrl = audioConfigs.dac_ctrl
-- 处理可选参数
local optional_params = {
{name = "dac_delay", type = "number"},
{name = "pa_delay", type = "number"},
{name = "dac_time_delay", type = "number"},
{name = "bits_per_sample", type = "number"},
{name = "pa_on_level", type = "number"}
}
for _, param in ipairs(optional_params) do
if audioConfigs[param.name] ~= nil then
if check_param(audioConfigs[param.name], param.type, param.name) then
audio_setup_param[param.name] = audioConfigs[param.name]
else
return false
end
end
end
-- 确保采样位数有默认值
audio_setup_param.bits_per_sample = audio_setup_param.bits_per_sample or 16
return audio_setup()
end
-- 模块接口:开始播放
function exaudio.play_start(playConfigs)
if not playConfigs or type(playConfigs) ~= "table" then
log.error("播放配置必须为table类型")
return false
end
-- 检查播放类型
if not check_param(playConfigs.type, "number", "type") then
log.error("type必须为数值(0:文件,1:TTS,2:流式)")
return false
end
audio_play_param.type = playConfigs.type
-- 处理优先级
if playConfigs.priority ~= nil then
if check_param(playConfigs.priority, "number", "priority") then
if playConfigs.priority > audio_play_param.priority then
log.error("是否完成播放",audio.isEnd(MULTIMEDIA_ID))
if not audio.isEnd(MULTIMEDIA_ID) then
if audio.play(MULTIMEDIA_ID) ~= true then
return false
end
sys.waitUntil(EX_MSG_PLAY_DONE)
end
audio_play_param.priority = playConfigs.priority
end
else
return false
end
end
-- 处理不同播放类型
local play_type = audio_play_param.type
if play_type == 0 then -- 文件播放
if not playConfigs.content then
log.error("文件播放需要指定content(文件路径或路径表)")
return false
end
local content_type = type(playConfigs.content)
if content_type == "table" then
for _, path in ipairs(playConfigs.content) do
if type(path) ~= "string" then
log.error("播放列表元素必须为字符串路径")
return false
end
end
elseif content_type ~= "string" then
log.error("文件播放content必须为字符串或路径表")
return false
end
audio_play_param.content = playConfigs.content
if audio.play(MULTIMEDIA_ID, audio_play_param.content) ~= true then
return false
end
elseif play_type == 1 then -- TTS播放
if not audio.tts then
log.error("本固件不支持TTS,请更换支持TTS 的固件")
return false
end
if not check_param(playConfigs.content, "string", "content") then
log.error("TTS播放content必须为字符串")
return false
end
audio_play_param.content = playConfigs.content
if audio.tts(MULTIMEDIA_ID, audio_play_param.content) ~= true then
return false
end
elseif play_type == 2 then -- 流式播放
if not check_param(playConfigs.sampling_rate, "number", "sampling_rate") then
return false
end
if not check_param(playConfigs.sampling_depth, "number", "sampling_depth") then
return false
end
audio_play_param.content = playConfigs.content
audio_play_param.sampling_rate = playConfigs.sampling_rate
audio_play_param.sampling_depth = playConfigs.sampling_depth
if playConfigs.signed_or_unsigned ~= nil then
audio_play_param.signed_or_unsigned = playConfigs.signed_or_unsigned
end
audio.start(
MULTIMEDIA_ID,
audio.PCM,
1,
playConfigs.sampling_rate,
playConfigs.sampling_depth,
audio_play_param.signed_or_unsigned
)
-- 发送初始数据
if audio.write(MULTIMEDIA_ID, string.rep("\0", 512)) ~= true then
return false
end
end
-- 处理回调函数
if playConfigs.cbfnc ~= nil then
if check_param(playConfigs.cbfnc, "function", "cbfnc") then
audio_play_param.cbfnc = playConfigs.cbfnc
else
return false
end
else
audio_play_param.cbfnc = nil
end
return true
end
-- 模块接口:流式播放数据写入
function exaudio.play_stream_write(data)
audio_play_queue_push(data)
return true
end
-- 模块接口:停止播放
function exaudio.play_stop()
return audio.play(MULTIMEDIA_ID)
end
-- 模块接口:检查播放是否结束
function exaudio.is_end()
return audio.isEnd(MULTIMEDIA_ID)
end
-- 模块接口:获取错误信息
function exaudio.get_error()
return audio.getError(MULTIMEDIA_ID)
end
-- 模块接口:开始录音
function exaudio.record_start(recodConfigs)
if not recodConfigs or type(recodConfigs) ~= "table" then
log.error("录音配置必须为table类型")
return false
end
-- 检查录音格式
if recodConfigs.format == nil or type(recodConfigs.format) ~= "number" or recodConfigs.format > 6 then
log.error("请指定正确的录音格式")
return false
end
audio_record_param.format = recodConfigs.format
-- 处理录音时间
if recodConfigs.time ~= nil then
if check_param(recodConfigs.time, "number", "time") then
audio_record_param.time = recodConfigs.time
else
return false
end
else
audio_record_param.time = 0
end
-- 处理存储路径/回调
if not recodConfigs.path then
log.error("必须指定录音路径或流式回调函数")
return false
end
audio_record_param.path = recodConfigs.path
-- 转换录音格式
local recod_format, amr_quailty
if audio_record_param.format == exaudio.AMR_NB then
recod_format = audio.AMR_NB
amr_quailty = 7
elseif audio_record_param.format == exaudio.AMR_WB then
recod_format = audio.AMR_WB
amr_quailty = 8
elseif audio_record_param.format == exaudio.PCM_8000 then
recod_format = 8000
elseif audio_record_param.format == exaudio.PCM_16000 then
recod_format = 16000
elseif audio_record_param.format == exaudio.PCM_24000 then
recod_format = 24000
elseif audio_record_param.format == exaudio.PCM_32000 then
recod_format = 32000
elseif audio_record_param.format == exaudio.PCM_48000 then
recod_format = 48000
end
-- 处理回调函数
if recodConfigs.cbfnc ~= nil then
if check_param(recodConfigs.cbfnc, "function", "cbfnc") then
audio_record_param.cbfnc = recodConfigs.cbfnc
else
return false
end
else
audio_record_param.cbfnc = nil
end
-- 开始录音
local path_type = type(audio_record_param.path)
if path_type == "string" then
return audio.record(
MULTIMEDIA_ID,
recod_format,
audio_record_param.time,
amr_quailty,
audio_record_param.path
)
elseif path_type == "function" then
-- 初始化缓冲区
if not pcm_buff0 or not pcm_buff1 then
pcm_buff0 = zbuff.create(16000)
pcm_buff1 = zbuff.create(16000)
end
return audio.record(
MULTIMEDIA_ID,
recod_format,
audio_record_param.time,
amr_quailty,
nil,
3,
pcm_buff0,
pcm_buff1
)
end
log.error("录音路径必须为字符串或函数")
return false
end
-- 模块接口:停止录音
function exaudio.record_stop()
return audio.recordStop(MULTIMEDIA_ID)
end
-- 模块接口:设置音量
function exaudio.vol(play_volume)
if check_param(play_volume, "number", "音量值") then
return audio.vol(MULTIMEDIA_ID, play_volume)
end
return false
end
-- 模块接口:设置麦克风音量
function exaudio.mic_vol(record_volume)
if check_param(record_volume, "number", "麦克风音量值") then
return audio.micVol(MULTIMEDIA_ID, record_volume)
end
return false
end
return exaudio

View File

@@ -0,0 +1,425 @@
--[[
@module excamera
@summary excamera扩展库
@version 1.0
@date 2025.10.21
@author 陈取德
@usage
用法实例
注意excamera.lua适用的产品范围
Air780系列、Air700系列、Air8000系列支持SPI摄像头
Air8101系列支持USB摄像、DVP摄像头
合宙所有型号的soc产品都仅支持一路摄像头所以excamera库不需要管理camera id只需要调用摄像头的开关和拍照功能即可
使用excamera库时会有两种应用场景
1、拍照模式使用拍照模式时
按照实际使用的摄像头类型填写配置表 - 创建摄像头excamera.open() - 拍照excamera.photo() - 关闭摄像头 excamera.close()的逻辑使用
2、扫描模式当前USB和DVP摄像头不支持扫描模式仅SPI摄像头可使用
按照实际使用的摄像头类型填写配置表 - 创建摄像头excamera.open() - 扫描excamera.scan() - 关闭摄像头 excamera.close()的逻辑使用
local excamera = require "excamera"
local spi_camera_param = {
id = "gc032a", -- SPI摄像头仅支持"gc032a"、"gc0310"、"bf30a2",请带引号填写
i2c_id = 1, -- 模块上使用的I2C编号
work_mode = 0, -- 工作模式0为拍照模式1为扫描模式
save_path = "ZBUFF", -- 拍照结果存储路径,可用"ZBUFF"交由excamera库内部管理
camera_pwr = 2 , -- 摄像头使能管脚填写GPIO号即可无则填nil
camera_pwdn = 5 , -- 摄像头pwdn开关脚填写GPIO号即可无则填nil
camera_light = 25 -- 摄像头补光灯控制管脚填写GPIO号即可无则填nil
}
local usb_camera_param = {
id = camera.USB , -- 摄像头类型默认camera.USB
sensor_width = 1280, -- 摄像头像素宽度,根据摄像头实际参数填写数值
sensor_height = 720, -- 摄像头像素高度,根据摄像头实际参数填写数值
usb_port = 1 ,
save_path = "/ram/test.jpg"
}
local dvp_camera_param = {
id = camera.DVP, -- 摄像头类型默认camera.DVP
sensor_width = 1280, -- 摄像头像素宽度,根据摄像头实际参数填写数值
sensor_height = 720, -- 摄像头像素高度,根据摄像头实际参数填写数值
save_path = "/ram/test.jpg"
}
sys.taskInit(function()
local camera_id
while true do
sys.waitUntil("ONCE_CAPTURE")
camera_id = excamera.open(spi_camera_param)
log.info("初始化状态", camera_id)
local result ,data = excamera.photo()
log.info("拍完了",data)
excamera.close()
end
end)
sys.run()
]] --
local excamera = {}
local h, w
local camera_id, path, camera_buff, camera_i2c, data, result
local cam_pwr, cam_pwdn, cam_light
-- 设备打开函数:初始化指定类型的摄像头设备
-- 参数camera_param - 摄像头配置参数表包含id、i2c_id、work_mode等配置
-- 返回值成功返回camera_id失败返回false
-- 支持SPI摄像、USB摄像头、DVP摄像头使用
-- 自动处理异步回调函数,将摄像头业务流程改为同步流程
-- 支持ZBUFF处理照片支持文件路径处理照片
function excamera.open(camera_param)
-- 判断摄像头类型是否为字符串类型(用于支持不同型号的摄像头模块)
if type(camera_param.id) == "string" then
-- 判断是否需要管理供电使能
if type(camera_param.camera_pwr) == "number" then
cam_pwr = gpio.setup(camera_param.camera_pwr, 1)
end
-- 判断是否需要管理摄像头pwdn开关
if type(camera_param.camera_pwdn) == "number" then
cam_pwdn = gpio.setup(camera_param.camera_pwdn, 0)
-- 为8000暂时兼容后续版本会移除
sys.wait(10)
end
-- 配置I2C接口用于与摄像头通信
if i2c.setup(camera_param.i2c_id, i2c.FAST) then
-- 保存I2C接口ID到camera_i2c用于局内调用
camera_i2c = camera_param.i2c_id
-- 保护执行配置文件加载并赋值给camera_module便于后续调用配置表信息
local result, camera_module = pcall(require, camera_param.id)
if not result then
log.error("excamera.open", camera_param.id .. ".lua文件加载失败")
return false
end
-- 通过摄像头配置表信息初始化摄像头
camera_id = camera.init(1, 24000000, camera_module.mode, camera_module.is_msb, camera_module.rx_bit,
camera_module.seq_type, camera_module.is_ddr, camera_param.work_mode, camera_param.work_mode,
camera_module.width, camera_module.height)
if not camera_id then
log.error("excamera.open", "camera.init失败")
return false
end
-- 通过I2C向摄像头发送配置信息
for i = 1, #camera_module.init_cmds do
result = i2c.send(camera_param.i2c_id, camera_module.i2c_slave_addr, camera_module.init_cmds[i], 1)
if not result then
log.error("excamera.open", "i2c.send失败")
return false
end
end
else
-- I2C配置失败记录错误日志
log.info("I2C配置错误,请确认I2C接口配置是否正确")
return false
end
else
-- 如果不是SPI摄像头则按照DVP/USB摄像头的初始化方式处理
-- 如果既不是SPI摄像头也不是DVP/USB摄像头则返回错误
if not camera.init(camera_param) then
log.info(
"配置表中“id”参数未配置正确,DVP/USB摄像头请使用camera.USB or camera.DVP这样的常量,不需要加引号,请检查配置表,选择正确类型的配置表填写")
return false
end
camera_id = camera_param.id
end
-- 注册摄像头事件回调处理
camera.on(camera_id, "scanned", function(id, str)
-- 如果返回字符串,表示扫码成功并获得结果
if type(str) == 'string' then
log.info("扫码结果", str)
sys.publish("SCAN_DONE", str)
-- 如果返回false表示摄像头没有有效数据
elseif str == false then
log.error("摄像头没有数据")
-- 如果返回true或数字表示成功捕获到图像文件大小
elseif str == true or type(str) == 'number' then
log.info("摄像头数据", str)
-- 发布CAPTURE_DONE事件通知其他任务拍照已完成
sys.publish("CAPTURE_DONE", true)
end
end)
-- 停止摄像头当前采集,释放内存空间
camera.stop(camera_id)
-- 处理图像保存路径,支持内存缓冲区(ZBUFF)或文件路径
if camera_param.save_path == "ZBUFF" then
-- 根据摄像头型号设置图像分辨率
if camera_param.id == "bf30a2" then
h, w = 240, 320 -- BF30A2摄像头分辨率
elseif camera_param.id == "gc032a" or "gc0310" then
h, w = 640, 480 -- GC032A/GC0310摄像头分辨率
elseif camera_param.id == camera.USB or camera.DVP then
-- USB或DVP摄像头使用传入的分辨率参数
h, w = camera_param.sensor_height, camera_param.sensor_width
end
-- 创建ZBUFF内存缓冲区用于存储图像数据
-- 参数1: 缓冲区大小(宽*高*22字节/像素)
-- 参数2: 对齐方式
camera_buff = zbuff.create(h * w * 2, 0)
if camera_buff == nil then
-- 缓冲区创建失败
log.info("ZBUFF创建失败")
return false
else
-- 缓冲区创建成功保存到path变量
path = camera_buff
end
else
-- 如果是文件路径则赋值到path便于后面调用
path = camera_param.save_path
end
-- 判断是否需要管理摄像头补光灯
if type(camera_param.camera_light) == "number" then
cam_light = gpio.setup(camera_param.camera_light, 0)
end
-- 返回初始化动作结果
return true
end
-- 拍照函数:使用指定摄像头拍摄照片并保存
-- 参数x, y, w, h - 可选,指定拍摄区域的起始坐标和尺寸(裁剪区域)
-- 返回值:成功返回(true, 保存路径)失败返回false
-- 使用ZBUFF处理照片时每次调用该接口为了避免内存爆满会覆盖写入ZBUFF区保证ZBUFF区始终只有一张照片处理上传或者存储后再调用该接口避免照片丢失
function excamera.photo(x, y, w, h)
if not camera_id then
log.info("摄像头初始化失败,请重新确认软硬件配置")
return false
end
-- 开始摄像头图像采集
camera.start(camera_id)
-- 如果使用内存缓冲区保存,重置缓冲区位置指针到开始位置
if type(path) == "userdata" then
camera_buff:seek(0)
end
-- 保护执行打开补光灯,如果上面没有配置补光灯,该函数也不会报错
pcall(cam_light, 1)
log.info("照片存储路径", path)
-- 执行拍照操作,保存到指定路径
if camera.capture(camera_id, path, 1, x, y, w, h) then
-- 等待拍照完成事件超时时间5000ms
result = sys.waitUntil("CAPTURE_DONE", 5000)
-- 保护执行关闭补光灯,如果上面没有配置补光灯,该函数也不会报错
pcall(cam_light, 0)
-- 停止摄像头采集,释放内存空间
camera.stop(camera_id)
if result then
-- 拍照成功
log.info("拍照完成")
else
-- 拍照超时
log.info("拍照成功,无照片生成")
return false
end
else
-- 保护执行关闭补光灯,如果上面没有配置补光灯,该函数也不会报错
pcall(cam_light, 0)
-- 停止摄像头采集,释放内存空间
camera.stop(camera_id)
-- 拍照操作失败
log.info("拍照失败,请重试")
return false
end
-- 返回成功状态和照片保存路径
return true, path
end
-- 扫描函数:使用摄像头进行扫描(如二维码/条形码扫描)
-- 参数扫描时长ms单位毫秒
-- 返回值:成功返回(true, 扫描数据)超时未有扫描结果返回false
function excamera.scan(ms)
if not camera_id then
log.info("摄像头初始化失败,请重新确认软硬件配置")
return false
end
-- 开始摄像头图像采集
camera.start(camera_id)
-- 保护执行打开补光灯,如果上面没有配置补光灯,该函数也不会报错
pcall(cam_light, 1)
-- 等待SCAN_DONE事件超时时间根据用户配置
result, data = sys.waitUntil("SCAN_DONE", ms)
-- 停止摄像头采集,释放内存空间
camera.stop(camera_id)
-- 保护执行关闭补光灯,如果上面没有配置补光灯,该函数也不会报错
pcall(cam_light, 0)
if result then
log.info("扫描完成,扫描结果为:", data)
else
log.info(ms .. "秒内未扫描成功,请将摄像头对准二维码")
return false
end
-- 返回成功状态和扫描到的数据
return true, data
end
-- 录像函数使用指定摄像头录制视频并存入tf卡中
-- 参数:
-- file_path - 视频保存路径,如"/sd/video.mp4"
-- duration - 录制时长,单位毫秒
-- fps - 可选,帧率配置
-- 返回值:成功返回(true, 保存路径)失败返回false
-- 注意在使用此函数前需要先使用excamera.open配置摄像头
-- spi_id,pin_cs
local function fatfs_spi_pin()
local rtos_bsp = rtos.bsp()
if rtos_bsp == "AIR101" then
return 0, pin.PB04
elseif rtos_bsp == "AIR103" then
return 0, pin.PB04
elseif rtos_bsp == "AIR105" then
return 2, pin.PB03
elseif rtos_bsp == "ESP32C3" then
return 2, 7
elseif rtos_bsp == "ESP32S3" then
return 2, 14
elseif rtos_bsp == "EC618" then
return 0, 8
elseif string.find(rtos_bsp,"EC718") then
return 0, 8
elseif string.find(rtos_bsp,"Air810") then
gpio.setup(13, 1, gpio.PULLUP)
gpio.setup(28, 1, gpio.PULLUP)
return 0, 3, fatfs.SDIO
else
log.info("main", "bsp not support")
return
end
end
-- TF卡挂载函数
local function mount_tf_card()
-- 检查TF卡是否已经挂载
local result = io.open("/sd/test.txt", "w")
if result then
result:close()
os.remove("/sd/test.txt")
log.info("excamera.mount_tf_card", "TF卡已经挂载")
return true
end
-- 尝试挂载TF卡
local spi_id, pin_cs, tp = fatfs_spi_pin()
if not spi_id then
log.error("excamera.mount_tf_card", "不支持的平台")
return false
end
-- SPI模式需要初始化SPI总线
if tp and tp == fatfs.SPI then
spi.setup(spi_id, nil, 0, 0, 8, 400 * 1000)
gpio.setup(pin_cs, 1)
end
-- 挂载TF卡
local ret = fatfs.mount(tp or fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000)
if ret then
log.info("excamera.mount_tf_card", "TF卡挂载成功")
-- 检查空间
local free_info = fatfs.getfree("/sd")
if free_info then
log.info("excamera.mount_tf_card", "剩余空间:", free_info.free_kb/1024, "MB")
end
return true
else
log.error("excamera.mount_tf_card", "TF卡挂载失败")
return false
end
end
function excamera.video(file_path, duration, fps)
if not file_path or not duration then
log.error("excamera.video", "参数错误")
return false
end
if not camera_id then
log.error("excamera.video", "摄像头未初始化")
return false
end
-- 如果文件路径以/sd开头确保TF卡已挂载
if string.sub(file_path, 1, 4) == "/sd/" then
if not mount_tf_card() then
log.error("excamera.video", "TF卡挂载失败无法录制视频")
return false
end
end
log.info("excamera.video", "开始录制视频到", file_path)
-- 如果指定了帧率,则设置摄像头帧率
if fps and fps > 0 then
camera.config(camera_id, camera.CONF_UVC_FPS, fps)
end
-- 打印内存信息
log.info("excamera.video", "lua内存:", rtos.meminfo())
log.info("excamera.video", "sys内存:", rtos.meminfo("sys"))
-- 1. 启动摄像头
if camera.start(camera_id) then
-- 2. 开始MP4录制
if camera.capture(camera_id, file_path, 1) then
-- 3. 等待录制时长
sys.wait(duration)
-- 4. 停止录制
camera.stop(camera_id)
-- 5. 关闭摄像头,释放资源
camera.close(camera_id)
-- 再次打印内存信息
log.info("excamera.video", "lua内存:", rtos.meminfo())
log.info("excamera.video", "sys内存:", rtos.meminfo("sys"))
log.info("excamera.video", "视频录制完成", file_path)
return true, file_path
else
-- 录制启动失败,关闭摄像头
camera.stop(camera_id)
camera.close(camera_id)
log.error("excamera.video", "无法开始录制")
return false
end
else
log.error("excamera.video", "无法启动摄像头")
camera.close(camera_id)
return false
end
end
-- 关闭函数:释放摄像头资源
-- 参数camera_id - 摄像头ID
function excamera.close()
if camera_id then
-- 关闭摄像头,释放摄像头硬件资源
camera.close(camera_id)
end
-- 关闭SPI摄像头时需要关闭I2C接口释放通信总线资源
-- USB和DVP摄像头不需要关闭i2c,所以需要判断摄像头ID返回值USB为32DVP为0SPI为1
if camera_id == 1 then
i2c.close(camera_i2c)
end
-- 保护执行摄像头使能关闭,如果上面没有配置摄像头使能管脚,该函数也不会报错
pcall(cam_pwr, 0)
-- 保护执行摄像头开关关闭,如果上面没有配置摄像头开关管脚,该函数也不会报错
pcall(cam_pwdn, 1)
-- 如果使用了内存缓冲区,释放相关资源
if type(path) == "userdata" then
-- 置空缓冲区引用,便于垃圾回收
camera_buff:free()
camera_buff = nil
path = nil
-- 记录当前系统剩余内存情况
log.info("剩余内存", rtos.meminfo("sys"))
end
return
end
return excamera

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,229 @@
--[[
@module exfotawifi
@summary 用于Air8000/8000A/8000W型号模组自动升级WIFI
@version 1.0.3
@date 2025.9.23
@author 拓毅恒
@usage
使用时在创建的一个task处理函数中直接调用exfotawifi.request()即可开始执行WiFi升级任务
升级完毕后最好取消调用,防止后期版本升级过高导致程序使用不稳定
-- 用法实例
local exfotawifi = require("exfotawifi")
local function fota_wifi_task()
-- ...此处省略很多代码
local result = exfotawifi.request()
if result then
log.info("exfotawifi", "升级任务执行成功")
else
log.info("exfotawifi", "升级任务执行失败")
end
-- ...此处省略很多代码
end
-- 启动WiFi自动更新任务
sys.taskInit(fota_wifi_task)
]]
local exfotawifi = {}
local is_request = false -- 标记是否正在执行request任务
local fota_result = false -- 记录fota任务的执行结果
-- 判断是否为空
local function is_nil(s)
return s == nil or s == ""
end
-- 判断json是否合法
local function is_json(str)
local success, result = pcall(json.decode, str)
return success and type(result) == "table"
end
-- 解析服务器响应的json数据
local function parse_response(body)
if not body or body == "" then
log.error("exfotawifi", "返回的body为空")
return nil
end
local success, json_body = pcall(json.decode, body)
if success and type(json_body) == "table" then
log.info("exfotawifi", "解析服务器响应成功")
return json_body
else
log.error("exfotawifi", "解析服务器响应失败body内容:", body)
return nil
end
end
-- 判断是否需要升级返回true或false
local function need_fota(version, server_version)
local version_num = tonumber(version)
local server_version_num = tonumber(server_version)
if version_num < server_version_num then
return true
end
return false
end
-- 下载升级文件,支持断点续传
local function download_file(url)
local download_dir = "/http_download/"
local result, reason = io.mkdir(download_dir)
if not result then
log.error("download_file","io.mkdir error", reason)
end
local file_path = download_dir.."fotawifi.bin"
local downloaded_size = 0
-- 检查文件是否存在,获取已下载的大小
if io.exists(file_path) then
downloaded_size = io.fileSize(file_path)
log.info("exfotawifi", "检测到未完成的下载,已下载大小:", downloaded_size)
end
-- 设置请求头,支持断点续传
local headers = {}
if downloaded_size > 0 then
headers["Range"] = "bytes=" .. downloaded_size .. "-"
end
local code, headers, body = http.request("GET", url, headers, nil, nil).wait()
if code == 200 or code == 206 then
-- 开始写入文件
local file_mode = downloaded_size > 0 and "a+" or "w+"
local file = io.open(file_path, file_mode)
if file then
file:seek("end", downloaded_size)
file:write(body)
file:close()
-- 判断文件是否下载完整
local file_size = io.fileSize(file_path)
local content_length = tonumber(headers["content-length"] or headers["Content-Length"])
if file_size >= (content_length or file_size) then
log.info("exfotawifi", "下载升级文件成功,文件路径:", file_path)
return file_path
else
log.info("exfotawifi", "下载中...当前大小:", file_size, "目标大小:", content_length)
end
else
log.error("exfotawifi", "无法创建文件")
-- 删除不完整的文件
os.remove(file_path)
end
else
log.error("exfotawifi", "下载失败,状态码:", code)
-- 删除不完整的文件
if io.exists(file_path) then
os.remove(file_path)
end
end
return nil
end
-- 执行升级操作
local function fota_start(file_path)
-- 检查文件是否存在
if not io.exists(file_path) then
log.error("exfotawifi", "升级文件不存在")
return false
end
-- 检查文件大小是否超过256K (256 * 1024 Bytes)
local file_size = io.fileSize(file_path)
if file_size < 256 * 1024 then
log.error("exfotawifi", "升级文件大小不足256K文件大小:", file_size)
return false
end
-- 执行airlink.sfota操作
local result = airlink.sfota(file_path)
if result then
log.info("exfotawifi", "升级成功")
-- 释放文件占用的空间
-- 因为sfota是异步执行的所以这里不能用os.remove()删除文件
file_path = nil
return true
else
log.error("exfotawifi", "升级失败")
os.remove(file_path)
return false
end
end
function exfotawifi.request()
local result, ip, adapter = sys.waitUntil("IP_READY", 30000)
if result then
log.info("exfotawifi", "开始执行升级任务")
if is_request then
log.warn("exfotawifi", "升级任务正在执行中,请勿重复调用")
return false
end
is_request = true
fota_result = false
-- 构建请求URL
local url = "http://wififota.openluat.com/air8000/update.json"
local imei = is_nil(mobile.imei()) and "未知imei" or mobile.imei()
local version = is_nil(airlink.sver()) and "未知版本" or airlink.sver()
local muid = is_nil(mobile.muid()) and "未知muid" or mobile.muid()
local hw = is_nil(hmeta.hwver()) and "未知硬件版本" or hmeta.hwver()
local coreversion = is_nil(rtos.version()) and "未知4G固件版本" or rtos.version()
local model = is_nil(hmeta.model()) and "未知4G设备型号" or hmeta.model()
local request_url = string.format("%s?imei=%s&version=%s&muid=%s&hw=%s&coreversion=%s&model=%s", url, imei, version, muid, hw, coreversion, model)
log.info("exfotawifi", "正在请求升级信息, URL:", request_url)
-- 发送HTTP请求获取服务器响应
local code, headers, body = http.request("GET", request_url, {}, nil, {timeout = 30000}).wait()
if code == 200 then
log.info("exfotawifi", "获取服务器响应成功")
-- 打印返回的body内容
-- log.info("exfotawifi", "body:", body)
-- 解析服务器响应的json数据
local response = parse_response(body)
if response then
-- 获取服务器返回的版本号和下载链接
local server_version = response.version
local download_url = response.url
-- 获取本地版本号
local local_version = airlink.sver()
-- 判断是否需要升级
if need_fota(local_version, server_version) then
log.info("exfotawifi", "需要升级, 本地版本:", local_version, "服务器版本:", server_version)
-- 下载升级文件
local file_path = download_file(download_url)
if file_path then
-- 开始升级
fota_result = fota_start(file_path)
end
else
log.info("exfotawifi", "当前已是最新WIFI固件")
fota_result = true
end
else
log.error("exfotawifi", "解析服务器响应失败")
end
else
log.error("exfotawifi", "获取服务器响应失败,状态码:", code)
end
else
log.error("当前正在升级WIFI&蓝牙固件请插入可以上网的SIM卡并重新启动")
end
-- 释放请求标记
is_request = false
return fota_result
end
return exfotawifi

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,327 @@
-- exlcd.lua
--[[
@module exlcd
@summary LCD显示拓展库
@version 1.0.5
@date 2025.12.23
@author 江访
@usage
本文件为LCD显示拓展库核心业务逻辑为
1、初始化LCD显示屏支持多种显示芯片
2、管理屏幕背光亮度及开关状态
3、提供屏幕状态管理功能
4、支持根据lcd_model自动配置参数
本文件的对外接口有6个
1、exlcd.init(param)LCD初始化函数
2、exlcd.set_bl(level)设置背光亮度接口level为亮度级别(0-100)
3、exlcd.get_bl():当前设置背光亮度级别查询
4、exlcd.sleep():屏幕休眠
5、exlcd.wakeup():屏幕唤醒
6、exlcd.get_sleep():休眠状态查询
]]
local exlcd = {}
-- 屏幕状态管理表
local screen_state = {
last_brightness = 100, -- 默认亮度100%
backlight_on = true, -- 背光默认开启
lcd_config = nil -- 存储LCD配置
}
-- 预定义屏幕配置表
local predefined_configs = {
Air780EHM_LCD_4 = {
lcd_model = "Air780EHM_LCD_4",
pin_vcc = 24,
pin_rst = 36,
pin_pwr = 25,
pin_pwm = 2,
port = lcd.HWID_0,
direction = 3,
w = 480,
h = 320,
xoffset = 0,
yoffset = 0,
sleepcmd = 0X10,
wakecmd = 0X11,
},
AirLCD_1000 = {
lcd_model = "AirLCD_1000",
pin_vcc = 29,
pin_rst = 36,
pin_pwr = 30,
pin_pwm = 1,
port = lcd.HWID_0,
direction = 0,
w = 320,
h = 480,
xoffset = 0,
yoffset = 0,
sleepcmd = 0X10,
wakecmd = 0X11,
},
AirLCD_1010 = {
lcd_model = "AirLCD_1010",
pin_vcc = 141,
pin_rst = 36,
pin_pwr = 1,
pin_pwm = 0,
port = lcd.HWID_0,
direction = 0,
w = 320,
h = 480,
xoffset = 0,
yoffset = 0,
sleepcmd = 0X10,
wakecmd = 0X11,
},
AirLCD_1020 = {
lcd_model = "AirLCD_1020",
pin_pwr = 8,
pin_pwm = 0,
port = lcd.RGB,
direction = 0,
w = 800,
h = 480,
xoffset = 0,
yoffset = 0,
}
}
--[[
初始化LCD显示屏
@api exlcd.init(param)
@table param LCD配置参数参考库的说明及demo用法
@return bool 初始化成功返回true失败返回false
@usage
-- 使用预定义配置初始化
exlcd.init({lcd_model = "Air780EHM_LCD_4"})
-- 自定义参数初始化
exlcd.init({
lcd_model = "st7796",
port = lcd.HWID_0,
pin_rst = 36,
pin_pwr = 25,
pin_pwm = 2,
w = 480,
h = 320,
direction = 0
})
]]
function exlcd.init(param)
if type(param) ~= "table" then
log.error("exlcd", "参数必须为表")
return false
end
-- 检查必要参数
if not param.lcd_model then
log.error("exlcd", "缺少必要参数: lcd_model")
return false
end
local config = {}
-- 根据lcd_model选择配置策略
if param.lcd_model == "Air780EHM_LCD_4" then
-- Air780EHM_LCD_4: 只使用lcd_model其他参数固定
config = predefined_configs.Air780EHM_LCD_4
log.info("exlcd", "使用Air780EHM_LCD_4固定配置")
elseif predefined_configs[param.lcd_model] then
-- 其他预定义型号: 使用预定义配置作为基础,传入参数覆盖预定义配置
config = {}
-- 复制预定义配置
for k, v in pairs(predefined_configs[param.lcd_model]) do
config[k] = v
end
-- 用传入参数覆盖预定义配置
for k, v in pairs(param) do
if k ~= "lcd_model" or v ~= param.lcd_model then -- 避免重复设置lcd_model
config[k] = v
end
end
log.info("exlcd", "使用" .. param.lcd_model .. "基础配置,传入参数已覆盖")
else
-- 未知型号: 直接使用传入参数
config = param
log.info("exlcd", "使用传入参数配置")
end
-- LCD型号映射表
local lcd_models = {
AirLCD_1000 = "st7796",
Air780EHM_LCD_4 = "st7796",
AirLCD_1010 = "st7796",
AirLCD_1020 = "h050iwv"
}
-- 确定LCD型号
local lcd_model = lcd_models[config.lcd_model] or config.lcd_model
-- 存储LCD配置供其他函数使用
screen_state.lcd_config = {
pin_pwr = config.pin_pwr,
pin_pwm = config.pin_pwm,
model = lcd_model,
lcd_model = config.lcd_model
}
-- 设置电源引脚 (可选)
if config.pin_vcc then
gpio.setup(config.pin_vcc, 1, gpio.PULLUP)
gpio.set(config.pin_vcc, 1)
end
-- 设置背光电源引脚 (可选)
if config.pin_pwr then
gpio.setup(config.pin_pwr, 1, gpio.PULLUP)
gpio.set(config.pin_pwr, 1) -- 默认开启背光
end
-- 设置PWM背光引脚 (可选)
if config.pin_pwm then
pwm.setup(config.pin_pwm, 1000, screen_state.last_brightness)
pwm.open(config.pin_pwm, 1000, screen_state.last_brightness)
end
-- 屏幕初始化 (spi_dev和init_in_service为可选参数)
local lcd_init = lcd.init(
lcd_model,
config,
config.spi_dev and config.spi_dev or nil,
config.init_in_service and config.init_in_service or nil
)
log.info("exlcd", "LCD初始化", lcd_init)
-- 自定义初始化完成确认
if lcd_model == "custom" then
lcd.user_done()
end
return lcd_init
end
--[[
设置背光亮度
@api exlcd.set_bl(level)
@number level 亮度级别0-1000表示关闭背光
@return bool 设置成功返回true失败返回false
@usage
-- 设置50%亮度
exlcd.set_bl(50)
-- 关闭背光
exlcd.set_bl(0)
]]
function exlcd.set_bl(level)
-- 检查PWM配置
if not screen_state.lcd_config.pin_pwm then
log.error("exlcd", "PWM配置不存在无法调节背光")
return false
end
-- 确保GPIO已关闭
if screen_state.lcd_config.pin_pwr then
gpio.close(screen_state.lcd_config.pin_pwr)
end
-- 设置并开启PWM
pwm.stop(screen_state.lcd_config.pin_pwm)
pwm.close(screen_state.lcd_config.pin_pwm)
pwm.setup(screen_state.lcd_config.pin_pwm, 1000, 100)
pwm.open(screen_state.lcd_config.pin_pwm, 1000, level)
screen_state.last_brightness = level
screen_state.backlight_on = (level > 0)
log.info("exlcd", "背光设置为", level, "%")
return true
end
--[[
获取当前背光亮度
@api exlcd.get_bl()
@return number 当前背光亮度级别(0-100)
@usage
local brightness = exlcd.get_bl()
log.info("当前背光亮度", brightness)
]]
function exlcd.get_bl()
return screen_state.last_brightness
end
--[[
屏幕进入休眠状态
@api exlcd.sleep()
@usage
exlcd.sleep()
]]
function exlcd.sleep()
if not screen_state.is_sleeping then
-- 关闭PWM背光 (如果配置了)
if screen_state.lcd_config and screen_state.lcd_config.pin_pwm then
pwm.close(screen_state.lcd_config.pin_pwm)
end
-- 关闭背光电源 (如果配置了)
if screen_state.lcd_config and screen_state.lcd_config.pin_pwr then
gpio.setup(screen_state.lcd_config.pin_pwr, 1, gpio.PULLUP)
gpio.set(screen_state.lcd_config.pin_pwr, 0)
end
-- 执行LCD睡眠
lcd.sleep()
screen_state.is_sleeping = true
log.info("exlcd", "LCD进入休眠状态")
end
end
--[[
屏幕从休眠状态唤醒
@api exlcd.wakeup()
@usage
exlcd.wakeup()
]]
function exlcd.wakeup()
if screen_state.is_sleeping then
-- 开启背光电源 (如果配置了)
if screen_state.lcd_config and screen_state.lcd_config.pin_pwr then
gpio.set(screen_state.lcd_config.pin_pwr, 1)
end
-- 唤醒LCD
lcd.wakeup()
sys.wait(100) -- 等待100ms稳定
-- 恢复背光设置 (如果配置了PWM引脚)
if screen_state.lcd_config and screen_state.lcd_config.pin_pwm then
pwm.setup(screen_state.lcd_config.pin_pwm, 1000, screen_state.last_brightness)
pwm.open(screen_state.lcd_config.pin_pwm, 1000, screen_state.last_brightness)
end
screen_state.is_sleeping = false
log.info("exlcd", "LCD唤醒")
end
end
--[[
获取屏幕休眠状态
@api exlcd.get_sleep()
@return bool true表示屏幕处于休眠状态false表示屏幕处于工作状态
@usage
if exlcd.get_sleep() then
log.info("屏幕处于休眠状态")
end
]]
function exlcd.get_sleep()
return screen_state.is_sleeping
end
return exlcd

View File

@@ -0,0 +1,387 @@
--[[
@module exmodbus
@summary exmodbus 控制Modbus RTU/ASCII/TCP主站/从站通信
@version 1.0
@date 2025.
@author 马梦阳
@usage
本文件的对外接口有 5 个:
1、exmodbus.create(config):创建 modbus 主站/从站,支持 RTU、ASCII、TCP 三种通信模式
2、modbus:read(config):主站向从站发起读取请求(仅适用于 RTU、ASCII、TCP 主站模式)
3、modbus:write(config):主站向从站发起写入请求(仅适用于 RTU、ASCII、TCP 主站模式)
4、modbus:destroy():销毁 modbus 主站/从站实例对象
5、modbus:on(callback):从站注册回调接口,用于处理主站发起的请求(仅适用于 RTU、ASCII、TCP 从站模式)
]]
local exmodbus = {}
-- 定义通信模式常量
exmodbus.RTU_MASTER = 0 -- RTU 主站模式
exmodbus.RTU_SLAVE = 1 -- RTU 从站模式
exmodbus.ASCII_MASTER = 2 -- ASCII 主站模式
exmodbus.ASCII_SLAVE = 3 -- ASCII 从站模式
exmodbus.TCP_MASTER = 4 -- TCP 主站模式
exmodbus.TCP_SLAVE = 5 -- TCP 从站模式
-- 定义数据类型常量
exmodbus.COIL_STATUS = 0 -- 线圈状态
exmodbus.INPUT_STATUS = 1 -- 离散输入状态
exmodbus.HOLDING_REGISTER = 4 -- 保持寄存器
exmodbus.INPUT_REGISTER = 3 -- 输入寄存器
-- 定义操作类型常量
exmodbus.READ_COILS = 0x01 -- 读线圈状态
exmodbus.READ_DISCRETE_INPUTS = 0x02 -- 读离散输入状态
exmodbus.READ_HOLDING_REGISTERS = 0x03 -- 读保持寄存器
exmodbus.READ_INPUT_REGISTERS = 0x04 -- 读输入寄存器
exmodbus.WRITE_SINGLE_COIL = 0x05 -- 写单个线圈状态
exmodbus.WRITE_SINGLE_HOLDING_REGISTER = 0x06 -- 写单个保持寄存器
exmodbus.WRITE_MULTIPLE_HOLDING_REGISTERS = 0x10 -- 写多个保持寄存器
exmodbus.WRITE_MULTIPLE_COILS = 0x0F -- 写多个线圈状态
-- 定义响应结果常量
exmodbus.STATUS_SUCCESS = 0 -- 收到响应数据且数据有效
exmodbus.STATUS_DATA_INVALID = 1 -- 收到响应数据但数据损坏/校验失败
exmodbus.STATUS_EXCEPTION = 2 -- 收到标准异常响应码
exmodbus.STATUS_TIMEOUT = 3 -- 超时未收到响应
exmodbus.STATUS_PARAM_INVALID = 4 -- 请求参数不正确
-- 异常响应码常量
exmodbus.ILLEGAL_FUNCTION = 0x01 -- 不支持请求的功能码
exmodbus.ILLEGAL_DATA_ADDRESS = 0x02 -- 请求的数据地址无效或超出范围
exmodbus.ILLEGAL_DATA_VALUE = 0x03 -- 请求的数据值无效
exmodbus.SLAVE_DEVICE_FAILURE = 0x04 -- 从站在执行操作时发生内部错误
exmodbus.ACKNOWLEDGE = 0x05 -- 请求已接受,但需要长时间处理
exmodbus.SLAVE_DEVICE_BUSY = 0x06 -- 从站正忙,无法处理请求
exmodbus.NEGATIVE_ACKNOWLEDGE = 0x07 -- 无法执行编程功能
exmodbus.MEMORY_PARITY_ERROR = 0x08 -- 内存奇偶校验错误
exmodbus.GATEWAY_PATH_UNAVAILABLE = 0x0A -- 网关路径不可用
exmodbus.GATEWAY_TARGET_NO_RESPONSE = 0x0B -- 网关目标设备无响应
-- 全局队列与调度器;
local request_queue = {}
local next_request_id = 1
local scheduler_started = false
-- 生成唯一请求 ID
local function gen_request_id()
local id = next_request_id
next_request_id = next_request_id + 1
-- 确保请求 ID 在 32 位有符号整数范围内;
if next_request_id == 0x7FFFFFFF then next_request_id = 1 end
return id
end
-- 处理队列中的请求;
local function process_request_queue()
while true do
if #request_queue > 0 then
local req = table.remove(request_queue, 1)
local instance = req.instance
local config = req.config
local is_read = req.is_read
local req_id = req.request_id
local result
if is_read then
result = instance:read_internal(config)
else
result = instance:write_internal(config)
end
sys.publish("exmodbus/resp/" .. req_id, result)
else
sys.waitUntil("start_scheduler")
end
end
end
-- 启动调度器;
local function start_scheduler()
if scheduler_started then return end
scheduler_started = true
sys.taskInit(process_request_queue)
end
-- 入队请求并等待响应;(内部使用)
function exmodbus.enqueue_request(instance, config, is_read)
-- 生成唯一请求 ID
local req_id = gen_request_id()
-- 检查队列是否为空;
-- 如果为空,先入队,然后发布主题告知调度器开始处理;
-- 如果不为空,则直接入队,不用告知调度器;
if #request_queue == 0 then
-- 入队请求;
table.insert(request_queue, {
instance = instance,
config = config,
is_read = is_read,
request_id = req_id
})
sys.publish("start_scheduler")
else
-- 入队请求;
table.insert(request_queue, {
instance = instance,
config = config,
is_read = is_read,
request_id = req_id
})
end
-- 启动调度器;
start_scheduler()
local ok, result = sys.waitUntil("exmodbus/resp/" .. req_id)
return result
end
--[[
创建一个新的实例;
@api exmodbus.create(config)
@param config table 配置参数表,包含以下字段:
mode number 通信模式,必须是 exmodbus 模块定义的常量(如 exmodbus.RTU_MASTER
uart_id number 串口 IDuart0 写 0uart1 写 1以此类推
baud_rate number 波特率
data_bits number 数据位
stop_bits number 停止位
parity_bits number 校验位
byte_order number 字节顺序
rs485_dir_gpio number RS485 方向转换 GPIO 引脚
rs485_dir_rx_level number RS485 接收方向电平
adapter number 网卡 ID
ip_address string 服务器 IP 地址
port number 服务器端口号
is_udp boolean 是否使用 UDP 协议
is_tls boolean 是否使用加密传输
keep_idle number 连接空闲多长时间后,开始发送第一个 keepalive 探针报文,单位:秒
keep_interval number 发送第一个探针后,如果没收到 ACK 回复,间隔多久再发送下一个探针,单位:秒
keep_cnt number 总共发送多少次探针后,如果依然没有回复,则判断连接已断开
server_cert string TCP 模式下的服务器 CA 证书数据UDP 模式下的 PSK
client_cert string TCP 模式下的客户端证书数据UDP 模式下的 PSK-ID
client_key string TCP 模式下的客户端私钥加密数据
client_password string TCP 模式下的客户端私钥口令数据
@return table/nil 成功时返回实例对象,失败时返回 nil
@usage
RTU/ASCII 通信模式:
local config = {
mode = exmodbus.RTU_MASTER, -- 通信模式RTU 主站
uart_id = 1, -- 串口 IDuart1
baud_rate = 115200, -- 波特率115200
data_bits = 8, -- 数据位8
stop_bits = 1, -- 停止位1
parity_bits = uart.None, -- 校验位:无校验
byte_order = uart.LSB, -- 字节顺序:小端序
rs485_dir_gpio = 23, -- RS485 方向转换 GPIO 引脚
rs485_dir_rx_level = 0 -- RS485 接收方向电平0 为低电平1 为高电平
}
local rtu_master = exmodbus.create(config)
TCP 通信模式:
local config = {
mode = exmodbus.TCP_MASTER, -- 通信模式TCP 主站
adapter = socket.LWIP_ETH, -- 网卡 IDLwIP 协议栈的以太网卡
ip_address = "192.168.1.100", -- 服务器 IP 地址192.168.1.100(主站:服务器 IP从站本地 IP从站可以不用填此参数
port = 502, -- 服务器端口号502主站服务器端口从站本地端口
is_udp = false, -- 是否使用 UDP 协议:不使用 UDP 协议false/nil 表示使用 TCP 协议
is_tls = false, -- 是否使用加密传输不使用加密传输false/nil 表示不使用加密
keep_idle = 300, -- 连接空闲多长时间后,开始发送第一个 keepalive 探针报文300 秒
keep_interval = 10, -- 发送第一个探针后,如果没收到 ACK 回复间隔多久再发送下一个探针10 秒
keep_cnt = 3, -- 总共发送多少次探针后如果依然没有回复则判断连接已断开3 次
server_cert = nil, -- TCP 模式下的服务器 CA 证书数据UDP 模式下的 PSK如果客户端不需要验证服务器证书则设为 nil 或空着
client_cert = nil, -- TCP 模式下的客户端证书数据UDP 模式下的 PSK-ID如果服务器不需要验证客户端证书则设为 nil 或空着
client_key = nil, -- TCP 模式下的客户端私钥加密数据:如果服务器不需要验证客户端私钥,则设为 nil 或空着
client_password = nil -- TCP 模式下的客户端私钥口令数据:如果服务器不需要验证客户端私钥口令,则设为 nil 或空着
}
local tcp_master = exmodbus.create(config)
--]]
function exmodbus.create(config)
-- 检查配置参数是否有效;
if not config or type(config) ~= "table" then
log.error("exmodbus", "配置必须是表格类型")
return false
end
-- 根据通信模式加载对应的模块;
if config.mode == exmodbus.RTU_MASTER or config.mode == exmodbus.RTU_SLAVE or
config.mode == exmodbus.ASCII_MASTER or config.mode == exmodbus.ASCII_SLAVE then
local result, mod = pcall(require, "exmodbus_rtu_ascii")
if not result then
log.error("exmodbus", "加载 RTU/ASCII 模块失败")
return false
end
return mod.create(config, exmodbus, gen_request_id)
elseif config.mode == exmodbus.TCP_MASTER or config.mode == exmodbus.TCP_SLAVE then
local result, mod = pcall(require, "exmodbus_tcp")
if not result then
log.error("exmodbus", "加载 TCP 模块失败")
return false
end
return mod.create(config, exmodbus, gen_request_id)
else
log.error("exmodbus", "通信模式不支持")
return false
end
end
--[[
主站向从站发送读取请求(仅适用于 RTU、ASCII、TCP 主站模式)
@api modbus:read(config)
@param config table 配置参数表,包含以下字段:
slave_id number 从站 ID
reg_type number 寄存器类型
start_addr number 寄存器起始地址
reg_count number 寄存器数量
raw_request string 原始请求帧
timeout number 超时时间,单位:毫秒
@return table 包含以下字段:
status number 响应结果状态码,参考 exmodbus 模块定义的常量(如 exmodbus.STATUS_SUCCESS
execption_code number 异常码,仅在 status 为 exmodbus.STATUS_EXCEPTION 时有效
data table 寄存器数值,仅在 status 为 exmodbus.STATUS_SUCCESS 时有效,包含以下字段
[start_addr] number 寄存器数值,索引为寄存器地址,值为寄存器数值
...
raw_response string 原始响应帧
@usage
用户在传入 config 参数时,有 原始帧 和 字段参数 两种方式
1. 原始帧方式
local read_config = {
raw_request = "010300000002C40B", -- 原始请求帧01 03 00 00 00 02 C4 0B读取保持寄存器 0x0000 开始的 2 个寄存器)
timeout = 1000 -- 超时时间1000 毫秒
}
local result = modbus:read(read_config)
if result.status == exmodbus.STATUS_SUCCESS then
log.info("exmodbus_test", "读取成功,原始响应帧: ", table.concat(result.raw_response, ", "))
elseif result.status == exmodbus.STATUS_TIMEOUT then
log.error("exmodbus_test", "读取请求超时")
else
log.error("exmodbus_test", "读取失败")
end
2. 字段参数方式
local read_config = {
slave_id = 1, -- 从站 ID1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 寄存器起始地址0
reg_count = 0x0002, -- 寄存器数量2
timeout = 1000 -- 超时时间1000 毫秒
}
local result = modbus:read(read_config)
-- 根据返回状态处理结果
if result.status == exmodbus.STATUS_SUCCESS then
-- 数据解析:
log.info("exmodbus_test", "成功读取到从站 1 保持寄存器 0-2 的值,寄存器 0 数值:", result.data[result.start_addr],
",寄存器 1 数值:", result.data[result.start_addr + 1])
elseif result.status == exmodbus.STATUS_DATA_INVALID then
log.info("exmodbus_test", "收到从站 1 的响应数据但数据损坏/校验失败")
elseif result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", result.execption_code)
elseif result.status == exmodbus.STATUS_TIMEOUT then
log.info("exmodbus_test", "未收到从站 1 的响应(超时)")
end
--]]
-- 该接口在各个子文件中,此处仅用作注释
-- function modbus:read(config) end
--[[
主站向从站发送写入请求(仅适用于 RTU、ASCII、TCP 主站模式)
@api modbus:write(config)
@param config table 配置参数表,包含以下字段:
slave_id number 从站 ID
reg_type number 寄存器类型
start_addr number 寄存器起始地址
reg_count number 寄存器数量
data table 寄存器数值,包含以下字段:
[start_addr] number 寄存器数值,索引为寄存器地址,值为寄存器数值
...
force_multiple boolean 是否强制使用写多个功能码进行写入单个寄存器操作
raw_request string 原始请求帧
timeout number 超时时间,单位:毫秒
@return table 包含以下字段:
status number 响应结果状态码,参考 exmodbus 模块定义的常量(如 exmodbus.STATUS_SUCCESS
execption_code number 异常码,仅在 status 为 exmodbus.STATUS_EXCEPTION 时有效
raw_response string 原始响应帧
@usage
用户在传入 config 参数时,有 原始帧 和 字段参数 两种方式
1. 原始帧方式
local write_config = {
raw_request = "011000000002007B01592471", -- 原始请求帧01 10 00 00 00 02 00 7B 01 59 24 71写入保持寄存器 0x0000 开始的 2 个寄存器,值为 0x007B 和 0x0159
timeout = 1000 -- 超时时间1000 毫秒
}
local result = modbus:write(write_config)
if result.status == exmodbus.STATUS_SUCCESS then
log.info("exmodbus_test", "写入成功,原始响应帧: ", table.concat(result.raw_response, ", "))
elseif result.status == exmodbus.STATUS_TIMEOUT then
log.error("exmodbus_test", "写入请求超时")
else
log.error("exmodbus_test", "写入失败")
end
2. 字段参数方式
local write_config = {
slave_id = 1, -- 从站 ID1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 寄存器起始地址0
reg_count = 0x0002, -- 寄存器数量2
data = {
[0x0000] = 0x007B, -- 寄存器 0 数值0x007B
[0x0001] = 0x0159, -- 寄存器 1 数值0x0159
},
timeout = 1000 -- 超时时间1000 毫秒
}
local result = modbus:write(write_config)
-- 根据返回状态处理结果
if result.status == exmodbus.STATUS_SUCCESS then
log.info("exmodbus_test", "成功写入从站 1 保持寄存器 0-2 的值")
elseif result.status == exmodbus.STATUS_DATA_INVALID then
log.info("exmodbus_test", "收到从站 1 的响应数据但数据损坏/校验失败")
elseif result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", result.execption_code)
elseif result.status == exmodbus.STATUS_TIMEOUT then
log.info("exmodbus_test", "未收到从站 1 的响应(超时)")
end
--]]
-- 该接口在各个子文件中,此处仅用作注释
-- function modbus:write(config) end
--[[
销毁 modbus 主站/从站实例对象
@api modbus:destroy()
@return nil
@usage
modbus:destroy()
--]]
-- 该接口在各个子文件中,此处仅用作注释
-- function modbus:destroy() end
--[[
从站注册回调接口,用于处理主站发起的请求(仅适用于 RTU、ASCII、TCP 从站模式)
@api modbus:on(callback)
@param callback function 回调函数,格式为:
function callback(request)
-- 用户代码
end
该回调函数接收 requset 一个参数,该参数为 table 类型,包含以下字段:
slave_id number 从站 ID
func_code number 功能码
reg_type number 寄存器类型
start_addr number 寄存器起始地址
reg_count number 寄存器数量
data table 寄存器数值,包含以下字段:
[start_addr] number 寄存器数值,索引为寄存器地址,值为寄存器数值
...
@return nil
@usage
function callback(request)
-- 用户处理代码
end
--]]
-- 该接口在各个子文件中,此处仅用作注释
-- modbus:on(callback)
return exmodbus

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,789 @@
--[[
@module exmtn
@summary 运维日志扩展库,负责日志的持久化存储
@version 1.0
@date 2025.12.9
@author zengeshuai
@usage
exmtn.init(1, 0) -- 初始化1个块缓存写入
exmtn.log("info", "tag", "message", 123) -- 输出运维日志
]]
local exmtn = {}
-- 常量定义
local LOG_MTN_CACHE_SIZE = 4096
local LOG_MTN_FILE_COUNT = 4
local LOG_MTN_CONFIG_FILE = "/exmtn.trc"
local LOG_MTN_DEFAULT_BLOCKS_DIVISOR = 40
local LOG_MTN_ADD_WRITE_THRESHOLD = 256
local LOG_MTN_CONFIG_VERSION = 1
-- 写入方式常量
exmtn.CACHE_WRITE = 0
exmtn.ADD_WRITE = 1
-- 内部状态
local ctx = {
inited = false,
enabled = false,
cur_index = 1, -- 1-4
block_size = 4096, -- 默认块大小
blocks_per_file = 1, -- 每文件块数
file_limit = 4096, -- 每文件大小限制
write_way = 0, -- 0=缓存写入, 1=直接追加
cache = "", -- 缓存缓冲区
cache_used = 0, -- 缓存已使用字节数
}
-- 重置缓存
local function reset_cache()
ctx.cache = ""
ctx.cache_used = 0
end
-- 获取当前文件路径
local function get_file_path(index)
return string.format("/hzmtn%d.trc", index or ctx.cur_index)
end
-- 获取当前文件大小
local function get_current_file_size()
local path = get_file_path()
local file = io.open(path, "rb")
if not file then
return 0
end
local size = file:seek("end")
file:close()
-- file:seek("end") 返回文件大小,如果失败返回 nil
if size and size > 0 then
return size
end
return 0
end
-- 检查文件是否存在
local function file_exists(path)
local file = io.open(path, "rb")
if file then
file:close()
return true
end
return false
end
-- 检查所有日志文件是否存在
local function files_exist()
for i = 1, LOG_MTN_FILE_COUNT do
local path = get_file_path(i)
if file_exists(path) then
return true
end
end
return false
end
-- 删除所有日志文件
local function remove_files()
for i = 1, LOG_MTN_FILE_COUNT do
local path = get_file_path(i)
os.remove(path)
end
end
-- 创建空文件
local function create_files()
for i = 1, LOG_MTN_FILE_COUNT do
local path = get_file_path(i)
local file = io.open(path, "wb")
if file then
file:close()
end
end
end
-- 读取配置文件
local function load_config()
local file = io.open(LOG_MTN_CONFIG_FILE, "rb")
if not file then
return nil -- 文件不存在,返回 nil
end
local content = file:read("*a")
file:close()
if not content or #content == 0 then
return nil -- 文件为空
end
-- 解析配置:格式为 "VERSION=1\nINDEX=2\nBLOCKS=10\nWRITE_WAY=0\n"
local config = {}
for line in content:gmatch("[^\r\n]+") do
-- 移除首尾空白字符
line = line:match("^%s*(.-)%s*$") or line
local key, value = line:match("([^=]+)=(.+)")
if key and value then
-- 移除 key 和 value 的首尾空白字符
key = key:match("^%s*(.-)%s*$") or key
value = value:match("^%s*(.-)%s*$") or value
local num_value = tonumber(value)
if num_value then
config[key] = num_value
else
config[key] = value
end
end
end
-- 验证版本号
if config.VERSION ~= LOG_MTN_CONFIG_VERSION then
return nil -- 版本不匹配
end
return config
end
-- 保存配置文件
local function save_config(index, blocks, write_way)
local content = string.format("VERSION=%d\nINDEX=%d\nBLOCKS=%d\nWRITE_WAY=%d\n",
LOG_MTN_CONFIG_VERSION, index, blocks, write_way)
local file = io.open(LOG_MTN_CONFIG_FILE, "wb")
if not file then
log.warn("exmtn", "无法打开配置文件: " .. LOG_MTN_CONFIG_FILE)
return false
end
local ok = file:write(content)
file:close()
if not ok then
log.warn("exmtn", "写入配置文件失败: " .. LOG_MTN_CONFIG_FILE)
return false
end
return true
end
-- 更新索引(同时保存完整配置)
local function update_index(index)
return save_config(index, ctx.blocks_per_file, ctx.write_way)
end
-- 格式化时间戳
-- 返回格式: [2025-11-05 15:06:49.947][00000027.994]
local function format_timestamp()
-- 获取系统运行时间(毫秒)
local ticks_ms = 0
if mcu and mcu.ticks then
local ticks = mcu.ticks()
if ticks then
ticks_ms = ticks
end
end
-- 获取当前日期时间
local date_time_str = ""
local ms = 0
if os and os.date then
-- 获取当前日期时间字符串: 2025-11-05 15:06:49
local dt = os.date("%Y-%m-%d %H:%M:%S")
if dt then
-- 计算毫秒:使用系统运行时间的毫秒部分
-- 如果 RTC 已设置,时间会更准确
ms = ticks_ms % 1000
date_time_str = string.format("%s.%03d", dt, ms)
end
end
-- 如果无法获取日期时间,使用默认格式
if date_time_str == "" then
date_time_str = "1970-01-01 00:00:00.000"
end
-- 计算系统运行时间(秒.毫秒)
local uptime_sec = math.floor(ticks_ms / 1000)
local uptime_ms = ticks_ms % 1000
-- 格式化运行时间部分: 00000027.994固定宽度9位整数+3位小数
local uptime_str = string.format("%09d.%03d", uptime_sec, uptime_ms)
-- 返回完整时间戳
return string.format("[%s][%s]", date_time_str, uptime_str)
end
-- 格式化调试信息
local function format_debug_info(level, include_level)
local info = debug.getinfo(2, "Sl")
if not info or not info.source then
return nil
end
local src = info.source
-- 跳过第一个字符(@ 或 =
if src:sub(1, 1) == "@" or src:sub(1, 1) == "=" then
src = src:sub(2)
end
local line = info.currentline or 0
if line > 64 * 1024 then
line = 0
end
if include_level and level then
return string.format("%s/%s:%d", level, src, line)
else
return string.format("%s:%d", src, line)
end
end
-- 格式化消息(与 log.info/warn/error 格式一致,但添加时间戳前缀)
local function format_message(level, tag, ...)
local argc = select("#", ...)
-- 获取 log.style 配置
local log_style = 0
if log and log.style then
log_style = log.style() or 0
end
-- 根据级别确定日志标识
local level_char = "I" -- 默认 info
if level == "warn" then
level_char = "W"
elseif level == "error" then
level_char = "E"
end
local msg = ""
local dbg_info_with_level = format_debug_info(level_char, true)
local dbg_info_only = format_debug_info(nil, false)
if log_style == 0 then
-- LOG_STYLE_NORMAL: "I/user.tag arg1 arg2 ...\n"
msg = string.format("%s/user.%s", level_char, tag)
for i = 1, argc do
local arg = select(i, ...)
msg = msg .. " " .. tostring(arg)
end
elseif log_style == 1 then
-- LOG_STYLE_DEBUG_INFO: "I/file.lua:123 tag arg1 arg2 ...\n"
if dbg_info_with_level then
msg = dbg_info_with_level
else
msg = level_char
end
msg = msg .. " " .. tag
for i = 1, argc do
local arg = select(i, ...)
msg = msg .. " " .. tostring(arg)
end
else
-- LOG_STYLE_FULL: "I/user.tag file.lua:123 arg1 arg2 ...\n"
msg = string.format("%s/user.%s", level_char, tag)
if dbg_info_only then
msg = msg .. " " .. dbg_info_only
end
for i = 1, argc do
local arg = select(i, ...)
msg = msg .. " " .. tostring(arg)
end
end
msg = msg .. "\n"
-- 添加时间戳前缀
local timestamp = format_timestamp()
return timestamp .. " " .. msg
end
-- 刷新缓存到文件
local function flush_cache()
if ctx.cache_used == 0 then
return true
end
local path = get_file_path()
local file = io.open(path, "ab")
if not file then
log.warn("exmtn", "无法打开文件: " .. path)
return false
end
-- file:write 返回 true/false 或 nil不返回字节数
local ok = file:write(ctx.cache)
file:close()
if not ok then
log.warn("exmtn", "写入文件失败: " .. path)
return false
end
reset_cache()
return true
end
-- 直接写入文件ADD_WRITE 模式)
local function direct_write(data)
local path = get_file_path()
local file = io.open(path, "ab")
if not file then
log.warn("exmtn", "无法打开文件: " .. path)
return false
end
-- file:write 返回 true/false 或 nil不返回字节数
local ok = file:write(data)
file:close()
if not ok then
log.warn("exmtn", "写入文件失败: " .. path)
return false
end
return true
end
-- 将数据追加到缓存或直接写入
local function buffer_append(data)
if not data or #data == 0 then
return true
end
local len = #data
-- ADD_WRITE 模式:直接写入文件
if ctx.write_way == exmtn.ADD_WRITE then
-- 小数据先缓存,累积到阈值再写入
if len < LOG_MTN_ADD_WRITE_THRESHOLD then
if ctx.cache_used + len > LOG_MTN_CACHE_SIZE then
if not flush_cache() then
return false
end
end
ctx.cache = ctx.cache .. data
ctx.cache_used = ctx.cache_used + len
-- 如果累积到阈值,立即写入
if ctx.cache_used >= LOG_MTN_ADD_WRITE_THRESHOLD then
return flush_cache()
end
return true
end
-- 大数据直接写入
return direct_write(data)
end
-- CACHE_WRITE 模式:原有逻辑
if len > LOG_MTN_CACHE_SIZE then
-- 先刷新缓存
if not flush_cache() then
return false
end
-- 大数据直接写入
return direct_write(data)
end
-- 检查缓存是否足够
if ctx.cache_used + len > LOG_MTN_CACHE_SIZE then
if not flush_cache() then
return false
end
end
ctx.cache = ctx.cache .. data
ctx.cache_used = ctx.cache_used + len
return true
end
-- 写入日志到文件
local function write_to_file(msg)
if not ctx.enabled then
return true -- 未启用时返回成功,不写入
end
local len = #msg
-- CACHE_WRITE 模式
if ctx.write_way == exmtn.CACHE_WRITE then
-- 检查文件大小 + 缓存大小 + 当前数据是否会超过限制
-- 如果会超过,先刷新缓存
if ctx.cache_used > 0 then
local file_sz = get_current_file_size()
if file_sz + ctx.cache_used + len > ctx.file_limit then
-- 先刷新缓存
if not flush_cache() then
return false
end
-- 重新获取文件大小
file_sz = get_current_file_size()
-- 检查文件是否已满
if file_sz >= ctx.file_limit then
-- 文件已满,切换到下一个文件
ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
local path = get_file_path()
local file = io.open(path, "wb")
if file then
file:close()
end
if not update_index(ctx.cur_index) then
log.warn("exmtn", "更新索引失败")
return false
end
reset_cache()
end
end
else
-- 缓存为空,检查文件大小
local file_sz = get_current_file_size()
if file_sz + len > ctx.file_limit then
-- 文件已满,切换到下一个文件
ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
local path = get_file_path()
local file = io.open(path, "wb")
if file then
file:close()
end
if not update_index(ctx.cur_index) then
log.warn("exmtn", "更新索引失败")
return false
end
reset_cache()
end
end
-- 如果加入这条数据后缓存会满,先刷新缓存
if ctx.cache_used + len > LOG_MTN_CACHE_SIZE then
if not flush_cache() then
return false
end
-- 刷新后再次检查文件大小
local file_sz = get_current_file_size()
if file_sz >= ctx.file_limit then
-- 文件已满,切换到下一个文件
ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
local path = get_file_path()
local file = io.open(path, "wb")
if file then
file:close()
end
if not update_index(ctx.cur_index) then
log.warn("exmtn", "更新索引失败")
return false
end
reset_cache()
end
end
-- 加入缓存
return buffer_append(msg)
else
-- ADD_WRITE 模式:先刷新缓存,确保文件大小准确
if ctx.cache_used > 0 then
if not flush_cache() then
return false
end
end
-- 获取当前文件大小
local file_sz = get_current_file_size()
-- 检查当前文件是否已写满
if file_sz >= ctx.file_limit then
-- 文件已满,切换到下一个文件
ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
local path = get_file_path()
local file = io.open(path, "wb")
if file then
file:close()
end
if not update_index(ctx.cur_index) then
log.warn("exmtn", "更新索引失败")
return false
end
reset_cache()
end
-- 检查当前数据是否能放入当前文件
if file_sz + len > ctx.file_limit then
-- 当前数据放不下,切换到下一个文件
ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
local path = get_file_path()
local file = io.open(path, "wb")
if file then
file:close()
end
if not update_index(ctx.cur_index) then
log.warn("exmtn", "更新索引失败")
return false
end
reset_cache()
end
-- 加入缓存或直接写入buffer_append 会根据大小决定)
return buffer_append(msg)
end
end
--[[
初始化运维日志
@api exmtn.init(blocks, write_way)
@int blocks 每个文件的块数0表示禁用正整数表示块数量
@int write_way 写入方式可选参数。exmtn.CACHE_WRITE(0)表示缓存写入exmtn.ADD_WRITE(1)表示直接追加写入默认为exmtn.CACHE_WRITE
@return boolean 成功返回true失败返回false
@usage
exmtn.init(1, exmtn.CACHE_WRITE) -- 初始化1个块缓存写入
]]
function exmtn.init(blocks, write_way)
-- 参数校验
if blocks == nil then
blocks = 0
end
blocks = math.floor(blocks)
if blocks < 0 then
log.warn("exmtn", "无效的块数")
return false
end
write_way = write_way or exmtn.CACHE_WRITE
if write_way ~= exmtn.CACHE_WRITE and write_way ~= exmtn.ADD_WRITE then
write_way = exmtn.CACHE_WRITE
end
-- 如果禁用
if blocks == 0 then
reset_cache()
remove_files()
ctx.enabled = false
ctx.cur_index = 1
-- 删除配置文件
os.remove(LOG_MTN_CONFIG_FILE)
ctx.inited = true
return true
end
-- 读取文件系统信息
if not ctx.inited then
ctx.block_size = 4096
ctx.blocks_per_file = 1
-- 尝试获取文件系统信息(需要 fs 模块支持)
-- fs.fsstat 返回: success, total_blocks, used_blocks, block_size, fs_type
if fs and fs.fsstat then
local success, total_blocks, used_blocks, block_size, fs_type = fs.fsstat("/")
if success and block_size and block_size > 0 then
ctx.block_size = block_size
if total_blocks and total_blocks > 0 then
local def_blocks = math.floor(total_blocks / LOG_MTN_DEFAULT_BLOCKS_DIVISOR)
if def_blocks > 0 then
ctx.blocks_per_file = def_blocks
end
end
end
end
end
-- 读取配置文件(仅在首次初始化时读取)
if not ctx.inited then
local config = load_config()
if config then
-- 读取索引
if config.INDEX and config.INDEX >= 1 and config.INDEX <= LOG_MTN_FILE_COUNT then
ctx.cur_index = config.INDEX
end
-- 读取块数配置
if config.BLOCKS and config.BLOCKS > 0 then
ctx.blocks_per_file = config.BLOCKS
end
-- 读取写入方式配置
if config.WRITE_WAY == 0 or config.WRITE_WAY == 1 then
ctx.write_way = config.WRITE_WAY
end
log.info("exmtn", "读取索引", ctx.cur_index)
log.info("exmtn", "读取块数配置", ctx.blocks_per_file)
log.info("exmtn", "读取写入方式配置", ctx.write_way)
end
end
-- 检查配置是否变化
-- 如果已初始化,比较当前配置和新配置;如果未初始化,不需要判断(首次初始化总是"变化"的)
local config_changed = false
if ctx.inited then
-- 已初始化:比较当前配置和新传入的配置
config_changed = (ctx.blocks_per_file ~= blocks) or (ctx.write_way ~= write_way)
end
-- 未初始化config_changed 保持为 false因为首次初始化不算"变化"
log.info("exmtn", "配置变化", config_changed)
-- 更新配置
ctx.blocks_per_file = blocks
ctx.write_way = write_way
ctx.file_limit = ctx.block_size * ctx.blocks_per_file
if ctx.file_limit == 0 then
ctx.file_limit = LOG_MTN_CACHE_SIZE
end
-- 处理文件的三种情况
if config_changed then
-- 情况1配置变化清空文件
log.info("exmtn", "配置变化,清空文件")
reset_cache()
remove_files()
create_files()
ctx.cur_index = 1
elseif files_exist() then
-- 情况2配置没有变化文件存在根据配置文件中保存的文件指针继续写
log.info("exmtn", "配置未变化,文件存在,继续写入")
-- ctx.cur_index 已经从配置文件读取(如果是首次初始化)或保持当前值(如果已初始化),不需要重置
else
-- 情况3配置没有变化文件不存在创建文件
log.info("exmtn", "配置未变化,文件不存在,创建文件")
create_files()
-- ctx.cur_index 已经从配置文件读取(如果是首次初始化)或保持当前值(如果已初始化),不需要重置
end
-- 保存配置到文件
if not save_config(ctx.cur_index, blocks, write_way) then
log.warn("exmtn", "保存配置失败")
return false
end
ctx.enabled = true
ctx.inited = true
-- 打印初始化信息
if blocks > 0 then
local total_size = ctx.file_limit * LOG_MTN_FILE_COUNT
local file_size_mb = ctx.file_limit / (1024 * 1024)
local total_size_mb = total_size / (1024 * 1024)
local file_size_kb = ctx.file_limit / 1024
local total_size_kb = total_size / 1024
if ctx.file_limit >= 1024 * 1024 then
log.info("exmtn", string.format("初始化成功: 每个文件 %.2f MB (%d 块 × %d 字节), 总空间 %.2f MB (%d 个文件)",
file_size_mb, ctx.blocks_per_file, ctx.block_size, total_size_mb, LOG_MTN_FILE_COUNT))
elseif ctx.file_limit >= 1024 then
log.info("exmtn", string.format("初始化成功: 每个文件 %.2f KB (%d 块 × %d 字节), 总空间 %.2f KB (%d 个文件)",
file_size_kb, ctx.blocks_per_file, ctx.block_size, total_size_kb, LOG_MTN_FILE_COUNT))
else
log.info("exmtn", string.format("初始化成功: 每个文件 %d 字节 (%d 块 × %d 字节), 总空间 %d 字节 (%d 个文件)",
ctx.file_limit, ctx.blocks_per_file, ctx.block_size, total_size, LOG_MTN_FILE_COUNT))
end
end
return true
end
--[[
输出运维日志并写入文件
@api exmtn.log(level, tag, ...)
@string level 日志级别,必须是 "info", "warn", 或 "error"
@string tag 日志标识,必须是字符串
@... 需打印的参数
@return boolean 成功返回true失败返回false
@usage
exmtn.log("info", "message", 123)
exmtn.log("warn", "message", 456)
exmtn.log("error", "message", 789)
]]
function exmtn.log(level, tag, ...)
if not level or type(level) ~= "string" then
log.warn("exmtn", "level 必须是字符串")
return false
end
if not tag or type(tag) ~= "string" then
log.warn("exmtn", "tag 必须是字符串")
return false
end
-- 根据级别调用对应的底层函数(会被日志级别过滤)
if level == "info" then
log.info(tag, ...)
elseif level == "warn" then
log.warn(tag, ...)
elseif level == "error" then
log.error(tag, ...)
else
log.warn("exmtn", "level 必须是 'info', 'warn' 或 'error'")
return false
end
-- 格式化消息(用于文件写入)
local msg = format_message(level, tag, ...)
if not msg then
log.warn("exmtn", "格式化消息失败")
return false
end
-- 写入文件(不受日志级别影响)
return write_to_file(msg)
end
--[[
获取当前配置
@api exmtn.get_config()
@return table|nil 配置信息失败返回nil
@usage
local config = exmtn.get_config()
if config then
log.info("exmtn", "blocks:", config.blocks, "write_way:", config.write_way)
end
]]
function exmtn.get_config()
if not ctx.inited then
return nil
end
return {
enabled = ctx.enabled,
cur_index = ctx.cur_index,
block_size = ctx.block_size,
blocks_per_file = ctx.blocks_per_file,
file_limit = ctx.file_limit,
write_way = ctx.write_way,
}
end
--[[
清除所有运维日志文件
@api exmtn.clear()
@return boolean 成功返回true失败返回false
@usage
local ok = exmtn.clear()
if ok then
log.info("exmtn", "日志文件已清除")
end
]]
function exmtn.clear()
-- 如果已初始化,先刷新缓存(确保数据不丢失)
if ctx.inited and ctx.cache_used > 0 then
if not flush_cache() then
return false
end
end
-- 删除所有日志文件
remove_files()
-- 重新创建空文件
create_files()
-- 重置索引为1
ctx.cur_index = 1
-- 更新配置文件
if not save_config(1, ctx.blocks_per_file, ctx.write_way) then
return false
end
log.info("exmtn", "运维日志文件已清除")
return true
end
return exmtn

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,591 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LuatOS 文件管理系统</title>
<style>
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 20px;
color: #333;
}
.container {
max-width: 1200px;
margin: auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.header {
background: #2c3e50;
color: white;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.login-form {
max-width: 400px;
margin: 100px auto;
padding: 40px;
background: white;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #2980b9;
}
.file-list {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.file-list th, .file-list td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.file-list th {
background-color: #f8f9fa;
font-weight: bold;
}
.file-list tr:hover {
background-color: #f5f5f5;
}
.download-btn {
background-color: #27ae60;
color: white;
padding: 5px 10px;
text-decoration: none;
border-radius: 3px;
font-size: 12px;
}
.download-btn:hover {
background-color: #219a52;
}
.delete-btn {
background-color: #e74c3c;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
font-size: 12px;
cursor: pointer;
}
.delete-btn:hover {
background-color: #c0392b;
}
.breadcrumb {
padding: 10px 0;
margin-bottom: 20px;
}
.breadcrumb a {
color: #3498db;
text-decoration: none;
cursor: pointer;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.hidden {
display: none;
}
.error {
color: #e74c3c;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<!-- 登录页面 -->
<div id="loginPage" class="login-form">
<h2>LuatOS 文件管理系统登录</h2>
<div class="form-group">
<label for="username">用户名:</label>
<input type="text" id="username">
</div>
<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password">
</div>
<button onclick="login()">登录</button>
<div id="loginError" class="error hidden"></div>
</div>
<!-- 文件管理页面 -->
<div id="filePage" class="hidden">
<div class="header">
<h1>LuatOS 文件管理系统</h1>
<div>
<label for="uploadTarget" style="margin-right: 10px; color: white;">上传目标:</label>
<select id="uploadTarget" style="margin-right: 10px; padding: 5px;">
<option value="/luadb">内存</option>
<option value="/sd">SD卡</option>
</select>
<button onclick="scanFiles()" style="margin-right: 10px;">扫描文件</button>
<button onclick="logout()">退出登录</button>
</div>
</div>
<div class="breadcrumb" id="breadcrumb">
<a onclick="navigateTo('/')">根目录</a>
<span> | </span>
<a onclick="navigateTo('/sd')">TF/SD目录</a>
</div>
<!-- 文件上传区域 -->
<div style="margin-bottom: 20px; padding: 15px; background-color: #f9f9f9; border-radius: 5px;">
<div class="form-group">
<label for="fileUpload">选择文件 (最大200KB):</label>
<input type="file" id="fileUpload" accept="*/*">
</div>
<button onclick="uploadFile()" id="uploadBtn">上传文件</button>
<div id="uploadError" class="error hidden"></div>
<div id="uploadProgress" style="display: none; margin-top: 10px;">
<div style="width: 100%; background-color: #ddd; border-radius: 4px;">
<div id="progressBar" style="width: 0%; height: 20px; background-color: #4CAF50; border-radius: 4px; transition: width 0.3s;"></div>
</div>
<div id="progressText" style="text-align: center; margin-top: 5px;">0%</div>
</div>
</div>
<table class="file-list">
<thead>
<tr>
<th>名称</th>
<th>大小</th>
<th>操作</th>
</tr>
</thead>
<tbody id="fileListBody">
</tbody>
</table>
</div>
</div>
<script>
let currentPath = '/';
let isLoggedIn = false;
function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 确保发送和接收cookies
body: JSON.stringify({username: username, password: password})
})
.then(response => {
// 打印所有响应头查看Cookie设置
console.log('登录响应头:', response.headers);
return response.json();
})
.then(data => {
console.log('登录响应数据:', data);
if (data.success) {
isLoggedIn = true;
// 存储session_id到localStorage作为备用认证方式
if (data.session_id) {
localStorage.setItem('session_id', data.session_id);
console.log('已存储SessionID到localStorage:', data.session_id);
}
document.getElementById('loginPage').classList.add('hidden');
document.getElementById('filePage').classList.remove('hidden');
loadFiles('/luadb');
} else {
document.getElementById('loginError').textContent = data.message || '登录失败';
document.getElementById('loginError').classList.remove('hidden');
}
})
.catch(error => {
console.error('登录请求错误:', error);
document.getElementById('loginError').textContent = '登录请求失败';
document.getElementById('loginError').classList.remove('hidden');
});
}
function logout() {
fetch('/logout', {
method: 'POST',
credentials: 'include' // 确保发送cookies
})
.then(() => {
isLoggedIn = false;
// 清除localStorage中的session_id
localStorage.removeItem('session_id');
document.getElementById('filePage').classList.add('hidden');
document.getElementById('loginPage').classList.remove('hidden');
document.getElementById('username').value = '';
document.getElementById('password').value = '';
document.getElementById('loginError').classList.add('hidden');
});
}
// 扫描文件函数
function scanFiles() {
if (!isLoggedIn) return;
// 获取用户名和密码用于URL参数认证
const username = document.getElementById('username')?.value || 'admin';
const password = document.getElementById('password')?.value || '123456';
// 构建带认证参数的扫描请求URL
const url = '/scan-files?username=' + encodeURIComponent(username) +
'&password=' + encodeURIComponent(password);
console.log('发送文件扫描请求URL:', url);
// 显示扫描提示
alert('开始扫描文件,请查看系统日志了解扫描进度...');
fetch(url, {
method: 'GET',
credentials: 'include'
})
.then(response => {
if (!response.ok) {
throw new Error('扫描请求错误: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('扫描响应:', data);
if (data && data.success) {
alert('文件扫描完成!已扫描到 ' + data.foundFiles + ' 个文件,显示扫描到的用户文件。');
// 重新加载文件列表
loadFiles(currentPath);
} else {
alert('文件扫描失败: ' + (data.message || '未知错误'));
}
})
.catch(error => {
console.error('扫描文件请求错误:', error);
alert('扫描文件请求失败');
});
}
function loadFiles(path) {
if (!isLoggedIn) return;
// 准备请求头
const headers = {
'Content-Type': 'application/json'
};
// 由于传统认证方式不可靠我们使用URL参数认证
// 获取用户名和密码用于URL参数认证
const username = document.getElementById('username')?.value || 'admin';
const password = document.getElementById('password')?.value || '123456';
// 构建带认证参数的URL
const url = '/list?path=' + encodeURIComponent(path) +
'&username=' + encodeURIComponent(username) +
'&password=' + encodeURIComponent(password);
console.log('使用URL参数认证请求URL:', url);
fetch(url, {
credentials: 'include', // 确保发送cookies
headers: headers
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应错误: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('文件列表数据:', data);
// 只使用服务器返回的数据
if (data && data.success && Array.isArray(data.files)) {
displayFiles(data.files, path);
} else {
// 如果数据无效,显示空列表
displayFiles([], path);
}
updateBreadcrumb(path);
})
.catch(error => {
console.error('加载文件列表错误:', error);
// 发生错误时显示空列表
displayFiles([], path);
updateBreadcrumb(path);
});
}
function displayFiles(files, path) {
const tbody = document.getElementById('fileListBody');
tbody.innerHTML = '';
// 确保files是数组
if (!Array.isArray(files)) {
files = [];
}
console.log('显示文件数量:', files.length);
files.forEach(file => {
// 确保文件对象有必要的属性
const safeFile = {
name: file.name || "未知文件名",
size: file.size || 0,
isDirectory: file.isDirectory || false,
path: file.path || (path + '/' + (file.name || "未知文件名"))
};
const row = document.createElement('tr');
let nameCell, actionCell;
if (safeFile.isDirectory) {
nameCell = `<td><a href="#" onclick="navigateTo('${encodeURIComponent(path + '/' + safeFile.name)}')">${safeFile.name}/</a></td>`;
actionCell = '<td></td>';
} else {
nameCell = `<td>${safeFile.name}</td>`;
// 为下载链接添加URL参数认证
const username = document.getElementById('username')?.value || 'admin';
const password = document.getElementById('password')?.value || '123456';
const downloadUrl = '/download?path=' + encodeURIComponent(safeFile.path) +
'&username=' + encodeURIComponent(username) +
'&password=' + encodeURIComponent(password);
// 添加下载和删除按钮
actionCell = `<td>
<a href="${downloadUrl}" class="download-btn" style="margin-right: 5px;">下载</a>
<button class="delete-btn" onclick="deleteFile('${encodeURIComponent(safeFile.path)}')">删除</button>
</td>`;
}
row.innerHTML = `
${nameCell}
<td>${formatSize(safeFile.size)}</td>
${actionCell}
`;
tbody.appendChild(row);
});
}
// 删除文件函数
function deleteFile(filePath) {
if (confirm('确定要删除这个文件吗?')) {
// 获取用户名和密码用于URL参数认证
const username = document.getElementById('username')?.value || 'admin';
const password = document.getElementById('password')?.value || '123456';
// 构建带认证参数的删除请求URL
const url = '/delete?path=' + filePath +
'&username=' + encodeURIComponent(username) +
'&password=' + encodeURIComponent(password);
console.log('使用URL参数认证进行删除操作请求URL:', url);
fetch(url, {
method: 'POST',
credentials: 'include'
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 删除成功后重新加载文件列表
loadFiles(currentPath);
} else {
alert('删除失败: ' + (data.message || '未知错误'));
}
})
.catch(error => {
alert('删除请求失败');
});
}
}
function updateBreadcrumb(path) {
const breadcrumb = document.getElementById('breadcrumb');
// 先设置根目录和TF/SD目录链接
breadcrumb.innerHTML = '<a onclick="navigateTo(\'\')">根目录</a><span> | </span><a onclick="navigateTo(\'/sd\')">TF/SD目录</a>';
// 然后添加当前路径的层次结构(如果不是根目录)
if (path !== '/' && path !== '/sd') {
const parts = path.split('/').filter(p => p);
let current = '';
// 仅在非根目录和非SD目录时添加分隔符
breadcrumb.innerHTML += ' > ';
parts.forEach((part, index) => {
current += '/' + part;
if (index > 0) {
breadcrumb.innerHTML += ' > ';
}
breadcrumb.innerHTML += '<a onclick="navigateTo(\'' + current + '\')">' + part + '</a>';
});
}
}
function navigateTo(path) {
currentPath = path;
loadFiles(path);
}
function formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 文件上传函数
function uploadFile() {
const fileInput = document.getElementById('fileUpload');
const uploadTarget = document.getElementById('uploadTarget').value;
const uploadBtn = document.getElementById('uploadBtn');
const uploadError = document.getElementById('uploadError');
const uploadProgress = document.getElementById('uploadProgress');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
// 重置错误信息
uploadError.textContent = '';
uploadError.classList.add('hidden');
// 检查是否选择了文件
if (!fileInput.files || fileInput.files.length === 0) {
uploadError.textContent = '请先选择要上传的文件';
uploadError.classList.remove('hidden');
return;
}
const file = fileInput.files[0];
// 检查文件大小200KB限制
if (file.size > 200 * 1024) {
uploadError.textContent = '文件大小超过200KB限制';
uploadError.classList.remove('hidden');
return;
}
// 获取用户名和密码用于URL参数认证
const username = document.getElementById('username')?.value || 'admin';
const password = document.getElementById('password')?.value || '123456';
// 构建上传URL
const url = '/upload?path=' + encodeURIComponent(uploadTarget) +
'&filename=' + encodeURIComponent(file.name) +
'&username=' + encodeURIComponent(username) +
'&password=' + encodeURIComponent(password);
// 显示进度条
uploadProgress.style.display = 'block';
progressBar.style.width = '0%';
progressText.textContent = '0%';
uploadBtn.disabled = true;
// 创建FormData对象
const formData = new FormData();
formData.append('file', file);
// 使用XMLHttpRequest以便监控上传进度
const xhr = new XMLHttpRequest();
// 监听上传进度
xhr.upload.addEventListener('progress', function(event) {
if (event.lengthComputable) {
const percentComplete = Math.round((event.loaded / event.total) * 100);
progressBar.style.width = percentComplete + '%';
progressText.textContent = percentComplete + '%';
}
});
// 监听上传完成
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success) {
alert('文件上传成功!');
// 重新加载当前路径的文件列表
loadFiles(uploadTarget);
// 重置文件选择
fileInput.value = '';
} else {
uploadError.textContent = '上传失败: ' + (response.message || '未知错误');
uploadError.classList.remove('hidden');
}
} catch (e) {
uploadError.textContent = '上传成功但解析响应失败';
uploadError.classList.remove('hidden');
}
} else {
uploadError.textContent = '上传失败: 服务器响应错误';
uploadError.classList.remove('hidden');
}
uploadBtn.disabled = false;
});
// 监听上传错误
xhr.addEventListener('error', function() {
uploadError.textContent = '上传失败: 网络错误';
uploadError.classList.remove('hidden');
uploadBtn.disabled = false;
});
// 发送请求
xhr.open('POST', url);
xhr.send(formData);
}
// 启动后检查认证状态
window.onload = function() {
fetch('/check-auth', {
credentials: 'include' // 确保发送cookies
})
.then(response => response.json())
.then(data => {
if (data.authenticated) {
isLoggedIn = true;
document.getElementById('loginPage').classList.add('hidden');
document.getElementById('filePage').classList.remove('hidden');
loadFiles('/luadb');
}
});
};
</script>
</body>
</html>

View File

@@ -0,0 +1,473 @@
--[[
@module exremotecam
@summary exremotecam 远程摄像头OSD控制扩展库提供摄像头OSD文字显示设置和拍照功能。
@version 1.0
@date 2025.12.29
@author 拓毅恒
@usage
在使用exremotecam 扩展库时,需要确保网络连接正常,能够访问到目标摄像头。
本文件的对外接口有2个
1、exremotecam.OSDsetup(Brand, Host, channel, text, X, Y)设置摄像头OSD文字显示
-- 参数说明:
-- Brand: 摄像头品牌,当前仅支持"Dhua"(大华)
-- Host: 摄像头/NVR的IP地址
-- channel: 摄像头通道号
-- text: OSD文本内容需用竖线分隔格式如"1111|2222|3333|4444"
-- X: 显示位置的X坐标
-- Y: 显示位置的Y坐标
2、exremotecam.getphoto(Brand, Host, channel):控制摄像头拍照
-- 参数说明:
-- Brand: 摄像头品牌,当前仅支持"Dhua"(大华)
-- Host: 摄像头/NVR的IP地址
-- channel: 摄像头通道号
-- 返回若SD卡可用则图片保存为/sd/1.jpeg
]]
--------------------------------各品牌摄像头HTTP参数配置--------------------------------
-- 大华参数
local DH_TextAlign = 0 -- 文本对齐方式0左对齐3右对齐 默认左对齐
local DH_channel = 0 -- 通道号
-- 大华OSD默认配置参数
local dh_osd_param = {
Host = "192.168.1.108",
url = "/cgi-bin/configManager.cgi?",
GetWidgest = "action=getConfig&name=VideoWidget",
SetWidgest = "action=setConfig&VideoWidget[0].FontColorType=Adapt&VideoWidget[0].CustomTitle[1].PreviewBlend=true&VideoWidget[0].CustomTitle[1].EncodeBlend=true&VideoWidget[0].CustomTitle[1].TextAlign="..DH_TextAlign.."&VideoWidget[0].CustomTitle[1].Text=",
Text = "NULL",
Postion = "&VideoWidget[0].CustomTitle[1].Rect[0]=83&VideoWidget[0].CustomTitle[1].Rect[1]=169&VideoWidget[0].CustomTitle[1].Rect[2]=2666&VideoWidget[0].CustomTitle[1].Rect[3]=607"
}
-- 大华抓图默认配置参数
local DAHUA_MD5Param = {
username = "admin",
password = "Air123456",
realm = "Login to 7720fd71f7dd8d36eaabc67104aa4f38",--值要获取
nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093", -- 示例nonce值
method = "GET:", -- HTTP方法
qop = "auth",
nc = "00000001",
cnonce = "KeA8e2Cy",
response = "NULL",
url = "/cgi-bin/snapshot.cgi?",
timerul = "/cgi-bin/global.cgi?"
}
--------------------------------各品牌摄像头HTTP参数配置完毕--------------------------------
--[[
按竖线(|)分割字符串,支持多种返回格式
@api split_string_by_pipe(input_str,return_type)
@string input_str 要分割的字符串,格式如"1111|2222|3333"
@string/number return_type 返回类型,可选值:
"all" - 返回完整拆分数组(默认值)
"count" - 返回元素数量
整数 - 返回指定索引的元素索引从1开始
@return 根据return_type参数不同返回不同结果
- "all": table - 包含所有分割元素的数组
- "count": number - 分割后的元素数量
- 整数索引: string - 指定索引的元素,索引越界时返回错误信息
- 无效参数: string - 错误提示信息
@usage:
-- 示例1: 完整数组返回
-- 输入: "OSD行1|OSD行2|OSD行3"
-- 代码: local result = split_string_by_pipe("OSD行1|OSD行2|OSD行3")
-- 输出: {"OSD行1", "OSD行2", "OSD行3"}
-- 示例2: 返回元素数量
-- 输入: "OSD行1|OSD行2|OSD行3"
-- 代码: local count = split_string_by_pipe("OSD行1|OSD行2|OSD行3", "count")
-- 输出: 3
-- 示例3: 返回指定索引元素
-- 输入: "OSD行1|OSD行2|OSD行3"
-- 代码: local second_item = split_string_by_pipe("OSD行1|OSD行2|OSD行3", 2)
-- 输出: "OSD行2"
-- 示例4: 在OSDsetup中的实际应用
-- 代码: OSDsetup("Dhua", "192.168.1.108", 0, "温度|湿度|天气|风向", 0, 2000)
-- 内部处理: split_string_by_pipe("温度|湿度|天气|风向") 得到 {"温度", "湿度", "天气", "风向"}
-- 最终效果: 在大华摄像头OSD上显示这四行文字
]]
local function split_string_by_pipe(input_str, return_type)
-- 处理默认参数(如果未指定 return_type默认返回完整数组
return_type = return_type or "all"
-- 存储拆分后的结果
local split_result = {}
-- 核心拆分逻辑:遍历字符串,按 | 分割
for item in string.gmatch(input_str, "[^|]+") do
table.insert(split_result, item) -- 将匹配到的元素加入数组
end
-- 根据 return_type 处理返回结果
if return_type == "all" then
-- 返回完整拆分数组
return split_result
elseif return_type == "count" then
-- 返回元素数量(#split_result 是 Lua 获取数组长度的方式)
return #split_result
elseif type(return_type) == "number" then
-- 返回指定索引的元素Lua 数组索引从 1 开始)
if return_type >= 1 and return_type <= #split_result then
return split_result[return_type]
else
-- 处理索引越界
return string.format("索引 %d 越界,当前只有 %d 个元素(索引 1 到 %d",
return_type, #split_result, #split_result)
end
else
-- 处理无效的 return_type 参数
return "return_type 无效!可选值:'all'、'count' 或整数索引"
end
end
--[[
解析并验证OSD显示元素确保不超出最大显示行数
@api ElementJudg(Data, number)
@string Data 竖线分隔的OSD文本内容格式如"1111|2222|3333"
@number number 最大允许显示的行数
@return table 分割后的所有OSD元素数组
@usage
local osd_elements = ElementJudg("行1|行2|行3|行4", 3)
-- 输出: "超出显示的范围,只能显示3行"
-- 返回: {"行1", "行2", "行3", "行4"}
注意事项:
1. 函数会打印所有解析到的元素及其索引
2. 当元素数量超过最大行数时,会记录警告日志
3. 无论是否超出限制,都会返回完整的元素数组
]]
local function ElementJudg(Data,number)
-- 使用split_string_by_pipe函数按竖线分割OSD数据
local all_items = split_string_by_pipe(Data)
-- 遍历并打印所有解析到的OSD元素及其索引
for i, item in ipairs(all_items) do
log.info("元素解析", "索引", i, "", item)
end
-- 获取OSD元素的总数
local NUM = split_string_by_pipe(Data,"count")
-- 检查元素数量是否超过最大允许行数
if NUM > number then
-- 记录警告日志,提示超出显示范围
log.info("超出显示的范围,只能显示"..number.."")
end
-- 返回完整的OSD元素数组无论是否超出限制
return all_items
end
--[[
URL编码函数用于将字符串转换为符合URL标准的编码格式
@api urlencode(str)
@string str 需要进行URL编码的字符串
@return string 编码后的URL安全字符串如果输入为nil则返回空字符串
@usage:
local encoded = urlencode("Hello World!")
-- 输出: "Hello+World%21"
]]
local function urlencode(str)
-- 检查输入参数是否存在
if (str) then
-- 将换行符转换为CRLF格式符合HTTP标准
str = string.gsub(str, "\n", "\r\n")
-- 对非字母数字和空格的字符进行%XX编码
str = string.gsub(str, "([^%w ])", function(c) return string.format("%%%02X", string.byte(c)) end)
-- 将空格转换为+号符合URL编码规范
str = string.gsub(str, " ", "+")
end
-- 返回编码后的字符串或空字符串如果输入为nil
return str or ""
end
--[[
计算Digest认证中的HA1值用于网络摄像头的身份验证
@api CameraHA1(username,realm,password)
@string username 用户名
@string realm 认证域由服务器在401响应中提供
@string password 用户密码
@return string 计算得到的HA1值小写的MD5哈希值
@usage:
local ha1 = CameraHA1("admin", "realm", "123456")
-- 输出: md5("admin:realm:123456")的小写哈希值
]]
local function CameraHA1(username,realm,password)
-- 计算HA1值MD5(用户名:认证域:密码),并转换为小写
-- Digest认证标准要求使用小写的哈希值
local ha1 = string.lower(crypto.md5(username..":"..realm..":"..password))
-- 返回计算得到的HA1值
return ha1
end
--[[
处理Digest认证仅在收到401响应时调用
@api handle_digest_auth(Host,url,params,headers,HA2)
@string Host 摄像头的IP地址
@string url 请求的URL路径
@string params 请求参数
@table headers 第一次HTTP请求返回的头部信息
@string HA2 预先计算好的HA2值
@return boolean, table 认证是否成功, 更新后的请求头部
@usage:
local code, headers, body = http.request("GET", "http://192.168.1.100/cgi-bin/test", initial_headers).wait()
if code == 401 then
local success, updated_headers = handle_digest_auth("192.168.1.100", "/cgi-bin/test", "param=value", headers, "ha2_value")
if success then
-- 使用更新后的头部发送第二次请求
end
end
]]
local function handle_digest_auth(Host, url, params, headers, HA2)
-- 将headers转换为JSON格式以便解析
local str = json.encode(headers)
local Authenticate = json.decode(str)
-- 获取WWW-Authenticate头信息
local www = Authenticate["WWW-Authenticate"]
if not www then
log.info("DigestAuth", "没有找到WWW-Authenticate头信息")
return false, nil
end
log.info("DigestAuth", "获取的鉴权信息:", www)
-- 从鉴权信息中提取所需参数
DAHUA_MD5Param.realm = string.match(www,"realm=\"(.-)\"") -- 提取认证域
DAHUA_MD5Param.nonce = string.match(www,"nonce=\"(.-)\"") -- 提取随机数
if not DAHUA_MD5Param.realm or not DAHUA_MD5Param.nonce then
log.info("DigestAuth", "无法提取realm或nonce参数")
return false, nil
end
-- 计算HA1值用户名、认证域、密码的MD5哈希
local HA1 = CameraHA1(DAHUA_MD5Param.username, DAHUA_MD5Param.realm, DAHUA_MD5Param.password)
-- 计算完整的response值Digest认证的核心
-- response = MD5(HA1:nonce:nc:cnonce:qop:HA2)
DAHUA_MD5Param.response = string.lower(crypto.md5(HA1..":"..DAHUA_MD5Param.nonce..":"..DAHUA_MD5Param.nc..":"..DAHUA_MD5Param.cnonce..":"..DAHUA_MD5Param.qop..":"..HA2))
-- 构建完整的Authorization头部
local authorization_header = "Digest username=\"" .. DAHUA_MD5Param.username .. "\", realm=\"" .. DAHUA_MD5Param.realm .. "\", nonce=\"" .. DAHUA_MD5Param.nonce .. "\", uri=\"" .. url..params.. "\", qop=" .. DAHUA_MD5Param.qop .. ", nc=" .. DAHUA_MD5Param.nc .. ", cnonce=\"" .. DAHUA_MD5Param.cnonce .. "\", response=\"" .. DAHUA_MD5Param.response.."\""
-- 更新请求头部,添加认证信息
local updated_headers = {['Host']=''..Host, ["Authorization"] = ''..authorization_header, ['Connection']='keep-alive'}
log.info("DigestAuth", "鉴权信息重组完成")
return true, updated_headers
end
--[[
设置大华(Dahua)摄像头的OSD(屏幕显示)模块
@api DH_set_osd_module(Host,Data,TextAlign,channel,x,y)
@string Host 摄像头的IP地址
@string Data 要显示的OSD文本内容
@number TextAlign OSD文本对齐方式默认为全局的DH_TextAlign
@number channel 摄像头通道号默认为全局的DH_channel
@number x OSD显示的X坐标默认为0
@number y OSD显示的Y坐标默认为0
@return nil 无返回值,函数通过日志输出执行结果
@usage:
DH_set_osd_module("192.168.1.100", "温度: 25℃", 0, 1, 100, 200)
-- 功能: 在IP为192.168.1.100的摄像头通道1上坐标(100,200)处显示"温度: 25℃"
]]
local function DH_set_osd_module(Host,Data,TextAlign,channel,x,y)
-- 设置默认参数值
DH_TextAlign = TextAlign or DH_TextAlign -- 对齐方式 如果没填用默认值左对齐
channel = channel or DH_channel -- 通道号 如果没填用默认值0
x = x or 0 -- x坐标 如果没填用默认值为0
y = y or 0 -- y坐标 如果没填用默认值为0
-- 构建OSD位置参数字符串
dh_osd_param.Postion = "&VideoWidget["..channel.."].CustomTitle[1].Rect[0]="..x.."&VideoWidget["..channel.."].CustomTitle[1].Rect[1]="..y.."&VideoWidget["..channel.."].CustomTitle[1].Rect[2]=0".."&VideoWidget["..channel.."].CustomTitle[1].Rect[3]=0"
-- 构建OSD设置参数字符串
dh_osd_param.SetWidgest = "action=setConfig&VideoWidget["..channel.."].FontColorType=Adapt&VideoWidget["..channel.."].CustomTitle[1].PreviewBlend=true&VideoWidget["..channel.."].CustomTitle[1].EncodeBlend=true&VideoWidget["..channel.."].CustomTitle[1].TextAlign="..DH_TextAlign.."&VideoWidget["..channel.."].CustomTitle[1].Text="
-- 对OSD文本内容进行URL编码确保特殊字符正确传输
local OsdData = urlencode(Data)
-- 拼接完整的OSD设置参数
local OSDTEXT = dh_osd_param.SetWidgest ..OsdData
---log.info("打印放置位置",dh_osd_param.Postion)
-- 计算HA2值用于Digest认证
-- HA2 = MD5(方法:URL路径:请求参数)
local HA2 = string.lower(crypto.md5(DAHUA_MD5Param.method..dh_osd_param.url..OSDTEXT..dh_osd_param.Postion))
-- 构建HTTP请求头部
local Camera_header = {["Accept-Encoding"]="identity",["Host"]=""..Host}
-- 发送第一次HTTP请求获取鉴权信息
local full_params = OSDTEXT..dh_osd_param.Postion
local full_url = "http://"..Host..dh_osd_param.url..full_params
local code, headers, body = http.request("GET", full_url, Camera_header).wait()
log.info("DHosd", "第一次请求httpcode", code, headers) -- 打印返回的状态码和头部信息
-- 处理HTTP请求返回结果
if code == 401 then -- 401表示需要身份认证
-- 使用Digest认证函数处理认证
local success, updated_headers = handle_digest_auth(Host, dh_osd_param.url, full_params, headers, HA2)
if success then
-- 发送第二次HTTP请求这次带有完整的认证信息
local code, headers, body = http.request("GET", full_url, updated_headers).wait()
log.info("DHosd", "第二次请求httpcode", code)
else
log.info("DHosd", "Digest认证失败")
return
end
elseif code == -4 then
-- 处理重组错误(参数错误)
log.info("DHosd", "重组错误,请检查参数是否正确")
return -- 退出函数,节省资源
else
-- 处理其他HTTP错误
log.info("DHosd", "HTTP请求错误code", code)
return -- 退出函数,节省资源
end
end
--[[
设置摄像头OSD(屏幕显示)文字功能
@api OSDsetup(Brand,Host,channel,text,X,Y)
@string Brand 摄像头品牌,当前仅支持: "Dhua" - 大华
@string Host 摄像头/NVR的IP地址
@number channel 摄像头通道号主要用于NVR
@string text OSD文本内容需用竖线分隔格式如"1111|2222|3333|4444"大华最多显示13行
@number X 显示位置的X坐标
@number Y 显示位置的Y坐标
@return 无 无返回值
@usage
-- 大华摄像头OSD测试
OSDsetup("Dhua", "192.168.0.163", 0, "行1|行2|行3", 0, 2000)
-- 多通道NVR示例
OSDsetup("Dhua", "192.168.0.200", 1, "温度: 25℃|湿度: 60%", 100, 50)
]]
local function OSDsetup(Brand,Host,channel,text,X,Y)
-- 判断摄像头品牌
if Brand == "Dhua" then
log.info("osdsetup","检测到大华摄像头,开始初始化")
-- 解析并验证OSD文本内容大华摄像头最多支持13行
ElementJudg(text,13)
-- 调用大华摄像头OSD设置函数
-- 参数IP地址、OSD文本数组、对齐方式、通道号、X坐标、Y坐标
DH_set_osd_module(Host,text,0,channel,X,Y)
-- 以下品牌型号暂不支持,代码已注释
-- elseif Brand == "Hikvision" then
-- log.info("osdsetup","检测到海康摄像头,开始初始化")
-- local all_items = ElementJudg(Text,4)
-- HKOSDBdoyGetFun(Host,channel,all_items[1],all_items[2],all_items[3],all_items[4],X,Y)
-- elseif Brand == "Uniview" then
-- log.info("osdsetup","检测到宇视摄像头,开始初始化")
-- local all_items = ElementJudg(Text,6)
-- EZ_OSDSETFun(Host,channel,all_items[1],all_items[2],all_items[3],all_items[4],all_items[5],all_items[6],X,Y)
-- elseif Brand == "TianDiWeiye" then
-- log.info("osdsetup","检测到天地伟业摄像头,开始初始化")
-- local all_items = ElementJudg(Text,6)
-- -- TDOSDModify(Host,t)
else
-- 处理不支持的品牌
log.info("osdsetup","型号填写错误或暂不支持!!!")
end
end
--[[
大华摄像头拍照功能,获取指定通道的快照图片
@api DHPicture(Host,channel)
@string Host 摄像头/NVR的IP地址
@number channel 摄像头通道号
@return 无 无返回值若SD卡可用则图片保存为/sd/1.jpeg
@usage
-- 获取大华摄像头通道0的快照图片
DHPicture("192.168.1.108", 0)
-- 获取大华NVR通道1的快照图片
DHPicture("192.168.0.200", 1)
]]
local function DHPicture(Host,channel)
log.info("DHPicture","开始执行")
-- 构建拍照请求参数:通道号和图片类型(0表示快照)
local resultStr = "channel="..channel.."&type=0"
-- 计算HA2值对HTTP方法、URL路径和请求参数进行MD5加密
local HA2 = string.lower(crypto.md5(DAHUA_MD5Param.method..DAHUA_MD5Param.url..resultStr))
-- 准备基础HTTP请求头部
local Camera_header = {["Accept-Encoding"]="identity",["Host"]=""..Host}
-- 发送第一次HTTP请求主要目的是获取Digest认证信息
local full_url = "http://"..Host..DAHUA_MD5Param.url..resultStr
local code, headers, body = http.request("GET", full_url, Camera_header).wait()
log.info("DHPicture","第一次请求httpcode",code,headers)
-- 获取到鉴权信息
if code ==401 then
-- 使用统一的Digest认证函数处理认证
local success, updated_headers = handle_digest_auth(Host, DAHUA_MD5Param.url, resultStr, headers, HA2)
if success then
Camera_header = updated_headers
log.info("DHPicture","鉴权信息重组完成")
else
log.info("DHPicture", "Digest认证失败")
return
end
end
-- 检查SD卡状态
local can_save_to_sd = false
local data, err = fatfs.getfree("/sd")
if data then
can_save_to_sd = true
log.info("DHPicture", "SD卡可用空间信息:", json.encode(data))
else
log.info("DHPicture", "无法获取SD卡空间信息:", err)
end
-- 根据SD卡状态发送请求
local code, headers, body
if can_save_to_sd then
-- 发送第二次请求(带有完整的认证信息),获取图片并保存到/sd/1.jpeg
code, headers, body = http.request("GET", full_url, Camera_header, nil, {dst = "/sd/1.jpeg"}).wait()
else
-- 发送第二次请求(带有完整的认证信息),不保存图片
code, headers, body = http.request("GET", full_url, Camera_header).wait()
log.info("DHPicture", "没有检测到SD卡无法保存图片到SD卡中请确认SD卡状态后重试")
end
log.info("DHPicture","第二次请求httpcode", code, body)
if code == 200 then
log.info("DHPicture","拍照完成")
end
end
--[[
多品牌摄像头拍照通用接口,根据品牌调用对应厂商的拍照功能
@api getphoto(Brand,Host,channel)
@string Brand 摄像头品牌,当前仅支持: "Dhua" - 大华
@string Host 摄像头/NVR的IP地址
@number channel 摄像头通道号
@return 无 无返回值若SD卡可用则图片保存为/sd/1.jpeg
@usage
-- 获取大华摄像头通道0的快照图片
getphoto("Dhua", "192.168.1.108", 1)
-- 获取大华NVR通道1的快照图片
getphoto("Dhua", "192.168.0.200", 1)
]]
local function getphoto(Brand,Host,channel)
-- 判断摄像头品牌
if Brand == "Dhua" then
log.info("getphoto","检测到大华摄像头,开始初始化")
DHPicture(Host,channel)
else
-- 处理不支持的品牌
log.info("getphoto","型号填写错误或暂不支持!!!")
return
end
end
return {
OSDsetup = OSDsetup,
getphoto = getphoto
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,588 @@
--[[
@module extalk
@summary extalk扩展库
@version 1.1.1
@date 2025.09.18
@author 梁健
@usage
local extalk = require "extalk"
-- 配置并初始化
extalk.setup({
key = "your_product_key",
heart_break_time = 30,
contact_list_cbfnc = function(dev_list) end,
state_cbfnc = function(state) end
})
-- 发起对讲
extalk.start("remote_device_id")
-- 结束对讲
extalk.stop()
]]
local extalk = {}
-- 模块常量(保留原始数据结构)
extalk.START = 1 -- 通话开始
extalk.STOP = 2 -- 通话结束
extalk.UNRESPONSIVE = 3 -- 未响应
extalk.ONE_ON_ONE = 5 -- 一对一来电
extalk.BROADCAST = 6 -- 广播
local AIRTALK_TASK_NAME = "airtalk_task"
-- 消息类型常量(保留原始数据结构)
local MSG_CONNECT_ON_IND = 0
local MSG_CONNECT_OFF_IND = 1
local MSG_AUTH_IND = 2
local MSG_SPEECH_ON_IND = 3
local MSG_SPEECH_OFF_IND = 4
local MSG_SPEECH_CONNECT_TO = 5
local MSG_SPEECH_STOP_TEST_END = 22
-- 设备状态常量(保留原始数据结构)
local SP_T_NO_READY = 0 -- 离线状态无法对讲
local SP_T_IDLE = 1 -- 对讲空闲状态
local SP_T_CONNECTING = 2 -- 主动发起对讲
local SP_T_CONNECTED = 3 -- 对讲中
local SUCC = "success"
-- 全局状态变量(保留原始数据结构)
local g_state = SP_T_NO_READY -- 设备状态
local g_mqttc = nil -- mqtt客户端
local g_local_id -- 本机ID
local g_stask_start = false -- 本机ID
local g_remote_id -- 对端ID
local g_s_type -- 对讲的模式,字符串形式
local g_s_topic -- 对讲用的topic
local g_s_mode -- 对讲的模式
local g_dev_list -- 对讲列表
local g_dl_topic -- 下行消息topic模板
-- 配置参数
local extalk_configs_local = {
key = 0, -- 项目key一般需要和main的PRODUCT_KEY保持一致
heart_break_time = 0, -- 心跳间隔(单位秒)
contact_list_cbfnc = nil, -- 联系人回调函数,含设备号和昵称
state_cbfnc = nil, -- 状态回调,分为对讲开始,对讲结束,未响应
}
-- 工具函数:参数检查
local function check_param(param, expected_type, name)
if type(param) ~= expected_type then
log.error(string.format("参数错误: %s 应为 %s 类型,实际为 %s",
name, expected_type, type(param)))
return false
end
return true
end
-- MQTT消息发布函数集中处理所有发布操作并打印日志
local function publish_message(topic, payload)
if g_mqttc then
log.info("MQTT发布 - 主题:", topic, "内容:", payload)
g_mqttc:publish(topic, payload)
else
log.error("MQTT客户端未初始化无法发布消息")
end
end
-- 对讲超时处理
function extalk.wait_speech_to()
log.info("主动请求对讲超时无应答")
extalk.speech_off(true, false)
end
-- 发送鉴权消息
local function auth()
if g_state == SP_T_NO_READY and g_mqttc then
local topic = string.format("ctrl/uplink/%s/0001", g_local_id)
local payload = json.encode({
["key"] = extalk_configs_local.key,
["device_type"] = 1
})
publish_message(topic, payload)
end
end
-- 发送心跳消息
local function heart()
if g_mqttc then
adc.open(adc.CH_VBAT)
local vbat = adc.get(adc.CH_VBAT)
adc.close(adc.CH_VBAT)
local topic = string.format("ctrl/uplink/%s/0005", g_local_id)
local payload = json.encode({
["csq"] = mobile.csq(),
["battery"] = vbat
})
publish_message(topic, payload)
end
end
-- 开始对讲
local function speech_on(ssrc, sample)
g_state = SP_T_CONNECTED
g_mqttc:subscribe(g_s_topic)
airtalk.set_topic(g_s_topic)
airtalk.set_ssrc(ssrc)
log.info("对讲模式", g_s_mode)
airtalk.speech(true, g_s_mode, sample)
sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, true)
-- sys.timerLoopStart(heart, extalk_configs_local.heart_break_time * 1000)
sys.timerStopAll(extalk.wait_speech_to)
end
-- 结束对讲
function extalk.speech_off(need_upload, need_ind)
if g_state == SP_T_CONNECTED then
g_mqttc:unsubscribe(g_s_topic)
airtalk.speech(false)
g_s_topic = nil
end
g_state = SP_T_IDLE
sys.timerStopAll(auth)
sys.timerStopAll(extalk.wait_speech_to)
if need_upload and g_mqttc then
local topic = string.format("ctrl/uplink/%s/0004", g_local_id)
publish_message(topic, json.encode({["to"] = g_remote_id}))
end
if need_ind then
sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_OFF_IND, true)
end
end
-- 命令处理:请求对讲应答
local function handle_speech_response(obj)
if g_state ~= SP_T_CONNECTING then
log.error("state", g_state, "need", SP_T_CONNECTING)
return
end
if obj and obj["result"] == SUCC and g_s_topic == obj["topic"] then
-- 开始对讲
local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
speech_on(obj["ssrc"], sample_rate)
return
else
log.info(obj["result"], obj["topic"], g_s_topic)
sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, false)
end
g_s_topic = nil
g_state = SP_T_IDLE
end
-- 命令处理:对端来电
local function handle_incoming_call(obj)
if not obj or not obj["topic"] or not obj["ssrc"] or not obj["audio_code"] or not obj["type"] then
local response = {
["result"] = "failed",
["topic"] = obj and obj["topic"] or "",
["info"] = "无效的请求参数"
}
publish_message(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
return
end
-- 非空闲状态无法接收来电
if g_state ~= SP_T_IDLE then
log.error("state", g_state, "need", SP_T_IDLE)
local response = {
["result"] = "failed",
["topic"] = obj["topic"],
["info"] = "device is busy"
}
publish_message(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
return
end
local response, from = {}, nil
-- 提取对端ID
from = string.match(obj["topic"], "audio/(.*)/.*/.*")
if not from then
response = {
["result"] = "failed",
["topic"] = obj["topic"],
["info"] = "topic error"
}
publish_message(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
return
end
-- 处理一对一通话
if obj["type"] == "one-on-one" then
g_s_topic = obj["topic"]
g_remote_id = from
g_s_type = "one-on-one"
g_s_mode = airtalk.MODE_PERSON
-- 触发回调
if extalk_configs_local.state_cbfnc then
extalk_configs_local.state_cbfnc({
state = extalk.ONE_ON_ONE,
id = from
})
end
response = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
speech_on(obj["ssrc"], sample_rate)
end
-- 处理广播
if obj["type"] == "broadcast" then
g_s_topic = obj["topic"]
g_remote_id = from
g_s_mode = airtalk.MODE_GROUP_LISTENER
g_s_type = "broadcast"
-- 触发回调
if extalk_configs_local.state_cbfnc then
extalk_configs_local.state_cbfnc({
state = extalk.BROADCAST,
id = from
})
end
response = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
speech_on(obj["ssrc"], sample_rate)
end
-- 发送响应
publish_message(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
end
-- 命令处理:对端挂断
local function handle_remote_hangup(obj)
local response = {}
if g_state == SP_T_IDLE then
response = {["result"] = "failed", ["info"] = "no speech"}
else
log.info("0103", obj, obj["type"], g_s_type)
if obj and obj["type"] == g_s_type then
response = {["result"] = SUCC, ["info"] = ""}
extalk.speech_off(false, true)
else
response = {["result"] = "failed", ["info"] = "type mismatch"}
end
end
publish_message(string.format("ctrl/uplink/%s/8103", g_local_id), json.encode(response))
end
-- 命令处理:更新设备列表
local function handle_device_list_update(obj)
local response = {}
if obj then
g_dev_list = obj["dev_list"]
response = {["result"] = SUCC, ["info"] = ""}
else
response = {["result"] = "failed", ["info"] = "json info error"}
end
publish_message(string.format("ctrl/uplink/%s/8101", g_local_id), json.encode(response))
end
-- 命令处理:鉴权结果
local function handle_auth_result(obj)
if obj and obj["result"] == SUCC then
publish_message(string.format("ctrl/uplink/%s/0002", g_local_id), "") -- 更新列表
sys.timerLoopStart(heart, extalk_configs_local.heart_break_time * 1000) -- 发起心跳
else
sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false,
"鉴权失败" .. (obj and obj["info"] or ""))
log.error("鉴权失败,可能是没有修改PRODUCT_KEY")
end
end
-- 命令处理:设备列表更新应答
local function handle_device_list_response(obj)
if obj and obj["result"] == SUCC then
g_dev_list = obj["dev_list"]
if extalk_configs_local.contact_list_cbfnc then
extalk_configs_local.contact_list_cbfnc(g_dev_list)
end
g_state = SP_T_IDLE
sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, true) -- 完整登录流程结束
else
sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, "更新设备列表失败")
end
end
-- 命令解析路由表
local cmd_handlers = {
["8003"] = handle_speech_response, -- 请求对讲应答
["0102"] = handle_incoming_call, -- 平台通知对端对讲开始
["0103"] = handle_remote_hangup, -- 平台通知终端对讲结束
["0101"] = handle_device_list_update,-- 平台通知终端更新对讲设备列表
["8001"] = handle_auth_result, -- 平台对鉴权应答
["8002"] = handle_device_list_response -- 平台对终端获取终端列表应答
}
-- 解析接收到的消息
local function analyze_v1(cmd, topic, obj)
-- 忽略心跳和结束对讲的应答
if cmd == "8005" or cmd == "8004" then
return
end
-- 查找并执行对应的命令处理器
local handler = cmd_handlers[cmd]
if handler then
handler(obj)
else
log.warn("未处理的命令", cmd)
end
end
-- MQTT回调处理
local function mqtt_cb(mqttc, event, topic, payload)
log.info(event, topic or "")
if event == "conack" then
-- MQTT连接成功开始自定义鉴权流程
sys.sendMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)
g_mqttc:subscribe("ctrl/downlink/" .. g_local_id .. "/#")
elseif event == "suback" then
if g_state == SP_T_NO_READY then
if topic then
auth()
else
sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false,
"订阅失败" .. "ctrl/downlink/" .. g_local_id .. "/#")
end
elseif g_state == SP_T_CONNECTED and not topic then
extalk.speech_off(false, true)
end
elseif event == "recv" then
local result = string.match(topic, g_dl_topic)
if result then
local obj = json.decode(payload)
analyze_v1(result, topic, obj)
end
elseif event == "disconnect" then
extalk.speech_off(false, true)
g_state = SP_T_NO_READY
elseif event == "error" then
log.error("MQTT错误发生",topic,payload)
end
end
-- 任务消息处理
local function task_cb(msg)
if msg[1] == MSG_SPEECH_CONNECT_TO then
extalk.speech_off(true, false)
else
log.info("未处理消息", msg[1], msg[2], msg[3], msg[4])
end
end
-- 对讲事件回调
local function airtalk_event_cb(event, param)
log.info("airtalk event", event, param)
if event == airtalk.EVENT_ERROR then
if param == airtalk.ERROR_NO_DATA and g_s_mode == airtalk.MODE_PERSON then
log.error("长时间没有收到音频数据")
extalk.speech_off(true, true)
end
end
end
-- MQTT任务主循环
local function airtalk_mqtt_task()
if g_stask_start then
log.info("airtalk task 已经初始化了")
return true
end
g_stask_start = true
local msg, online = nil, false
-- 初始化本地ID
g_local_id = mobile.imei()
g_dl_topic = "ctrl/downlink/" .. g_local_id .. "/(%w%w%w%w)"
-- 创建MQTT客户端
g_mqttc = mqtt.create(nil, "mqtt.airtalk.luatos.com", 1883, false, {rxSize = 32768})
-- 配置对讲参数
airtalk.config(airtalk.PROTOCOL_MQTT, g_mqttc, 200) -- 缓冲至少200ms播放
airtalk.on(airtalk_event_cb)
airtalk.start()
-- 配置MQTT客户端
g_mqttc:auth(g_local_id, g_local_id, mobile.muid())
g_mqttc:keepalive(240) -- 默认值240s
g_mqttc:autoreconn(true, 15000) -- 自动重连机制
g_mqttc:debug(false)
g_mqttc:on(mqtt_cb)
log.info("设备信息", g_local_id, mobile.muid())
-- 开始连接
g_mqttc:connect()
online = false
while true do
-- 等待MQTT连接成功
msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)
log.info("connected")
-- 处理登录流程
while not online do
msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, 30000) -- 30秒超时
if type(msg) == 'table' then
online = msg[2]
if online then
-- 鉴权通过60分钟后重新鉴权
sys.timerLoopStart(auth, 3600000)
else
log.info(msg[3])
-- 鉴权失败5分钟后重试
sys.timerLoopStart(auth, 300000)
end
else
-- 超时未收到鉴权结果,重新发送
auth()
end
end
log.info("对讲管理平台已连接")
-- 处理在线状态下的消息
while online do
msg = sys.waitMsg(AIRTALK_TASK_NAME)
if type(msg) == 'table' and type(msg[1]) == "number" then
if msg[1] == MSG_SPEECH_STOP_TEST_END then
if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
log.info("没有对讲", g_state)
else
extalk.speech_off(true, false)
end
elseif msg[1] == MSG_SPEECH_ON_IND then
if extalk_configs_local.state_cbfnc then
local state = msg[2] and extalk.START or extalk.UNRESPONSIVE
extalk_configs_local.state_cbfnc({state = state})
end
elseif msg[1] == MSG_SPEECH_OFF_IND then
if extalk_configs_local.state_cbfnc then
extalk_configs_local.state_cbfnc({state = extalk.STOP})
end
elseif msg[1] == MSG_CONNECT_OFF_IND then
log.info("connect", msg[2])
online = msg[2]
end
else
log.info(type(msg), type(msg and msg[1]))
end
msg = nil -- 清理引用
end
online = false -- 重置在线状态
end
end
-- 模块初始化
function extalk.setup(extalk_configs)
if not extalk_configs or type(extalk_configs) ~= "table" then
log.error("AirTalk配置必须为table类型")
return false
end
-- 检查配置参数
if not check_param(extalk_configs.key, "string", "key") then
return false
end
extalk_configs_local.key = extalk_configs.key
if not check_param(extalk_configs.heart_break_time, "number", "heart_break_time") then
return false
end
extalk_configs_local.heart_break_time = extalk_configs.heart_break_time
if not check_param(extalk_configs.contact_list_cbfnc, "function", "contact_list_cbfnc") then
return false
end
extalk_configs_local.contact_list_cbfnc = extalk_configs.contact_list_cbfnc
if not check_param(extalk_configs.state_cbfnc, "function", "state_cbfnc") then
return false
end
extalk_configs_local.state_cbfnc = extalk_configs.state_cbfnc
-- 启动任务
sys.taskInitEx(airtalk_mqtt_task, AIRTALK_TASK_NAME, task_cb)
return true
end
-- 开始对讲
function extalk.start(id)
if g_state ~= SP_T_IDLE then
log.warn("正在对讲无法开始,当前状态:", g_state)
return false
end
if id == nil then
-- 广播模式
g_remote_id = "all"
g_state = SP_T_CONNECTING
g_s_mode = airtalk.MODE_GROUP_SPEAKER
g_s_type = "broadcast"
g_s_topic = string.format("audio/%s/all/%s",
g_local_id, string.sub(tostring(mcu.ticks()), -4, -1))
publish_message(string.format("ctrl/uplink/%s/0003", g_local_id),
json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
sys.timerStart(extalk.wait_speech_to, 15000)
else
-- 一对一模式
log.info("", id, "主动发起对讲")
if id == g_local_id then
log.error("不允许本机给本机拨打电话")
return false
end
g_state = SP_T_CONNECTING
g_remote_id = id
g_s_mode = airtalk.MODE_PERSON
g_s_type = "one-on-one"
g_s_topic = string.format("audio/%s/%s/%s",
g_local_id, id, string.sub(tostring(mcu.ticks()), -4, -1))
publish_message(string.format("ctrl/uplink/%s/0003", g_local_id),
json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
sys.timerStart(extalk.wait_speech_to, 15000)
end
return true
end
-- 结束对讲
function extalk.stop()
if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
log.info("没有对讲,当前状态:", g_state)
return false
end
log.info("主动断开对讲")
extalk.speech_off(true, false)
return true
end
return extalk

View File

@@ -0,0 +1,431 @@
-- extp.lua - 触摸系统模块
--[[
@module extp
@summary 触摸系统拓展库
@version 1.1.1
@date 2025.11.20
@author 江访
@usage
本文件为触摸系统拓展库,核心业务逻辑为:
1、初始化触摸设备支持多种触摸芯片
2、处理原始触摸数据并解析为各种手势事件
3、通过统一消息接口发布触摸事件
4、提供消息发布控制功能
5、提供滑动和长按阈值配置功能
支持的触摸事件类型包括:
RAW_DATA、TOUCH_DOWN、MOVE_X、MOVE_Y、SWIPE_LEFT、SWIPE_RIGHT、
SWIPE_UP、SWIPE_DOWN、SINGLE_TAP、LONG_PRESS
本文件的对外接口有5个
1、extp.init(param):触摸设备初始化函数
2、extp.set_publish_enabled(msg_type, enabled):设置消息发布状态
3、extp.get_publish_enable(msg_type):获取消息发布状态
4、extp.set_swipe_threshold(threshold):设置滑动判定阈值
5、extp.set_long_press_threshold(threshold):设置长按判定阈值
所有触摸事件均通过sys.publish("BASE_TOUCH_EVENT", event_type, ...)发布
]]
local extp = {}
-- 触摸状态变量
local state = "IDLE" -- 当前状态IDLE(空闲), DOWN(按下), MOVE(移动)
local touch_down_x = 0 -- 按下时的X坐标
local touch_down_y = 0 -- 按下时的Y坐标
local touch_down_time = 0 -- 按下时的时间戳
local swipe_threshold = 45 -- 滑动判定阈值(像素)
local long_press_threshold = 500 -- 长按判定阈值(毫秒)
local swipe_direction = nil -- 滑动方向用于MOVE状态
-- 消息发布控制表,默认全部打开
local publish_control = {
RAW_DATA = false, -- 原始触摸数据
TOUCH_DOWN = false, -- 按下事件
MOVE_X = false, -- 水平移动
MOVE_Y = false, -- 垂直移动
SWIPE_LEFT = false, -- 向左滑动
SWIPE_RIGHT = false, -- 向右滑动
SWIPE_UP = false, -- 向上滑动
SWIPE_DOWN = false, -- 向下滑动
SINGLE_TAP = true, -- 单击
LONG_PRESS = true -- 长按
}
-- 定义支持的触摸芯片配置
local tp_configs = {
cst820 = { i2c_speed = i2c.FAST, tp_model = "cst820" },
gt9157 = { i2c_speed = i2c.FAST, tp_model = "gt9157" },
cst9220 = { i2c_speed = i2c.SLOW, tp_model = "cst9220" },
jd9261t = { i2c_speed = i2c.FAST, tp_model = "jd9261t" },
gt911 = { i2c_speed = i2c.SLOW, tp_model = "gt911" },
AirLCD_1010 = { i2c_speed = i2c.SLOW, tp_model = "gt911" },
Air780EHM_LCD_4 = { i2c_speed = i2c.SLOW, tp_model = "gt911" },
AirLCD_1020 = { i2c_speed = i2c.SLOW, tp_model = "gt911" }
}
-- 特殊型号的默认配置
local special_tp_configs = {
Air780EHM_LCD_4 = {
i2c_id = 1,
pin_rst = 1,
pin_int = 22
},
AirLCD_1010 = {
i2c_id = 0,
pin_rst = 20,
pin_int = gpio.WAKEUP0
},
AirLCD_1020 = {
i2c_id = i2c.createSoft(0, 1),
pin_rst = 28,
pin_int = 7
}
}
--[[
设置消息发布状态
@api extp.set_publish_enabled(msg_type, enabled)
@string msg_type 消息类型,支持"ALL"或具体事件类型
@bool enabled 是否启用发布
@return bool 操作成功返回true失败返回false
@usage
-- 启用单击事件
extp.set_publish_enabled("SINGLE_TAP", true)
-- 禁用所有消息发布
extp.set_publish_enabled("ALL", false)
]]
function extp.set_publish_enabled(msg_type, enabled)
if msg_type == "ALL" then
for k, _ in pairs(publish_control) do
publish_control[k] = enabled
end
log.info("extp", "所有消息发布", enabled and "启用" or "禁用")
return true
elseif publish_control[msg_type] ~= nil then
publish_control[msg_type] = enabled
log.info("extp", msg_type, "消息发布", enabled and "启用" or "禁用")
return true
else
log.error("extp", "未知的消息类型:", msg_type)
return false
end
end
--[[
获取消息发布状态
@api extp.get_publish_enable(msg_type)
@string msg_type 消息类型,"ALL"或具体事件类型
@return bool|table 发布状态或所有状态表
@usage
-- 获取单击事件状态
local enabled = extp.get_publish_enable("SINGLE_TAP")
-- 获取所有状态
local all_status = extp.get_publish_enable("ALL")
]]
function extp.get_publish_enable(msg_type)
if msg_type == "ALL" then
-- 返回完整的发布控制表
return publish_control
elseif publish_control[msg_type] ~= nil then
return publish_control[msg_type]
else
log.error("extp", "未知的消息类型:", msg_type)
return nil
end
end
--[[
设置滑动判定阈值
@api extp.set_swipe_threshold(threshold)
@number threshold 滑动判定阈值(像素)
@return bool 设置成功返回true失败返回false
@usage
-- 设置滑动阈值为50像素
extp.set_swipe_threshold(50)
]]
function extp.set_swipe_threshold(threshold)
if type(threshold) == "number" and threshold > 0 then
swipe_threshold = threshold
log.info("extp", "滑动判定阈值设置为:", threshold)
return true
else
log.error("extp", "无效的滑动阈值:", threshold)
return false
end
end
--[[
设置长按判定阈值
@api extp.set_long_press_threshold(threshold)
@number threshold 长按判定阈值(毫秒)
@return bool 设置成功返回true失败返回false
@usage
-- 设置长按阈值为800毫秒
extp.set_long_press_threshold(800)
]]
function extp.set_long_press_threshold(threshold)
if type(threshold) == "number" and threshold > 0 then
long_press_threshold = threshold
log.info("extp", "长按判定阈值设置为:", threshold)
return true
else
log.error("extp", "无效的长按阈值:", threshold)
return false
end
end
-- 触摸回调函数
-- 参数: tp_device-触摸设备对象, tp_data-触摸数据
local function tp_callback(tp_device, tp_data)
-- 发布原始数据
if publish_control.RAW_DATA then
sys.publish("BASE_TOUCH_EVENT", "RAW_DATA", tp_device, tp_data)
end
if type(tp_data[1]) ~= "table" then return end
local event_type = tp_data[1].event
local x = tp_data[1].x
local y = tp_data[1].y
-- 获取高精度时间戳
local _, ms_l = mcu.ticks2(1)
-- 使用系统时间戳或触摸数据中的时间戳
local timestamp = ms_l or tp_data[1].timestamp
if not event_type then return end
if event_type == 2 then -- 抬手事件
if state == "DOWN" or state == "MOVE" then
local moveX = x - touch_down_x
local moveY = y - touch_down_y
if moveX < -swipe_threshold then
if publish_control.SWIPE_LEFT then
sys.publish("BASE_TOUCH_EVENT", "SWIPE_LEFT", moveX, 0)
end
elseif moveX > swipe_threshold then
if publish_control.SWIPE_RIGHT then
sys.publish("BASE_TOUCH_EVENT", "SWIPE_RIGHT", moveX, 0)
end
elseif moveY < -swipe_threshold then
if publish_control.SWIPE_UP then
sys.publish("BASE_TOUCH_EVENT", "SWIPE_UP", 0, moveY)
end
elseif moveY > swipe_threshold then
if publish_control.SWIPE_DOWN then
sys.publish("BASE_TOUCH_EVENT", "SWIPE_DOWN", 0, moveY)
end
else
-- 计算按下时间
local press_time = timestamp - touch_down_time
-- 判断是单击还是长按
if press_time < long_press_threshold then
if publish_control.SINGLE_TAP then
sys.publish("BASE_TOUCH_EVENT", "SINGLE_TAP", touch_down_x, touch_down_y)
end
else
if publish_control.LONG_PRESS then
sys.publish("BASE_TOUCH_EVENT", "LONG_PRESS", touch_down_x, touch_down_y)
end
end
end
state = "IDLE"
end
elseif event_type == 1 or event_type == 3 then -- 按下或移动事件
if state == "IDLE" and event_type == 1 then
-- 从空闲状态接收到按下事件
state = "DOWN"
touch_down_x = x
touch_down_y = y
touch_down_time = timestamp
swipe_direction = nil
-- 发布按下事件
if publish_control.TOUCH_DOWN then
sys.publish("BASE_TOUCH_EVENT", "TOUCH_DOWN", x, y)
end
elseif state == "DOWN" and event_type == 3 then
-- 在按下状态下接收到移动事件
if math.abs(x - touch_down_x) >= swipe_threshold or math.abs(y - touch_down_y) >= swipe_threshold then
state = "MOVE"
-- 确定滑动方向
if math.abs(x - touch_down_x) > math.abs(y - touch_down_y) then
-- 水平滑动
if x - touch_down_x < 0 then
swipe_direction = "LEFT"
else
swipe_direction = "RIGHT"
end
else
-- 垂直滑动
if y - touch_down_y < 0 then
swipe_direction = "UP"
else
swipe_direction = "DOWN"
end
end
end
elseif state == "MOVE" and event_type == 3 then
-- 在移动状态下接收到移动事件
-- 根据滑动方向发布相应的移动事件
if swipe_direction == "LEFT" or swipe_direction == "RIGHT" then
-- 水平滑动发布MOVE_X事件
if publish_control.MOVE_X then
sys.publish("BASE_TOUCH_EVENT", "MOVE_X", x - touch_down_x, 0)
end
else
-- 垂直滑动发布MOVE_Y事件
if publish_control.MOVE_Y then
sys.publish("BASE_TOUCH_EVENT", "MOVE_Y", 0, y - touch_down_y)
end
end
end
end
end
--[[
初始化触摸设备
@api extp.init(param)
@table param 触摸芯片配置参数参考库的说明及demo用法
@return bool 初始化成功返回true失败返回false
@usage
-- 基础触摸初始化
extp.init({
tp_model = "gt911",
i2c_id = 0,
pin_rst = 20,
pin_int = 21
})
-- 使用预定义配置
extp.init({tp_model = "AirLCD_1010"})
-- 带屏幕尺寸的初始化
extp.init({
tp_model = "gt911",
i2c_id = 0,
pin_rst = 20,
pin_int = 21,
w = 480,
h = 320
})
]]
function extp.init(param)
if type(param) ~= "table" then
log.error("extp", "参数必须为表")
return false
end
-- 检查必要参数
if not param.tp_model then
log.error("extp", "缺少必要参数: tp_model")
return false
end
local tp_model = param.tp_model
-- 检查是否支持该型号
local config = tp_configs[tp_model]
if not config then
log.error("extp", "不支持的触摸型号:", tp_model)
return false
end
-- 特殊型号参数处理
local final_param = {}
final_param.tp_model = tp_model
-- 处理特殊型号的默认配置
if special_tp_configs[tp_model] then
local default_config = special_tp_configs[tp_model]
if tp_model == "Air780EHM_LCD_4" then
-- Air780EHM_LCD_4: 强制使用默认配置,忽略传入的其他参数
final_param.i2c_id = default_config.i2c_id
final_param.pin_rst = default_config.pin_rst
final_param.pin_int = default_config.pin_int
log.info("extp", "Air780EHM_LCD_4使用固定配置")
else
-- AirLCD_1010 和 AirLCD_1020: 使用传入参数,如果未传入则使用默认配置
final_param.i2c_id = param.i2c_id or default_config.i2c_id
final_param.pin_rst = param.pin_rst or default_config.pin_rst
final_param.pin_int = param.pin_int or default_config.pin_int
-- 记录使用的配置来源
if param.i2c_id then
log.info("extp", tp_model, "使用传入的i2c_id")
else
log.info("extp", tp_model, "使用默认i2c_id")
end
if param.pin_rst then
log.info("extp", tp_model, "使用传入的pin_rst")
else
log.info("extp", tp_model, "使用默认pin_rst")
end
if param.pin_int then
log.info("extp", tp_model, "使用传入的pin_int")
else
log.info("extp", tp_model, "使用默认pin_int")
end
end
else
-- 其他型号:直接使用传入参数
final_param.i2c_id = param.i2c_id
final_param.pin_rst = param.pin_rst
final_param.pin_int = param.pin_int
end
local tp_i2c_id, tp_pin_rst, tp_pin_int = final_param.i2c_id, final_param.pin_rst or 255, final_param.pin_int
-- 构建tp.init的参数表增加w和h可选参数
local tp_init_params = {
port = tp_i2c_id,
pin_rst = tp_pin_rst,
pin_int = tp_pin_int
}
-- 如果传入了w和h参数则添加到参数表中
if param.w then
tp_init_params.w = param.w
log.info("extp", "设置屏幕宽度:", param.w)
end
if param.h then
tp_init_params.h = param.h
log.info("extp", "设置屏幕高度:", param.h)
end
-- 统一初始化流程
if type(tp_i2c_id) ~= "userdata" and config.i2c_speed ~= nil then
i2c.setup(tp_i2c_id, config.i2c_speed)
end
-- 使用包含w和h参数的table调用tp.init
local tp_device = tp.init(config.tp_model, tp_init_params, tp_callback)
if tp_device ~= nil then return true end
if tp_device == nil then
-- 如果第一次初始化失败尝试不带pin_rst的初始化
tp_init_params.pin_rst = 255
local tp_device = tp.init(config.tp_model, tp_init_params, tp_callback)
if tp_device ~= nil then return true end
end
-- 若硬件触摸初始化失败尝试PC触摸回退
log.warn("extp", "触摸初始化失败尝试PC触摸回退")
local ok_pc, dev_pc = pcall(tp.init, "pc", { port = 0 }, tp_callback)
if ok_pc and dev_pc then
log.info("extp", "PC触摸回退成功")
return true
end
log.error("extp", "PC触摸回退失败")
return false
end
return extp

View File

@@ -0,0 +1,313 @@
--[[
@module exvib
@summary exvib 三轴加速度传感器扩展库
@version 1.0
@date 2025.08.10
@author 李源龙
@usage
-- 用法实例
注意:
1. exvib.lua可适用于合宙内部集成了G-Sensor加速度传感器DA221的模组型号
目前仅有Air8000系列模组内置了DA221Air7000推出时也会内置该型号G-Sensor
2. DA221在Air8000内部通过I2C1与之通信并通过WAKEUP2接收运动监测中断
如您使用合宙其它型号模组外接DA221时比如Air780EGH建议与Air8000保持一致也选用I2C1和WAKEUP2
(该管脚即为Air780EGH的PIN79:USIM_DET)这样便可以无缝使用本扩展库DA221的供应商为苏州明皜
如需采购DA221或者其他更高端的加速度传感器可以联系他们
3. DA221作为加速度传感器LuatOS仅支持运动检测这一功能主要用于震动检测运动检测跌倒检测
搭配GNSS实现震动然后定位的功能其余功能请自行研究合宙提供了三种应用场景如果需要适配自己的场景需求
请参考手册参数自行修改代码调试适合自己场景的传感器值合宙不提供DA221任何其它功能的任何形式的技术支持
关于exvib库的三种模式主要用于以下场景
1微小震动检测用于检测轻微震动的场景例如用手敲击桌面加速度量程2g
2运动检测用于电动车或汽车行驶时的检测和人行走和跑步时的检测加速度量程4g
3跌倒检测用于人或物体瞬间跌倒时的检测加速度量程8g
exvib=require("exvib")
local intPin=gpio.WAKEUP2 --中断检测脚内部固定wakeup2
local tid --获取定时打开的定时器id
local num=0 --计数器
local ticktable={0,0,0,0,0} --存放5次中断的tick值用于做有效震动对比
local eff=false --有效震动标志位,用于判断是否触发定位
--有效震动模式
--tick计数器每秒+1用于存放5次中断的tick值用于做有效震动对比
-- local function tick()
-- num=num+1
-- end
-- --每秒运行一次计时
-- sys.timerLoopStart(tick,1000)
-- --有效震动判断
-- local function ind()
-- log.info("int", gpio.get(intPin))
-- if gpio.get(intPin) == 1 then
-- --接收数据如果大于5就删掉第一个
-- if #ticktable>=5 then
-- log.info("table.remove",table.remove(ticktable,1))
-- end
-- --存入新的tick值
-- table.insert(ticktable,num)
-- log.info("tick",num,(ticktable[5]-ticktable[1]<10),ticktable[5]>0)
-- log.info("tick2",ticktable[1],ticktable[2],ticktable[3],ticktable[4],ticktable[5])
-- --表长度为5且第5次中断时间间隔减去第一次间隔小于10s且第5次值为有效值
-- if #ticktable>=5 and (ticktable[5]-ticktable[1]<10 and ticktable[1]>0) then
-- log.info("vib", "xxx")
-- --是否要去触发有效震动逻辑
-- if eff==false then
-- sys.publish("EFFECTIVE_VIBRATION")
-- end
-- end
-- end
-- end
-- --设置30s分钟之后再判断是否有效震动函数
-- local function num_cb()
-- eff=false
-- end
-- local function eff_vib()
-- --触发之后eff设置为true30分钟之后再触发有效震动
-- eff=true
-- --30分钟之后再触发有效震动
-- sys.timerStart(num_cb,180000)
-- end
-- sys.subscribe("EFFECTIVE_VIBRATION",eff_vib)
--持续震动模式
--持续震动模式中断函数
local function ind()
log.info("int", gpio.get(intPin))
--上升沿为触发震动中断
if gpio.get(intPin) == 1 then
local x,y,z = exvib.read_xyz() --读取xyz轴的数据
log.info("x", x..'g', "y", y..'g', "z", z..'g')
end
end
local function vib_fnc()
-- 1微小震动检测用于检测轻微震动的场景例如用手敲击桌面加速度量程2g
-- 2运动检测用于电动车或汽车行驶时的检测和人行走和跑步时的检测加速度量程4g
-- 3跌倒检测用于人或物体瞬间跌倒时的检测加速度量程8g
--打开震动检测功能
exvib.open(1)
--设置gpio防抖100ms
gpio.debounce(intPin, 100)
--设置gpio中断触发方式wakeup2唤醒脚默认为双边沿触发
gpio.setup(intPin, ind)
end
sys.taskInit(vib_fnc)
]]
local exvib={}
local i2cId=0
local bsp=rtos.bsp()
if bsp:find("780") then
i2cId = 1
end
local da221Addr = 0x27
local soft_reset = {0x00, 0x24} -- 软件复位地址
local chipid_addr = 0x01 -- 芯片ID地址
local rangeaddr = {0x0f, 0x00} -- 设置加速度量程默认2g
-- local rangeaddr = {0x0f, 0x01} -- 设置加速度量程默认4g
-- local rangeaddr = {0x0f, 0x10} -- 设置加速度量程默认8g
local int_set1_reg = {0x16, 0x87} --设置x,y,z发生变化时产生中断
local int_set2_reg = {0x17, 0x10} --使能新数据中断,数据变化时,产生中断,本程序不设置
local int_map1_reg = {0x19, 0x04} --运动的时候,产生中断
local int_map2_reg = {0x1a, 0x01}
local active_dur_addr = {0x27, 0x01} -- 设置激活时间默认0x01
local active_ths_addr = {0x28, 0x33} -- 设置激活阈值,灵敏度最高
-- local active_ths_addr = {0x28, 0x80} -- 设置激活阈值,灵敏度适中
-- local active_ths_addr = {0x28, 0xFE} -- 设置激活阈值,灵敏度最低
local odr_addr = {0x10, 0x08} -- 设置采样率 100Hz
local mode_addr = {0x11, 0x00} -- 设置正常模式
local int_latch_addr = {0x21, 0x02} -- 设置中断锁存
local x_lsb_reg = 0x02 -- X轴LSB寄存器地址
local x_msb_reg = 0x03 -- X轴MSB寄存器地址
local y_lsb_reg = 0x04 -- Y轴LSB寄存器地址
local y_msb_reg = 0x05 -- Y轴MSB寄存器地址
local z_lsb_reg = 0x06 -- Z轴LSB寄存器地址
local z_msb_reg = 0x07 -- Z轴MSB寄存器地址
local active_state = 0x0b -- 激活状态寄存器地址
local active_state_data
local rangemode=1
local x_accel
local y_accel
local z_accel
--[[
获取da221的xyz轴数据
@api exvib.read_xyz()
@return number x轴数据number y轴数据number z轴数据
@usage
local x,y,z = exvib.read_xyz() --读取xyz轴的数据
log.info("x", x..'g', "y", y..'g', "z", z..'g')
]]
function exvib.read_xyz()
-- da221是LSB在前MSB在后每个寄存器都是1字节数据每次读取都是6个寄存器数据一起获取
-- 因此直接从X轴LSB寄存器(0x02)开始连续读取6字节数据(X/Y/Z各2字节),避免出现数据撕裂问题
i2c.send(i2cId, da221Addr, x_lsb_reg, 1)
local recv_data = i2c.recv(i2cId, da221Addr, 6)
-- LSB数据格式为: D[3] D[2] D[1] D[0] unused unused unused unused
-- MSB数据格式为: D[11] D[10] D[9] D[8] D[7] D[6] D[5] D[4]
-- 数据位为12位需要将MSB数据左移4位LSB数据右移4位最后进行或运算
-- 解析X轴数据 (LSB在前MSB在后)
local x_data = (string.byte(recv_data, 2) << 4) | (string.byte(recv_data, 1) >> 4)
-- 解析Y轴数据 (LSB在前MSB在后)
local y_data = (string.byte(recv_data, 4) << 4) | (string.byte(recv_data, 3) >> 4)
-- 解析Z轴数据 (LSB在前MSB在后)
local z_data = (string.byte(recv_data, 6) << 4) | (string.byte(recv_data, 5) >> 4)
-- 转换为12位有符号整数
-- 判断X轴数据是否大于2047若大于则表示数据为负数
-- 因为12位有符号整数的范围是 -2048 到 2047原始数据为无符号形式大于2047的部分需要转换为负数
-- 通过减去4096 (2^12) 将无符号数转换为对应的有符号负数
if x_data > 2047 then x_data = x_data - 4096 end
-- 判断Y轴数据是否大于2047若大于则进行同样的有符号转换
if y_data > 2047 then y_data = y_data - 4096 end
-- 判断Z轴数据是否大于2047若大于则进行同样的有符号转换
if z_data > 2047 then z_data = z_data - 4096 end
-- 转换为加速度值单位g
if rangemode == 1 then
x_accel = x_data / 1024
y_accel = y_data / 1024
z_accel = z_data / 1024
elseif rangemode == 2 then
x_accel = x_data / 512
y_accel = y_data / 512
z_accel = z_data / 512
elseif rangemode == 3 then
x_accel = x_data / 256
y_accel = y_data / 256
z_accel = z_data / 256
else
x_accel = x_data / 1024
y_accel = y_data / 1024
z_accel = z_data / 1024
end
-- 输出加速度值单位g
return x_accel, y_accel, z_accel
end
--初始化da221
local function da221_init()
if bsp:find("780") then
gpio.setup(23, 1, gpio.PULLUP) -- gsensor 开关
else
gpio.setup(24, 1, gpio.PULLUP) -- gsensor 开关
end
--关闭i2c
i2c.close(i2cId)
--重新打开i2c,i2c速度设置为低速
i2c.setup(i2cId, i2c.SLOW)
sys.wait(50)
i2c.send(i2cId, da221Addr, soft_reset, 1) --复位da221
sys.wait(50)
i2c.send(i2cId, da221Addr, chipid_addr, 1) --读取芯片id
local chipid = i2c.recv(i2cId, da221Addr, 1) --接收返回的芯片id
log.info("i2c", "chipid",chipid:toHex())
if string.byte(chipid) == 0x13 then
log.info("exvib init success")
else
log.info("exvib init fail")
end
-- 设置寄存器
i2c.send(i2cId, da221Addr, rangeaddr, 1) --设置加速度量程默认2g
sys.wait(5)
i2c.send(i2cId, da221Addr, int_set1_reg, 1) --设置x,y,z发生变化时产生中断
sys.wait(5)
i2c.send(i2cId, da221Addr, int_map1_reg, 1)--运动的时候,产生中断
sys.wait(5)
i2c.send(i2cId, da221Addr, active_dur_addr, 1)-- 设置激活时间默认0x00
sys.wait(5)
i2c.send(i2cId, da221Addr, active_ths_addr, 1)-- 设置激活阈值
sys.wait(5)
i2c.send(i2cId, da221Addr, mode_addr, 1)-- 设置模式
sys.wait(5)
i2c.send(i2cId, da221Addr, odr_addr, 1)-- 设置采样率
sys.wait(5)
i2c.send(i2cId, da221Addr, int_latch_addr, 1)-- 设置中断锁存 中断一旦触发将保持,直到手动清除
sys.wait(5)
end
--[[
打开da221
@api exvib.open(mode)
@number da221模式设置1微小震动检测用于检测轻微震动的场景例如用手敲击桌面加速度量程2g
2运动检测用于电动车或汽车行驶时的检测和人行走和跑步时的检测加速度量程4g
3跌倒检测用于人或物体瞬间跌倒时的检测加速度量程8g
@return nil 无返回值
@usage
exvib.open(1)
]]
function exvib.open(mode)
rangemode=mode
if mode==1 or tonumber(mode)==1 then
--轻微检测
log.info("轻微检测")
rangeaddr = {0x0f, 0x00} -- 设置加速度量程默认2g
active_ths_addr = {0x28, 0x33} -- 设置激活阈值
odr_addr = {0x10, 0x04} -- 设置采样率 15.63Hz
active_dur_addr = {0x27, 0x01} -- 设置激活时间
elseif mode==2 or tonumber(mode)==2 then
--常规检测
log.info("运动检测")
rangeaddr = {0x0f, 0x01} -- 设置加速度量程默认4g
active_ths_addr = {0x28, 0x26} -- 设置激活阈值
odr_addr = {0x10, 0x08} -- 设置采样率 250Hz
active_dur_addr = {0x27, 0x14} -- 设置激活时间
elseif mode==3 or tonumber(mode)==3 then
log.info("高动态检测")
--高动态检测
rangeaddr = {0x0f, 0x02} -- 设置加速度量程默认8g
active_ths_addr = {0x28, 0x80} -- 设置激活阈值
odr_addr = {0x10, 0x0F} -- 设置采样率 1000Hz
active_dur_addr = {0x27, 0x04} -- 设置激活时间
end
sys.taskInit(da221_init)
end
--[[
关闭da221
@api exvib.close()
@return nil 无返回值
@usage
exvib.close()
]]
function exvib.close()
if bsp:find("780") then
gpio.close(23) -- gsensor供电关闭
else
gpio.close(24) -- gsensor供电关闭
end
gpio.close(24) -- gsensor供电关闭
log.info("exvib close..")
end
return exvib

View File

@@ -0,0 +1,211 @@
--[[
@summary exvib1扩展库
@version 1.0
@date 2025.09.07
@author 孟伟
@usage
-- 应用场景
此库适用于滚珠震动传感器BL_2529,主要目的是对振动中断进行过滤,识别有效震动
对于一些震动传感器的中断管脚算法处理,也可以用做参考。
实现的功能:
1. GPIO 中断检测:通过 GPIO 引脚检测震动传感器产生的脉冲信号
2. 双重消抖机制:
- io中断消抖 gpio.debounce()
3. 时间窗口检测:在指定时间窗口(time_window)内统计脉冲数量
4. 阈值触发:当脉冲数超过设定阈值(pulse_threshold)时触发回调
5. 脉冲超时机制在检测状态下如果超过pulse_timeout时间没有新的脉冲则提前结束当前检测周期并判断是否触发回调
状态机工作流程:
- IDLE状态等待第一个有效脉冲
- DETECTING状态进入检测窗口统计脉冲数量
- 触发条件:
时间窗口结束
脉冲空闲时间超过设定超时
- 结果判断:脉冲数≥阈值则调用用户回调
-- 用法实例
本扩展库对外提供了以下2个接口
1启动震动检测功能 exvib1.open(opts)
2停止震动检测功能 exvib1.close()
--加载exvib1扩展库
local exvib1= require "exvib1"
-- 震动事件回调
local function vibration_cb(pulse_cnt)
log.info("VIB", "detected! pulses =", pulse_cnt)
end
--演示最简单的使用方法,都使用默认配置
exvib1.open({
gpio_pin = 24,
on_event = vibration_cb,
})
以下为exvib1扩展库两个函数的详细说明及代码实现
]]
local exvib1 = {}
-- 默认配置
local cfg = {
gpio_pin = nil, -- 传感器中断所接 GPIO
pull = gpio.PULLUP,
trigger = gpio.RISING,
debounce_irq = 100, -- gpio消抖时间gpio.debounce 时间(ms)
time_window = 1000, -- 检测窗口(ms)
pulse_threshold = 3, -- 触发阈值
pulse_timeout = 200, -- 脉冲超时(ms)
poll_interval = 10, -- 状态机轮询(ms)
on_event = nil, -- 用户回调
}
-- 内部状态
local st = {
pulse_cnt = 0,
last_valid = 0,
detect_t0 = 0,
state = "IDLE",
}
-- 重置内部状态,将状态机置为空闲状态并清零脉冲计数
local function reset()
st.state = "IDLE"
st.pulse_cnt = 0
end
-- GPIO 中断处理函数,用于处理传感器的脉冲信号
local function isr()
local now = mcu.ticks()
st.pulse_cnt = st.pulse_cnt + 1
st.last_valid = now
-- 如果当前状态为空闲状态
if st.state == "IDLE" then
-- 切换到检测状态
st.state = "DETECTING"
-- 记录检测开始时间
st.detect_t0 = now
end
end
-- 状态机处理函数,用于检测是否满足震动触发条件
local function fsm()
-- 如果当前状态不是检测状态,则直接返回
if st.state ~= "DETECTING" then return end
local now = mcu.ticks()
-- 处理时间戳溢出情况
if now < st.detect_t0 or now < st.last_valid then
st.detect_t0 = 0
st.last_valid = 0
return -- 等待下次调用重新判断
end
-- 计算从检测开始到现在经过的时间
local elapsed = now - st.detect_t0
-- 判断是否脉冲空闲时间过长
local idle_too_long = (now - st.last_valid) >= cfg.pulse_timeout
-- 当检测窗口结束或者脉冲空闲时间过长时
if elapsed >= cfg.time_window or idle_too_long then
-- 检查脉冲计数是否达到触发阈值,并且用户回调函数存在
if st.pulse_cnt >= cfg.pulse_threshold and st.on_event then
-- 调用用户回调函数并传入脉冲计数值
st.on_event(st.pulse_cnt)
end
-- 重置内部状态
reset()
end
end
--[[
启动震动检测功能
@api exvib1.open(opts)
@table opts 配置参数表,用于自定义震动检测功能的各项属性。
@return nil 无返回值
@usage
-- 配置参数介绍
--local otps = {
-- gpio_pin --"传感器中断所接 GPIO 引脚号,默认值为 nil",
-- pull --"上拉/下拉模式,可选 gpio.PULLUP 或 gpio.PULLDOWN默认值为 gpio.PULLUP",
-- trigger --"触发方式,可选 gpio.RISING 或 gpio.FALLING默认值为 gpio.RISING",
-- debounce_irq --"GPIO 消抖时间,单位为毫秒,默认值为 100",
-- time_window --"检测窗口时间,单位为毫秒,默认值为 1000",
-- pulse_threshold --"触发阈值,即连续脉冲次数,默认值为 3",
-- pulse_timeout --"脉冲超时时间,单位为毫秒,默认值为 200",
-- poll_interval --"状态机轮询时间,单位为毫秒,默认值为 10",
-- on_event --"用户回调函数,用于处理检测到的震动事件,默认值为 nil",
--}
-- 震动事件回调
local function vibration_cb(pulse_cnt)
log.info("VIB", "detected! pulses =", pulse_cnt)
end
exvib1.open({
gpio_pin = 24,
on_event = vibration_cb,
})
--不同场景下的参数配置可参考下面的示例
--高灵敏度,响应快,误触可能高
exvib1.open({
gpio_pin = 24,
on_event = vibration_cb,
time_window = 300, -- 检测窗口(ms)
pulse_threshold = 1, -- 触发阈值
pulse_timeout = 100, -- 脉冲超时(ms)
})
--默认配置,较高灵敏度
exvib1.open({
gpio_pin = 24,
on_event = vibration_cb,
time_window = 1000, -- 检测窗口(ms)
pulse_threshold = 3, -- 触发阈值
pulse_timeout = 200, -- 脉冲超时(ms)
})
--中等灵敏度,
exvib1.open({
gpio_pin = 24,
on_event = vibration_cb,
time_window = 2000, -- 检测窗口(ms)
pulse_threshold = 3, -- 触发阈值
pulse_timeout = 300, -- 脉冲超时(ms)
})
--低灵敏度,减少误报
exvib1.open({
gpio_pin = 24,
on_event = vibration_cb,
time_window = 3000, -- 检测窗口(ms)
pulse_threshold = 10, -- 触发阈值
pulse_timeout = 500, -- 脉冲超时(ms)
})
]]
-- 启动震动检测功能
function exvib1.open(opts)
-- 如果没有传入配置参数,则使用空表
opts = opts or {}
-- 用传入的配置参数更新默认配置
for k, v in pairs(opts) do cfg[k] = v end
-- 更新用户回调函数,如果传入了新的回调则使用新的,否则保持原有回调
st.on_event = opts.on_event or st.on_event
-- 配置 GPIO 消抖时间,设置中断处理函数、上拉模式和触发方式
gpio.debounce(cfg.gpio_pin, cfg.debounce_irq)
gpio.setup(cfg.gpio_pin, isr, cfg.pull, cfg.trigger)
-- 启动定时器循环调用状态机处理函数
sys.timerLoopStart(fsm, cfg.poll_interval)
log.info("Vibration", "start on gpio", cfg.gpio_pin)
end
--[[
关闭震动检测功能
@api exvib1.close()
@return nil 无返回值
@usage
exvib1.close() --关闭震动检测功能
--]]
function exvib1.close()
-- 关闭 GPIO 引脚
gpio.close(cfg.gpio_pin)
-- 停止定时器
sys.timerStop(fsm)
reset()
end
return exvib1

View File

@@ -0,0 +1,55 @@
local config = {
mode = 1,
is_msb = 1,
rx_bit = 2,
seq_type = 1,
is_ddr = 0x00010101,
i2c_slave_addr = 0x21,
width = 640,
height = 480,
init_cmds = {{0xfe, 0xf0}, {0xfe, 0xf0}, {0xfe, 0x00}, {0xfc, 0x16}, {0xfc, 0x16}, {0xf2, 0x07}, {0xf3, 0x83},
{0xf5, 0x07}, {0xf7, 0x88}, {0xf8, 0x00}, {0xf9, 0x4f}, {0xfa, 0x11}, {0xfc, 0xce}, {0xfd, 0x00},
{0x00, 0x2f}, {0x01, 0x0f}, {0x02, 0x04}, {0x03, 0x02}, {0x04, 0x12}, {0x09, 0x00}, {0x0a, 0x00},
{0x0b, 0x00}, {0x0c, 0x04}, {0x0d, 0x01}, {0x0e, 0xe8}, {0x0f, 0x02}, {0x10, 0x88}, {0x16, 0x00},
{0x17, 0x14}, {0x18, 0x1a}, {0x19, 0x14}, {0x1b, 0x48}, {0x1c, 0x6c}, {0x1e, 0x6b}, {0x1f, 0x28},
{0x20, 0x8b}, {0x21, 0x49}, {0x22, 0xd0}, {0x23, 0x04}, {0x24, 0xff}, {0x34, 0x20}, {0x26, 0x23},
{0x28, 0xff}, {0x29, 0x00}, {0x32, 0x04}, {0x33, 0x10}, {0x37, 0x20}, {0x38, 0x10}, {0x47, 0x80},
{0x4e, 0x66}, {0xa8, 0x02}, {0xa9, 0x80}, {0x40, 0xff}, {0x41, 0x21}, {0x42, 0xcf}, {0x44, 0x00},
{0x45, 0xa0}, {0x46, 0x02}, {0x4a, 0x11}, {0x4b, 0x01}, {0x4c, 0x20}, {0x4d, 0x05}, {0x4f, 0x01},
{0x50, 0x01}, {0x55, 0x01}, {0x56, 0xe0}, {0x57, 0x02}, {0x58, 0x80}, {0x70, 0x70}, {0x5a, 0x84},
{0x5b, 0xc9}, {0x5c, 0xed}, {0x77, 0x74}, {0x78, 0x40}, {0x79, 0x5f}, {0x82, 0x14}, {0x83, 0x0b},
{0x89, 0xf0}, {0x8f, 0xaa}, {0x90, 0x8c}, {0x91, 0x90}, {0x92, 0x03}, {0x93, 0x03}, {0x94, 0x05},
{0x95, 0x65}, {0x96, 0xf0}, {0xfe, 0x00}, {0x9a, 0x20}, {0x9b, 0x80}, {0x9c, 0x40}, {0x9d, 0x80},
{0xa1, 0x30}, {0xa2, 0x32}, {0xa4, 0x30}, {0xa5, 0x30}, {0xaa, 0x10}, {0xac, 0x22}, {0xfe, 0x00},
{0xbf, 0x08}, {0xc0, 0x16}, {0xc1, 0x28}, {0xc2, 0x41}, {0xc3, 0x5a}, {0xc4, 0x6c}, {0xc5, 0x7a},
{0xc6, 0x96}, {0xc7, 0xac}, {0xc8, 0xbc}, {0xc9, 0xc9}, {0xca, 0xd3}, {0xcb, 0xdd}, {0xcc, 0xe5},
{0xcd, 0xf1}, {0xce, 0xfa}, {0xcf, 0xff}, {0xd0, 0x40}, {0xd1, 0x34}, {0xd2, 0x34}, {0xd3, 0x40},
{0xd6, 0xf2}, {0xd7, 0x1b}, {0xd8, 0x18}, {0xdd, 0x03}, {0xfe, 0x01}, {0x05, 0x30}, {0x06, 0x75},
{0x07, 0x40}, {0x08, 0xb0}, {0x0a, 0xc5}, {0x0b, 0x11}, {0x0c, 0x00}, {0x12, 0x52}, {0x13, 0x38},
{0x18, 0x95}, {0x19, 0x96}, {0x1f, 0x20}, {0x20, 0xc0}, {0x3e, 0x40}, {0x3f, 0x57}, {0x40, 0x7d},
{0x03, 0x60}, {0x44, 0x00}, {0xfe, 0x01}, {0x1c, 0x91}, {0x21, 0x15}, {0x50, 0x80}, {0x56, 0x04},
{0x59, 0x08}, {0x5b, 0x02}, {0x61, 0x8d}, {0x62, 0xa7}, {0x63, 0xd0}, {0x65, 0x06}, {0x66, 0x06},
{0x67, 0x84}, {0x69, 0x08}, {0x6a, 0x25}, {0x6b, 0x01}, {0x6c, 0x00}, {0x6d, 0x02}, {0x6e, 0xf0},
{0x6f, 0x80}, {0x76, 0x80}, {0x78, 0xaf}, {0x79, 0x75}, {0x7a, 0x40}, {0x7b, 0x50}, {0x7c, 0x0c},
{0x90, 0xc9}, {0x91, 0xbe}, {0x92, 0xe2}, {0x93, 0xc9}, {0x95, 0x1b}, {0x96, 0xe2}, {0x97, 0x49},
{0x98, 0x1b}, {0x9a, 0x49}, {0x9b, 0x1b}, {0x9c, 0xc3}, {0x9d, 0x49}, {0x9f, 0xc7}, {0xa0, 0xc8},
{0xa1, 0x00}, {0xa2, 0x00}, {0x86, 0x00}, {0x87, 0x00}, {0x88, 0x00}, {0x89, 0x00}, {0xa4, 0xb9},
{0xa5, 0xa0}, {0xa6, 0xba}, {0xa7, 0x92}, {0xa9, 0xba}, {0xaa, 0x80}, {0xab, 0x9d}, {0xac, 0x7f},
{0xae, 0xbb}, {0xaf, 0x9d}, {0xb0, 0xc8}, {0xb1, 0x97}, {0xb3, 0xb7}, {0xb4, 0x7f}, {0xb5, 0x00},
{0xb6, 0x00}, {0x8b, 0x00}, {0x8c, 0x00}, {0x8d, 0x00}, {0x8e, 0x00}, {0x94, 0x55}, {0x99, 0xa6},
{0x9e, 0xaa}, {0xa3, 0x0a}, {0x8a, 0x00}, {0xa8, 0x55}, {0xad, 0x55}, {0xb2, 0x55}, {0xb7, 0x05},
{0x8f, 0x00}, {0xb8, 0xcb}, {0xb9, 0x9b}, {0xfe, 0x01}, {0xd0, 0x38}, {0xd1, 0x00}, {0xd2, 0x02},
{0xd3, 0x04}, {0xd4, 0x38}, {0xd5, 0x12}, {0xd6, 0x30}, {0xd7, 0x00}, {0xd8, 0x0a}, {0xd9, 0x16},
{0xda, 0x39}, {0xdb, 0xf8}, {0xfe, 0x01}, {0xc1, 0x3c}, {0xc2, 0x50}, {0xc3, 0x00}, {0xc4, 0x40},
{0xc5, 0x30}, {0xc6, 0x30}, {0xc7, 0x10}, {0xc8, 0x00}, {0xc9, 0x00}, {0xdc, 0x20}, {0xdd, 0x10},
{0xdf, 0x00}, {0xde, 0x00}, {0x01, 0x10}, {0x0b, 0x31}, {0x0e, 0x50}, {0x0f, 0x0f}, {0x10, 0x6e},
{0x12, 0xa0}, {0x15, 0x60}, {0x16, 0x60}, {0x17, 0xe0}, {0xcc, 0x0c}, {0xcd, 0x10}, {0xce, 0xa0},
{0xcf, 0xe6}, {0x45, 0xf7}, {0x46, 0xff}, {0x47, 0x15}, {0x48, 0x03}, {0x4f, 0x60}, {0xfe, 0x00},
{0x05, 0x01}, {0x06, 0x32}, {0x07, 0x00}, {0x08, 0x0c}, {0xfe, 0x01}, {0x25, 0x00}, {0x26, 0x3c},
{0x27, 0x01}, {0x28, 0xdc}, {0x29, 0x01}, {0x2a, 0xe0}, {0x2b, 0x01}, {0x2c, 0xe0}, {0x2d, 0x01},
{0x2e, 0xe0}, {0x3c, 0x20}, -- SPI配置
{0xfe, 0x03}, {0x52, 0xa2}, {0x53, 0x24}, {0x54, 0x20}, {0x55, 0x00}, {0x59, 0x1f}, {0x5a, 0x00}, {0x5b, 0x80},
{0x5c, 0x02}, {0x5d, 0xe0}, {0x5e, 0x01}, {0x51, 0x03}, {0x64, 0x04}, {0xfe, 0x00}, {0x44, 0x02}}
}
return config

View File

@@ -0,0 +1,60 @@
local config = {
mode = 1,
is_msb = 1,
rx_bit = 2,
seq_type = 1,
is_ddr = 0x00010101,
i2c_slave_addr = 0x21,
width = 640,
height = 480,
init_cmds = {{0xf3, 0x83}, {0xf5, 0x08}, {0xf7, 0x01}, {0xf8, 0x01}, {0xf9, 0x4e}, {0xfa, 0x00}, {0xfc, 0x02},
{0xfe, 0x02}, {0x81, 0x03}, {0xfe, 0x00}, {0x77, 0x64}, {0x78, 0x40}, {0x79, 0x60}, {0xfe, 0x00},
{0x03, 0x01}, {0x04, 0xcb}, {0x05, 0x01}, {0x06, 0xb2}, {0x07, 0x00}, {0x08, 0x10}, {0x0a, 0x00},
{0x0c, 0x00}, {0x0d, 0x01}, {0x0e, 0xe8}, {0x0f, 0x02}, {0x10, 0x88}, {0x17, 0x54}, {0x19, 0x08},
{0x1a, 0x0a}, {0x1f, 0x40}, {0x20, 0x30}, {0x2e, 0x80}, {0x2f, 0x2b}, {0x30, 0x1a}, {0xfe, 0x02},
{0x03, 0x02}, {0x05, 0xd7}, {0x06, 0x60}, {0x08, 0x80}, {0x12, 0x89}, {0xfe, 0x03}, {0x52, 0xba},
{0x53, 0x24}, {0x54, 0x20}, {0x55, 0x00}, {0x59, 0x1f}, {0x5a, 0x00}, {0x5b, 0x80}, {0x5c, 0x02},
{0x5d, 0xe0}, {0x5e, 0x01}, {0x51, 0x03}, {0x64, 0x04}, {0xfe, 0x00}, {0xfe, 0x00}, {0x18, 0x02},
{0xfe, 0x02}, {0x40, 0x22}, {0x45, 0x00}, {0x46, 0x00}, {0x49, 0x20}, {0x4b, 0x3c}, {0x50, 0x20},
{0x42, 0x10}, {0xfe, 0x01}, {0x0a, 0xc5}, {0x45, 0x00}, {0xfe, 0x00}, {0x40, 0xff}, {0x41, 0x25},
{0x42, 0xef}, {0x43, 0x10}, {0x44, 0x83}, {0x46, 0x22}, {0x49, 0x03}, {0x52, 0x02}, {0x54, 0x00},
{0xfe, 0x02}, {0x22, 0xf6}, {0xfe, 0x01}, {0xc1, 0x38}, {0xc2, 0x4c}, {0xc3, 0x00}, {0xc4, 0x2c},
{0xc5, 0x24}, {0xc6, 0x18}, {0xc7, 0x28}, {0xc8, 0x11}, {0xc9, 0x15}, {0xca, 0x20}, {0xdc, 0x7a},
{0xdd, 0xa0}, {0xde, 0x80}, {0xdf, 0x88}, {0xfe, 0x01}, {0x50, 0xc1}, {0x56, 0x34}, {0x58, 0x04},
{0x65, 0x06}, {0x66, 0x0f}, {0x67, 0x04}, {0x69, 0x20}, {0x6a, 0x40}, {0x6b, 0x81}, {0x6d, 0x12},
{0x6e, 0xc0}, {0x7b, 0x2a}, {0x7c, 0x0c}, {0xfe, 0x01}, {0x90, 0xe3}, {0x91, 0xc2}, {0x92, 0xff},
{0x93, 0xe3}, {0x95, 0x1c}, {0x96, 0xff}, {0x97, 0x44}, {0x98, 0x1c}, {0x9a, 0x44}, {0x9b, 0x1c},
{0x9c, 0x64}, {0x9d, 0x44}, {0x9f, 0x71}, {0xa0, 0x64}, {0xa1, 0x00}, {0xa2, 0x00}, {0x86, 0x00},
{0x87, 0x00}, {0x88, 0x00}, {0x89, 0x00}, {0xa4, 0xc2}, {0xa5, 0x9b}, {0xa6, 0xc8}, {0xa7, 0x92},
{0xa9, 0xc9}, {0xaa, 0x96}, {0xab, 0xa9}, {0xac, 0x99}, {0xae, 0xce}, {0xaf, 0xa9}, {0xb0, 0xcf},
{0xb1, 0x9d}, {0xb3, 0xcf}, {0xb4, 0xac}, {0xb5, 0x00}, {0xb6, 0x00}, {0x8b, 0x00}, {0x8c, 0x00},
{0x8d, 0x00}, {0x8e, 0x00}, {0x94, 0x55}, {0x99, 0xa6}, {0x9e, 0xaa}, {0xa3, 0x0a}, {0x8a, 0x00},
{0xa8, 0x55}, {0xad, 0x55}, {0xb2, 0x55}, {0xb7, 0x05}, {0x8f, 0x00}, {0xb8, 0xc7}, {0xb9, 0xa0},
{0xfe, 0x01}, {0xd0, 0x40}, {0xd1, 0x00}, {0xd2, 0x00}, {0xd3, 0xfa}, {0xd4, 0x4a}, {0xd5, 0x02},
{0xd6, 0x44}, {0xd7, 0xfa}, {0xd8, 0x04}, {0xd9, 0x08}, {0xda, 0x5c}, {0xdb, 0x02}, {0xfe, 0x00},
{0xfe, 0x00}, {0xba, 0x00}, {0xbb, 0x04}, {0xbc, 0x0a}, {0xbd, 0x0e}, {0xbe, 0x22}, {0xbf, 0x30},
{0xc0, 0x3d}, {0xc1, 0x4a}, {0xc2, 0x5d}, {0xc3, 0x6b}, {0xc4, 0x7a}, {0xc5, 0x85}, {0xc6, 0x90},
{0xc7, 0xa5}, {0xc8, 0xb5}, {0xc9, 0xc2}, {0xca, 0xcc}, {0xcb, 0xd5}, {0xcc, 0xde}, {0xcd, 0xea},
{0xce, 0xf5}, {0xcf, 0xff}, {0xfe, 0x00}, {0x5a, 0x08}, {0x5b, 0x0f}, {0x5c, 0x15}, {0x5d, 0x1c},
{0x5e, 0x28}, {0x5f, 0x36}, {0x60, 0x45}, {0x61, 0x51}, {0x62, 0x6a}, {0x63, 0x7d}, {0x64, 0x8d},
{0x65, 0x98}, {0x66, 0xa2}, {0x67, 0xb5}, {0x68, 0xc3}, {0x69, 0xcd}, {0x6a, 0xd4}, {0x6b, 0xdc},
{0x6c, 0xe3}, {0x6d, 0xf0}, {0x6e, 0xf9}, {0x6f, 0xff}, {0xfe, 0x00}, {0x70, 0x50}, {0xfe, 0x00},
{0x4f, 0x01}, {0xfe, 0x01}, {0x0c, 0x01}, {0x0d, 0x00}, {0x12, 0xa0}, {0x13, 0x38}, {0x1f, 0x40},
{0x20, 0x40}, {0x23, 0x0a}, {0x26, 0x9a}, {0x3e, 0x20}, {0x3f, 0x2d}, {0x40, 0x40}, {0x41, 0x5b},
{0x42, 0x82}, {0x43, 0xb7}, {0x04, 0x0a}, {0x02, 0x79}, {0x03, 0xc0}, {0xfe, 0x01}, {0xcc, 0x08},
{0xcd, 0x08}, {0xce, 0xa4}, {0xcf, 0xec}, {0xfe, 0x00}, {0x81, 0xb8}, {0x82, 0x04}, {0x83, 0x10},
{0x84, 0x01}, {0x86, 0x50}, {0x87, 0x18}, {0x88, 0x10}, {0x89, 0x70}, {0x8a, 0x20}, {0x8b, 0x10},
{0x8c, 0x08}, {0x8d, 0x0a}, {0xfe, 0x00}, {0x8f, 0xaa}, {0x90, 0x1c}, {0x91, 0x52}, {0x92, 0x03},
{0x93, 0x03}, {0x94, 0x08}, {0x95, 0x6a}, {0x97, 0x00}, {0x98, 0x00}, {0xfe, 0x00}, {0x9a, 0x30},
{0x9b, 0x50}, {0xa1, 0x30}, {0xa2, 0x66}, {0xa4, 0x28}, {0xa5, 0x30}, {0xaa, 0x28}, {0xac, 0x32},
{0xfe, 0x00}, {0xd1, 0x3f}, {0xd2, 0x3f}, {0xd3, 0x38}, {0xd6, 0xf4}, {0xd7, 0x1d}, {0xdd, 0x72},
{0xde, 0x84}, {0xfe, 0x00}, {0x05, 0x01}, {0x06, 0xad}, {0x07, 0x00}, {0x08, 0x10}, {0xfe, 0x01},
{0x25, 0x00}, {0x26, 0x4d}, {0x27, 0x01}, {0x28, 0xce}, {0x29, 0x01}, {0x2a, 0xce}, {0x2b, 0x01},
{0x2c, 0xce}, {0x2d, 0x01}, {0x2e, 0xce}, {0x2f, 0x01}, {0x30, 0xce}, {0x31, 0x01}, {0x32, 0xce},
{0x33, 0x01}, {0x34, 0xce}, {0x3c, 0x10}, {0xfe, 0x00}, {0x44, 0x03}}
}
return config

View File

@@ -0,0 +1,79 @@
--[[
@module httpdns
@summary 使用Http进行域名解析
@version 1.0
@date 2023.07.13
@author wendal
@usage
-- 通过阿里DNS获取结果
local ip = httpdns.ali("air32.cn")
log.info("httpdns", "air32.cn", ip)
-- 通过腾讯DNS获取结果
local ip = httpdns.tx("air32.cn")
log.info("httpdns", "air32.cn", ip)
]]
local httpdns = {}
--[[
通过阿里DNS获取结果
@api httpdns.ali(domain_name, opts)
@string 域名
@table opts 可选参数, 与http.request的opts参数一致
@return string ip地址
@usage
local ip = httpdns.ali("air32.cn")
log.info("httpdns", "air32.cn", ip)
-- 指定网络适配器
local ip = httpdns.ali("air32.cn", {adapter=socket.LWIP_STA, timeout=3000})
log.info("httpdns", "air32.cn", ip)
]]
function httpdns.ali(n, opts)
if n == nil then return end
if opts == nil then
opts = {timeout=3000}
elseif opts.timeout == nil then
opts.timeout = 3000
end
local code, _, body = http.request("GET", "http://223.5.5.5/resolve?short=1&name=" .. tostring(n), nil, nil, opts).wait()
if code == 200 and body and #body > 2 then
local jdata = json.decode(body)
if jdata and #jdata > 0 then
return jdata[1]
end
end
end
--[[
通过腾讯DNS获取结果
@api httpdns.tx(domain_name, opts)
@string 域名
@table opts 可选参数, 与http.request的opts参数一致
@return string ip地址
@usage
local ip = httpdns.tx("air32.cn")
log.info("httpdns", "air32.cn", ip)
-- 指定网络适配器
local ip = httpdns.tx("air32.cn", {adapter=socket.LWIP_STA, timeout=3000})
log.info("httpdns", "air32.cn", ip)
]]
function httpdns.tx(n, opts)
if n == nil then return end
if opts == nil then
opts = {timeout=3000}
elseif opts.timeout == nil then
opts.timeout = 3000
end
local code, _, body = http.request("GET", "http://119.29.29.29/d?dn=" .. tostring(n), nil, nil, opts).wait()
if code == 200 and body and #body > 2 then
local tmp = body:split(",")
if tmp then return tmp[1] end
end
end
return httpdns

View File

@@ -0,0 +1,714 @@
--[[
@module httpplus
@summary http库的补充
@version 1.0
@date 2023.11.23
@author wendal
@demo httpplus
@tag LUAT_USE_NETWORK
@usage
-- 本库支持的功能有:
-- 1. 大文件上传的问题,不限大小
-- 2. 任意长度的header设置
-- 3. 任意长度的body设置
-- 4. 鉴权URL自动识别
-- 5. body使用zbuff返回,可直接传输给uart等库
-- 与http库的差异
-- 1. 不支持文件下载
-- 2. 不支持fota
-- 支持 http 1.0 和 http 1.1, 不支持http2.0
-- 支持 GET/POST/PUT/DELETE/HEAD 等常用方法,也支持自定义method
-- 支持 HTTP 和 HTTPS 协议
-- 支持 IPv4 和 IPv6
-- 支持 HTTP 鉴权
-- 支持 multipart/form-data 上传文件和表单
-- 支持 application/x-www-form-urlencoded 上传表单
-- 支持 application/json 上传json数据
-- 支持 自定义 body 上传任意数据
-- 支持 自定义 headers
-- 支持 大文件上传,不限大小
-- 支持 zbuff 作为 body 上传和响应返回
-- 支持 bodyfile 直接把文件内容作为body上传
-- 支持 上传时使用自定义缓冲区, 2025.9.25 新增
]]
local httpplus = {}
local TAG = "httpplus"
local function http_opts_parse(opts)
if not opts then
log.error(TAG, "opts不能为nil")
return -100, "opts不能为nil"
end
if not opts.url or #opts.url < 5 then
log.error(TAG, "URL不存在或者太短了", opts.url)
return -100, "URL不存在或者太短了"
end
if not opts.headers then
opts.headers = {}
end
if opts.debug or httpplus.debug then
if not opts.log then
opts.log = log.debug
end
else
opts.log = function()
-- log.info(TAG, "无日志")
end
end
-- 解析url
-- 先判断协议是否加密
local is_ssl = false
local tmp = ""
if opts.url:startsWith("https://") then
is_ssl = true
tmp = opts.url:sub(9)
elseif opts.url:startsWith("http://") then
tmp = opts.url:sub(8)
else
tmp = opts.url
end
-- log.info("http分解阶段1", is_ssl, tmp)
-- 然后判断host段
local uri = ""
local host = ""
local port = 0
if tmp:find("/") then
uri = tmp:sub((tmp:find("/"))) -- 注意find会返回多个值
tmp = tmp:sub(1, tmp:find("/") - 1)
else
uri = "/"
end
-- log.info("http分解阶段2", is_ssl, tmp, uri)
if tmp == nil or #tmp == 0 then
log.error(TAG, "非法的URL", opts.url)
return -101, "非法的URL"
end
-- 有无鉴权信息
if tmp:find("@") then
local auth = tmp:sub(1, tmp:find("@") - 1)
if not opts.headers["Authorization"] then
opts.headers["Authorization"] = "Basic " .. auth:toBase64()
end
-- log.info("http鉴权信息", auth, opts.headers["Authorization"])
tmp = tmp:sub(tmp:find("@") + 1)
end
-- 解析端口
if tmp:find(":") then
host = tmp:sub(1, tmp:find(":") - 1)
port = tmp:sub(tmp:find(":") + 1)
port = tonumber(port)
else
host = tmp
end
if not port or port < 1 then
if is_ssl then
port = 443
else
port = 80
end
end
-- 收尾工作
if not opts.headers["Host"] then
if (is_ssl and port == 443) or ((not is_ssl) and port == 80) then
opts.headers["Host"] = host
else
opts.headers["Host"] = string.format("%s:%d", host, port)
end
end
-- Connection 必须关闭
opts.headers["Connection"] = "Close"
-- 复位一些变量,免得判断出错
opts.is_closed = nil
opts.body_len = 0
-- multipart需要boundary
local boundary = "------------------------16ef6e68ef" .. tostring(os.time())
opts.boundary = boundary
opts.mp = {}
if opts.files then
-- 强制设置为true
opts.multipart = true
end
-- 表单数据
if opts.forms then
if opts.multipart then
for kk, vv in pairs(opts.forms) do
local tmp = string.format("--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n", boundary, kk)
table.insert(opts.mp, {vv, tmp, "form"})
opts.body_len = opts.body_len + #tmp + #vv + 2
-- log.info("当前body长度", opts.body_len, "数据长度", #vv)
end
else
if not opts.headers["Content-Type"] then
opts.headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8"
end
local buff = zbuff.create(256)
for kk, vv in pairs(opts.forms) do
buff:copy(nil, string.urlEncode(tostring(kk)))
buff:copy(nil, "=")
buff:copy(nil, string.urlEncode(tostring(vv)))
buff:copy(nil, "&")
end
if buff:used() > 0 then
buff:del(-1, 1)
opts.body = buff
opts.body_len = buff:used()
opts.log(TAG, "普通表单", opts.body)
end
end
end
if opts.files then
-- 强制设置为true
opts.multipart = true
local contentType =
{
txt = "text/plain", -- 文本
jpg = "image/jpeg", -- JPG 格式图片
jpeg = "image/jpeg", -- JPEG 格式图片
png = "image/png", -- PNG 格式图片
gif = "image/gif", -- GIF 格式图片
html = "text/html", -- HTML
json = "application/json", -- JSON
mp4 = "video/mp4", -- MP4 格式视频
mp3 = "audio/mp3", -- MP3 格式音频
webm = "video/webm", -- WebM 格式视频
}
for kk, vv in pairs(opts.files) do
local ct = contentType[vv:match("%.(%w+)$")] or "application/octet-stream"
local fname = vv:match("([^/\\]+)$") or vv
local tmp = string.format("--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n", boundary, kk, fname, ct)
-- log.info("文件传输头", tmp)
table.insert(opts.mp, {vv, tmp, "file"})
opts.body_len = opts.body_len + #tmp + io.fileSize(vv) + 2
-- log.info("当前body长度", opts.body_len, "文件长度", io.fileSize(vv), fname, ct)
end
end
-- 如果multipart模式
if opts.multipart then
-- 如果没主动设置body, 那么补个结尾
if not opts.body then
opts.body_len = opts.body_len + #boundary + 2 + 2 + 2
end
-- Content-Type没设置? 那就设置一下
if not opts.headers["Content-Type"] then
opts.headers["Content-Type"] = "multipart/form-data; boundary="..boundary
end
end
-- 直接设置bodyfile
if opts.bodyfile then
local fd = io.open(opts.bodyfile, "rb")
if not fd then
log.error("httpplus", "bodyfile失败,文件不存在", opts.bodyfile)
return -104, "bodyfile失败,文件不存在"
end
fd:close()
opts.body_len = io.fileSize(opts.bodyfile)
end
-- 有设置body, 而且没设置长度
if opts.body and (not opts.body_len or opts.body_len == 0) then
-- body是zbuff的情况
if type(opts.body) == "userdata" then
opts.body_len = opts.body:used()
-- body是json的情况
elseif type(opts.body) == "table" then
opts.body = json.encode(opts.body, "7f")
if opts.body then
opts.body_len = #opts.body
if not opts.headers["Content-Type"] then
opts.headers["Content-Type"] = "application/json;charset=UTF-8"
opts.log(TAG, "JSON", opts.body)
end
end
-- 其他情况就只能当文本了
else
opts.body = tostring(opts.body)
opts.body_len = #opts.body
end
end
-- 一定要设置Content-Length,而且强制覆盖客户自定义的值
-- opts.body_len = opts.body_len or 0
opts.headers["Content-Length"] = tostring(opts.body_len or 0)
-- 如果没设置method, 自动补齐
if not opts.method or #opts.method == 0 then
if opts.body_len > 0 then
opts.method = "POST"
else
opts.method = "GET"
end
else
-- 确保一定是大写字母
opts.method = opts.method:upper()
end
if opts.debug then
opts.log(TAG, is_ssl, host, port, uri, json.encode(opts.headers))
end
-- 把剩余的属性设置好
opts.host = host
opts.port = port
opts.uri = uri
opts.is_ssl = is_ssl
if not opts.timeout or opts.timeout == 0 then
opts.timeout = 30
end
return -- 成功完成,不需要返回值
end
local function zbuff_find(buff, str)
-- log.info("zbuff查找", buff:used(), #str)
if buff:used() < #str then
return
end
local maxoff = buff:used()
maxoff = maxoff - #str
local tmp = zbuff.create(#str)
tmp:write(str)
-- log.info("tmp数据", tmp:query():toHex())
for i = 0, maxoff, 1 do
local flag = true
for j = 0, #str - 1, 1 do
-- log.info("对比", i, j, string.char(buff[i+j]):toHex(), string.char(tmp[j]):toHex(), buff[i+j] ~= tmp[j])
if buff[i+j] ~= tmp[j] then
flag = false
break
end
end
if flag then
return i
end
end
end
local function resp_parse(opts)
-- log.info("这里--------")
local header_offset = zbuff_find(opts.rx_buff, "\r\n\r\n")
-- log.info("头部偏移量", header_offset)
if not header_offset then
log.warn(TAG, "没有检测到http响应头部,非法响应")
opts.resp_code = -198
return
end
local state_line_offset = zbuff_find(opts.rx_buff, "\r\n")
local state_line = opts.rx_buff:query(0, state_line_offset)
local tmp = state_line:split(" ")
if not tmp or #tmp < 2 then
log.warn(TAG, "非法的响应行", state_line)
opts.resp_code = -197
return
end
local code = tonumber(tmp[2])
if not code then
log.warn(TAG, "非法的响应码", tmp[2])
opts.resp_code = -196
return
end
opts.resp_code = code
opts.resp = {
headers = {}
}
opts.log(TAG, "state code", code)
-- TODO 解析header和body
opts.rx_buff:del(0, state_line_offset + 2)
-- opts.log(TAG, "剩余的响应体", opts.rx_buff:query())
-- 解析headers仅按首个冒号拆分保留值中的冒号
while 1 do
local offset = zbuff_find(opts.rx_buff, "\r\n")
if not offset then
log.warn(TAG, "不合法的剩余headers", opts.rx_buff:query())
break
end
if offset == 0 then
-- header的最后一个空行
opts.rx_buff:del(0, 2)
break
end
local line = opts.rx_buff:query(0, offset)
opts.rx_buff:del(0, offset + 2)
local name, value = line:match("^([^:]+):%s*(.*)$")
if name and value then
name = name:trim()
value = value:trim()
opts.log(TAG, name, value)
opts.resp.headers[name] = value
else
opts.log(TAG, "忽略非法header行", line)
end
end
-- if opts.resp_code < 299 then
-- 解析body
-- 有Content-Length就好办
if opts.resp.headers["Content-Length"] then
opts.log(TAG, "有Content-Length", opts.resp.headers["Content-Length"])
local declared = tonumber(opts.resp.headers["Content-Length"]) or 0
if declared > 0 and opts.rx_buff:used() >= declared then
opts.rx_buff:resize(declared)
end
opts.resp.body = opts.rx_buff
elseif opts.resp.headers["Transfer-Encoding"] == "chunked" then
-- 解析 chunked 编码:长度行(可含分号扩展)+ 数据 + CRLF末块长度为0
local function zbuff_find_from(buff, str, start_off)
local used = buff:used()
if used - start_off < #str then return end
local maxoff = used - #str
local tmp2 = zbuff.create(#str)
tmp2:write(str)
for i = start_off, maxoff, 1 do
local ok = true
for j = 0, #str - 1, 1 do
if buff[i+j] ~= tmp2[j] then ok = false; break end
end
if ok then return i end
end
end
local body = zbuff.create(opts.rx_buff:used())
local pos = 0
while true do
local line_end = zbuff_find_from(opts.rx_buff, "\r\n", pos)
if not line_end then
log.error(TAG, "非法的chunk长度行")
break
end
local len_line = opts.rx_buff:query(pos, line_end - pos)
local semi = len_line:find(";")
local hex = semi and len_line:sub(1, semi - 1) or len_line
local clen = tonumber(hex, 16)
if not clen then
log.error(TAG, "非法的chunk长度值", len_line)
break
end
pos = line_end + 2
if clen == 0 then
-- 末块:忽略后续 trailers
break
end
if pos + clen > opts.rx_buff:used() then
log.error(TAG, "chunk数据长度不足")
break
end
local chunk = opts.rx_buff:query(pos, clen)
body:copy(nil, chunk)
pos = pos + clen + 2 -- 跳过数据及其后的CRLF
end
opts.resp.body = body
end
-- end
-- 清空rx_buff
opts.rx_buff = nil
-- 完结散花
end
-- socket 回调函数
local function http_socket_cb(opts, event)
opts.log(TAG, "tcp.event", string.format("%08X", event))
if event == socket.ON_LINE then
-- TCP链接已建立, 那就可以上行了
-- opts.state = "ON_LINE"
sys.publish(opts.topic)
elseif event == socket.TX_OK then
-- 数据传输完成, 如果是文件上传就需要这个消息
-- opts.state = "TX_OK"
sys.publish(opts.topic)
elseif event == socket.EVENT then
-- 收到数据或者链接断开了, 这里总需要读取一次才知道
local succ, data_len = socket.rx(opts.netc, opts.rx_buff)
if succ and data_len > 0 then
opts.log(TAG, "收到数据", data_len, "总长", opts.rx_buff:used())
-- opts.log(TAG, "数据", opts.rx_buff:query())
else
if not opts.is_closed then
opts.log(TAG, "服务器已经断开了连接或接收出错")
opts.is_closed = true
sys.publish(opts.topic)
end
end
elseif event == socket.CLOSED then
log.info(TAG, "连接已关闭")
opts.is_closed = true
sys.publish(opts.topic)
end
end
local function http_exec(opts)
local fail_check = true
local netc = socket.create(opts.adapter, function(sc, event)
if opts.netc then
return http_socket_cb(opts, event)
end
end)
if not netc then
log.error(TAG, "创建socket失败了!!")
return -102
end
opts.netc = netc
opts.rx_buff = zbuff.create(1024)
opts.topic = tostring(netc)
socket.config(netc, nil,nil, opts.is_ssl)
if opts.debug_socket then
socket.debug(netc, true)
end
if not socket.connect(netc, opts.host, opts.port, opts.try_ipv6) then
log.warn(TAG, "调用socket.connect返回错误了")
return -103, "调用socket.connect返回错误了"
end
local ret = sys.waitUntil(opts.topic, 5000)
if ret == false then
log.warn(TAG, "建立连接超时了!!!")
return -104, "建立连接超时了!!!"
end
-- 首先是头部
local line = string.format("%s %s HTTP/1.1\r\n", opts.method:upper(), opts.uri)
-- opts.log(TAG, line)
socket.tx(netc, line)
for k, v in pairs(opts.headers) do
line = string.format("%s: %s\r\n", k, v)
socket.tx(netc, line)
end
line = "\r\n"
socket.tx(netc, line)
-- 然后是body
local rbody = ""
local write_counter = 0
local fbuf = nil
if (opts.mp and #opts.mp > 0) or opts.bodyfile or (opts.body and type(opts.body) == "userdata" and opts.body:used() > 4*1024) then
if opts.upload_file_buff then
fbuf = opts.upload_file_buff
else
if hmeta and hmeta.chip and hmeta.chip() == "EC718HM" then
fbuf = zbuff.create(1024 * 128, 0, zbuff.HEAP_PSRAM) -- 718hm可以128k的,放手去用
elseif hmeta and hmeta.chip and hmeta.chip() == "EC718PM" then
fbuf = zbuff.create(1024 * 64, 0, zbuff.HEAP_PSRAM) -- Air8101/7258可以128k的,放手去用
elseif hmeta and hmeta.chip and hmeta.chip() == "BK7258" then
fbuf = zbuff.create(1024 * 128, 0, zbuff.HEAP_PSRAM) -- Air8101/7258可以128k的,放手去用
else
fbuf = zbuff.create(1024 * 24, 0, zbuff.HEAP_PSRAM) -- 其他模组就是小的用吧
end
end
if fbuf == nil then
fbuf = zbuff.create(1024 * 8, 0, zbuff.HEAP_PSRAM) -- 创建一个小的,作为防御
if fbuf == nil then
fbuf = zbuff.create(1500, 0, zbuff.HEAP_PSRAM) -- 创建一个最小的,最后防御
end
end
opts.log(TAG, "上传使用缓冲区", fbuf:len())
end
if opts.mp and #opts.mp > 0 then
opts.log(TAG, "执行mulitpart上传模式")
for k, v in pairs(opts.mp) do
fail_check = socket.tx(netc, v[2])
write_counter = write_counter + #v[2]
if v[3] == "file" then
-- log.info("写入文件数据头", v[2])
local fd = io.open(v[1], "rb")
-- log.info("写入文件数据", v[1])
if fd then
local total = 0
while not opts.is_closed do
fbuf:seek(0)
local ok, flen = fd:fill(fbuf)
if not ok or flen <= 0 then
break
end
fbuf:seek(flen)
opts.log(TAG, "写入文件数据", "长度", flen, "总计", total)
if socket.tx(netc, fbuf) == false then
log.warn(TAG, "socket.tx返回错误了, 传送失败!!!!")
fail_check = false
break
end
write_counter = write_counter + flen
-- 注意, 这里要等待TX_OK事件
sys.waitUntil(opts.topic, 1000)
end
fd:close()
end
else
socket.tx(netc, v[1])
write_counter = write_counter + #v[1]
end
socket.tx(netc, "\r\n")
write_counter = write_counter + 2
end
-- rbody = rbody .. "--" .. opts.boundary .. "--\r\n"
socket.tx(netc, "--")
socket.tx(netc, opts.boundary)
socket.tx(netc, "--\r\n")
write_counter = write_counter + #opts.boundary + 2 + 2 + 2
elseif opts.bodyfile then
local fd = io.open(opts.bodyfile, "rb")
-- log.info("写入文件数据", v[1])
if fd then
local total = 0
while not opts.is_closed do
fbuf:seek(0)
local ok, flen = fd:fill(fbuf)
if not ok or flen <= 0 then
break
end
fbuf:seek(flen)
total = total + flen
opts.log(TAG, "写入文件数据", "长度", flen, "总计", total)
if socket.tx(netc, fbuf) == false then
log.warn(TAG, "socket.tx返回错误了, 传送失败!!!!")
fail_check = false
break
end
write_counter = write_counter + flen
-- 注意, 这里要等待TX_OK事件
sys.waitUntil(opts.topic, 1000)
end
fd:close()
end
elseif opts.body then
if type(opts.body) == "string" and #opts.body > 0 then
socket.tx(netc, opts.body)
write_counter = write_counter + #opts.body
elseif type(opts.body) == "userdata" then
opts.log(TAG, "使用zbuff上传数据", opts.body:used())
write_counter = write_counter + opts.body:used()
if opts.body:used() <= 4*1024 then
fail_check = socket.tx(netc, opts.body)
else
local offset = 0
local tmpbuff = opts.body
local tsize = tmpbuff:used()
while offset < tsize do
-- TODO 应该使用fbuf来做缓冲区而不是toStr
opts.log(TAG, "body(zbuff)分段写入", offset, tsize)
fbuf:seek(0)
if tsize - offset > fbuf:len() then
fbuf:copy(0, tmpbuff, offset, fbuf:len())
fbuf:seek(fbuf:len())
if socket.tx(netc, fbuf) == false then
log.warn(TAG, "socket.tx返回错误了, 传送失败!!!!")
fail_check = false
break
end
offset = offset + fbuf:len()
sys.waitUntil(opts.topic, 1000)
else
fbuf:copy(0, tmpbuff, offset, tsize - offset)
fbuf:seek(tsize - offset)
fail_check = socket.tx(netc, fbuf)
break
end
end
end
end
end
-- log.info("写入长度", "期望", opts.body_len, "实际", write_counter)
-- log.info("hex", rbody)
if not fail_check then
log.warn(TAG, "发送数据失败, 终止请求")
opts.resp_code = -199
return
end
-- 处理响应信息
while not opts.is_closed and opts.timeout > 0 do
log.info(TAG, "等待服务器完成响应")
sys.waitUntil(opts.topic, 1000)
opts.timeout = opts.timeout - 1
end
log.info(TAG, "服务器已完成响应,开始解析响应")
resp_parse(opts)
-- log.info("执行完成", "返回结果")
end
--[[
执行HTTP请求
@api httpplus.request(opts)
@table 请求参数,是一个table,最起码得有url属性
@return int 响应码,服务器返回的状态码>=100, 若本地检测到错误,会返回<0的值
@return 服务器正常响应时返回结果, 否则是错误信息或者nil
@usage
-- 请求参数介绍
local opts = {
url = "https://httpbin.air32.cn/abc", -- 必选, 目标URL
method = "POST", -- 可选,默认GET, 如果有body,files,forms参数,会设置成POST
headers = {}, -- 可选,自定义的额外header
files = {}, -- 可选,键值对的形式,文件上传,若存在本参数,会强制以multipart/form-data形式上传
forms = {}, -- 可选,键值对的形式,表单参数,若存在本参数,如果不存在files,按application/x-www-form-urlencoded上传
body = "abc=123",-- 可选,自定义body参数, 字符串/zbuff/table均可, 但不能与files和forms同时存在
debug = false, -- 可选,打开调试日志,默认false
try_ipv6 = false, -- 可选,是否优先尝试ipv6地址,默认是false
adapter = nil, -- 可选,网络适配器编号, 默认是自动选
timeout = 30, -- 可选,读取服务器响应的超时时间,单位秒,默认30
bodyfile = "xxx", -- 可选,直接把文件内容作为body上传, 优先级高于body参数
upload_file_buff = zbuff.create(1024*64) -- 可选,上传时使用的缓冲区,默认会根据型号创建一个buff
}
local code, resp = httpplus.request({url="https://httpbin.air32.cn/get"})
log.info("http", code)
-- 返回值resp的说明
-- 情况1, code >= 100 时, resp会是个table, 包含2个元素
if code >= 100 then
-- headers, 是个table
log.info("http", "headers", json.encode(resp.headers))
-- body, 是个zbuff
-- 通过query函数可以转为lua的string
log.info("http", "headers", resp.body:query())
-- 也可以通过uart.tx等支持zbuff的函数转发出去
-- uart.tx(1, resp.body)
end
-- 情况2, code < 0 时, resp会是个错误信息字符串
-- 对upload_file_buff参数的说明
-- 1. 如果上传的文件比较大,建议传入这个参数,避免每次都创建和销毁缓冲区
-- 2. 如果不传入这个参数,本库会根据不同的模组型号创建一个合适的缓冲区
-- 3. 多个同时执行的httpplus请求,不可以共用同一个缓冲区
]]
function httpplus.request(opts)
-- 参数解析
local ret = http_opts_parse(opts)
if ret then
return ret
end
-- 执行请求
local ret, msg = pcall(http_exec, opts)
if opts.netc then
-- 清理连接
if not opts.is_closed then
socket.close(opts.netc)
end
socket.release(opts.netc)
opts.netc = nil
end
-- 处理响应或错误
if not ret then
log.error(TAG, msg)
return -199, msg
end
return opts.resp_code, opts.resp
end
return httpplus

View File

@@ -0,0 +1,283 @@
--[[
@module lbsLoc
@summary lbsLoc 发送基站定位请求
@version 1.0
@date 2022.12.16
@author luatos
@usage
-- lbsloc 是异步回调接口,
-- lbsloc2 是是同步接口。
-- lbsloc比lbsloc2多了一个请求地址文本的功能。
-- lbsloc 和 lbsloc2 都是免费LBS定位的实现方式
-- airlbs 扩展库是收费 LBS 的实现方式。
--注意:因使用了sys.wait()所有api需要在协程中使用
--用法实例
--注意此处的PRODUCT_KEY仅供演示使用不能用于生产环境
--量产项目中一定要使用自己在iot.openluat.com中创建的项目productKey,项目详情里可以查看
--基站定位的坐标系是 WSG84
PRODUCT_KEY = "123"
local lbsLoc = require("lbsLoc")
-- 功能:获取基站对应的经纬度后的回调函数
-- 参数:-- resultnumber类型0表示成功1表示网络环境尚未就绪2表示连接服务器失败3表示发送数据失败4表示接收服务器应答超时5表示服务器返回查询失败为0时后面的5个参数才有意义
-- latstring类型纬度整数部分3位小数部分7位例如031.2425864
-- lngstring类型经度整数部分3位小数部分7位例如121.4736522
-- addr目前无意义
-- timestring类型或者nil服务器返回的时间6个字节年月日时分秒需要转为十六进制读取
-- 第一个字节年减去2000例如2017年则为0x11
-- 第二个字节例如7月则为0x0712月则为0x0C
-- 第三个字节例如11日则为0x0B
-- 第四个字节例如18时则为0x12
-- 第五个字节例如59分则为0x3B
-- 第六个字节例如48秒则为0x30
-- locTypenumble类型或者nil定位类型0表示基站定位成功255表示WIFI定位成功
function getLocCb(result, lat, lng, addr, time, locType)
log.info("testLbsLoc.getLocCb", result, lat, lng)
-- 获取经纬度成功, 坐标系WGS84
if result == 0 then
log.info("服务器返回的时间", time:toHex())
log.info("定位类型,基站定位成功返回0", locType)
end
end
sys.taskInit(function()
sys.waitUntil("IP_READY", 30000)
while 1 do
mobile.reqCellInfo(15)
sys.waitUntil("CELL_INFO_UPDATE", 3000)
lbsLoc.request(getLocCb)
sys.wait(60000)
end
end)
]]
local sys = require "sys"
local sysplus = require("sysplus")
local libnet = require("libnet")
local lbsLoc = {}
local d1Name = "lbsLoc"
--- ASCII字符串 转化为 BCD编码格式字符串(仅支持数字)
-- @string inStr 待转换字符串
-- @number destLen 转换后的字符串期望长度如果实际不足则填充F
-- @return string data,转换后的字符串
-- @usage
local function numToBcdNum(inStr,destLen)
local l,t,num = string.len(inStr or ""),{}
destLen = destLen or (inStr:len()+1)/2
for i=1,l,2 do
num = tonumber(inStr:sub(i,i+1),16)
if i==l then
num = 0xf0+num
else
num = (num%0x10)*0x10 + (num-(num%0x10))/0x10
end
table.insert(t,num)
end
local s = string.char(unpack(t))
l = string.len(s)
if l < destLen then
s = s .. string.rep("\255",destLen-l)
elseif l > destLen then
s = string.sub(s,1,destLen)
end
return s
end
--- BCD编码格式字符串 转化为 号码ASCII字符串(仅支持数字)
-- @string num 待转换字符串
-- @return string data,转换后的字符串
-- @usage
local function bcdNumToNum(num)
local byte,v1,v2
local t = {}
for i=1,num:len() do
byte = num:byte(i)
v1,v2 = bit.band(byte,0x0f),bit.band(bit.rshift(byte,4),0x0f)
if v1 == 0x0f then break end
table.insert(t,v1)
if v2 == 0x0f then break end
table.insert(t,v2)
end
return table.concat(t)
end
local function netCB(msg)
--log.info("未处理消息", msg[1], msg[2], msg[3], msg[4])
end
local function enCellInfo(s)
local ret,t,mcc,mnc,lac,ci,rssi,k,v,m,n,cntrssi = "",{}
for k,v in pairs(s) do
mcc,mnc,lac,ci,rssi = v.mcc,v.mnc,v.tac,v.cid,((v.rsrq + 144) >31) and 31 or (v.rsrq + 144)
local handle = nil
for k,v in pairs(t) do
if v.lac == lac and v.mcc == mcc and v.mnc == mnc then
if #v.rssici < 8 then
table.insert(v.rssici,{rssi=rssi,ci=ci})
end
handle = true
break
end
end
if not handle then
table.insert(t,{mcc=mcc,mnc=mnc,lac=lac,rssici={{rssi=rssi,ci=ci}}})
end
log.debug("rssi,mcc,mnc,lac,ci", rssi,mcc,mnc,lac,ci)
end
for k,v in pairs(t) do
ret = ret .. pack.pack(">HHb",v.lac,v.mcc,v.mnc)
for m,n in pairs(v.rssici) do
cntrssi = bit.bor(bit.lshift(((m == 1) and (#v.rssici-1) or 0),5),n.rssi or n.rsrp)
ret = ret .. pack.pack(">bi",cntrssi,n.ci)
end
end
return string.char(#t)..ret
end
local function enWifiInfo(tWifi)
local ret,cnt = "", 0
if tWifi then
for k,v in pairs(tWifi) do
-- log.info("lbsLoc.enWifiInfo",k,v)
ret = ret..pack.pack("Ab",(k:gsub(":","")):fromHex(),(v<0) and (v+255) or v)
cnt = cnt+1
end
end
return string.char(cnt)..ret
end
local function enMuid() --获取模块MUID
local muid = mobile.muid()
return string.char(muid:len())..muid
end
local function trans(str)
local s = str
if str:len()<10 then
s = str..string.rep("0",10-str:len())
end
return s:sub(1,3).."."..s:sub(4,10)
end
local function taskClient(cbFnc, reqAddr, timeout, productKey, host, port,reqTime, reqWifi)
if mobile.status() == 0 then
if not sys.waitUntil("IP_READY", timeout) then return cbFnc(1) end
sys.wait(500)
end
if productKey == nil then
productKey = ""
end
local retryCnt = 0
local reqStr = pack.pack("bAbAAAAA", productKey:len(), productKey,
(reqAddr and 2 or 0) + (reqTime and 4 or 0) + 8 +(reqWifi and 16 or 0) + 32, "",
numToBcdNum(mobile.imei()), enMuid(),
enCellInfo(mobile.getCellInfo()),
enWifiInfo(reqWifi))
log.debug("reqStr", reqStr:toHex())
local rx_buff = zbuff.create(17)
-- sys.wait(5000)
while true do
local result,succ,param
local netc = socket.create(nil, d1Name) -- 创建socket对象
if not netc then cbFnc(6) return end -- 创建socket失败
socket.debug(netc, false)
socket.config(netc, nil, true, nil)
--result = libnet.waitLink(d1Name, 0, netc)
result = libnet.connect(d1Name, 5000, netc, host, port)
if result then
while true do
-- log.info(" lbsloc socket_service connect true")
result = libnet.tx(d1Name, 0, netc, reqStr) ---发送数据
if result then
result, param = libnet.wait(d1Name, 15000 + retryCnt * 5, netc)
if not result then
socket.close(netc)
socket.release(netc)
retryCnt = retryCnt+1
if retryCnt>=3 then return cbFnc(4) end
break
end
succ, param = socket.rx(netc, rx_buff) -- 接收数据
-- log.info("是否接收和数据长度", succ, param)
if param ~= 0 then -- 如果接收成功
socket.close(netc) -- 关闭连接
socket.release(netc)
local read_buff = rx_buff:toStr(0, param)
rx_buff:clear()
log.debug("lbsLoc receive", read_buff:toHex())
if read_buff:len() >= 11 and(read_buff:byte(1) == 0 or read_buff:byte(1) == 0xFF) then
local locType = read_buff:byte(1)
cbFnc(0, trans(bcdNumToNum(read_buff:sub(2, 6))),
trans(bcdNumToNum(read_buff:sub(7, 11))), reqAddr and
read_buff:sub(13, 12 + read_buff:byte(12)) or nil,
reqTime and read_buff:sub(reqAddr and (13 + read_buff:byte(12)) or 12, -1) or "",
locType)
else
log.warn("lbsLoc.query", "根据基站查询经纬度失败")
if read_buff:byte(1) == 2 then
log.warn("lbsLoc.query","main.lua中的PRODUCT_KEY和此设备在iot.openluat.com中所属项目的ProductKey必须一致请去检查")
else
log.warn("lbsLoc.query","基站数据库查询不到所有小区的位置信息")
-- log.warn("lbsLoc.query","在trace中向上搜索encellinfo然后在电脑浏览器中打开http://bs.openluat.com/手动查找encellinfo后的所有小区位置")
-- log.warn("lbsLoc.query","如果手动可以查到位置则服务器存在BUG直接向技术人员反映问题")
-- log.warn("lbsLoc.query","如果手动无法查到位置,则基站数据库还没有收录当前设备的小区位置信息,向技术人员反馈,我们会尽快收录")
end
cbFnc(5)
end
return
else
socket.close(netc)
socket.release(netc)
retryCnt = retryCnt+1
if retryCnt>=3 then return cbFnc(4) end
break
end
else
socket.close(netc)
socket.release(netc)
retryCnt = retryCnt+1
if retryCnt>=3 then return cbFnc(3) end
break
end
end
else
socket.close(netc)
socket.release(netc)
retryCnt = retryCnt + 1
if retryCnt >= 3 then return cbFnc(2) end
end
end
end
--[[
发送基站定位请求
@api lbsLoc.request(cbFnc,reqAddr,timeout,productKey,host,port,reqTime,reqWifi)
@function cbFnc 用户回调函数回调函数的调用形式为cbFnc(result,lat,lng,addr,time,locType)
@bool reqAddr 是否请求服务器返回具体的位置字符串信息,已经不支持,填false或者nil
@number timeout 请求超时时间单位毫秒默认20000毫秒
@string productKey IOT网站上的产品KEY如果在main.lua中定义了PRODUCT_KEY变量则此参数可以传nil
@string host 服务器域名, 默认 "bs.openluat.com" ,可选备用服务器(不保证可用) "bs.air32.cn"
@string port 服务器端口,默认"12411",一般不需要设置
@return nil 无返回值
@usage
-- 提醒: 返回的坐标值, 是WGS84坐标系
]]
function lbsLoc.request(cbFnc,reqAddr,timeout,productKey,host,port,reqTime,reqWifi)
sysplus.taskInitEx(taskClient, d1Name, netCB, cbFnc, reqAddr,timeout or 20000,productKey or _G.PRODUCT_KEY,host or "bs.openluat.com",port or "12411", reqTime == nil and true or reqTime,reqWifi)
end
return lbsLoc

View File

@@ -0,0 +1,229 @@
--[[
@module lbsLoc2
@summary 基站定位v2
@version 1.0
@date 2023.5.23
@author wendal
@demo lbsLoc2
@usage
-- lbsloc 是异步回调接口,
-- lbsloc2 是是同步接口。
-- lbsloc比lbsloc2多了一个请求地址文本的功能。
-- lbsloc 和 lbsloc2 都是免费LBS定位的实现方式
-- airlbs 扩展库是收费 LBS 的实现方式。
-- 注意:
-- 1. 因使用了sys.wait()所有api需要在协程中使用
-- 2. 仅支持单基站定位, 即当前联网的基站
-- 3. 本服务当前处于测试状态
sys.taskInit(function()
sys.waitUntil("IP_READY", 30000)
-- mobile.reqCellInfo(60)
-- sys.wait(1000)
while mobile do -- 没有mobile库就没有基站定位
mobile.reqCellInfo(15)
sys.waitUntil("CELL_INFO_UPDATE", 3000)
local lat, lng, t = lbsLoc2.request(5000)
-- local lat, lng, t = lbsLoc2.request(5000, "bs.openluat.com")
log.info("lbsLoc2", lat, lng, (json.encode(t or {})))
sys.wait(60000)
end
end)
]]
local sys = require "sys"
local lbsLoc2 = {}
local function numToBcdNum(inStr,destLen)
local l,t,num = string.len(inStr or ""),{}
destLen = destLen or (inStr:len()+1)/2
for i=1,l,2 do
num = tonumber(inStr:sub(i,i+1),16)
if i==l then
num = 0xf0+num
else
num = (num%0x10)*0x10 + (num-(num%0x10))/0x10
end
table.insert(t,num)
end
local s = string.char(unpack(t))
l = string.len(s)
if l < destLen then
s = s .. string.rep("\255",destLen-l)
elseif l > destLen then
s = string.sub(s,1,destLen)
end
return s
end
--- BCD编码格式字符串 转化为 号码ASCII字符串(仅支持数字)
-- @string num 待转换字符串
-- @return string data,转换后的字符串
-- @usage
local function bcdNumToNum(num)
local byte,v1,v2
local t = {}
for i=1,num:len() do
byte = num:byte(i)
v1,v2 = bit.band(byte,0x0f),bit.band(bit.rshift(byte,4),0x0f)
if v1 == 0x0f then break end
table.insert(t,v1)
if v2 == 0x0f then break end
table.insert(t,v2)
end
return table.concat(t)
end
lbsLoc2.imei = numToBcdNum(mobile.imei())
local function enCellInfo(s)
-- 改造成单基站, 反正服务器也只认单基站
local v = s[1]
log.info("cell", json.encode(v))
local ret = pack.pack(">HHbbi",v.tac,v.mcc,v.mnc,31,v.cid)
return string.char(1)..ret
end
local function trans(str)
local s = str
if str:len()<10 then
s = str..string.rep("0",10-str:len())
end
return s:sub(1,3).."."..s:sub(4,10)
end
--[[
执行定位请求
@api lbsLoc2.request(timeout, host, port, reqTime)
@number 请求超时时间,单位毫秒,默认15000
@number 服务器地址,有默认值,可以是域名,一般不需要填
@number 服务器端口,默认12411,一般不需要填
@bool 是否要求返回服务器时间
@return string 若成功,返回定位坐标的纬度,否则会返还nil
@return string 若成功,返回定位坐标的经度,否则会返还nil
@return table 服务器时间,东八区时间. 当reqTime为true且定位成功才会返回
@usage
-- 关于坐标系
-- 部分情况下会返回GCJ02坐标系, 部分情况返回的是WGS84坐标
-- 历史数据已经无法分辨具体坐标系
-- 鉴于两种坐标系之间的误差并不大,小于基站定位本身的误差, 纠偏的意义不大
sys.taskInit(function()
sys.waitUntil("IP_READY", 30000)
-- mobile.reqCellInfo(60)
-- sys.wait(1000)
while mobile do -- 没有mobile库就没有基站定位
mobile.reqCellInfo(15)
sys.waitUntil("CELL_INFO_UPDATE", 3000)
local lat, lng, t = lbsLoc2.request(5000)
-- local lat, lng, t = lbsLoc2.request(5000, "bs.openluat.com")
log.info("lbsLoc2", lat, lng, (json.encode(t or {})))
sys.wait(60000)
end
end)
]]
function lbsLoc2.request(timeout, host, port, reqTime)
if mobile.status() == 0 then
return
end
local hosts = host and {host} or {"free.bs.air32.cn", "bs.openluat.com"}
port = port and tonumber(port) or 12411
local sc = socket.create(nil, function(sc, event)
-- log.info("lbsLoc", "event", event, socket.ON_LINE, socket.TX_OK, socket.EVENT)
if event == socket.ON_LINE then
--log.info("lbsLoc", "已连接")
sys.publish("LBS_CONACK")
elseif event == socket.TX_OK then
--log.info("lbsLoc", "发送完成")
sys.publish("LBS_TX")
elseif event == socket.EVENT then
--log.info("lbsLoc", "有数据来")
sys.publish("LBS_RX")
end
end)
if sc == nil then
return
end
-- socket.debug(sc, true)
socket.config(sc, nil, true)
local rxbuff = zbuff.create(64)
for k, rhost in pairs(hosts) do
local reqStr = string.char(0, (reqTime and 4 or 0) +8) .. lbsLoc2.imei
local tmp = nil
if mobile.scell then
local scell = mobile.scell()
if scell and scell.mcc then
-- log.debug("lbsLoc2", "使用当前驻网基站的信息")
tmp = pack.pack(">bHHbbi", 1, scell.tac, scell.mcc, scell.mnc, 31, scell.eci)
end
end
if tmp == nil then
local cells = mobile.getCellInfo()
if cells == nil or #cells == 0 then
socket.release(sc)
return
end
reqStr = reqStr .. enCellInfo(cells)
else
reqStr = reqStr .. tmp
end
-- log.debug("lbsLoc2", "待发送数据", (reqStr:toHex()))
log.debug("lbsLoc2", rhost, port)
if socket.connect(sc, rhost, port) and sys.waitUntil("LBS_CONACK", 1000) then
if socket.tx(sc, reqStr) and sys.waitUntil("LBS_TX", 1000) then
socket.wait(sc)
if sys.waitUntil("LBS_RX", timeout or 15000) then
local succ, data_len = socket.rx(sc, rxbuff)
-- log.debug("lbsLoc", "rx", succ, data_len)
if succ and data_len > 0 then
socket.close(sc)
break
else
log.debug("lbsLoc", "rx数据失败", rhost)
end
else
log.debug("lbsLoc", "等待数据超时", rhost)
end
else
log.debug("lbsLoc", "tx调用失败或TX_ACK超时", rhost)
end
else
log.debug("lbsLoc", "connect调用失败或CONACK超时", rhost)
end
socket.close(sc)
--sys.wait(100)
end
sys.wait(100)
socket.release(sc)
if rxbuff:used() > 0 then
local resp = rxbuff:toStr(0, rxbuff:used())
log.debug("lbsLoc2", "rx", (resp:toHex()))
if resp:len() >= 11 and(resp:byte(1) == 0 or resp:byte(1) == 0xFF) then
local lat = trans(bcdNumToNum(resp:sub(2, 6)))
local lng = trans(bcdNumToNum(resp:sub(7, 11)))
local t = nil
if resp:len() >= 17 then
t = {
year=resp:byte(12) + 2000,
month=resp:byte(13),
day=resp:byte(14),
hour=resp:byte(15),
min=resp:byte(16),
sec=resp:byte(17),
}
end
return lat, lng, t
end
end
rxbuff:del()
end
return lbsLoc2

View File

@@ -0,0 +1,145 @@
--[[
@module libfota
@summary libfota fota升级
@version 1.0
@date 2023.02.01
@author Dozingfiretruck
@demo fota
@usage
--注意:因使用了sys.wait()所有api需要在协程中使用
--用法实例
local libfota = require("libfota")
-- 功能:获取fota的回调函数
-- 参数:
-- result:number类型
-- 0表示成功
-- 1表示连接失败
-- 2表示url错误
-- 3表示服务器断开
-- 4表示接收报文错误
-- 5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
function libfota_cb(result)
log.info("fota", "result", result)
-- fota成功
if result == 0 then
rtos.reboot() --如果还有其他事情要做,就不要立刻reboot
end
end
--注意!!!:使用合宙iot平台,必须用luatools量产生成的.bin文件!!! 自建服务器可使用.ota文件!!!
--注意!!!:使用合宙iot平台,必须用luatools量产生成的.bin文件!!! 自建服务器可使用.ota文件!!!
--注意!!!:使用合宙iot平台,必须用luatools量产生成的.bin文件!!! 自建服务器可使用.ota文件!!!
--下方示例为合宙iot平台,地址:http://iot.openluat.com
libfota.request(libfota_cb)
--如使用自建服务器,自行更换url
-- 对自定义服务器的要求是:
-- 若需要升级, 响应http 200, body为升级文件的内容
-- 若不需要升级, 响应300或以上的代码,务必注意
libfota.request(libfota_cb,"http://xxxxxx.com/xxx/upgrade?version=" .. _G.VERSION)
-- 若需要定时升级
-- 合宙iot平台
sys.timerLoopStart(libfota.request, 4*3600*1000, libfota_cb)
-- 自建平台
sys.timerLoopStart(libfota.request, 4*3600*1000, libfota_cb, "http://xxxxxx.com/xxx/upgrade?version=" .. _G.VERSION)
]]
local sys = require "sys"
local sysplus = require "sysplus"
local libfota = {}
local function fota_task(cbFnc,storge_location, len, param1,ota_url,ota_port,libfota_timeout,server_cert, client_cert, client_key, client_password, show_otaurl)
if cbFnc == nil then
cbFnc = function() end
end
-- 若ota_url没有传,那就是用合宙iot平台
if ota_url == nil then
if _G.PRODUCT_KEY == nil then
-- 必须在main.lua定义 PRODUCT_KEY = "xxx"
-- iot平台新建项目后, 项目详情中可以查到
log.error("fota", "iot.openluat.com need PRODUCT_KEY!!!")
cbFnc(5)
return
else
local x,y,z = string.match(_G.VERSION,"(%d+).(%d+).(%d+)")
if x and y and z then
local query = ""
local firmware_name = _G.PROJECT.. "_" .. rtos.firmware()
local version = _G.VERSION
if mobile then
query = "imei=" .. mobile.imei()
version = rtos.version():sub(2) .. "." .. x .. "." .. z
firmware_name = _G.PROJECT.. "_LuatOS-SoC_" .. rtos.bsp()
elseif wlan and wlan.getMac then
query = "mac=" .. wlan.getMac()
version = rtos.version():sub(2) .. "." .. x .. "." .. z
firmware_name = _G.PROJECT.. "_LuatOS-SoC_" .. rtos.bsp()
else
query = "uid=" .. mcu.unique_id():toHex()
end
local tmp = "http://iot.openluat.com/api/site/firmware_upgrade?project_key=%s&firmware_name=%s&version=%s&%s"
ota_url = string.format(tmp, _G.PRODUCT_KEY, firmware_name, version, query)
else
log.error("fota", "_G.VERSION must be xxx.yyy.zzz!!!")
cbFnc(5)
return
end
end
end
local ret
local opts = {timeout = libfota_timeout}
if fota then
opts.fota = true
else
os.remove("/update.bin")
opts.dst = "/update.bin"
end
if show_otaurl == nil or show_otaurl == true then
log.info("fota.url", ota_url)
end
local code, headers, body = http.request("GET", ota_url, nil, nil, opts, server_cert, client_cert, client_key, client_password).wait()
log.info("http fota", code, headers, body)
if code == 200 or code == 206 then
if body == 0 then
ret = 4
else
ret = 0
end
elseif code == -4 then
ret = 1
elseif code == -5 then
ret = 3
else
ret = 4
end
cbFnc(ret)
end
--[[
fota升级
@api libfota.request(cbFnc,ota_url,storge_location, len, param1,ota_port,libfota_timeout,server_cert, client_cert, client_key, client_password)
@function cbFnc 用户回调函数回调函数的调用形式为cbFnc(result) , 必须传
@string ota_url 升级URL, 若不填则自动使用合宙iot平台
@number/string storge_location 可选,fota数据存储的起始位置<br>如果是int则是由芯片平台具体判断<br>如果是string则存储在文件系统中<br>如果为nil则由底层决定存储位置
@number len 可选,数据存储的最大空间
@userdata param1,可选,如果数据存储在spiflash时,为spi_device
@number ota_port 可选,请求端口,默认80
@number libfota_timeout 可选,请求超时时间,单位毫秒,默认30000毫秒
@string server_cert 可选,服务器ca证书数据
@string client_cert 可选,客户端证书数据
@string client_key 可选,客户端私钥加密数据
@string client_password 可选,客户端私钥口令数据
@boolean show_otaurl 可选,是否从日志中输出打印OTA升级包的URL路径默认会打印
@return nil 无返回值
]]
function libfota.request(cbFnc,ota_url,storge_location, len, param1,ota_port,libfota_timeout,server_cert, client_cert, client_key, client_password, show_otaurl)
sys.taskInit(fota_task, cbFnc,storge_location, len, param1,ota_url, ota_port,libfota_timeout or 180000,server_cert, client_cert, client_key, client_password, show_otaurl)
end
return libfota

View File

@@ -0,0 +1,219 @@
--[[
@module libfota2
@summary fota升级v2
@version 1.1
@date 2024.11.22
@author wendal/HH
@demo fota2
@usage
--用法实例
local libfota2 = require("libfota2")
-- 功能:获取fota的回调函数
-- 参数:
-- result:number类型
-- 0表示成功
-- 1表示连接失败
-- 2表示url错误
-- 3表示服务器断开
-- 4表示接收报文错误
-- 5表示使用iot平台VERSION需要使用 xxx.yyy.zzz形式
function libfota_cb(result)
log.info("fota", "result", result)
-- fota成功
if result == 0 then
rtos.reboot() --如果还有其他事情要做,自行决定reboot的时机
end
end
--下方示例为合宙iot平台,地址:http://iot.openluat.com
libfota2.request(libfota_cb)
--如使用自建服务器,自行更换url
-- 对自定义服务器的要求是:
-- 若需要升级, 响应http 200, body为升级文件的内容
-- 若不需要升级, 响应300或以上的代码,务必注意
local opts = {url="http://xxxxxx.com/xxx/upgrade"}
-- opts的详细说明, 看后面的函数API文档
libfota2.request(libfota_cb, opts)
-- 若需要定时升级
-- 合宙iot平台
sys.timerLoopStart(libfota2.request, 4*3600*1000, libfota_cb)
-- 自建平台
sys.timerLoopStart(libfota2.request, 4*3600*1000, libfota_cb, opts)
]]
local sys = require "sys"
require "sysplus"
local libfota2 = {}
-- 单独判断下服务器下发的数据是不是"{"开头"}"结尾的字符串
local function isjson(str)
local start, _ = string.find(str, "^%{")
local _, end_ = string.find(str, "%}$")
return start == 1 and end_ == #str and string.sub(str, 2, #str - 1):find("%B{") == nil
end
local function fota_task(cbFnc, opts)
local ret = 0
local url = opts.url
local code, headers, body = http.request(opts.method, opts.url, opts.headers, opts.body, opts, opts.server_cert,
opts.client_cert, opts.client_key, opts.client_password).wait()
-- log.info("http fota", code, headers, body)
if code == 200 or code == 206 then
if body == 0 then
ret = 4
else
ret = 0
end
elseif code == -4 then
ret = 1
elseif code == -5 then
ret = 3
else
log.info("libfota2", code, body)
ret = 4
local hziot = "iot.openluat.com"
local msg, json_body, result
if string.find(url, hziot) then
log.info("使用合宙服务器,接下来解析body里的code")
json_body, result = json.decode(body)
-- 如果json解析失败证明服务器下发的不是json
if result == 1 and isjson(body) then
code = json_body["code"]
else
-- 这个值随便取的,只要不和其他定义重复就行
code = 1111111111111
end
if code == 43 then
log.info("请等待",
",云平台生成差分升级包需要等待,一到三分钟后云平台生成完成差分包便可以请求成功")
elseif code == 3 then
log.info("无效的设备", "检查请求键名(imei小写)正确性")
elseif code == 17 then
log.info("无权限",
"设备会上报imei、固件名、项目key,服务器会以此查出设备、固件、项目三 条记录,如果 这三者不在同一个用户名下就会认为无权限。设备不在项目key对应的账户下可寻找合宙技术支持查询该设备在哪个账户下核实情况后可修改设备归属")
elseif code == 21 then
log.info("不允许升级", "请检查IOT平台,是否对应imei被禁止了升级")
elseif code == 25 then
log.info("无效的项目",
"productkey不一致,检查是否存在拼写错误,检查模块是否在本人账户下,若不在本人账户下,请联系合宙工作人员处理")
elseif code == 26 then
log.info("无效的固件",
"固件名称错误,项目中没有对应的固件,也有可能是用户自己修改了固件名称,可对照升级日志中设备当前固件名与升级配置中固件名是否相同(固件名称,固件功能要完全一致,只是版本号不同)")
elseif code == 27 then
log.info("已是最新版本",
"1.设备的固件/脚本版本高于或等于云平台上的版本号 2.用户项目升级配置中未添加该设备 3.云平台升级配置中,是否升级配置为否")
elseif code == 40 then
log.info("循环升级",
"云平台进入设备列表搜索被禁止的imei,解除禁止升级即可. 云平台防止模块在升级失败后,反复请求升级导致流量卡流量耗尽,在模块一天请求升级六次后会禁止模块升级. 可在平台解除")
elseif code == 1111111111111 then
log.info("云平台下发的不是json", "我看看body是个什么东西", type(body), body)
else
log.info("不是上面的那些错误code", code)
end
end
end
cbFnc(ret)
end
--[[
fota升级
@api libfota2.request(cbFnc, opts)
@function cbFnc 用户回调函数回调函数的调用形式为cbFnc(result) , 必须传
@table fota参数, 后面有详细描述
@return nil 无返回值
@usaga
-- 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,默认不需要
]]
function libfota2.request(cbFnc, opts)
if not opts then
opts = {}
end
if fota then
opts.fota = true
else
os.remove("/update.bin")
opts.dst = "/update.bin"
end
if not cbFnc then
cbFnc = function(ret)
end
end
-- 处理URL
if not opts.url then
opts.url = "http://iot.openluat.com/api/site/firmware_upgrade?"
end
local query = ""
if opts.url:sub(1, 3) ~= "###" and not opts.url_done then
-- 补齐project_key函数
if not opts.project_key then
opts.project_key = _G.PRODUCT_KEY
if not opts.project_key then
log.error("libfota2", "iot.openluat.com need PRODUCT_KEY!!!")
cbFnc(5)
return
end
end
-- 补齐version参数
if not opts.version then
local x, y, z = string.match(_G.VERSION, "(%d+).(%d+).(%d+)")
opts.version = rtos.version():sub(2) .. "." .. x .. "." .. z
end
-- 补齐firmware_name参数
if not opts.firmware_name then
opts.firmware_name = _G.PROJECT .. "_LuatOS-SoC_" .. rtos.bsp()
end
-- 补齐imei参数
if not opts.imei then
if mobile then
query = "imei=" .. mobile.imei()
elseif wlan and wlan.getMac then
query = "mac=" .. wlan.getMac()
else
query = "uid=" .. mcu.unique_id():toHex()
end
end
-- 然后拼接到最终的url里
if not opts.imei then
opts.url = string.format("%s%s&project_key=%s&firmware_name=%s&version=%s", opts.url, query, opts.project_key, opts.firmware_name, opts.version)
else
opts.url = string.format("%simei=%s&project_key=%s&firmware_name=%s&version=%s", opts.url, opts.imei, opts.project_key, opts.firmware_name, opts.version)
end
else
if opts.url:sub(1,3)=="###" then
opts.url = opts.url:sub(4)
end
end
opts.url_done = true
-- 处理method
if not opts.method then
opts.method = "GET"
end
log.info("libfota2.url", opts.method, opts.url)
log.info("libfota2.imei/mac/uid", query)
log.info("libfota2.project_key", opts.project_key)
log.info("libfota2.firmware_name", opts.firmware_name)
log.info("libfota2.version", opts.version)
sys.taskInit(fota_task, cbFnc, opts)
end
return libfota2

View File

@@ -0,0 +1,168 @@
--[[
@module libnet
@summary libnet 在socket库基础上的同步阻塞apisocket库本身是异步非阻塞api
@version 1.0
@date 2023.03.16
@author lisiqi
]]
local libnet = {}
--[[
阻塞等待网卡的网络连接上只能用于sysplus.taskInitEx创建的任务函数中
@api libnet.waitLink(taskName,timeout,...)
@string 任务标志
@int 超时时间,如果==0或者空则没有超时一致等待
@... 其他参数和socket.linkup一致
@return boolean 失败或者超时返回false 成功返回true
]]
function libnet.waitLink(taskName, timeout, ...)
local succ, result = socket.linkup(...)
if not succ then
return false
end
if not result then
result = sysplus.waitMsg(taskName, socket.LINK, timeout)
else
return true
end
if type(result) == 'table' and result[2] == 0 then
return true
else
return false
end
end
--[[
阻塞等待IP或者域名连接上如果加密连接还要等握手完成只能用于sysplus.taskInitEx创建的任务函数中
@api libnet.connect(taskName,timeout,...)
@string 任务标志
@int 超时时间,如果==0或者空则没有超时一致等待
@... 其他参数和socket.connect一致
@return boolean 失败或者超时返回false 成功返回true
]]
function libnet.connect(taskName,timeout, ... )
local succ, result = socket.connect(...)
if not succ then
return false
end
if not result then
result = sysplus.waitMsg(taskName, socket.ON_LINE, timeout)
else
return true
end
if type(result) == 'table' and result[2] == 0 then
return true
else
return false
end
end
--[[
阻塞等待客户端连接上只能用于sysplus.taskInitEx创建的任务函数中
@api libnet.listen(taskName,timeout,...)
@string 任务标志
@int 超时时间,如果==0或者空则没有超时一致等待
@... 其他参数和socket.listen一致
@return boolean 失败或者超时返回false 成功返回true
]]
function libnet.listen(taskName,timeout, ... )
local succ, result = socket.listen(...)
if not succ then
return false
end
if not result then
result = sysplus.waitMsg(taskName, socket.ON_LINE, timeout)
else
return true
end
if type(result) == 'table' and result[2] == 0 then
return true
else
return false
end
end
--[[
阻塞等待数据发送完成只能用于sysplus.taskInitEx创建的任务函数中
@api libnet.tx(taskName,timeout,...)
@string 任务标志
@int 超时时间,如果==0或者空则没有超时一直等待
@... 其他参数和socket.tx一致
@return boolean 失败或者超时返回false缓冲区满了或者成功返回true
@return boolean 缓存区是否满了
]]
function libnet.tx(taskName,timeout, ...)
local succ, is_full, result = socket.tx(...)
if not succ then
return false, is_full
end
if is_full then
return true, true
end
if not result then
result = sysplus.waitMsg(taskName, socket.TX_OK, timeout)
else
return true, is_full
end
if type(result) == 'table' and result[2] == 0 then
return true, false
else
return false, is_full
end
end
--[[
阻塞等待新的网络事件只能用于sysplus.taskInitEx创建的任务函数中可以通过sysplus.sendMsg(taskName,socket.EVENT,0)或者sys_send(taskName,socket.EVENT,0)强制退出
@api libnet.wait(taskName,timeout, netc)
@string 任务标志
@int 超时时间,如果==0或者空则没有超时一致等待
@userdata socket.create返回的netc
@return boolean 网络异常返回false其他返回true
@return boolean 超时返回false有新的网络事件到返回true
]]
function libnet.wait(taskName,timeout, netc)
local succ, result = socket.wait(netc)
if not succ then
return false,false
end
if not result then
result = sysplus.waitMsg(taskName, socket.EVENT, timeout)
else
return true,true
end
if type(result) == 'table' then
if result[2] == 0 then
return true, true
else
return false, false
end
else
return true, false
end
end
--[[
阻塞等待网络断开连接只能用于sysplus.taskInitEx创建的任务函数中
@api libnet.close(taskName,timeout, netc)
@string 任务标志
@int 超时时间,如果==0或者空则没有超时一致等待
@userdata socket.create返回的netc
]]
function libnet.close(taskName,timeout, netc)
local succ, result = socket.discon(netc)
if not succ then
socket.close(netc)
return
end
if not result then
result = sysplus.waitMsg(taskName, socket.CLOSED, timeout)
else
socket.close(netc)
return
end
socket.close(netc)
end
return libnet

View File

@@ -0,0 +1,251 @@
--[[
@module netLed
@summary netLed 网络状态指示灯
@version 1.0
@date 2023.02.21
@author DingHeng
@usage
--注意:因使用了sys.wait()所有api需要在协程中使用
-- 用法实例
local netLed = require ("netLed")
local LEDA = gpio.setup(27,1,gpio.PULLUP) --LED引脚判断赋值结束
sys.taskInit(function()
--呼吸灯
sys.wait(5080)--延时5秒等待网络注册
log.info("mobile.status()", mobile.status())
while true do
if mobile.status() == 1 then--已注册
sys.wait(688)
netLed.setupBreateLed(LEDA)
end
end
end)
]]
netLed = {}
-- 引用sys库
local sys = require("sys")
local simError --SIM卡状态true为异常,false或者nil为正常
local flyMode --是否处于飞行模式true为是,false或者nil为否
local gprsAttached --是否附着上GPRS网络,true为是,false或者nil为否
local socketConnected --是否有socket连接上后台,true为是,false或者nil为否
--[[
网络指示灯表示的工作状态
NULL功能关闭状态
FLYMODE飞行模式
SIMERR未检测到SIM卡或者SIM卡锁pin码等SIM卡异常
IDLE未注册GPRS网络
GPRS已附着GPRS数据网络
SCKsocket已连接上后台
]]
local ledState = "NULL"
local ON,OFF = 1,2
--各种工作状态下配置的点亮、熄灭时长(单位毫秒)
local ledBlinkTime =
{
NULL = {0,0xFFFF}, --常灭
FLYMODE = {0,0xFFFF}, --常灭
SIMERR = {300,5700}, --亮300毫秒,灭5700毫秒
IDLE = {300,3700}, --亮300毫秒,灭3700毫秒
GPRS = {300,700}, --亮300毫秒,灭700毫秒
SCK = {100,100}, --亮100毫秒,灭100毫秒
}
local ledSwitch = false --网络指示灯开关,true为打开,false或者nil为关闭
local LEDPIN = 27 --网络指示灯默认PIN脚GPIO27
local lteSwitch = false --LTE指示灯开关,true为打开,false或者nil为关闭
local LTEPIN = 26 --LTE指示灯默认PIN脚GPIO26
--[[
更新网络指示灯表示的工作状态
@api netLed.setState
@return nil 无返回值
@usage
netLed.setState()
]]
function netLed.setState()
log.info("netLed.setState",ledSwitch,ledState,flyMode,simError,gprsAttached,socketConnected)
if ledSwitch then
local newState = "IDLE"
if flyMode then
newState = "FLYMODE"
elseif simError then
newState = "SIMERR"
elseif socketConnected then
newState = "SCK"
elseif gprsAttached then
newState = "GPRS"
end
--指示灯状态发生变化
if newState~=ledState then
ledState = newState
sys.publish("NET_LED_UPDATE")
end
end
end
--[[
网络指示灯模块的运行任务
@api netLed.taskLed(ledPinSetFunc)
@return nil 无返回值
@usage
local LEDA = gpio.setup(27,1,gpio.PULLUP) --LED引脚判断赋值结束
netLed.taskLed(LEDA)
]]
function netLed.taskLed(ledPinSetFunc)
while true do
--log.info("netLed.taskLed",ledPinSetFunc,ledSwitch,ledState)
if ledSwitch then
local onTime,offTime = ledBlinkTime[ledState][ON],ledBlinkTime[ledState][OFF]
if onTime>0 then
ledPinSetFunc(1)
if not sys.waitUntil("NET_LED_UPDATE", onTime) then
if offTime>0 then
ledPinSetFunc(0)
sys.waitUntil("NET_LED_UPDATE", offTime)
end
end
else if offTime>0 then
ledPinSetFunc(0)
sys.waitUntil("NET_LED_UPDATE", offTime)
end
end
else
ledPinSetFunc(0)
break
end
end
end
--[[
LTE指示灯模块的运行任务
@api netLed.taskLte(ledPinSetFunc)
@return nil 无返回值
@usage
local LEDA = gpio.setup(27,1,gpio.PULLUP) --LED引脚判断赋值结束
netLed.taskLte(LEDA)
]]
function netLed.taskLte(ledPinSetFunc)
while true do
local _,arg = sys.waitUntil("LTE_LED_UPDATE")
if lteSwitch then
ledPinSetFunc(arg and 1 or 0)
end
end
end
--[[
配置网络指示灯和LTE指示灯并且立即执行配置后的动作
@api netLed.setup(flag,ledpin,ltepin)
@bool flag 是否打开网络指示灯和LTE指示灯功能,true为打开,false为关闭
@number ledPin 控制网络指示灯闪烁的GPIO引脚,例如pio.P0_1表示GPIO1
@number ltePin 控制LTE指示灯闪烁的GPIO引脚,例如pio.P0_4表示GPIO4
@return nil 无返回值
@usage
netLed.setup(true,27,0)
]]
function netLed.setup(flag,ledPin,ltePin)
--log.info("netLed.setup",flag,pin,ledSwitch)
local oldSwitch = ledSwitch
if flag~=ledSwitch then
ledSwitch = flag
sys.publish("NET_LED_UPDATE")
end
if flag and not oldSwitch then
sys.taskInit(netLed.taskLed, gpio.setup(ledPin or LEDPIN, 0))
end
if flag~=lteSwitch then
lteSwitch = flag
end
if flag and ltePin and not oldSwitch then
sys.taskInit(netLed.taskLte, gpio.setup(ltePin, 0))
end
end
--[[
配置某种工作状态下指示灯点亮和熄灭的时长(如果用户不配置,使用netLed.lua中ledBlinkTime配置的默认值
@api netLed.setBlinkTime(state,on,off)
@string state 某种工作状态,仅支持"FLYMODE"、"SIMERR"、"IDLE"、"GSM"、"GPRS"、"SCK"
@number on 指示灯点亮时长,单位毫秒,0xFFFF表示常亮,0表示常灭
@number off 指示灯熄灭时长,单位毫秒,0xFFFF表示常灭,0表示常亮
@return nil 无返回值
@usage
netLed.setBlinkTime(("FLYMODE",1000,500) --表示飞行模式工作状态下,指示灯闪烁规律为: 亮1秒,灭8.5秒
]]
function netLed.setBlinkTime(state,on,off)
if not ledBlinkTime[state] then log.error("netLed.setBlinkTime") return end
local updated
if on and ledBlinkTime[state][ON]~=on then
ledBlinkTime[state][ON] = on
updated = true
end
if off and ledBlinkTime[state][OFF]~=off then
ledBlinkTime[state][OFF] = off
updated = true
end
--log.info("netLed.setBlinkTime",state,on,off,updated)
if updated then sys.publish("NET_LED_UPDATE") end
end
--[[
呼吸灯
@api netLed.setupBreateLed(ledPin)
@function ledPin 呼吸灯的ledPin(1)用pins.setup注册返回的方法
@return nil 无返回值
@usage
local netLed = require ("netLed")
local LEDA = gpio.setup(27,1,gpio.PULLUP) --LED引脚判断赋值结束
sys.taskInit(function()
--呼吸灯
sys.wait(5080)--延时5秒等待网络注册
log.info("mobile.status()", mobile.status())
while true do
if mobile.status() == 1 then--已注册
sys.wait(688)
netLed.setupBreateLed(LEDA)
end
end
end)
]]
function netLed.setupBreateLed(ledPin)
-- 呼吸灯的状态、PWM周期
local bLighting, bDarking, LED_PWM = false, true, 18
if bLighting then
for i = 1, LED_PWM - 1 do
ledPin(0)
sys.wait(i)
ledPin(1)
sys.wait(LED_PWM - i)
end
bLighting = false
bDarking = true
ledPin(0)
sys.wait(700)
end
if bDarking then
for i = 1, LED_PWM - 1 do
ledPin(0)
sys.wait(LED_PWM - i)
ledPin(1)
sys.wait(i)
end
bLighting = true
bDarking = false
ledPin(1)
sys.wait(700)
end
end
sys.subscribe("FLYMODE", function(mode) if flyMode~=mode then flyMode=mode netLed.setState() end end)
sys.subscribe("SIM_IND", function(para) if simError~=(para~="RDY") and simError~=(para~="GET_NUMBER") then simError=(para~="RDY") netLed.setState() end log.info("sim status", para) end)
sys.subscribe("IP_LOSE", function() if gprsAttached then gprsAttached=false netLed.setState() end log.info("mobile", "IP_LOSE", (adapter or -1) == socket.LWIP_GP) end)
sys.subscribe("IP_READY", function(ip, adapter) if gprsAttached~=adapter then gprsAttached=adapter netLed.setState() end log.info("mobile", "IP_READY", ip, (adapter or -1) == socket.LWIP_GP) end)
sys.subscribe("SOCKET_ACTIVE", function(active) if socketConnected~=active then socketConnected=active netLed.setState() end end)
return netLed

View File

@@ -0,0 +1,26 @@
-- screen_data_table.lua )
-- 此文件只包含屏幕相关配置数据
local screen_data_table = {
lcdargs = {
LCD_MODEL = "AirLCD_1001",
pin_vcc = 24,
pin_rst = 36,
pin_pwr = 25,
pin_pwm = 2,
port = lcd.HWID_0,
direction = 0,
w = 320,
h = 480,
xoffset = 0,
yoffset = 0,
},
touch = {
TP_MODEL = "Air780EHM_LCD_4", -- 触摸芯片型号
i2c_id = 1, -- I2C总线ID
pin_rst = 255, -- 触摸芯片复位引脚(非必须)
pin_int = 22 -- 触摸芯片中断引脚
},
}
return screen_data_table

View File

@@ -0,0 +1,101 @@
--[[
@module udpsrv
@summary UDP服务器
@version 1.0
@date 2023.7.28
@author wendal
@demo socket
@tag LUAT_USE_NETWORK
@usage
-- 具体用法请查
阅demo
]]
local sys = require "sys"
local udpsrv = {}
local srvs = {}
--[[
创建UDP服务器
@api udpsrv.create(port, topic, adapter)
@int 端口号, 必填, 必须大于0小于65525
@string 收取UDP数据的topic,必填
@int 网络适配编号, 默认为nil,可选
@return table UDP服务的实体, 若创建失败会返回nil
]]
function udpsrv.create(port, topic, adapter)
local srv = {}
-- udpsrv.port = port
-- srv.topic = topic
srv.rxbuff = zbuff.create(1500)
local sc = socket.create(adapter, function(sc, event)
-- log.info("udpsrv", sc, event, "EVENT", socket.EVENT, "CLOSED", socket.CLOSED)
if event == socket.EVENT then
local rxbuff = srv.rxbuff
while 1 do
local succ, data_len, remote_ip, remote_port = socket.rx(sc, rxbuff)
-- log.info("udpsrv", "???", succ, data_len, remote_ip, remote_port)
if succ and data_len and data_len > 0 then
local resp = rxbuff:toStr(0, rxbuff:used())
rxbuff:del()
if remote_ip and #remote_ip == 5 then
local ip1,ip2,ip3,ip4 = remote_ip:byte(2),remote_ip:byte(3),remote_ip:byte(4),remote_ip:byte(5)
remote_ip = string.format("%d.%d.%d.%d", ip1, ip2, ip3, ip4)
else
remote_ip = nil
end
sys.publish(topic, resp, remote_ip, remote_port)
else
if not succ then
socket.close(sc)
socket.connect(sc, "255.255.255.255", 0)
end
break
end
end
elseif event == socket.CLOSED then
log.info("dhcpsrv", "网络中断,执行关闭流程")
socket.close(self.sc)
end
end)
if sc == nil then
return
end
srv.sc = sc
-- socket.debug(sc, true)
socket.config(sc, port, true)
if socket.connect(sc, "255.255.255.255", 0) then
srv.send = function(self, data, ip, port)
if self.sc and data then
-- log.info("why?", self.sc, data, ip, port)
return socket.tx(self.sc, data, ip, port)
end
end
srv.close = function(self)
socket.close(self.sc)
-- sys.wait(200)
socket.release(self.sc)
srvs[self.sc] = nil
self.sc = nil
end
srvs[srv.sc] = true
-- log.info("udpsrv", "监听开始")
return srv
end
socket.close(sc)
-- sys.wait(200)
socket.release(sc)
-- log.info("udpsrv", "监听失败")
end
sys.subscribe("IP_READY", function()
for sc, value in pairs(srvs) do
log.info("udpsrv", "自动重连udpsrv", sc)
if sc then
socket.connect(sc, "255.255.255.255", 0)
end
end
end)
return udpsrv

View File

@@ -0,0 +1,222 @@
--[[
@module xmodem
@summary xmodem 协议
@version 1.0
@date 2025.10.17
@author Dozingfiretruck
@usage
--加载xmodem模块
xmodem=require ("xmodem")
--设置默认filepath为脚本区的send.bin文件
local filepath="/luadb/send.bin"
local taskName = "xmodem_run"
local uart_id = 1 --串口号
local baudrate = 115200 --波特率
local file_path=filepath --文件路径
local send_type=true --true表示单次发送128字节false表示单次发送1024字节
local inform_data="wait C" --发送前提示信息告知对方要发送C字符来接收文件
-- 处理未识别的消息
local function xmodem_run_cb(msg)
log.info("xmodem_run_cb", msg[1], msg[2], msg[3], msg[4])
end
--http获取文件函数
local function http_recived_cb()
while not socket.adapter(socket.dft()) do
log.warn("httpplus_app_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意此处的1000毫秒超时不要修改的更长
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒能够保证即使时序不匹配也能1秒钟退出阻塞状态再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
local path = "/send.bin"
-- 以下链接仅用于测试,禁止用于生产环境
local code, headers, body_size = http.request("GET", "http://airtest.openluat.com:2900/download/send.bin", nil, nil, {dst=path}).wait()
log.info("http", code==200 and "success" or "error", code)
if code==200 then
log.info("HTTP receive ok",body_size)
file = io.open(path, "rb")
if file then
content = file:read("*a")
log.info("文件读取", "路径:" .. path, "内容:" .. content)
file:close()
else
log.error("文件操作", "无法打开文件读取内容", "路径:" .. path)
end
file_path=path
end
end
-- 定义一个xmodem_run函数用于用xmodem发送文件
local function xmodem_run()
--如果需要http下载文件然后发送下载的文件可以打开下面的http_recived_cb()函数
-- http_recived_cb()
--启动xmodem发送
local result=xmodem.send(uart_id,baudrate,file_path,send_type,inform_data)
--等待时间12秒等待接收方发送C字符启动发送发送结束后接收端发送ACK:0x06表示接收完成文件全部传输完成之后模块发送EOT0x04表示传输结束接收端返回0x06表示确认结束
log.info("Xmodem", "start")
log.info("Xmodem", "send result", result)
--判断是否传输成功传输是否成功都需要关闭xmodem
if result then
log.info("Xmodem", "send success")
xmodem.close(uart_id)
else
log.info("Xmodem", "send failed")
xmodem.close(uart_id)
end
end
--创建并且启动一个task
--运行这个task的主函数xmodem_run
sys.taskInit(xmodem_run, taskName,xmodem_run_cb)
]]
local xmodem = {}
local sys = require "sys"
local HEAD
local DATA_SIZE
local SOH = 0x01 -- Modem数据头 128
local STX = 0x02 -- Modem数据头 1K
local EOT = 0x04 -- 发送结束
local ACK = 0x06 -- 应答
local NAK = 0x15 -- 非应答
local CAN = 0x18 -- 取消发送
local CTRLZ = 0x1A -- 填充
local CRC_CHR = 0x43 -- C: ASCII字符C
local CRC_SIZE = 2
local FRAME_ID_SIZE = 2
local DATA_SIZE_SOH = 128
local DATA_SIZE_STX = 1024
local function uart_cb(id, len)
local data = uart.read(id, 1024)
if #data == 0 then
return
end
log.info("xmodem", "uart读取到数据:", data:toHex())
data = data:byte(1)
sys.publish("xmodem", data)
end
--[[
xmodem 发送文件
@api xmodem.send(uart_id,baudrate,type,inform_data)
@number uart_id uart端口号
@number uart_br uart波特率
@string file_path 文件路径
@bool type 1k/128 默认1k
@return bool 发送结果
@usage
xmodem.send(1, 115200, "/luadb/send.bin",true)
]]
function xmodem.send(uart_id,baudrate,file_path,type,inform_data)
local ret, flen, cnt, crc
if type then
HEAD = SOH
DATA_SIZE = DATA_SIZE_SOH
else
HEAD = STX
DATA_SIZE = DATA_SIZE_STX
end
local XMODEM_SIZE = 1+FRAME_ID_SIZE+DATA_SIZE+CRC_SIZE
local packsn = 0
local xmodem_buff = zbuff.create(XMODEM_SIZE)
local data_buff = zbuff.create(DATA_SIZE)
local fd = io.open(file_path, "rb")
if fd then
uart.setup(uart_id,baudrate)
uart.on(uart_id, "receive", uart_cb)
if inform_data and inform_data~="" then
uart.write(uart_id,inform_data)
end
local result, data = sys.waitUntil("xmodem", 12000)
if result and (data == CRC_CHR or data == NAK) then
cnt = 1
while true do
data_buff:set(0, CTRLZ)
ret, flen = fd:fill(data_buff,0,DATA_SIZE)
log.info("xmodem", "发送第", cnt, "")
if flen > 0 then
data_buff:seek(0)
crc = crypto.crc16("XMODEM",data_buff)
packsn = (packsn+1) & 0xff
xmodem_buff[0] = 0x02
xmodem_buff[1] = packsn
xmodem_buff[2] = 0xff-xmodem_buff[1]
data_buff:seek(DATA_SIZE)
xmodem_buff:copy(3, data_buff)
xmodem_buff[1027] = crc>>8
xmodem_buff[1028] = crc&0xff
xmodem_buff:seek(XMODEM_SIZE)
-- log.info(xmodem_buff:used())
:: RESEND ::
uart.tx(uart_id, xmodem_buff)
result, data = sys.waitUntil("xmodem", 10000)
if result and data == ACK then
cnt = cnt + 1
elseif result and data == NAK then
goto RESEND
else
uart.write(uart_id, string.char(EOT))
log.info("xmodem", "发送失败")
return false
end
if flen ~= DATA_SIZE then
log.info("xmodem", "文件到头了")
break
end
else
log.info("xmodem", "文件到头了")
break
end
end
uart.write(uart_id, string.char(EOT))
fd:close()
return true
else
log.info("xmodem", "不支持的起始数据包",data)
return false
end
else
log.info("xmodem", "待传输的文件不存在")
return false
end
end
--[[
关闭xmodem
@api xmodem.close(uart_id)
@number uart_id uart端口号
@usage
-- 执行xmodem传输后, 无论是否传输成功, 都建议关闭xmodem上下文, 也会关闭uart
xmodem.close(2)
]]
function xmodem.close(uart_id)
uart.on(uart_id, "receive")
uart.close(uart_id)
end
return xmodem