Senseair S8 CO2 传感器到 TimescaleDB
概览
本示例展示了如何通过 Modbus RTU 轮询 Senseair S8 CO2 传感器,并将读数存储在 TimescaleDB 中。它展示了 CycBox 的以下功能:
- 轮询 Modbus RTU 设备 —— 使用内置的 Modbus RTU 编解码器(modbus_rtu_codec)
- 读取 CO2 浓度 —— 从 Senseair S8 的输入寄存器中读取
- 将时间序列数据存储在 TimescaleDB 中 —— 使用异步插入
- 定期数据采集 —— 具有可配置的轮询间隔
这是室内空气质量监测中的一种常见模式,需要将 CO2 测量值记录在时间序列数据库中,以便进行分析和警报。
场景
一个 Senseair S8 CO2 传感器通过串口(例如 /dev/ttyACM0)连接,并以 9600 波特率使用 Modbus RTU 协议进行通信。要求如下:
- 每 10 秒使用 Modbus RTU 读取输入寄存器(功能码 0x04)轮询传感器一次
- 从输入寄存器 IR4 中解析 CO2 浓度值(单位为 ppm)
- 将每个读数连同传感器标识符一起存储在 TimescaleDB 超表(hypertable)中
Senseair S8 寄存器映射
Senseair S8 默认使用地址 0xFE (254, "任意传感器")。CO2 值位于输入寄存器 IR4:
| IR# | 寄存器地址 | 描述 |
|---|---|---|
| IR1 | 0x0000 | 仪表状态 (错误标志) |
| IR2 | 0x0001 | 警报状态 |
| IR3 | 0x0002 | 输出状态 (警报/PWM) |
| IR4 | 0x0003 | 空间 CO2 (ppm) |
| IR22 | 0x0015 | PWM 输出 |
| IR26 | 0x0019 | 传感器类型 ID 高位 |
| IR27 | 0x001A | 传感器类型 ID 低位 |
| IR29 | 0x001C | 固件版本 (主.次) |
| IR30 | 0x001D | 传感器序列号 高位 |
| IR31 | 0x001E | 传感器序列号 低位 |
通信参数: 9600 波特率,8 位数据位,无奇偶校验,1 位停止位。
配置
CycBox 配置了一个使用 Modbus RTU 编解码器的串口连接:
{
"version": "1.11.1",
"name": "Senseair S8 CO2 传感器到 TimescaleDB",
"description": "具有 TimescaleDB 存储功能的 Modbus RTU",
"configs": [
{
"app": {
"app_transport": "serial",
"app_codec": "modbus_rtu_codec",
"app_transformer": "disable",
"app_encoding": "UTF-8"
},
"serial": {
"serial_port": "/dev/ttyACM0",
"serial_baud_rate": 9600,
"serial_data_bits": 8,
"serial_parity": "none",
"serial_stop_bits": "1",
"serial_flow_control": "none"
},
"modbus_rtu_codec": {
"with_receive_timeout": 20
}
}
]
}
连接详情
| 参数 | 值 |
|---|---|
| 传输协议 | 串口 |
| 编解码器 | modbus_rtu_codec |
| 端口 | /dev/ttyACM0 |
| 波特率 | 9600 |
| 数据位 | 8 |
| 奇偶校验 | 无 |
| 停止位 | 1 |
| 接收超时 | 20 (x100ms) |
TimescaleDB 模式 (Schema)
在运行脚本前创建超表:
CREATE TABLE IF NOT EXISTS co2_readings
(
time
TIMESTAMPTZ
DEFAULT
NOW
(
),
sensor TEXT NOT NULL,
co2 INTEGER NOT NULL
);
SELECT create_hypertable('co2_readings', 'time', if_not_exists = > TRUE);
Lua 脚本逻辑
Lua 脚本处理三个任务:TimescaleDB 连接、定期轮询以及带存储功能的响应解析。
脚本拆解
local SLAVE_ADDR = 0xFE -- 254, "任意传感器" 地址
local CO2_REG_ADDR = 0x0003 -- IR4 起始地址 (寄存器号 - 1)
local CO2_REG_QTY = 1
local CONNSTR = "host=localhost port=5432 dbname=cycbox user=postgres password=xxxxxx sslmode=disable"
local POOL_SIZE = 3
local POLL_INTERVAL = 10000 -- 10 秒 (单位 ms)
运作原理
1. 初始化 (on_start)
function on_start()
local ok, err = timescaledb_connect(CONNSTR, POOL_SIZE)
if not ok then
log("error", "连接 TimescaleDB 失败: " .. (err or "未知错误"))
else
log("info", "已连接到 TimescaleDB")
end
-- 立即发送第一次读取请求
modbus_rtu_read_input_registers(SLAVE_ADDR, CO2_REG_ADDR, CO2_REG_QTY, 0, 0)
log("info", "Senseair S8 轮询已开始")
end
- 建立一个具有 3 个连接的 TimescaleDB 连接池
- 立即发送一个 Modbus 读取请求,以便无需等待第一个定时器触发即可获得数据
2. 响应解析与存储 (on_receive)
function on_receive()
if message.connection_id ~= 0 then
return false
end
local co2 = message:get_value("modbus_rtu_254:input_30004")
if co2 == nil then
log("warn", "响应中没有 CO2 数值")
return false
end
log("info", string.format("CO2: %d ppm", co2))
message:add_int_value("CO2", co2)
-- 异步插入 TimescaleDB
timescaledb_insert_async("co2_readings", {"sensor", "co2"}, {"senseair_s8", co2})
return true
end
- 过滤消息,仅处理来自连接 0(串口)的响应
- 使用编解码器分配的数值 ID
modbus_rtu_254:input_30004获取 CO2 数值 - 通过
message:add_int_value()将数值添加到消息元数据中,用于 UI 图表展示 - 将读数异步插入 TimescaleDB —— 非阻塞式,因此不会延迟下一次轮询
3. 定期轮询 (on_timer)
local timer_counter = 0
function on_timer(elapsed_ms)
timer_counter = timer_counter + 100
if timer_counter < POLL_INTERVAL then
return
end
timer_counter = 0
modbus_rtu_read_input_registers(SLAVE_ADDR, CO2_REG_ADDR, CO2_REG_QTY, 0, 0)
end
on_timer()每 100ms 调用一次- 累加运行时间,并每 10 秒触发一次 Modbus 读取
- 发送读取输入寄存器请求(功能码 0x04)以读取 IR4 (CO2)
4. 清理 (on_stop)
function on_stop()
timescaledb_disconnect()
log("info", "Senseair S8 轮询已停止")
end
- 断开 TimescaleDB 连接并释放连接池
Modbus RTU 编解码器数值 ID
Modbus RTU 编解码器会自动解析寄存器值并按照以下模式分配 ID:
modbus_rtu_{slave_addr}:input_{30001 + register_address}
对于本示例(从站地址为 254 (0xFE),寄存器地址为 0x0003):
| 寄存器 | 数值 ID | 描述 |
|---|---|---|
| IR4 | modbus_rtu_254:input_30004 | CO2 数值 (ppm) |
TimescaleDB 输出
每个读数都作为一行插入 co2_readings 超表中:
| time | sensor | co2 |
|---|---|---|
| 2026-03-06 10:00:00+00 | senseair_s8 | 412 |
| 2026-03-06 10:00:10+00 | senseair_s8 | 415 |
| 2026-03-06 10:00:20+00 | senseair_s8 | 408 |
查看 GitHub 上的完整示例脚本。