跳到主要内容

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 协议进行通信。要求如下:

  1. 每 10 秒使用 Modbus RTU 读取输入寄存器(功能码 0x04)轮询传感器一次
  2. 从输入寄存器 IR4 中解析 CO2 浓度值(单位为 ppm)
  3. 将每个读数连同传感器标识符一起存储在 TimescaleDB 超表(hypertable)中

Senseair S8 寄存器映射

Senseair S8 默认使用地址 0xFE (254, "任意传感器")。CO2 值位于输入寄存器 IR4:

IR#寄存器地址描述
IR10x0000仪表状态 (错误标志)
IR20x0001警报状态
IR30x0002输出状态 (警报/PWM)
IR40x0003空间 CO2 (ppm)
IR220x0015PWM 输出
IR260x0019传感器类型 ID 高位
IR270x001A传感器类型 ID 低位
IR290x001C固件版本 (主.次)
IR300x001D传感器序列号 高位
IR310x001E传感器序列号 低位

通信参数: 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描述
IR4modbus_rtu_254:input_30004CO2 数值 (ppm)

TimescaleDB 输出

每个读数都作为一行插入 co2_readings 超表中:

timesensorco2
2026-03-06 10:00:00+00senseair_s8412
2026-03-06 10:00:10+00senseair_s8415
2026-03-06 10:00:20+00senseair_s8408

查看 GitHub 上的完整示例脚本

相关文档