跳到主要内容

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_receiveon_sendon_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-3U16 BE负载长度(总是 28 = 26 字节数据 + 2 字节校验和)
PM1.0 CF=14-5U16 BEPM1.0 浓度(μg/m³,CF=1)
PM2.5 CF=16-7U16 BEPM2.5 浓度(μg/m³,CF=1)
PM10 CF=18-9U16 BEPM10 浓度(μg/m³,CF=1)
PM1.0 ATM10-11U16 BEPM1.0 浓度(μg/m³,大气)
PM2.5 ATM12-13U16 BEPM2.5 浓度(μg/m³,大气)
PM10 ATM14-15U16 BEPM10 浓度(μg/m³,大气)
粒子 >0.3μm16-17U16 BE每 0.1L 空气中的数量
粒子 >0.5μm18-19U16 BE每 0.1L 空气中的数量
粒子 >1.0μm20-21U16 BE每 0.1L 空气中的数量
粒子 >2.5μm22-23U16 BE每 0.1L 空气中的数量
粒子 >5.0μm24-25U16 BE每 0.1L 空气中的数量
粒子 >10μm26-27U16 BE每 0.1L 空气中的数量
预留28-29U16 BE版本/错误代码
校验和30-31U16 BESum16 校验和

完整实现

-- 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 字符串)- 必需,不能为 nil
  • delay_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 参数以定位特定连接。