Skip to content

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

三条语义:

  1. 只限自定义事件。对系统事件名(cloud / net_wifi / key_down …)emit 会直接报错 —— 那些由固件投递,脚本伪造它们等于伪造硬件状态。要发自己的事件请另起一个名字,如 "my_online"。
  2. 广播,不是叫醒第一个。N 个任务在等同一个名字,N 个全部收到。
  3. 回调当场跑完,等待者下一拍才醒。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 / false

event.gather(name) 做三件事:

  1. 把正在 event.wait(name) 的任务全部叫醒(不看编号);
  2. 自己挂起,等它们各自调用一次 event.done(name);
  3. 全部报到完 → 返回参与者个数;没有人等 → 立刻返回 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_wifiWiFi 连上路由器并且拿到 IP(只连上路由器、还没拿到 IP 不算)WiFi 断开 / IP 丢失 / 重连过程中
net_eth网口插好网线并且拿到 IP拔掉网线 / IP 丢失
net_4g4G 拨号成功、拿到 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✅✅✅✅

怎么确认我这台有没有这个事件 ​

两条判据,同时用最稳:

  1. 看硬件:机身有 WiFi 天线座 → net_wifi;有 RJ45 网口 → net_eth;有 SIM 卡槽 / 4G 天线 → net_4g。
  2. 读 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 秒里定时器、网络回调照常。

验证方法:

  1. 下载运行后短按一下:日志出现 短按 N(N 为键号)。
  2. 按住超过 3 秒再松:日志出现 长按 N,且松开后不会再冒出一条 短按 N。
  3. 等待期间另挂 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 会让任务永远挂在那里。

验证方法:

  1. 设备已联网时下载运行:本例会等到下一个网络边沿或走 15 秒超时兜底(第 4 版没有"先查当前值",已连上不会立即返回;要立即感知可加 D8005 轮询)。
  2. 拔掉网线 / 关 WiFi 后重启脚本:日志 15 秒后出现 15 秒还没网…,M60 置 ON。
  3. 在等待的 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 兜底。
  • 标志位 + 等待组合,比"在回调里直接调上报函数"更稳:回调执行期间所有事件和任务都停着, 重活放进任务是铁律(见事件页常见问题)。

验证方法:

  1. 联网下载运行:日志出现 云连接恢复(或安静等待),D100 每秒 +1。
  2. 断开云连接(服务器停机 / 拔网线):日志出现 云连接断开,暂停上报,D100 停止增长。
  3. 恢复连接:日志出现 云连接恢复 与 等到重连,补报开始,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 当场跑完回调、把等待者标就绪就返回,下一拍消费者自己会醒。

验证方法:

  1. 下载运行:日志每秒一组 发出第 N 批,命中 2 个接收方 → 旁路监听… → 处理第 N 批…。
  2. 把消费者的 event.wait 超时改成 5000、生产者 sleep 改成 6000:日志出现 10 秒没来新批次 一类的 超时提示后继续 —— 等待有超时,不会永久挂起。
  3. 把 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 模块)。

验证方法:

  1. 下载运行:每 2 秒一组日志,顺序为 换算 处理完 → 写库 处理完 → 判警 处理完 (按各自耗时,不按创建顺序),最后 3 个处理全部完成,打包上传。
  2. 把某个参与者的 system.sleep(ms) 改成 6000:日志出现 有参与者没在 5 秒内报到,本轮丢弃, 且该参与者随后仍打印 处理完(它的 event.done 返回 false,无人接收,不报错)。
  3. 注释掉 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. 下载运行后依次发 1 / 2 / 3 号命令(用按键或临时加 event.emit("cmd", N) 的定时器): 日志分别出现 启动 / 停止 / 复位,Y0 与 D100 随之变化。
  2. 连发两条不同编号(event.emit("cmd", 1) 后接 event.emit("cmd", 2)): 两条都被对应任务处理 —— 同一拍连发不合并,各走各的邮箱。
  3. 发一个没人等的编号(如 event.emit("cmd", 9)):返回 0,什么也不发生,也不报错。

注意事项:

  • 编号从 0 开始计数;省略 emit 的编号就是按 0 发,只给等 0(或任意编号)的接收方。
  • 命令类事件是脉冲语义:发一次算一次,不缓存。要先订阅/等待、再发,顺序反了收不到。

常见问题 ​

订阅了却收不到

先查这几件事:

  1. 名字写一致了吗 —— event.emit 返回 0 就说明没人订阅,通常是名字或编号对不上。
  2. 发的地方在订阅之后吗 —— 先 event.on / 起等待任务,再 event.emit,顺序反了收不到(事件不会缓存)。
  3. 有没有被 event.off 退掉了,或者用的是 once = true(已经用完自己退了)。
  4. 这台机器根本不产生这个事件 —— 系统事件是"有机器的那部分硬件才会有": 没有网口就没有 net_eth,4G 机型的固件目前不投 net_4g / cloud, key_down / key_up 当前所有机型都还没投。订阅不报错,只是永远等不到。 判据见各平台的系统事件不一样(读 plc.ds(5) 的驱动位最快)。
  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(现已删除,见并发模型概述)。

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