Skip to content

AI 易错点与反模式 ​

写 LPPLC 的 Lua 程序时,下面这些坑踩一个就足以让程序看起来"能跑但完全不对"。 每条都给出错误写法 vs 正确写法。写程序前先扫一遍,能省掉绝大多数调试时间。

两条总纲:

  1. API 只许用本 Wiki 已文档化的,查不到就写 TODO(待确认),严禁凭其它平台或通用 Lua 的习惯编函数名。
  2. 不许阻塞主循环。设备 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(...) end

2.2 plc.x / plc.y 返回整数 0/1,不是布尔 ​

lua
-- ❌ 错误:条件永远成立
if plc.x(0) then ... end

-- ✅ 正确
if plc.x(0) == 1 then ... end

2.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("失败或未连接") end

2.5 定时器周期过小 ​

最小周期 10ms,传入小于 10 的值会被强制按 10 处理。采集类任务建议 ≥ 100ms, 否则会挤占主线程。


三、当前版本的已知不可用/受限项 ​

项状态变通做法
mqtt.unsubscribe2026-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_abS16/U16AB
set_32_abcdS32/U32CDAB
set_float_abcdFLOATCDAB
set_double_abcdefghDOUBLEGHEFCDAB
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 包住。
    lua
    timer.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 模拟器没有)

相关文档 ​

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