Skip to content

system — 系统功能 ​

Lua 运行时结构:core1 梯形图扫描,core0 事件循环分发回调

模块名: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 侧主循环,脚本没有也不需要入口去驱动它。它每一拍做四件事:

  1. 推进 task 起的任务(睡到的、等到的都在这时恢复);
  2. 取出系统消息队列里的消息,按类型调用已注册的回调;
  3. 队列空且等满一拍时,跑到期定时器、步进一次垃圾回收;
  4. 进入下一拍。
你遇到的问题看哪里
回调不触发脚本里是不是还有 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)

延时指定毫秒,行为随执行上下文不同。

参数

#参数类型必填默认值说明
1msinteger是—延时毫秒数。小于 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,模拟器侧改用协程。

参数

#参数类型必填默认值说明
1fnfunction是—线程入口。不是 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

查询文件系统的总空间、已用空间和可用空间。

参数

#参数类型必填默认值说明
1pathstring否"/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

列出目录下的所有文件和子目录。

参数

#参数类型必填默认值说明
1pathstring是—目录路径

返回值

类型说明
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

检查文件或目录是否存在。

参数

#参数类型必填默认值说明
1pathstring是—文件或目录路径

返回值

类型说明
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:这是状态不是急停,别占用输出点,也不要让梯形图再写它。

验证方法:

  1. 下载运行后日志每分钟打印一行 剩余 XX%(…KB / …KB);上位机看 D310 应与之相符。
  2. 手动拷一堆文件进 /spiffs(或改小阈值),M20 应置 ON、D310 掉到 20 以下。
  3. 删掉 /spiffs/config.ini 再等一轮:日志打印「配置文件缺失」,M21 置 ON。
  4. 故意把路径写成不存在的挂载点: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 后线程自行退出。

验证方法:

  1. 固件上下载运行:每 2 秒日志打印 后台任务在跑: true,且期间 MQTT / 串口回调仍正常(另挂一个定时器能看到它在跑)。
  2. 模拟器上下载同一份脚本:不报错,走协程分支,行为一致 —— 这就是 if system.start_thread then 的价值。
  3. 把 if 判断删掉、直接 system.start_thread(...) 在模拟器上跑:日志出现 attempt to call field 'start_thread' (a nil value)。
  4. 在线程/协程里人为写个死循环(不带 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 等)要重建 —— 这正是恢复的意义。

验证方法:

  1. 把 Broker 停掉并保持断开,观察日志:上报失败 1 次 … 5 次 → 3 秒后重启。
  2. 设备约 3 秒后重启,启动日志重新出现,重新连云后 M30 复位。
  3. 恢复 Broker 后再跑到 3 次失败又成功一次:fails 清零不再累积到重启。
  4. 临时把 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。
  • 系统消息队列深度有限,回调里避免长耗时操作,以免消息积压丢失。

LPPLC 工业物联网云平台 · 软硬件与云平台全栈自研