主题
4.3 Lua 接口与编写详解
这一节把自定义块里 Lua 的写法讲透。只要记住「一个固定骨架 + 一张 I 表 + return 对应输出」,就能写出绝大多数控制块。建议先做完 4.2 手把手 再看本页。
4.3.1 固定骨架
每个自定义块的 Lua 都是同一个形状,头尾不要改,只在中间写您的计算:
lua
return function(I)
-- 在这里:用 I.inputs / I.params 取输入,用 I.state 记东西
local result = 0
return result -- 最后 return 结果给输出引脚
end它本质是「返回一个函数」,设备每个扫描周期调用这个函数一次。
4.3.2 I 表完整参考
运行时设备把这个块的输入、参数、私有状态打包成一张表 I 传进来:
| 写法 | 含义 | 注意 |
|---|---|---|
I.inputs[1]、I.inputs[2] | 第 1、第 2 个输入引脚当前的值 | 序号按输入列从上到下从 1 开始,与 key 无关 |
I.params.键名 | key 为该名字的参数当前值 | 如参数 key 是 kp 就写 I.params.kp |
I.state | 本块私有、跨周期保留的表 | 用来记累计值、上一次的值,详见 4.3.4 |
I.scan_ms | 当前程序扫描周期(毫秒) | 做时间换算时用,默认 200 |
输入用「序号」、参数用「键名」,别搞混
输入引脚是按位置序号取的(I.inputs[1]),所以调整输入引脚的上下顺序会影响 Lua;参数是按 key 名字取的(I.params.kp),调换参数顺序不影响。建议引脚顺序定好后就别再拖动。
4.3.3 返回值如何对应输出引脚
return 出去的值,按输出引脚从上到下的顺序一一对应:
- 1 个输出引脚就
return 一个值; - 2 个输出引脚就
return 值1, 值2,第 1 个给第 1 个输出,第 2 个给第 2 个; - return 的个数少于输出引脚数时,没给到的输出写 0;
- 输出具体送到哪个寄存器,由拖用时在块上选,Lua 里不用关心地址。
两个输出的例子(同时输出「平均值」和「是否超限标志」):
lua
return function(I)
local avg = (I.inputs[1] + I.inputs[2]) / 2
local alarm = (avg > (I.params.hi or 100)) and 1 or 0
return avg, alarm -- 第1个输出=平均值,第2个输出=超限(1/0)
end4.3.4 I.state:让块「记住」东西(重点)
普通变量每次调用都会重置,而 I.state 里的内容会跨扫描周期一直保留,非常适合做累计、计时、滤波、沿判断。它还有一个关键特性:同一个块拖多份,每份的 I.state 互相独立,不会串数据。
第一次使用时先给初值(I.state.x = I.state.x or 初值 是固定写法):
lua
return function(I)
I.state.acc = I.state.acc or 0 -- 首次进来初始化为 0
if I.inputs[1] ~= 0 then -- 输入使能时累加一个周期
I.state.acc = I.state.acc + I.scan_ms / 1000
end
return I.state.acc -- 输出累计秒数
endstate 什么时候清零
I.state 是设备运行内存里的状态:设备断电重启、或重新下发程序后会重新初始化;正常扫描过程中一直保留。需要「上电只执行一次」的逻辑,也用 I.state.ready 之类的标志判断。
4.3.5 三条必须遵守的约定
- 骨架固定:始终是
return function(I) ... end,别改成别的形式,别在函数外写执行语句。 - 返回与输出对应:几个输出引脚就 return 几个值,顺序一致;不想输出的值也用 0 占位。
- 绝不能阻塞:这段逻辑每个扫描周期都跑一遍,禁止
sleep、死循环、以及可能算很久的操作,否则会拖慢甚至卡住整个程序的扫描。循环必须能在有限次数内结束。
4.3.6 拿来即用的常用片段
限幅(把输入夹在上下限之间)——参数 lo、hi:
lua
return function(I)
local v, lo, hi = I.inputs[1], (I.params.lo or 0), (I.params.hi or 100)
if v < lo then v = lo elseif v > hi then v = hi end
return v
end滑动平均(近 N 次平均,参数 n)——用 state 存历史:
lua
return function(I)
I.state.buf = I.state.buf or {}
local buf = I.state.buf
buf[#buf + 1] = I.inputs[1]
local n = I.params.n or 4
while #buf > n do table.remove(buf, 1) end
local s = 0
for _, x in ipairs(buf) do s = s + x end
return s / #buf
end沿判断(输入从 0 变 1 的那一拍输出一个脉冲):
lua
return function(I)
local now = I.inputs[1] ~= 0 and 1 or 0
local prev = I.state.prev or 0
I.state.prev = now
return (now == 1 and prev == 0) and 1 or 0 -- 上升沿=1,其余=0
end更多完整块见 4.5 实战案例集。
4.3.7 运行出错时会怎样
设备端对每个自定义块做了错误隔离:某一块的 Lua 一旦运行报错,只会记录一条日志、这一轮不再更新它的输出(输出保持上一次的值),不会影响主循环里的其它块和自带块,也不会让整个程序停机。
因此当某个自定义块输出不变、疑似没有执行时,优先去日志里看有没有「功能块 xxx 执行出错」,再回来检查:变量是不是 nil(输入没接线)、有没有除以 0、return 数量对不对。
4.3.8 语言能力边界
- 可以放心使用:局部变量、
if / elseif / else、for / while(必须能结束)、算术与比较运算、与或非、字符串到数字的常规处理、I.state上的表操作(ipairs、table.insert/remove、#)。 - 不要使用:
require、文件 / 网络 / 操作系统相关库、sleep类延时、任何全局写操作。块应当是「给输入、出结果」的纯计算,副作用只通过输出引脚体现。 - 当前版本一个自定义块内部不能再调用另一个自定义块,需要的功能请在同一个块里写完。