主题
event — 事件订阅与发布
模块名:event
让程序的不同部分互通消息:一部分代码发出事件,关心它的代码收到通知。 与 task 是配套使用的 —— 任务负责"在后台跑",事件负责"跑来跑去之间怎么通气"。
lua
-- 关心"谁断线了"的代码(第 2 参 "*" = 不管编号):
event.on("cloud", "*", function(k, a)
print("云连接状态变为 " .. a) -- a = 1 在线 / 0 离线
end)
-- 另一处要把这件事告诉别人(自定义事件,两边写同一个名字即可):
event.emit("my_alarm", 1, 85) -- 名字, 编号, 附加数值一个事件由三部分组成:
| 组成 | 说明 |
|---|---|
名字 name | 字符串,说明"发生了什么事",如 key_down |
编号 key | 数字,说明"谁的事",如按键号 3、设备号 1。发的时候给(emit / 固件投递),订阅和等待时可以按编号过滤 |
附加数值 a b | 两个整数,携带额外信息,如温度值、错误码 |
六种用法,按需选
| 你想做的事 | 用什么 | 特点 |
|---|---|---|
| 有事通知我,我继续干别的 | event.on | 不阻塞,到了就调你的函数 |
| 只要通知一次 | event.on(name, key, cb, true) | 收到一次自动退订 |
| 我要发出一件事 | event.emit | 当场通知所有关心它的人 |
| 我要等它们都干完再继续 | event.gather | 叫醒一批任务,等它们各自报到 |
| 停下来等这件事再往下写 | event.wait | 挂起当前任务,代码是平铺的 |
| 现在到底是连上还是没连上 | 任务里 event.wait("cloud", "*") 或读专用标志(如 D8005) | 第 4 版:状态型也只是边沿通知,不再先查当前值;要"当前值"请读 D8005 等,不要从事件里取 |
event.on 和 event.wait 是同一件事的两种写法,底层共用一张订阅表: event.on 是"有事叫我",event.wait 是"我等着"。选哪个只看你希望代码是回调式还是顺序式。
函数总览
event 对外有 6 个函数(与 task 的 3 个合起来是 9 个)。
| 函数 | 作用 |
|---|---|
event.on(name, key, cb, [once]) | 订阅一个事件,返回句柄 |
event.wait(name, key, [ms]) | 在任务里停下来等一个事件 |
event.off(handle) | 退订 |
event.emit(name, [key, a, b]) | 发出一个自定义事件 |
event.gather(name, [ms]) | 叫醒一批等待者,等它们都报完到 |
event.done(name) | 报到:我这一轮干完了 |
task.start / task.stop / task.list | 起停任务、查任务状态(配套用法) |
延时统一用
system.sleep(ms)(任务内等价于让出一拍),没有单独的task.sleep。
event.on
event.on(name, key, cb, [once]) -> handle| 项 | 说明 |
|---|---|
name | 事件名,字符串。系统事件见事件列表;自定义名字首次使用自动登记,不用先声明 |
key | 编号过滤:写数字 = 只收这个编号;写 "*" = 任意编号都收。习惯上统一写 "*",要过滤在回调里判第一个参数 |
cb | 回调函数,收到时被调用,参数是 (key, a, b) |
once | 可选,填 true 表示只收一次,收到后自动退订 |
返回 handle | 句柄,给 event.off 用 |
同一个事件可以被订阅多次,各自独立收到,互不影响。订阅总数上限按机型不同: ESP32 机型 24 个(满了报错),4G 机型与内置软 PLC 模拟器不设总数上限(订阅表动态增长)。 ⚠ 4G 机型单次派发最多执行 8 个 event.on 回调(同一事件同一拍订阅超过 8 个时,多出的本轮丢弃并打印告警), ESP32 与模拟器无此限制。
示例:持续监听
lua
local h = event.on("key_down", "*", function(k, a, b)
print("按下了 " .. k .. " 号键")
end)示例:只关心某一个编号
lua
-- 只有 3 号键才处理;1、2 号键进来后直接忽略
event.on("key_down", 3, function(k)
print("3 号键按下了")
end)示例:只收一次
lua
-- 收到第一次就自动退订,不用自己管句柄
event.on("cloud", "*", function(k, on)
print("第一次连上云")
end, true)event.wait
event.wait(name, key, [ms]) -> key, a, b -- 等到了,固定 3 个值
event.wait(name, key, [ms]) -> nil -- 超时在任务里停下来等一个事件。等到了返回 3 个值,超时返回 nil。
| 项 | 说明 |
|---|---|
name | 事件名,字符串(只支持单个名字,见下方限制) |
key | 编号过滤:数字 = 只等这个编号;"*" 或省略 = 任意 |
ms | 可选,最多等多少毫秒。省略或 0 = 一直等 |
| 返回 | 等到:key, a, b(编号与两个附加值);超时:nil |
返回值只有一种解包写法(没有"名字"这一项,等的时候已经指定了):
lua
local key, a, b = event.wait("my_alarm", "*", 5000)
if key == nil then
print("5 秒内没等到")
else
print("等到了,编号 " .. key)
end状态型事件:投递时 a 携带 on/off,但"记住当前值"已成历史
cloud、net、net_wifi、net_eth、net_4g、plc_run 这几个叫状态型,意思是 投递时 a 携带 1/0(在线/离线、运行/停止),方便回调里直接判;key_down 这类脉冲型a 是事件附带值。
⚠ 第 4 版(2026-10-22)起,所有机型统一为"边沿通知":状态型事件也只在状态发生 变化的那一刻投一条,脚本不会因为"当前已是想要的值"就立刻返回。脚本比事件发生得 晚就收不到 —— 需要"现在到底是什么状态"请读专用状态/标志(如 4G 机型读 D8005), 不要从事件里取。例子见例程 2的兜底写法。
要抓"它断开的那一刻",用 event.on 在回调里判 a == 0(见例程 3)—— event.wait 只等"下一次",没有反向等待。
同一拍连发多条,排队不合并
任务是队列消费:同一拍内对同一个事件 emit 多次,等待者一次 event.wait 收一条, 按 FIFO 一条条取,不合并、不丢(邮箱深度 16 条,见限制)。 所以"要连收 N 条"就写 N 次 event.wait 循环,不要指望一次收全。
限制
- 只能在
task.start起的任务里用。 在主脚本(main.lua 顶层)或event.on回调里调用会报错:event.wait: 不能在主脚本里等—— 主流程卡住会把消息泵堵死,而事件正是靠消息泵送进来的。 解决办法就是把这段代码放进task.start(function() ... end)里。 - 用
coroutine.wrap起的裸协程不支持event.wait,要用task.start起的任务。 - 只支持单个事件名。想"几个里任意一个先到算哪个",起多个任务各等一个、或用
event.on记录先到的那一个(见例程 2的写法)。
event.off
event.off(handle) -> true / false退订。句柄就是 event.on 返回的那个。已经退过返回 false,不会报错。
lua
local h = event.on("key_down", "*", function() print("收到") end)
event.off(h) -- true:之后不会再收到
event.off(h) -- false:本来就没在监听用 once = true 订阅的不需要自己退,它自己会退。
event.emit
event.emit(name, [key, a, b]) -> 命中数发出一个自定义事件,当场通知所有关心它的人,返回命中数。
lua
event.emit("my_alarm") -- 编号按 0 发:只给等编号 0(或任意编号)的
event.emit("my_alarm", 3) -- 编号 3:关心 3 号的和等任意编号的都收到
event.emit("my_alarm", 3, 100, 200) -- 回调收到 (3, 100, 200)
local n = event.emit("my_alarm")
if n == 0 then
print("没人关心这个事件") -- 可以用来发现问题(比如名字写错了)
end三条语义:
- 只限自定义事件。对系统事件名(
cloud/net_wifi/key_down…)emit会直接报错 —— 那些由固件投递,脚本伪造它们等于伪造硬件状态。要发自己的事件请另起一个名字,如"my_online"。 - 广播,不是叫醒第一个。N 个任务在等同一个名字,N 个全部收到。
- 回调当场跑完,等待者下一拍才醒。
emit返回时订阅回调已经执行过了; 而event.wait的等待者只是被标成就绪,要等消息泵推下一拍才恢复。
返回值是"投递命中数":被唤醒或收下这条事件的等待者 + 执行过的订阅回调。 存在"还没被 resume 的等待者"时它会偏大(同拍后续事件进了它的邮箱,也计入), 所以别拿它当"唤醒了几个任务"的对账数字;判断"有没有人接"写 > 0。
一个完整例子:一个按钮通知多个任务
lua
-- 自定义事件名不用声明,两边写一致即可
-- 三个任务都在等 "go"
for i = 1, 3 do
task.start(function()
event.wait("go", "*")
print("任务 " .. i .. " 收到")
end)
end
-- 任何一方发出,三个都会动(打印 3)
print(event.emit("go"))event.gather 与 event.done
这一对是栅栏,解决"一帧数据要让几个任务都处理完,我再往下走"这类协同问题。
event.gather(name, [ms]) -> 参与者个数 | 0 | nil
event.done(name) -> true / falseevent.gather(name) 做三件事:
- 把正在
event.wait(name)的任务全部叫醒(不看编号); - 自己挂起,等它们各自调用一次
event.done(name); - 全部报到完 → 返回参与者个数;没有人等 → 立刻返回
0(不会卡住);超时 → 返回nil。
lua
-- 发令方
task.start(function()
while true do
local n = event.gather("frame", 5000) -- 叫醒所有等 frame 的任务
if n == nil then
print("有参与者没在 5 秒内报到")
else
print("这一帧 " .. n .. " 个任务都处理完了")
end
system.sleep(100)
end
end)
-- 参与者
task.start(function()
while true do
event.wait("frame", "*") -- 被 gather 叫醒
print("我处理我这一份")
event.done("frame") -- 报到:干完了
end
end)event.done 返回 false 是正常的:发令方已超时、或已经被停掉时,参与者照常跑完后报到就没人接, 这时不报错、只返回 false。
同一个栅栏名不能同时有两个 gather:后一个会直接报错(同一栅栏名已有 gather 在进行), 这是为了防止两边互相等、随机卡死。
事件列表
固件推送的系统事件
这些名字由固件定义,脚本只订阅、不能 emit(event.emit 一个系统事件名会报错)。
| 事件名 | 类型 | 载荷 | 含义 |
|---|---|---|---|
key_down | 脉冲 | key = 按键号 | 按键按下(当前固件尚未投递,事件表里预留) |
key_up | 脉冲 | key = 按键号 | 按键抬起(同上,尚未投递) |
task_error | 脉冲 | a = 任务号,b = 错误码 | 任务异常结束(含被看门狗掐断) |
plc_run | 状态 | a = 1 运行 / 0 停止 | PLC 运行状态 |
cloud | 状态 | a = 1 在线 / 0 离线 | 与云平台登录握手成功 / 断开 |
net | 状态 | a = 1 | 任意一种网络可用(WiFi / 网口 / 4G / 云任一在线,派生值) |
net_wifi | 状态 | a = 1 已连上 | WiFi 连上路由器并拿到 IP |
net_eth | 状态 | a = 1 已连上 | 网口插好网线并拿到 IP |
net_4g | 状态 | a = 1 已连上 | 4G 拨号成功拿到 IP |
状态型事件投递时 a 携带 1/0(在线/离线、运行/停止);脉冲型 a 是事件附带值。 两类都只在发生的那一刻通知一次(event.on 每发生一次调一次、event.wait 只等下一次)。 第 4 版已取消"记住当前值":没有 event.get,event.wait 也不会先查当前值,错过就错过。
这 9 个名字全平台统一,但不是每台机器都会产生每一个 —— 取决于机器有什么硬件、跑的是哪套固件。 订阅本机不会产生的事件不报错,只是永远等不到,详见下一节。
各平台的系统事件不一样
这一节回答两个问题:我这台机器有哪些系统事件、该怎么写才不会白等。
网络链路事件:什么时候算"连上"
| 事件 | 变 1 的那一刻 | 变 0 的那一刻 |
|---|---|---|
net_wifi | WiFi 连上路由器并且拿到 IP(只连上路由器、还没拿到 IP 不算) | WiFi 断开 / IP 丢失 / 重连过程中 |
net_eth | 网口插好网线并且拿到 IP | 拔掉网线 / IP 丢失 |
net_4g | 4G 拨号成功、拿到 IP(开机到这一步通常要十几秒) | 4G 掉线 / 信号丢失 / 重新拨号中 |
cloud | 与云平台登录握手成功 | 与云断开(服务器停、链路断、心跳超时) |
net | 上面四个任一为 1(派生值,脚本不能 emit) | 四个全为 0 |
"有网"不等于"能上云":net_wifi / net_eth / net_4g 只说明本机拿到了 IP, 要确认能连到云平台,看的是 cloud。
平台和机型对照
| 事件 | ESP32 机型 LP3201 / 3202 / 3203 / 3222 / 3232 / 3288 | 4G 机型 LP8910 / 8913 / 8916 / 8922 | 上位机工具内置模拟器 | 软 PLC 模拟器 LP6666 |
|---|---|---|---|---|
net_wifi | ✅ 机器带 WiFi(ESP32 机型都带) | ✗ 无 WiFi 硬件 | ✗ | ✗ |
net_eth | ✅ 机器带网口(LP3202 / LP3232 等) | ✗ 无网口 | ✅ | ✗ |
net_4g | ✅ 机器带 4G 模组(LP3232 等) | ⚠ 固件未接入,见下方说明 | ✗ | ✗ |
cloud | ✅ | ⚠ 固件未接入 | ✅ | ✅ |
net | ✅ | ⚠ 派生值,同样等不到 | ✅ | ✅ |
plc_run | ✅ | ✗ 固件未投递 | ✅ | ✗ 固件未投递 |
key_down / key_up | ✗ 固件预留,尚未投递 | ✗ | ✗ | ✗ |
task_error | ✅ | ✅ | ✅ | ✅ |
怎么确认我这台有没有这个事件
两条判据,同时用最稳:
- 看硬件:机身有 WiFi 天线座 →
net_wifi;有 RJ45 网口 →net_eth;有 SIM 卡槽 / 4G 天线 →net_4g。 - 读 D8005 的驱动位(
plc.ds(5)就是 D8005,位定义见 部分特殊软元件详细说明):
lua
local d = plc.ds(5) -- D8005 硬件标志
print("有网口驱动 ", plc.bite(d, 0)) -- .0
print("有 4G 驱动 ", plc.bite(d, 1)) -- .1
print("有 WiFi 驱动", plc.bite(d, 2)) -- .2驱动位是 0 的那一类,对应的 net_* 事件永远不会来。
接收的三种写法
① 开机等"任意一条链路通" —— 跨机型最安全的写法,不用管本机是 WiFi、网口还是 4G:
lua
task.start(function()
local key = event.wait("net", "*", 60000) -- 4G 拨号慢,超时给足
if key == nil then
print("60 秒仍无网络,先本地记录")
return
end
print("网络就绪,开始上报")
end)② 链路一变化就记一笔(WiFi / 网口 / 4G 写法一样,换事件名即可):
lua
event.on("net_wifi", "*", function(k, a)
if a == 1 then
print("WiFi 已连接(拿到 IP)")
else
print("WiFi 掉线")
end
end)③ 4G 机型(LP8910 / 8913 / 8916 等)当前固件没有 net_4g / cloud, 改用定时器观测 D8005。注意不要用顶层 while 死等 —— 顶层就是主线程, 循环期间所有回调都冻住(见串口透传页):
lua
timer.start(5000, function()
local d = plc.ds(5) -- D8005
print("4G 驱动 " .. plc.bite(d, 1) .. ",4G 已联网 " .. plc.bite(d, 5))
end)4G 机型(LP8910 / 8913 / 8916 / 8922)请注意
这批机型的固件目前只投递 task_error,网络与云事件尚未接入: event.wait("net_4g") / event.on("cloud", …) 都不报错,但永远等不到。 要拿链路状态请读 D8005 的位(.1 有 4G 驱动、.5 4G 已连接到网络)。 固件后续补齐后以实测为准 —— 判据很简单:订阅后等 60 秒,看日志有没有输出。
注意事项
- 状态型只在变化时投一次。脚本比网络起得晚时,
event.wait只等下一次边沿,可能漏掉已经发生的开机那一次; 要兜底请用event.on(每次变化都通知)或读 D8005 这类专用状态。 - 4G 从开机到拿到 IP 通常十几秒,超时别只给 10 秒;WiFi 重连一般几秒内。
- 模拟器上没有真实链路:上位机内置模拟器会产生
net_eth/cloud;软 PLC 模拟器(LP6666) 只有cloud与派生的net。两者的 WiFi / 4G 事件都不会来。 - 事件只带"通 / 不通",不携带 IP 地址、信号强度这类细节。要看具体数值读 D8005, IP 用
plc.eth_ip()一类的接口。
用户自定义事件
自定义事件名不用声明:event.on / event.wait / event.emit 任一处第一次写到就自动登记, 收发两边写同一个名字就对上了。最多 24 个自定义事件名,每个名字不超过 24 个字节。
lua
event.on("my_alarm", "*", function(k, v) -- A 处订阅
print("报警值 " .. v)
end)
event.emit("my_alarm", 1, 85) -- B 处发出:编号 1,数值 85⚠ 代价:自定义名拼错不会报错,只会静默等超时(
event.emit返回 0 也说明没人接)。 所以两边名字要照着抄,不要手打。系统事件名拼错则会当场报错并列出现有事件名。
建议命名用小写下划线,<域>_<事件>,例如 tank_full、motor_fault。
例程
每个例程都是可直接下载运行的完整脚本:注册完回调和任务后脚本就结束,固件自动进入消息泵。 API 只用本页与任务页已文档化的函数;统一模板「场景 / 完整代码 / 要点 / 验证方法 / 注意事项」。
例程 1:按键短按 / 长按(脉冲型 + 超时判长按)
适用:LP3201 / LP3202 / LP3203 / LP3222 / LP3232 / LP3288 / LP8910 / LP8913 / LP8916 / 软PLC模拟器 | 依赖模块:event task | 难度:★★☆
场景:面板上一个按键要区分短按(切换显示)和长按(进入设置)。 用"按下后等 3 秒内是否收到抬起"来判断:等到了是短按,等超时了说明还按着,是长按。 等待必须放在任务里 —— 这正是 event.wait 的标准形状。
完整代码(可直接下载运行):
lua
-- 短按/长按判别:key_down 与 key_up 都是脉冲型,一次一次等
task.start(function()
while true do
local key = event.wait("key_down", "*") -- 等下一次按下(等的是编号)
local up = event.wait("key_up", "*", 3000) -- 3 秒内收到抬起 = 短按
if up ~= nil then
print("短按 " .. key)
else
print("长按 " .. key) -- 超时说明一直按着
event.wait("key_up", "*") -- 等它真松开,避免连触发
end
end
end)要点:
- 脉冲型事件没有"当前值",只能等下一次发生 —— 所以这段逻辑天然适合"一次按键一轮判别"。
event.wait(name, "*", 3000)的第 3 参是超时毫秒;超时返回nil,这是判长按的唯一依据。- 长按分支补一次不带超时的
event.wait("key_up"),把这次按压"消费完",否则松开时的那条key_up会被下一轮循环当成新的短按。 - 整个循环挂在
system.sleep无关的等待上,不占主线程;这 3 秒里定时器、网络回调照常。
验证方法:
- 下载运行后短按一下:日志出现
短按 N(N 为键号)。 - 按住超过 3 秒再松:日志出现
长按 N,且松开后不会再冒出一条短按 N。 - 等待期间另挂
timer.start(200, function() print("alive") end):alive在长按判别的 3 秒里持续出现, 证明等待不阻塞其它功能。
注意事项:
- 回调式写法(
event.on("key_down", …))也能做,但"等 3 秒"要把状态拆成标志位 + 定时器, 比任务里的顺序写法难读得多 —— 这类"发生后再等一段"的逻辑优先用任务。 - ⚠ 当前所有机型的固件都还没有投递
key_down/key_up(事件表里是预留项,见 平台对照表)。所以这段脚本在真机上会一直安静等待 —— 不是脚本写错。 要等固件补齐,或改用输入点(plc.x)轮询。
例程 2:等网络就绪再上报,超时降级
适用:同上 | 依赖模块:event task plc | 难度:★☆☆
场景:开机后先把一批配置报到云端,但不能假设网络已就绪。 第 4 版下 event.wait("net", "*") 只等下一次边沿 —— 已经连着不会立刻返回;要立即感知请读 D8005, 再加超时,等不到就走本地降级逻辑。
完整代码(可直接下载运行):
lua
task.start(function()
local key = event.wait("net", "*", 15000) -- 等"任意一种网"通,最多 15 秒
if key == nil then
print("15 秒还没网,本次先本地记录")
plc.m(60, 1) -- 置"待上报"标志,梯形图可见
return
end
print("网络就绪,开始上报")
plc.m(60, 0)
plc.d(100, plc.d(100) + 1) -- 现场换成真实上报
end)要点:
net是派生状态:WiFi / 网口 / 4G / 云任一在线即为 1。只认 WiFi 的脚本换到 4G 机型就不干活, 等"任意一种网"是跨机型安全的写法。- 第 4 版下
event.wait只等下一次边沿:脚本比网络通得晚可能错过,所以例程里用 D8005 轮询 + 超时兜底更稳。 - 超时是必须的:现场可能长期无网,没有超时的
event.wait会让任务永远挂在那里。
验证方法:
- 设备已联网时下载运行:本例会等到下一个网络边沿或走 15 秒超时兜底(第 4 版没有"先查当前值",已连上不会立即返回;要立即感知可加 D8005 轮询)。
- 拔掉网线 / 关 WiFi 后重启脚本:日志 15 秒后出现
15 秒还没网…,M60 置 ON。 - 在等待的 15 秒内恢复网络:日志立即转到
网络就绪…—— 等待被网络事件打断,不用等满超时。
注意事项:
- 只想"每次重连都记一笔"要用
event.on(变化才触发);event.wait是"当前是否可用"。 - 上报失败(对端不回)要靠协议自己的超时/重传,事件只解决"网络通没通"。
- 4G 机型(LP8910 / 8913 / 8916)固件目前不投网络事件:本例会等到 15 秒超时后走降级分支, 之后网络恢复也不会被唤醒(见平台对照表),要用 D8005 观测。 即使后续固件补齐了投递,这批机型也没有"先查当前值"(无状态表),开机比网络起得晚仍会错过 —— 在 4G 机型上"等网络"只有"超时降级 + 轮询 D8005"这一条稳的路。
例程 3:掉线检测与重连等待
适用:同上 | 依赖模块:event task plc | 难度:★★☆
场景:云连接可能中途断开。两类需求分开做: 抓"断开的那一刻"(记日志、停上报)用 event.on 在回调里判 a == 0; 等"重新连上再继续"(把没报完的补上)用任务里 event.wait("cloud", "*")。 状态型事件没有"等它断开"的反向等待,这是标准分工。
完整代码(可直接下载运行):
lua
-- ① 掉线那一刻:只记一笔、停上报(回调要短)
local reporting = true
event.on("cloud", "*", function(k, a)
if a == 0 then
reporting = false
print("云连接断开,暂停上报")
else
reporting = true
print("云连接恢复")
end
end)
-- ② 上报任务:只在在线时发;掉线后挂起在 wait 上,重连即继续
task.start(function()
while true do
if not reporting then
event.wait("cloud", "*") -- 已断开:等下一次变为"在线"
print("等到重连,补报开始")
end
plc.d(100, plc.d(100) + 1) -- 现场换成真实上报
system.sleep(1000)
end
end)要点:
event.on("cloud", "*", cb)的a是新状态:0 = 刚断开、1 = 刚连上。回调里只做轻活 (改个标志位),重活放任务里。- 任务里的
event.wait("cloud", "*"):断开时它等的是"下一次变为在线";如果调用时云已经又连上了, 第 4 版下不会立刻返回(事件只投边沿),可能要等下一次变化或靠超时/D8005 兜底。 - 标志位 + 等待组合,比"在回调里直接调上报函数"更稳:回调执行期间所有事件和任务都停着, 重活放进任务是铁律(见事件页常见问题)。
验证方法:
- 联网下载运行:日志出现
云连接恢复(或安静等待),D100 每秒 +1。 - 断开云连接(服务器停机 / 拔网线):日志出现
云连接断开,暂停上报,D100 停止增长。 - 恢复连接:日志出现
云连接恢复与等到重连,补报开始,D100 继续增长。
注意事项:
- 回调里不能
event.wait(会报错):等待类调用只在任务里合法。上面 ② 的等待就在任务里。 reporting这类标志位是本脚本私有的;要与梯形图共享状态用 M / D 寄存器,别跨任务传 Lua 变量。
例程 4:生产者 / 消费者解耦(自定义事件 + 多订阅)
适用:同上 | 依赖模块:event task | 难度:★★☆
场景:"采到一批数据"之后,可能有多个下游都要用(上报、本地存储、报警判断)。 不要让生产者挨个调用下游 —— 用自定义事件解耦:生产者只负责 emit, 下游想加入就 event.on 一个,想排队处理就起个任务 event.wait。
完整代码(可直接下载运行):
lua
-- 自定义事件 "job_ready":第 2 参 key = 批次号,a = 本批条数
event.on("job_ready", "*", function(k, a)
print("旁路监听:第 " .. k .. " 批,共 " .. a .. " 条")
end)
task.start(function() -- 消费者:排队处理
while true do
local key, a = event.wait("job_ready", "*", 10000)
if key == nil then
print("10 秒没来新批次")
else
print("处理第 " .. key .. " 批(" .. a .. " 条)")
end
end
end)
task.start(function() -- 生产者:每秒一批
local batch = 0
while true do
batch = batch + 1
local n = event.emit("job_ready", batch, 10) -- 返回命中数
print("发出第 " .. batch .. " 批,命中 " .. n .. " 个接收方")
system.sleep(1000)
end
end)要点:
- 自定义名首次使用即登记:上面三处(
on/wait/emit)谁先写都行,不用提前声明。 - 一个
emit是广播:等它的任务和订阅它的回调都收到,各干各的,互不阻塞。 emit的返回值是命中数(等待者 + 回调):这里固定为 2(一个等待任务 + 一个回调)。 同拍连发时它会偏大,别当对账数字,判断"有没有人接"用> 0。- 生产者不等任何人:
emit当场跑完回调、把等待者标就绪就返回,下一拍消费者自己会醒。
验证方法:
- 下载运行:日志每秒一组
发出第 N 批,命中 2 个接收方→旁路监听…→处理第 N 批…。 - 把消费者的
event.wait超时改成 5000、生产者 sleep 改成 6000:日志出现10 秒没来新批次一类的 超时提示后继续 —— 等待有超时,不会永久挂起。 - 把
event.on那行注释掉:命中数变成 1(只有等待任务),证明订阅确实参与了派发。
注意事项:
- 事件不是数据通道:高频数据(每秒几百条)不要用事件传,超出邮箱深度 16 会丢最新的。 批量数据走 D 寄存器或
data模块;事件只传"状态变化 / 批次到达"这类低频信号。 - 自定义事件名两边要照着抄:拼错不会报错,只会静默互相等不到。
例程 5:一帧数据几个任务都处理完再打包(栅栏)
适用:同上 | 依赖模块:event task | 难度:★★★
场景:同一个事件要触发多个独立处理(换算、判警、写库),全部完成后才能打包上传。 笨办法是计数器 + 轮询;用 event.gather / event.done 这一对栅栏,代码就是直白的"发令 → 等齐 → 打包"。
完整代码(可直接下载运行):
lua
-- 三个参与者:被 gather 叫醒后各干各的,干完 done 报到
local function worker(name, ms)
task.start(function()
while true do
event.wait("frame", "*") -- 等发令
system.sleep(ms) -- 各自的活儿(耗时不同)
print(name .. " 处理完")
event.done("frame") -- 报到
end
end)
end
worker("换算", 100)
worker("判警", 300)
worker("写库", 200)
-- 发令方:叫醒所有等 frame 的任务,等它们全部 done 后打包
task.start(function()
system.sleep(1000) -- 等参与者都挂上
while true do
local n = event.gather("frame", 5000) -- 没人等会立即返回 0
if n == nil then
print("有参与者没在 5 秒内报到,本轮丢弃")
else
print(n .. " 个处理全部完成,打包上传")
end
system.sleep(2000)
end
end)要点:
event.gather(name)唤醒所有在等这个名字的任务(不看编号),然后挂起等它们逐个done; 全部报到 → 返回参与者个数,超时 →nil,没人等 → 立即0。- 参与者中途死掉(报错 / 被 stop)不会拖死发令方:它对
done的义务被自动撤销, 表现为 gather 提前返回或超时 —— 现场要防的是"少了一个参与者但没人知道", 可以把返回的n与期望值比对。 - 同一个栅栏名不能有两个 gather 同时进行(后一个直接报错)—— 这是防"互相等死"的护栏。
- 参与者收到唤醒时
event.wait返回的key恒为 0:栅栏是广播,不带编号和载荷, 要传数据让参与者自己去读(D 寄存器 /data模块)。
验证方法:
- 下载运行:每 2 秒一组日志,顺序为
换算 处理完→写库 处理完→判警 处理完(按各自耗时,不按创建顺序),最后3 个处理全部完成,打包上传。 - 把某个参与者的
system.sleep(ms)改成 6000:日志出现有参与者没在 5 秒内报到,本轮丢弃, 且该参与者随后仍打印处理完(它的event.done返回false,无人接收,不报错)。 - 注释掉
worker("判警", 300):gather 返回 2 而不是 3 —— 与期望值比对即可发现少人。
注意事项:
- 参与者循环体不能在自己被叫醒前就
done(event.done对没被唤醒的任务返回false,正常)。 - 栅栏适合"一帧一同步"的场景;如果每帧处理很快(毫秒级),直接顺序调用更简单,不必上栅栏。
例程 6:一条命令多个任务响应(编号过滤)
适用:同上 | 依赖模块:event task plc | 难度:★★☆
场景:一条命令(按键 / 云端下发)要触发多个动作,但不同动作只关心自己的编号: 1 号 = 启动、2 号 = 停止、3 号 = 复位。用编号过滤,每个任务只等自己那条, 互不干扰,也不用在回调里写一长串 if。
完整代码(可直接下载运行):
lua
-- 三个任务各等一个编号;谁的命令来谁动,互不干扰
task.start(function()
while true do
event.wait("cmd", 1) -- 只等 1 号(启动)
print("启动")
plc.y(0, 1)
end
end)
task.start(function()
while true do
event.wait("cmd", 2) -- 只等 2 号(停止)
print("停止")
plc.y(0, 0)
end
end)
task.start(function()
while true do
event.wait("cmd", 3) -- 只等 3 号(复位)
print("复位")
plc.d(100, 0)
end
end)
-- 命令来源:按键 / 云端下发 / 另一段脚本,往 "cmd" 上发即可
-- 例:3 号键 → 复位(注:key_down 当前固件尚未投递,实际项目请用云端下发或输入点轮询)
event.on("key_down", 3, function()
event.emit("cmd", 3)
end)要点:
event.wait("cmd", 1)的第 2 参是编号:只等编号 1 的命令,其它编号不到访。event.emit("cmd", 3)发 3 号,只有等 3 号的那个任务醒。event.on("key_down", 3, cb)同样按编号过滤:只在 3 号键按下时调这个回调。- 命令入口集中在一处(这里是
key_down→cmd),新增命令来源(比如云端下发) 只要再event.on一个 →event.emit("cmd", …),下游三个任务不用动。
验证方法:
- 下载运行后依次发 1 / 2 / 3 号命令(用按键或临时加
event.emit("cmd", N)的定时器): 日志分别出现启动/停止/复位,Y0 与 D100 随之变化。 - 连发两条不同编号(
event.emit("cmd", 1)后接event.emit("cmd", 2)): 两条都被对应任务处理 —— 同一拍连发不合并,各走各的邮箱。 - 发一个没人等的编号(如
event.emit("cmd", 9)):返回 0,什么也不发生,也不报错。
注意事项:
- 编号从 0 开始计数;省略
emit的编号就是按 0 发,只给等 0(或任意编号)的接收方。 - 命令类事件是脉冲语义:发一次算一次,不缓存。要先订阅/等待、再发,顺序反了收不到。
常见问题
订阅了却收不到
先查这几件事:
- 名字写一致了吗 ——
event.emit返回 0 就说明没人订阅,通常是名字或编号对不上。 - 发的地方在订阅之后吗 —— 先
event.on/ 起等待任务,再event.emit,顺序反了收不到(事件不会缓存)。 - 有没有被
event.off退掉了,或者用的是once = true(已经用完自己退了)。 - 这台机器根本不产生这个事件 —— 系统事件是"有机器的那部分硬件才会有": 没有网口就没有
net_eth,4G 机型的固件目前不投net_4g/cloud,key_down/key_up当前所有机型都还没投。订阅不报错,只是永远等不到。 判据见各平台的系统事件不一样(读plc.ds(5)的驱动位最快)。 - 是不是在回调里等的?
event.wait只能在任务里用;在event.on回调里调它会直接报错。
在回调里想"等一下"
不行。event.on 的回调里调用 event.wait 会报错:回调是在消息泵里同步执行的, 里面再等待会把泵堵死,事件就永远进不来。需要在回调触发后做多步流程, 请在回调里 task.start 起一个任务,或在任务里用 event.wait。
lua
-- 正确:回调只负责起头,多步流程放进 task
event.on("cmd", "*", function(k)
if k ~= 1 then return end
task.start(function()
print("第 1 步")
system.sleep(1000)
print("第 2 步")
end)
end)event.wait 超时了但事件确实来过
三种可能:编号对不上(等的编号与发的不同)、事件在订阅/起任务之前就发了(脉冲不缓存)、 同拍连发超过 16 条(邮箱深度上限,超出丢最新的,见限制)。 把 event.wait 的超时去掉、配合 task.list() 看 waited 字段,最容易定位。
event.emit 一个系统事件名报错
系统事件(cloud / net_wifi / key_down / task_error …)由固件投递,脚本 emit 它们会直接报错。 要表达自己的状态,另起一个自定义名字,如 event.emit("my_online", 1)。
事件会不会丢
会,两种丢法:① 同拍连发超过邮箱深度 16,超出的最新几条被丢; ② 消息队列整体积压(长阻塞)时高频事件可能被丢弃。 所以不要用事件传高频数据 —— 采集值请走 plc 寄存器或 data 模块;事件适合传递"状态变化"。
限制与性能
| 项目 | 上限 / 口径 | 说明 |
|---|---|---|
| 同时存活的任务 | 32 个(全平台) | 满了 task.start 报错,见任务页 |
| 订阅总数 | ESP32 24 个(满报错);4G 与模拟器不设上限 | 含 once 订阅;event.off 后名额立即复用 |
| 单次派发回调 | 4G 机型最多 8 个(超出的本轮丢);ESP32 / 模拟器无此上限 | 同一事件同一拍订阅过多时,4G 多出的 event.on 回调本轮不执行(打印告警) |
| 自定义事件名 | ESP32 24 个 / 模拟器 512 个 / 4G 不设上限;均 < 24 字节 | 首次使用自动登记;ESP32 超长报错 |
| 事件邮箱深度 | 16 条 / 任务 | 同拍连发超出部分丢最新的(先到的不被挤掉) |
| 单次执行指令 | ESP32 约 50 万条;4G 与模拟器约 200 万条 | 超过即被看门狗掐断该任务,见任务页 |
| 事件派发延迟 | ≤ 1 拍(约 10ms) | 所有事件统一,无"先查当前值"捷径 |
| 内存占用 | 没用过 = 0 | 首次用到 task / event 时才分配调度上下文 |
调度模型:协作式、无抢占。消息泵每一拍:先推进所有就绪任务,再派发到期消息、跑定时器回调。 所有 Lua 代码都在同一条 Lua 线程上跑,C 侧只往队列塞定长消息,不碰脚本状态 —— 所以任务之间、回调之间都不需要加锁。
老固件的行为不一样(2026-10-22 之前)
更早的固件上,本页有几个写法不成立,老工程迁移时注意:
- 等待事件的方法叫
task.wait,返回 4 个值(事件名, key, a, b);现在叫event.wait、返回 3 个值(key, a, b)。 event.on(name, cb, [once])没有编号参数(两参形式现在仍兼容,等价key = "*")。event.emit(name, a, b)的第 2 参是附加值;现在第 2 参是编号,a/b后移一位。- 等待可以传一张表(
task.wait({"net_wifi","net_4g"})等任意一个);现在只支持单个事件名。 - 订阅总数是 24(原 8)、任务上限随机型(原 ESP32 16 / 其它 8,现全平台 32)。
system.recv还存在,脚本末尾常手写while true do system.recv(10) end(现已删除,见并发模型概述)。