主题
system — 系统功能
模块名:system
提供系统重启、延时/休眠、独立系统线程(仅固件)以及文件系统查询。
消息泵由固件负责,脚本不用自己转圈:main.lua 执行完最后一条语句后,固件自动进入消息泵(C 侧主循环)—— 每一拍从系统消息队列取出消息,按类型调用已注册的回调(定时器、MQTT、TCP、UDP、串口、HTTP 等),这些回调都在 Lua 主线程里执行。所以脚本要做的事只有三件:周期任务用 timer,等待与多步流程用 task + event,收发数据用各协议模块的回调。
函数总览
| 函数 | 作用 |
|---|---|
system.restart | 关机/重启设备,调用后不返回 |
system.sleep(integer, ...) | 延时,主线程里阻塞,任务/协程内让出 |
system.start_thread(...) | 起一条独立的系统线程(仅固件,模拟器上没有) |
system.fs_info | 查询文件系统总空间/已用/可用 |
| system.fs_list(string) | 列出目录下的文件和子目录 | | system.fs_exists(string) | 检查路径是否存在及类型 |
system.restart
lua
system.restart()关闭并重启设备。
参数
无参数。
返回值
无返回值。
示例
lua
system.restart()
-- 之后的代码不会执行实用例程
lua
serial:start()--启动串口数据采集
t=timer.start(5000,function()--启动定时器 5秒发布一个数据
if mqtt.is_linked(x) then
--if m站号:get_num()>plc.d(302) then
mqtt.publish(x,"aPub"..zid,m站号:get_num()*80)
--else
mqtt.publish(x,"bPub"..zid,m测试:get_num()*80)
--end
mqtt.publish(x,"cPub"..zid,m测试1:get_num()*80)
print(zid.."<名称>",m站号:get_num()*80+m测试:get_num()*80+m测试1:get_num()*80)
print(zid.."<名称>",m站号:get_num()*80)
print(zid.."<名称>",m测试:get_num()*80)
print(zid.."<名称>",m测试1:get_num()*80)
else
print("未连接")
end
end
)
--
t2=timer.start(60*1*1000,function()--启动定时器1分读取D301
if zid ~="SPTX" .. string.format("%04s", tostring(plc.d(301))) then
print("<名称>")
system.restart();
end
end
)把上面的地址、主题、引脚替换成你项目里的即可。
消息泵:谁在分发消息
消息泵是固件的 C 侧主循环,脚本没有也不需要入口去驱动它。它每一拍做四件事:
- 推进
task起的任务(睡到的、等到的都在这时恢复); - 取出系统消息队列里的消息,按类型调用已注册的回调;
- 队列空且等满一拍时,跑到期定时器、步进一次垃圾回收;
- 进入下一拍。
| 你遇到的问题 | 看哪里 |
|---|---|
| 回调不触发 | 脚本里是不是还有 while true do ... system.sleep(n) end 这类自写主循环(见下方警示框) |
| 任务不动了 | task.list() 看它卡在等什么 |
| 等待某件事再继续 | 任务里用 event.wait,不要自己写循环 |
2026-10-22 起脚本侧的
system.recv已删除(泵在 C 侧)。更早的固件上手写的while true do system.recv(10) end现在会报attempt to call a nil value—— 把那一段删掉即可,其余代码不用动,泵由固件自动接管。
system.sleep
lua
system.sleep(ms)延时指定毫秒,行为随执行上下文不同。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | ms | integer | 是 | — | 延时毫秒数。小于 0 会被钳制为 0 |
返回值
无返回值。
行为
| 执行上下文 | 行为 |
|---|---|
| 主线程 | 阻塞式延时,完全不推进消息泵,会冻结整个 Lua 事件循环 |
task.start 起的任务 | 交给任务调度器,让出一拍,到点自动恢复 |
| 协程(coroutine) | 非阻塞:注册定时器后 yield 挂起,到期由主线程恢复 |
主线程里的 sleep 会让所有回调失效
消息分发只发生在消息泵里,主线程里的 system.sleep 只是单纯地停住线程。所以主线程写成这样,on_change、node:reg()、timer.start、MQTT/TCP/串口的回调一个都不会触发:
lua
-- 错误:sleep 期间队列里的事件全都积压着,回调是死的
while true do
do_something()
system.sleep(5000)
end正确做法是不要自己写主循环:周期性的事情交给定时器,脚本 return 后固件自动接管消息泵。
lua
-- 正确:周期任务用定时器,消息泵交给固件
timer.start(5000, function()
do_something()
end)确实需要"等一会儿再继续"或"等多步"时,用任务 + event.wait —— 那里的 system.sleep 是让出,不阻塞:
lua
-- 正确:顺序流程放进任务,等待期间泵照常转
task.start(function()
while true do
do_something()
system.sleep(5000) -- 任务内:让出一拍
end
end)示例
lua
-- 协程内:挂起 1 秒,不阻塞其它任务(协程里的 sleep 是 yield,主循环照常跑)
coroutine.wrap(function()
while true do
system.sleep(1000)
print("协程运行中")
end
end)()
-- 主线程里要延后做一件事:用单次定时器,不要用 sleep
timer.start(500, function()
print("500ms 到了")
end, true)说明
- 关于几种上下文的完整解释见 并发模型概述。
- 判断依据很简单:要等回调,就别在主线程用
sleep。
system.start_thread
lua
system.start_thread(fn, [...]) --> 无返回值把 fn 连同后面的参数交给一条独立的系统线程去执行。与协程不同,这是真正并行的一条 FreeRTOS 任务:它跑长耗时循环不会冻结主循环,主循环泵消息也不影响它。
可用性(先读这条)
| 平台 | 是否提供 |
|---|---|
| ESP32 系列固件(LP3201/3202/3203/3222/3232/3288) | 有 |
| 软PLC模拟器 | 没有。调用直接报 attempt to call field 'start_thread' (a nil value) |
跨平台脚本必须先判断:if system.start_thread then ... end,模拟器侧改用协程。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | fn | function | 是 | — | 线程入口。不是 function 会直接报错 |
| 2.. | ... | any | 否 | — | 原样传给 fn 作为它的参数 |
返回值
无返回值。注意:任务创建失败(内存不足时)既不报错也没有任何返回——fn 就是永远不跑,脚本层面察觉不到。用了本函数就要在 fn 里自己"报到"(比如写一个全局标志或 D 寄存器),在外面确认线程真的起来了。
示例
lua
collector_alive = false
if system.start_thread then
system.start_thread(function(name)
collector_alive = true -- 报到
while true do
system.sleep(5000) -- 线程内 sleep 只阻塞本线程
-- ... 长耗时采集,放这里不会卡主循环回调 ...
end
end, "collector")
end
timer.start(2000, function()
print("采集线程在线:", collector_alive)
end)说明
- 线程函数跑在自己的 Lua 状态里,但与主线程共用同一张全局表:两边读写同一个全局变量、同一片 PLC 寄存器没有锁保护,并发模型见 并发模型概述。
- 线程函数内部运行报错不会传到主线程,只写进设备日志;函数
return后线程自行退出并被回收。 - 没有等待线程结束的接口:想停,让线程函数里的循环条件自己满足退出。
system.fs_info
lua
system.fs_info([path]) --> total, used, free查询文件系统的总空间、已用空间和可用空间。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | path | string | 否 | "/spiffs" | 挂载点路径。路径含 spiffs 走 SPIFFS 查询,否则按 FatFS(如 SD 卡 /sdcard)查询 |
返回值
| 类型 | 说明 |
|---|---|
| integer, integer, integer | 成功:total, used, free,单位字节 |
| nil, string | 失败:nil 加错误信息 |
示例
lua
local total, used, free = system.fs_info()
if total then
print(string.format("总 %dKB 已用 %dKB 剩 %dKB",
total // 1024, used // 1024, free // 1024))
end说明
- SPIFFS 是扁平文件系统,不支持真正的目录;FatFS(SD 卡)支持目录结构。
system.fs_list
lua
system.fs_list(path) --> table列出目录下的所有文件和子目录。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | path | string | 是 | — | 目录路径 |
返回值
| 类型 | 说明 |
|---|---|
| table | 成功:数组,每个元素为 {name=string, type="file"|"dir", size=integer},自动跳过 . 和 .. |
| nil, string | 失败:nil, "failed to open directory" |
示例
lua
local files = system.fs_list("/spiffs")
if files then
for _, f in ipairs(files) do
print(f.name, f.type, f.size)
end
end说明
- SPIFFS 下所有条目
type通常为"file";FatFS 才会出现"dir"。
system.fs_exists
lua
system.fs_exists(path) --> exists, type检查文件或目录是否存在。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | path | string | 是 | — | 文件或目录路径 |
返回值
| 类型 | 说明 |
|---|---|
| boolean | 是否存在 |
| string | nil | 存在时为 "file" 或 "dir";不存在时为 nil |
示例
lua
local exists, ftype = system.fs_exists("/spiffs/config.ini")
if exists then
print("存在,类型:", ftype)
end例程
每个例程都是可直接下载运行的完整脚本:注册完定时器后脚本就结束,固件自动进入消息泵。 API 只用本页已文档化的函数;统一模板「场景 / 完整代码 / 要点 / 验证方法 / 注意事项」。
例程 1:定时巡检文件系统,剩余空间不足就报警
适用:LP3201 / LP3202 / LP3203 / LP3222 / LP3232 / LP3288 / 软PLC模拟器 | 依赖模块:system timer plc | 难度:★☆☆
场景:设备常年现场跑,日志或历史数据慢慢把存储写满。每分钟体检一次:确认配置文件还在、数一数文件数量、把剩余空间比例写进 D310 并置报警位 M20,云端或触摸屏就能盯到。
完整代码(可直接下载运行):
lua
-- 每分钟体检一次文件系统:配置在不在、剩多少空间
timer.start(60000, function()
local ok_cfg, _ = system.fs_exists("/spiffs/config.ini")
if not ok_cfg then
print("配置文件缺失,按默认值运行")
plc.m(21, 1) -- M21 = 配置缺失标志
end
local files = system.fs_list("/spiffs")
print("文件数:", files and #files or 0)
local total, used, free = system.fs_info()
if not total then
print("文件系统查询失败,可能是路径不对")
return
end
local percent = math.floor(free * 100 / total) -- 剩余百分比
plc.d(310, percent)
plc.m(20, (percent < 20) and 1 or 0) -- M20 = 空间不足报警
print(string.format("剩余 %d%%(%dKB / %dKB)", percent, free // 1024, total // 1024))
end)要点:
- 三个文件函数的失败返回各不相同:
fs_exists返回false, nil,fs_list失败返回nil, "failed to open directory",fs_info失败返回nil, 错误信息—— 一律要判空。 fs_list的元素是表:{name=..., type="file"|"dir", size=...};SPIFFS 是扁平文件系统,type通常都是"file"。- 空间百分比用整数存 D,给梯形图 / 触摸屏直接显示(不像浮点那样还要转换)。
- 报警位用 M 而不用 Y:这是状态不是急停,别占用输出点,也不要让梯形图再写它。
验证方法:
- 下载运行后日志每分钟打印一行
剩余 XX%(…KB / …KB);上位机看 D310 应与之相符。 - 手动拷一堆文件进
/spiffs(或改小阈值),M20 应置 ON、D310 掉到 20 以下。 - 删掉
/spiffs/config.ini再等一轮:日志打印「配置文件缺失」,M21 置 ON。 - 故意把路径写成不存在的挂载点:
fs_info返回失败,日志打印查询失败且脚本没崩(返回值判空生效)。
注意事项:
system.fs_*失败是静默的(返回nil),不判空就往下取值会直接attempt to index a nil value。- SPIFFS 没有真正的目录,
fs_list("/spiffs/sub")这种子目录写法不成立。 - 别把巡检间隔设得太密:
fs_list有 IO 开销,1 分钟一次足够。
例程 2:长耗时任务 —— 固件用线程、模拟器降级协程(跨平台写法)
适用:LP3201 / LP3202 / LP3203 / LP3222 / LP3232 / LP3288(线程)/ 软PLC模拟器(协程降级)| 依赖模块:system timer plc | 难度:★★★
场景:有一段必须周期性执行、而且每一步都很慢的活(比如逐条轮询几十台串口从站)。放主线程会把事件泵卡死;用 start_thread 可以真并行,但模拟器上没有这个函数(调用直接报 attempt to call field 'start_thread' (a nil value))。所以跨平台脚本必须写成「有线程用线程、没线程降级协程」。
完整代码(可直接下载运行):
lua
-- 同样的业务,有线程跑线程、没线程跑协程,两边都能用
local function slow_job()
while true do
plc.d(500, plc.d(500) + 1) -- 每 5 秒推进一次(示意)
system.sleep(5000) -- 线程内:只睡本线程;协程内:yield,都不卡主循环
end
end
worker_alive = false
if system.start_thread then
system.start_thread(slow_job) -- 固件:真的一条 FreeRTOS 任务
else
coroutine.wrap(slow_job)() -- 模拟器:协程降级(协作式,靠 system.sleep 让出)
end)
timer.start(2000, function()
print("后台任务在跑:", plc.d(500) > 0)
end)要点:
- 平台判断必须写在前面:
if system.start_thread then … else … end,不做判断在模拟器上直接报错。 start_thread没有返回值,创建失败也察觉不到(比如内存不足),所以要在线程里"报到"并到外面确认。- 线程与主线程共用同一张全局表(也共用 PLC 寄存器),没有锁保护,跨线程读写同一个全局变量要格外小心 —— 建议只在线程里写不同的寄存器。
- 协程降级不是真并行:它靠
system.sleep让出(yield),OK 的前提是里面真的调用了 sleep,纯 CPU 循环照样卡住。 - 线程函数自己报错不冒泡到主线程,只进设备日志;函数
return后线程自行退出。
验证方法:
- 固件上下载运行:每 2 秒日志打印
后台任务在跑: true,且期间 MQTT / 串口回调仍正常(另挂一个定时器能看到它在跑)。 - 模拟器上下载同一份脚本:不报错,走协程分支,行为一致 —— 这就是
if system.start_thread then的价值。 - 把
if判断删掉、直接system.start_thread(...)在模拟器上跑:日志出现attempt to call field 'start_thread' (a nil value)。 - 在线程/协程里人为写个死循环(不带 sleep)作为对照:主循环的定时器不再打印,直观看到"协作式降级不是真并行"。
注意事项:
- 写进例程的那句
system.sleep(5000)只有在线程或协程里才不阻塞;主线程里同样的调用会冻结整台设备。 - 别指望有"停止线程"的接口:想停就让循环条件自己满足退出。
- 线程里不要频繁创建大表:GC 由消息泵在无消息的空拍步进,密集任务期内存峰值会升高。
例程 3:连续失联后延时重启(system.restart 的正确位置)
适用:同例程 1 | 依赖模块:system timer plc | 难度:★★☆
场景:联网失败这种问题,重试几次还不行就该让设备自恢复。做法是数失败次数,到阈值后先用单次定时器留出几秒收尾时间(写日志、置报警位、通知云端),再调 system.restart() —— 它会立刻重启,调用之后的代码一句都不执行。
完整代码(可直接下载运行):
lua
-- 连续 5 次上报失败 → 留 3 秒收尾 → 重启
local fails = 0
local MAXFAIL = 5
local restarting = false
-- MQTT 部分的最小骨架,详见 [mqtt 客户端](05-02-mqtt.html)
mqttc = mqtt.creat("192.168.1.20", 1883, plc.plc_name(), "", "")
mqtt.start(mqttc)
timer.start(10000, function()
if restarting then return end
local ok = mqtt.publish(mqttc, "dev/alive", "1", 0, 0) -- 发不通返回 nil
if ok then
fails = 0
plc.m(30, 0)
return
end
fails = fails + 1
print("上报失败", fails, "次")
if fails < MAXFAIL then return end
restarting = true
plc.m(30, 1) -- 告知梯形图:即将重启
print("连续失败已达上限,3 秒后重启")
timer.start(3000, function()
system.restart() -- 到这里为止,之后的代码不会执行
end, true)
end)要点:
system.restart()不返回:它后面不要写任何"善后"代码,善后必须在调用前做完。- 用单次定时器留出收尾时间窗口,运维看到日志时能知道"为什么重启"。
restarting标志防止多次触发重复排多个重启定时器。- 频繁重启会伤现场体验:阈值要给足(几分钟级),并且最好同时上报一次"我要重启了"。
- 重启后脚本会从头执行,所有长生命周期对象(MQTT client 等)要重建 —— 这正是恢复的意义。
验证方法:
- 把 Broker 停掉并保持断开,观察日志:
上报失败 1 次 … 5 次→3 秒后重启。 - 设备约 3 秒后重启,启动日志重新出现,重新连云后 M30 复位。
- 恢复 Broker 后再跑到 3 次失败又成功一次:
fails清零不再累积到重启。 - 临时把
MAXFAIL改成 1 验证一道效果(别留在现场版本里)。
注意事项:
- 以为
system.restart()后面还能写清理代码 —— 它直接重启,后面一句都不会跑。 - 阈值太小会把设备卷进"重启循环":网络闪断 10 秒就重启是最差的做法。
- 重启前把"我要重启"这件事上报出去,否则现场看到设备自己重启会一头雾水。
注意事项
- 删除文件用
os.remove(path),重命名用os.rename(old, new),system模块本身不提供这两个操作。 start_thread只有固件有、模拟器没有,且创建失败静默无返回——用它必须让线程自己"报到",并给跨平台脚本留if system.start_thread then判断。- 主线程内调用
system.sleep会阻塞整个事件循环且不推进消息泵,期间所有回调都不会触发。周期任务用timer.start,需要等待时用任务 +event.wait。 - 系统消息队列深度有限,回调里避免长耗时操作,以免消息积压丢失。