主题
AI 易错点与反模式
写 LPPLC 的 Lua 程序时,下面这些坑踩一个就足以让程序看起来"能跑但完全不对"。 每条都给出错误写法 vs 正确写法。写程序前先扫一遍,能省掉绝大多数调试时间。
两条总纲:
- API 只许用本 Wiki 已文档化的,查不到就写
TODO(待确认),严禁凭其它平台或通用 Lua 的习惯编函数名。 - 不许阻塞主循环。设备 Lua 是单线程事件循环,周期任务一律
timer.start。
一、致命级:会让整个程序"静默失效"
1.1 阻塞主循环(头号杀手)
主线程里的 sleep 只停住线程、不泵消息队列,定时器 / MQTT / TCP / 串口回调全部永不触发。
lua
-- ❌ 错误:消息泵被掐死,回调全是死代码
while true do
do_work()
system.sleep(5000)
end
-- ✅ 正确:周期任务交给定时器,脚本注册完即可结束
timer.start(5000, do_work)典型症状:MQTT 显示已连接,但收不到任何下发、也不上报。
要"一直跑、中间还要等",用 task.start 起任务(那里的 sleep 是让出,不是阻塞):
lua
-- ✅ 正确:顺序流程放进任务,等待期间消息泵照常转
task.start(function()
while true do
do_work()
system.sleep(5000)
end
end)1.2 在回调里用 sleep 延时
lua
-- ❌ 错误:卡住整个事件循环 1 秒
timer.start(2000, function()
plc.y(0, 1)
system.sleep(1000)
plc.y(0, 0)
end)
-- ✅ 正确:用单次定时器接续
timer.start(2000, function()
plc.y(0, 1)
timer.start(1000, function() plc.y(0, 0) end, true)
end)1.3 编造 API
lua
-- ❌ 错误:这些函数在 LPPLC 上根本不存在
mqtt.on_message(function(t, p) end) -- 不存在
mqtt.connect() -- 不存在
modbus.read_holding(1, 0, 1) -- 不存在
io.write(1, 1) -- 不存在
sleep(1000) -- 不存在(只有 system.sleep)
-- ✅ 正确:真实 API
mqttc = mqtt.creat("broker.emqx.io", 1883, "dev01", "", "")
mqtt.reg(mqttc, mqtt.RECVDATA, function(topic, payload) end)
mqtt.start(mqttc)
plc.y(0, 1)
timer.start(1000, function() end)1.4 对象被 GC 回收
MQTT 客户端、定时器(老固件)、data 对象被回收后会静默失效。
lua
-- ❌ 错误:局部变量,脚本执行完可能被回收
local c = mqtt.creat(...)
c:start()
-- ✅ 正确:用全局变量或长生命周期变量持有
mqttc = mqtt.creat(...)
mqttc:start()老固件(2026-08-22 之前)的
timer.start返回值也必须用全局/长生命周期变量接住, 否则定时器会被 GC 杀掉,现象是"定时器完全不工作、也没有任何报错"。
二、高频级:不报错,但结果永远是错的
2.1 mqtt.is_linked() 返回整数不是布尔
lua
-- ❌ 错误:Lua 里只有 nil/false 为假,0 也是真 —— 条件永远成立
if mqtt.is_linked(mqttc) then mqtt.publish(...) end
-- ✅ 正确:必须 == 1
if mqtt.is_linked(mqttc) == 1 then mqtt.publish(...) end2.2 plc.x / plc.y 返回整数 0/1,不是布尔
lua
-- ❌ 错误:条件永远成立
if plc.x(0) then ... end
-- ✅ 正确
if plc.x(0) == 1 then ... end2.3 32 位与浮点没有属性式写法
plc.DD100、plc.DF200 读出来是 nil、写进去没反应,而且不报错。
lua
-- ❌ 错误:静默失败
local t = plc.DF200
-- ✅ 正确:必须用函数式
plc.dd(100, 100000) -- 写 32 位整数到 D100/D101
local total = plc.dd(100) -- 读
plc.df(200, 25.5) -- 写浮点
local temp = plc.df(200) -- 读2.4 mqtt.publish 未连接时返回 nil(不是 false)
lua
-- ❌ 错误:漏判 nil
if mqtt.publish(mqttc, t, p) == false then print("失败") end
-- ✅ 正确
if not mqtt.publish(mqttc, t, p) then print("失败或未连接") end2.5 定时器周期过小
最小周期 10ms,传入小于 10 的值会被强制按 10 处理。采集类任务建议 ≥ 100ms, 否则会挤占主线程。
三、当前版本的已知不可用/受限项
| 项 | 状态 | 变通做法 |
|---|---|---|
mqtt.unsubscribe | 2026-08-16 之前编译的 ESP32 固件、以及 4G 平台(LP8910/8911/8912/8913/8915/8916/8922)和 H750 平台(LP2300/LP2323)的固件上调用直接抛错(bad argument #1 ... string expected, got userdata,主题参数下标写错一位) | 能升级的升级到 2026-08-16 之后的固件;不确定的先 pcall 试一次。收不关心的主题也可以在 RECVDATA 里 return 忽略,或 set_cleansession(1) 重连让 Broker 清掉 |
system.start_thread | 仅固件有,软 PLC 模拟器没有;且创建失败静默无返回 | 跨平台先判断 if system.start_thread then;并在线程内"报到"确认真的起来了 |
| MQTT | 只支持明文,不支持 TLS | 需要加密只有 plc.aes_encode / plc.aes_decode(+ plc.base64_encode)。crypto 模块只在部分机型上有(LP3232、LP3288(ESP32-S3 平台),LP8910、LP8911、LP8912、LP8913、LP8915、LP8916、LP8922(4G 平台),LP2300、LP2323(STM32H750 平台),以及上位机的内置软设备与软 PLC 模拟器),经典 ESP32 与 F103 机型上调用得到 nil —— 推荐之前先问客户机型,并给出 if crypto then 的退回写法。另注意 aes_encode 只加密长度为 16 整数倍的头部,不足 16 字节原样返回明文,不能当传输加密用 |
| BACnet / OPC UA / 水务 | 仅 ESP32-S3 机型与软 PLC 模拟器 | 其它机型上对应全局表根本不存在,直接用会报 attempt to index a nil value |
mqtt.set_version/set_keepalive/set_cleansession | 必须在 mqtt.start 之前调用,之后再改无效 | — |
四、串口 Modbus 采集(com_modbus)
4.1 字节序设置函数要和通道数据类型配对
四个设置函数各自保存独立的字节序,只对对应类型生效:
| 设置函数 | 生效类型 | 默认值 |
|---|---|---|
set_16_ab | S16/U16 | AB |
set_32_abcd | S32/U32 | CDAB |
set_float_abcd | FLOAT | CDAB |
set_double_abcdefgh | DOUBLE | GHEFCDAB |
lua
-- ❌ 错误:通道是 S16,set_32_abcd 永远不会被读取("设置了没反应"头号原因)
dev:set_32_abcd(com_modbus.BADC)
dev:add_channel(400001, com_modbus.S16, v, com_modbus.READ)
-- ✅ 正确:16 位就用 set_16_ab
dev:set_16_ab(com_modbus.BA)
dev:add_channel(400001, com_modbus.S16, v, com_modbus.READ)32 位/浮点读出来数值离谱(温度显示成几万),十有八九是字序没设对, 依次试 CDAB → BADC → ABCD → DCBA。
4.2 组合地址从 1 开始,分离形式从 0 开始
lua
dev:add_channel(400001, com_modbus.FLOAT, 100, com_modbus.READ) -- 40001 = 偏移 0
dev:add_channel(4, 0, com_modbus.FLOAT, 100, com_modbus.READ) -- 分离形式从 0 数两种写法不要混用。
4.3 其它
- 建议传
creat的第 3 个status参数,否则无法区分"从站掉线"和"数值确实是 0"。 - 轮询启动后不要再对同一串口手动
send,会打乱报文时序。
五、并发与异常
- 回调之间不会并发,天然串行,不需要加锁 —— 这是刻意设计,不是缺陷。
- 任何一个回调阻塞,其它所有回调都会被延迟;消息队列深度只有 50 条,长时间阻塞会让高频事件(串口/TCP 数据)被丢弃。
- 回调里未捕获的错误会导致 Lua 状态机重建、脚本 5 秒后从头重新执行。回调内部建议用
pcall包住。luatimer.start(1000, function() local ok, err = pcall(function() do_work() end) if not ok then print("timer error:", err) end end) coroutine.resume吞掉错误(返回false, err),调试必须检查第一个返回值;coroutine.wrap则会把错误抛出,需要外层pcall。- 协程里做纯 CPU 死循环同样会卡住事件循环,协作式调度不等于能占着不放。
- 真正长耗时的活要拆成小步,见 并发模型概述 的「长耗时任务怎么处理」。
- 与梯形图交换数据统一用
plc.*(内部线程安全),不要用 Lua 全局变量跨任务同步。
六、操作类
- 脚本下载后必须重启设备才生效(只下载不重启,跑的还是旧脚本)。
- 修改脚本后要重新点「下载」,仅保存编辑器内容不会更新设备。
- 可用内存:ESP32 Lua 堆约 40–80KB,避免在循环/回调里不断创建新表与大字符串。
- 垃圾回收由消息泵在无消息的空拍步进,事件密集期 GC 会滞后、内存峰值升高。
七、给 AI 的自检清单
写完一段 LPPLC 的 Lua 程序后,逐条确认:
- [ ] 没有
while true+sleep组合(周期任务用了timer.start) - [ ] 所有 API 都能在模块页 / API 速查表 / API 全集 里查到,没有编造函数名
- [ ]
mqtt.is_linked()判断写的是== 1 - [ ]
plc.x/plc.y判断写的是== 1/== 0 - [ ] 32 位/浮点用的是
plc.dd()/plc.df() - [ ] MQTT 客户端、定时器对象是全局或长生命周期变量
- [ ] 回调里有
pcall保护 - [ ] 机型限制已确认(BACnet/OPC UA 仅 ESP32-S3;
start_thread模拟器没有)