Lua 脚本
Lua 脚本模块为 CycBox 提供了一个强大的沙盒脚本环境,提供多个钩子函数,用于实现自定义的运行逻辑与消息处理逻辑。包括以下特性:
- 钩子函数:支持
on_start()、on_receive()、on_send()、on_send_confirm()和on_timer()钩子函数。 - 多连接支持:单一 Lua 实例处理所有连接的消息,支持跨连接编排。
- 消息路由:通过
connection_id字段在连接间路由消息。也可以通过send_after()等辅助函数将消息拷贝到特定连接。 - 辅助函数:提供日志记录、MQTT 发送、Modbus 发送等实用函数。
架构
CycBox 运行一个单一共享的 Lua 任务来处理来自所有连接的消息。这样可以实现强大的跨连接编排,同时保持资源效率。
┌────────────────────────────────────────────────────────────────┐
│ 连接 0 连接 1 │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐ │
│ │传输层 │ │处理线程 │ │传输层 │ │处理线程 │ │
│ └────┬────┘ └────┬─────┘ └────┬────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │
│ └─────────────┬─────────────┘ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ 共享 Lua 任务 │ ← 单一 Lua 实例 │
│ │ - on_receive() │ │
│ │ - on_send() │ │
│ │ - on_send_confirm() │ │
│ │ - on_timer() │ │
│ └───────────┬─────────────┘ │
│ ▼ │
│ UI │
└────────────────────────────────────────────────────────────────┘
接收路径:
传输层 → [编解码解码] → [转换器] → [格式化] → Lua [on_receive] → UI
发送路径:
UI → Lua [on_send] → [转换器] → [编解码编码] → 传输层
↓
[格式化] → Lua [on_send_confirm] → UI
定时器:
每 100ms → [on_timer - 全局]
基本脚本结构
Lua 脚本可以实现这五个可选钩子的任意组合:
-- 在 Lua 任务启动时调用一次(在连接之前)
-- 用于初始化、设置或启动日志记录
-- 可以查询连接信息
function on_start()
local count = get_connection_count()
log("info", "引擎启动,拥有 " .. count .. " 个连接")
-- 查询每个连接的传输层和编解码器
for i = 0, count - 1 do
local transport = get_transport(i)
local codec = get_codec(i)
log("info", "连接 " .. i .. ": " .. transport .. " with " .. codec)
end
end
-- 为来自任何连接的每条接收消息调用(RX 路径)
-- 访问全局 'message' 对象以读取/修改消息字段
-- message.connection_id 标识消息来自哪个连接
-- 如果消息被修改,必须返回 true,否则返回 false
function on_receive()
local conn_id = message.connection_id
local payload = message.payload
log("info", "从连接 " .. conn_id .. " 接收: " .. #payload .. " 字节")
-- 您的处理逻辑写在这里
return false -- 如果消息被修改,返回 true
end
-- 为每条传出消息调用(TX 路径,在处理器之前)
-- 访问全局 'message' 对象以读取/修改消息字段
-- message.connection_id 控制哪个连接将接收消息
-- 可以修改 connection_id 以路由到不同的连接
-- 如果消息被修改,必须返回 true,否则返回 false
function on_send()
local conn_id = message.connection_id
log("info", "发送到连接 " .. conn_id)
-- 您的处理逻辑写在这里
return false -- 如果消息被修改,返回 true
end
-- 为每条 TX 确认调用(成功发送后)
-- 用于记录确认或触发后续操作
-- 如果消息被修改,必须返回 true,否则返回 false
function on_send_confirm()
local conn_id = message.connection_id
log("info", "消息在连接 " .. conn_id .. " 上发送成功")
return false
end
-- 定期调用(每 100ms)- 全局定时器
-- 用于轮询、定时器或跨所有连接的定期任务
function on_timer(elapsed_ms)
-- elapsed_ms: 自 Lua 任务启动以来的毫秒数
-- 示例:根据经过的时间轮询不同的连接
end
重要:对于消息钩子(on_receive、on_send、on_send_confirm),返回值控制修改后的消息是否被复制回:
return true- 消息修改被应用(负载、值、connection_id 等)return false- 消息通过未改变(性能优化)
连接 ID:每条消息都有一个 connection_id 字段(0 基索引)。Lua 可以:
- 读取
message.connection_id以确定on_receive()中的源连接 - 修改
message.connection_id以将 TX 消息路由到不同的连接
多连接特性
连接查询 API
Lua 环境提供了查询所有活跃连接信息的函数:
-- 获取连接总数
local count = get_connection_count() -- 返回:整数(例如,2)
-- 获取特定连接的传输类型
local transport = get_transport(0) -- 返回:字符串(例如,"serial"、"tcp_client"、"websocket_client")
-- 获取特定连接的编解码器类型
local codec = get_codec(1) -- 返回:字符串(例如,"modbus_rtu"、"line"、"frame")
注意:连接 ID 是 0 基索引(0、1、2、...、count-1)。
连接间消息路由
connection_id 字段启用强大的跨连接路由:
-- 示例:将串行数据转发到 TCP 连接
function on_receive()
local conn_id = message.connection_id
local transport = get_transport(conn_id)
-- 如果从串行(连接 0)接收,转发到 TCP(连接 1)
if transport == "serial" and get_connection_count() > 1 then
log("info", "将串行数据转发到 TCP 连接")
-- 向连接 1 发送副本(将在下一个 on_send 中处理)
send_after(message.payload, 0, 1) -- delay=0ms, connection_id=1
end
return false -- 原始消息继续到 UI
end
示例:PMS9103M 空气质量传感器
PMS9103M 是一个颗粒物传感器,输出自定义二进制协议的数据:
协议规范
| 字段 | 偏移量 | 类型 | 描述 |
|---|---|---|---|
| 前缀 | 0-1 | 字节 | 固定头 0x42 0x4D ("BM") |
| 长度 | 2-3 | U16 BE | 负载长度(总是 28 = 26 字节数据 + 2 字节校验和) |
| PM1.0 CF=1 | 4-5 | U16 BE | PM1.0 浓度(μg/m³,CF=1) |
| PM2.5 CF=1 | 6-7 | U16 BE | PM2.5 浓度(μg/m³,CF=1) |
| PM10 CF=1 | 8-9 | U16 BE | PM10 浓度(μg/m³,CF=1) |
| PM1.0 ATM | 10-11 | U16 BE | PM1.0 浓度(μg/m³,大气) |
| PM2.5 ATM | 12-13 | U16 BE | PM2.5 浓度(μg/m³,大气) |
| PM10 ATM | 14-15 | U16 BE | PM10 浓度(μg/m³,大气) |
| 粒子 >0.3μm | 16-17 | U16 BE | 每 0.1L 空气中的数量 |
| 粒子 >0.5μm | 18-19 | U16 BE | 每 0.1L 空气中的数量 |
| 粒子 >1.0μm | 20-21 | U16 BE | 每 0.1L 空气中的数量 |
| 粒子 >2.5μm | 22-23 | U16 BE | 每 0.1L 空气中的数量 |
| 粒子 >5.0μm | 24-25 | U16 BE | 每 0.1L 空气中的数量 |
| 粒子 >10μm | 26-27 | U16 BE | 每 0.1L 空气中的数量 |
| 预留 | 28-29 | U16 BE | 版本/错误代码 |
| 校验和 | 30-31 | U16 BE | Sum16 校验和 |
完整实现
-- PMS9103M 空气质量传感器解析器
-- 解析颗粒物浓度和粒子计数
function on_receive()
local conn_id = message.connection_id
local payload = message.payload
-- 验证负载长度(前缀/长度/校验和之后为 26 字节)
if #payload ~= 26 then
log("warn", "连接 " .. conn_id .. ": 无效的 PMS9103M 负载长度: " .. #payload .. " (预期 26)")
return false
end
-- 解析 PM 浓度(CF=1,标准颗粒),单位 μg/m³
local pm1_0_cf1 = read_u16_be(payload, 1) -- 偏移 1(完整帧中的字节 4-5)
local pm2_5_cf1 = read_u16_be(payload, 3) -- 偏移 3(字节 6-7)
local pm10_cf1 = read_u16_be(payload, 5) -- 偏移 5(字节 8-9)
-- 解析 PM 浓度(大气环境),单位 μg/m³
local pm1_0_atm = read_u16_be(payload, 7) -- 偏移 7(字节 10-11)
local pm2_5_atm = read_u16_be(payload, 9) -- 偏移 9(字节 12-13)
local pm10_atm = read_u16_be(payload, 11) -- 偏移 11(字节 14-15)
-- 解析粒子计数(每 0.1L 空气中的粒子数)
local particles_0_3um = read_u16_be(payload, 13) -- 偏移 13(字节 16-17)
local particles_0_5um = read_u16_be(payload, 15) -- 偏移 15(字节 18-19)
local particles_1_0um = read_u16_be(payload, 17) -- 偏移 17(字节 20-21)
local particles_2_5um = read_u16_be(payload, 19) -- 偏移 19(字节 22-23)
local particles_5_0um = read_u16_be(payload, 21) -- 偏移 21(字节 24-25)
local particles_10um = read_u16_be(payload, 23) -- 偏移 23(字节 26-27)
-- 将 PM 浓度添加到图表(CF=1 vs 大气对比)
message:add_int_value("PM1.0 (μg/m³)", pm1_0_cf1)
message:add_int_value("PM1.0 (μg/m³)", pm1_0_atm,)
message:add_int_value("PM2.5 (μg/m³)", pm2_5_cf1)
message:add_int_value("PM2.5 (μg/m³)", pm2_5_atm)
message:add_int_value("PM10 (μg/m³)", pm10_cf1)
message:add_int_value("PM10 (μg/m³)", pm10_atm)
-- 将粒子计数添加到图表(全部在同一组以进行比较)
message:add_int_value("粒子 >0.3μm", particles_0_3um)
message:add_int_value("粒子 >0.5μm", particles_0_5um)
message:add_int_value("粒子 >1.0μm", particles_1_0um)
message:add_int_value("粒子 >2.5μm", particles_2_5um)
message:add_int_value("粒子 >5.0μm", particles_5_0um)
message:add_int_value("粒子 >10μm", particles_10um)
-- 记录解析的值以便调试
log("info", string.format("PM2.5: CF=%d ATM=%d μg/m³", pm2_5_cf1, pm2_5_atm))
-- 返回 true,因为我们向消息添加了值
return true
end
Lua API 参考
全局钩子
所有钩子都是可选的。仅实现您需要的钩子。
on_start()
在 Lua 任务启动时调用一次(在处理任何消息之前)。
用途:初始化、记录启动信息、查询连接配置
示例:
function on_start()
log("info", "脚本启动,拥有 " .. get_connection_count() .. " 个连接")
end
on_receive() → boolean
为来自任何连接的每条接收消息调用(RX 路径)。
全局变量:
message- 接收的消息对象(设置了connection_id)
返回值:
true- 消息被修改,更改将被应用false- 消息未改变,优化以避免复制
用途:解析协议、提取遥测、记录接收的数据、路由消息
示例:
function on_receive()
local conn_id = message.connection_id
local payload = message.payload
-- 解析并添加值
message:add_int_value("温度", read_i16_be(payload, 1))
return true -- 消息被修改
end
on_send() → boolean
为来自 UI 的每条传出消息调用(TX 路径,在处理器之前)。
全局变量:
message- 传出的消息对象(设置了connection_id)
返回值:
true- 消息被修改,更改将被应用false- 消息未改变
用途:修改负载、更改路由、添加校验和、记录传出的数据
示例:
function on_send()
-- 基于连接类型路由
if get_transport(message.connection_id) == "serial" then
-- 为串行连接修改
return false
end
return false
end
on_send_confirm() → boolean
为每条成功发送后的 TX 确认调用。
全局变量:
message- 确认的消息对象(设置了connection_id)
返回值:
true- 消息被修改(很少需要)false- 消息未改变
用途:记录确认、触发后续操作、跟踪发送的消息
示例:
function on_send_confirm()
log("info", "在连接 " .. message.connection_id .. " 上发送了 " .. #message.payload .. " 字节")
return false
end
on_timer(elapsed_ms)
定期调用,每100ms 一次(全局定时器,不是按连接)。
参数:
elapsed_ms(数字)- 自 Lua 任务启动以来的毫秒数
用途:定期轮询、计划任务、超时检测
示例:
local last_poll_time = 0
function on_timer(elapsed_ms)
-- 每秒轮询一次
if elapsed_ms - last_poll_time >= 1000 then
send_after("POLL\n", 0, 0) -- 发送到连接 0
last_poll_time = elapsed_ms
end
end
连接查询函数
get_connection_count() → number
返回活跃连接的总数。
返回:整数计数(例如,2)
示例:
local count = get_connection_count()
for i = 0, count - 1 do
log("info", "连接 " .. i .. " 存在")
end
get_transport(connection_id) → string
返回指定连接的传输类型。
参数:
connection_id(数字)- 连接索引(0 基)
返回:传输类型字符串(例如,"serial"、"tcp_client"、"websocket_client"、"mqtt"、"udp")
示例:
local transport = get_transport(0)
if transport == "serial" then
log("info", "连接 0 是一个串行端口")
end
get_codec(connection_id) → string
返回指定连接的编解码器类型。
参数:
connection_id(数字)- 连接索引(0 基)
返回:编解码器类型字符串(例如,"modbus_rtu"、"modbus_tcp"、"line"、"frame"、"passthrough")
示例:
local codec = get_codec(1)
if codec == "modbus_rtu" then
log("info", "连接 1 使用 Modbus RTU")
end
实用函数
log(level, message)
将消息记录到应用日志。
参数:
level(字符串,可选)- 日志级别:"debug"、"info"、"warn"、"error"(默认:"info")message(字符串,可选)- 要记录的消息(默认:"<nil>")
示例:
log("info", "处理开始")
log("warn", "接收到无效数据")
log("error", "临界故障")
log("debug", "变量 x = " .. x)
send_after(payload, delay_ms, connection_id) → boolean
调度一条消息在延迟后发送到特定连接。
参数:
payload(字符串)- 消息负载(二进制数据作为 Lua 字符串)- 必需,不能为 nildelay_ms(数字)- 发送前的延迟(毫秒)connection_id(数字,可选)- 目标连接 ID(默认:0)
返回:
true- 消息调度成功false- 调度失败(通道关闭或错误)- 错误如果负载为 nil
示例:
-- 1 秒后发送到默认连接(0)
send_after("Hello\n", 1000)
-- 立即发送到连接 1
send_after("PING\n", 0, 1)
-- 500ms 后向连接 0 发送二进制数据
send_after("\x01\x03\x00\x00\x00\x0A", 500, 0)
消息对象 API
钩子中的全局 message 对象提供对消息数据和元数据的访问。
字段
message.connection_id(数字)- 源/目标连接 ID(0 基,可在 on_send 中修改)message.payload(字符串)- 原始消息负载作为二进制字符串message.contents(字符串)- 格式化显示文本(只读)message.values(表)- 提取的数据值(只读)message.metadata(表)- 传输特定的元数据(只读)
方法
message:add_content(text)- 添加显示文本message:add_int_value(id, value, group)- 添加整数遥测值message:add_float_value(id, value, group)- 添加浮点遥测值message:add_string_value(id, value, group)- 添加字符串值
详见消息 API 文档了解完整详情。
二进制数据读取函数
用于解析二进制协议负载的函数。全部使用1 基索引(Lua 约定)。
可用函数:
read_u8(payload, offset)- 读取无符号 8 位整数read_i8(payload, offset)- 读取有符号 8 位整数read_u16_be(payload, offset)- 读取无符号 16 位大端序read_u16_le(payload, offset)- 读取无符号 16 位小端序read_i16_be(payload, offset)- 读取有符号 16 位大端序read_i16_le(payload, offset)- 读取有符号 16 位小端序read_u32_be(payload, offset)- 读取无符号 32 位大端序read_u32_le(payload, offset)- 读取无符号 32 位小端序read_i32_be(payload, offset)- 读取有符号 32 位大端序read_i32_le(payload, offset)- 读取有符号 32 位小端序read_float_be(payload, offset)- 读取 32 位浮点大端序read_float_le(payload, offset)- 读取 32 位浮点小端序read_double_be(payload, offset)- 读取 64 位双精度大端序read_double_le(payload, offset)- 读取 64 位双精度小端序
示例:
local payload = message.payload
local temp = read_i16_be(payload, 1) -- 读取字节 1-2 作为有符号 16 位大端序
local humidity = read_u16_le(payload, 3) -- 读取字节 3-4 作为无符号 16 位小端序
协议辅助函数
其他协议特定的辅助函数在单独的页面上记录:
- Modbus API -
modbus_read_coils()、modbus_write_single_register()等 - MQTT API -
mqtt_publish(topic, payload, qos, retain, delay, connection_id) - UDP API -
udp_send(destination_address, destination_port, payload, delay, connection_id)
所有协议辅助函数都接受可选的 connection_id 参数以定位特定连接。