跳到主要内容

PMS9103M 空气质量监测器与多渠道通知

概览

本示例展示了如何解析 PMS9103M 颗粒物传感器的数据,并在 PM2.5 浓度超过安全阈值时,通过多个通知渠道发送警报。它展示了 CycBox 的以下功能:

  • 基于帧的协议解码 —— 使用内置的帧编解码器(frame codec),支持前缀、长度字段和校验和验证
  • 二进制负载解析 —— 从原始传感器数据中提取多个 uint16 大端(big-endian)数值
  • 多渠道通知 —— 同时通过 Discord webhook、ntfy 推送通知和 SMTP 邮件发送警报
  • 基于滞后(Hysteresis)的警报 —— 使用高/低双阈值来避免通知频繁波动

这是环境监测中的一个常见模式,即需要根据空气质量数据触发跨多个通信渠道的实时警报。

场景

一个 PMS9103M 颗粒物传感器通过串口(例如 /dev/ttyUSB0)连接,并以 9600 波特率持续流式传输数据帧。要求如下:

  1. 使用前缀检测、长度字段解析和校验和验证来接收并验证传感器数据帧
  2. 从二进制负载中解析所有 PM 浓度值(PM1.0, PM2.5, PM10)和颗粒计数
  3. 当 PM2.5 超过 75 ug/m3 时,通过 Discord、ntfy 和邮件发送警报
  4. 当 PM2.5 降至 35 ug/m3 以下时,发送恢复通知
  5. 使用滞后机制防止数值在阈值附近波动时重复发送通知

PMS9103M 帧格式

字段大小描述
前缀2 字节0x42 0x4D ("BM")
长度2 字节uint16 大端,负载 + 校验和的长度
负载26 字节传感器数据 (13 x uint16 大端值)
校验和2 字节前缀 + 长度 + 负载的 Sum16 大端和

PMS9103M 负载结构

所有数值均为 uint16 大端:

字节偏移描述
0-1PM1.0 浓度 (CF=1),单位 ug/m3
2-3PM2.5 浓度 (CF=1),单位 ug/m3
4-5PM10 浓度 (CF=1),单位 ug/m3
6-7PM1.0 浓度 (大气环境下),单位 ug/m3
8-9PM2.5 浓度 (大气环境下),单位 ug/m3
10-11PM10 浓度 (大气环境下),单位 ug/m3
12-13每 0.1L 空气中 >0.3um 的颗粒数
14-15每 0.1L 空气中 >0.5um 的颗粒数
16-17每 0.1L 空气中 >1.0um 的颗粒数
18-19每 0.1L 空气中 >2.5um 的颗粒数
20-21每 0.1L 空气中 >5.0um 的颗粒数
22-23每 0.1L 空气中 >10um 的颗粒数

CF=1 数值是工厂校准的标准颗粒浓度。大气环境数值针对环境条件进行了修正,通常用于空气质量评估。

配置

CycBox 配置了一个串口连接,使用帧编解码器处理 PMS9103M 二进制协议:

{
"version": "1.8.1",
"name": "带通知功能的 PMS9103M 空气质量监测器",
"description": "解析 PMS9103M 传感器数据,并通过 Discord、ntfy 和 SMTP 发送 PM2.5 警报",
"configs": [
{
"app": {
"app_transport": "serial",
"app_codec": "frame_codec",
"app_transformer": "disable",
"app_encoding": "UTF-8"
},
"serial": {
"serial_port": "/dev/ttyUSB0",
"serial_baud_rate": 9600,
"serial_data_bits": 8,
"serial_parity": "none",
"serial_stop_bits": "1",
"serial_flow_control": "none"
},
"frame_codec": {
"frame_codec_prefix": "42 4d",
"frame_codec_header_size": 0,
"frame_codec_tailer_length": 0,
"frame_codec_suffix": "",
"frame_codec_length_mode": "u16_be",
"frame_codec_fixed_payload_size": 32,
"frame_codec_length_meaning": "payload_checksum",
"frame_codec_checksum_algo": "sum16_be",
"frame_codec_checksum_scope": "prefix_header_length_payload"
}
}
]
}

帧编解码器设置

参数描述
frame_codec_prefix42 4dPMS "BM" 起始字节
frame_codec_length_modeu16_be2 字节大端长度字段
frame_codec_length_meaningpayload_checksum长度包含负载 + 校验和字节
frame_codec_checksum_algosum16_beSum16 大端校验和
frame_codec_checksum_scopeprefix_header_length_payload校验和覆盖除校验和自身外的整个帧

帧编解码器会自动处理所有成帧工作 —— 前缀检测、基于长度的提取和校验和验证。Lua 脚本仅接收经过验证的 26 字节负载。

Lua 脚本逻辑

该脚本在 on_receive() 中解析传感器数据,并使用基于滞后的阈值检查来触发通知。

通知配置

-- PM2.5 阈值 (ug/m3)
local PM25_HIGH_THRESHOLD = 75 -- 当 PM2.5 超过此值时发送警报
local PM25_LOW_THRESHOLD = 35 -- 当 PM2.5 降至此值以下时发送恢复通知

-- Discord webhook
local DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN"
local DISCORD_USERNAME = "CycBox 空气监测器"

-- ntfy 推送通知
local NTFY_TOPIC = "cycbox_air_quality"

-- SMTP 邮件
local SMTP_CONFIG = {
server = "smtp.gmail.com",
port = 587,
tls = "starttls",
username = "your-email@gmail.com",
password = "your-app-password",
from = "your-email@gmail.com",
to = "recipient@example.com",
}

运作原理

1. 负载解析 (on_receive)

function on_receive()
local payload = message.payload

if #payload ~= 26 then
return false
end

-- 解析 PM 浓度 (CF=1),单位 ug/m3
local pm1_0_cf1 = read_u16_be(payload, 1)
local pm2_5_cf1 = read_u16_be(payload, 3)
local pm10_cf1 = read_u16_be(payload, 5)

-- 解析 PM 浓度 (大气环境下),单位 ug/m3
local pm1_0_atm = read_u16_be(payload, 7)
local pm2_5_atm = read_u16_be(payload, 9)
local pm10_atm = read_u16_be(payload, 11)

-- 解析每 0.1L 空气中的颗粒数
local particles_0_3um = read_u16_be(payload, 13)
local particles_0_5um = read_u16_be(payload, 15)
-- ... (共 6 个颗粒大小区间)
end
  • 验证负载长度(帧编解码器剥离前缀、长度和校验和后应为 26 字节)
  • 使用 read_u16_be(payload, offset) 提取所有 13 个 uint16 大端值
  • CF=1 是出厂校准值;大气环境值是经过环境修正的值

2. UI 图表展示

    message:add_int_value("PM1.0-CF1", pm1_0_cf1)
message:add_int_value("PM2.5-CF1", pm2_5_cf1)
message:add_int_value("PM10-CF1", pm10_cf1)
message:add_int_value("PM2.5-ATM", pm2_5_atm)
-- ... 所有 12 个值都已添加,用于实时图表绘制
  • 所有解析后的数值都通过 message:add_int_value() 添加到消息元数据中,用于 UI 实时图表绘制

空气质量数据

空气质量图表

3. 基于滞后的警报

local alert_state = "normal"

if alert_state == "normal" and pm2_5_atm > PM25_HIGH_THRESHOLD then
alert_state = "alert"
send_high_alert(pm2_5_atm)
elseif alert_state == "alert" and pm2_5_atm < PM25_LOW_THRESHOLD then
alert_state = "normal"
send_recovery_alert(pm2_5_atm)
end
  • 使用两个阈值(高=75,低=35)创建一个滞后区间
  • 只有当 PM2.5 升至 75 ug/m3 以上时才触发一次警报
  • 只有当 PM2.5 降至 35 ug/m3 以下时才触发一次恢复
  • 当数值在两个阈值之间波动时,不会重复发送通知

4. 多渠道通知

每条警报都同时通过三个渠道发送:

-- Discord webhook
discord_send_async(DISCORD_WEBHOOK_URL, msg, DISCORD_USERNAME, nil)

-- ntfy 推送通知
ntfy_send_async({
topic = NTFY_TOPIC,
message = msg,
title = "PM2.5 高浓度警报",
priority = "high",
tags = "warning,skull",
})

-- SMTP 邮件
smtp_send_async({
server = SMTP_CONFIG.server,
port = SMTP_CONFIG.port,
tls = SMTP_CONFIG.tls,
username = SMTP_CONFIG.username,
password = SMTP_CONFIG.password,
from = SMTP_CONFIG.from,
to = SMTP_CONFIG.to,
subject = string.format("PM2.5 警报: %d μg/m³", pm2_5_value),
text = msg,
})
  • 所有三个通知均异步发送(带有 _async 后缀),因此不会阻塞传感器数据处理
  • Discord 使用带有自定义用户名的 webhook URL
  • ntfy 支持优先级和表情标签
  • SMTP 支持用于安全邮件传输的 STARTTLS

警报流程图

PM2.5 数值
|
| ──── 75 ug/m3 ──── 高阈值 ──── 触发警报 ────→ "alert" 状态
| │
| (此区间内不重复发送通知) │
| │
| ──── 35 ug/m3 ──── 低阈值 ──── 触发恢复 ────→ "normal" 状态
|

通知示例

高浓度警报消息:

PM2.5 警报:82 ug/m3 (阈值: 75 ug/m3)。空气质量不健康!

恢复消息:

PM2.5 恢复:28 ug/m3 (阈值: 35 ug/m3)。空气质量已恢复正常。

查看 GitHub 上的完整示例脚本

相关文档