报警推送
车间报警触发后需要第一时间通知合适的人, 并避免"狼来了"疲劳。DarraRT 报警推送分两层:
- 本地层 (HMI): 浏览器 Notification / Web TTS / 铃声 / 视觉滚动
- 外部层 (Service 代理): 邮件 / 企业微信 / 飞书 / 钉钉 / 短信
注: 本章只涉及设备级报警, 不处理业务报警 (库存超限/订单延期等由其他系统负责)。
报警分级
PLC 侧定义报警时必须标注 Severity 字段, 驱动推送策略:
| 级别 | 严重度 | 颜色 | 典型场景 | 推送目标 |
|---|---|---|---|---|
| Info | 0 | 蓝 | 启停/换型/换班 | 仅日志 |
| Warn | 1 | 黄 | 温度接近上限 / 备件寿命剩 <10% | 本地通知 |
| Error | 2 | 橙 | 工艺偏差 / 单工位故障 | 本地 + 外部 (工程师) |
| Fatal | 3 | 红 | 急停 / 设备损坏 / 数据库断开 | 全员 + 电话升级 + 自动停机 |
PLC 声明报警 UDT:
TYPE T_Alarm :
STRUCT
Code : DINT; (* 唯一编号, 例如 1001 *)
Severity : INT; (* 0..3 *)
Active : BOOL;
Acked : BOOL;
Message : STRING[128];
Source : STRING[32]; (* 设备/工位 *)
FirstTime : DT;
LastTime : DT;
END_STRUCT
END_TYPE
本地层: HMI 触发
订阅报警事件
darra.on('alarm', ...) 接收所有活动报警:
darra.on('alarm', (alarm) => {
// alarm: { code, severity, message, source, firstTime, acked }
console.log('收到报警:', alarm);
handleAlarm(alarm);
});
function handleAlarm(a) {
switch (a.severity) {
case 0: logOnly(a); break;
case 1: showNotification(a); playSound('warn'); break;
case 2: showNotification(a); playSound('error');
requestExternal(a); break;
case 3: showNotification(a); playSound('fatal');
speakTTS(a); requestExternal(a);
requireAck(a, 300); break;
}
}
浏览器 Notification API
推荐在登录成功后申请授权:
if (Notification.permission === 'default') {
const p = await Notification.requestPermission();
if (p !== 'granted') {
showBanner('提示: 浏览器通知被禁用, 报警只能在页面内显示');
}
}
function showNotification(a) {
if (Notification.permission !== 'granted') return;
new Notification(`[${severityLabel(a.severity)}] ${a.source}`, {
body: a.message,
icon: `/static/img/alarm-${a.severity}.png`,
tag: `alarm-${a.code}`, // 同 code 的通知自动替换
requireInteraction: a.severity >= 2 // 严重以上必须手动关闭
});
}
Web TTS 语音播报
function speakTTS(a) {
if (!window.speechSynthesis) return;
const u = new SpeechSynthesisUtterance(
`${severityLabel(a.severity)}: ${a.source} ${a.message}`
);
u.lang = 'zh-CN';
u.rate = 0.9;
u.volume = 1.0;
speechSynthesis.cancel(); // 打断前一条
speechSynthesis.speak(u);
}
音频铃声
不同级别不同铃声, 且可循环直到确认:
<audio id="snd-warn" src="/static/sound/warn.mp3"></audio>
<audio id="snd-error" src="/static/sound/error.mp3"></audio>
<audio id="snd-fatal" src="/static/sound/fatal.mp3" loop></audio>
function playSound(kind) {
const el = document.getElementById(`snd-${kind}`);
if (el) {
el.currentTime = 0;
el.play().catch(e => console.warn('audio play failed:', e));
}
}
function stopFatalSound() {
document.getElementById('snd-fatal').pause();
}
Fatal 铃声 loop 循环直到 PLC 侧 Alm.Ack 置 TRUE 后前端停止:
darra.bind('M.Alm.Ack', v => { if (v) stopFatalSound(); });
可视化列表
<darra-alarm-list> 自带排序、颜色、滚动:
<darra-alarm-list max-rows="12"
severity-filter="1,2,3"
sort="severity,firsttime">
</darra-alarm-list>
外部层: Service 代理推送
HMI 不直接调 SMTP / 钉钉 SDK (凭据放前端不安全), 而是把推送请求交给 Service:
HMI ──(内部通道)──> DarraRT Service ──(SMTP/Webhook/SMS)──> 邮箱/企业微信/钉钉/短信
Service 侧在 appsettings.json 配置通道凭据:
{
"AlarmPush": {
"Enabled": true,
"Channels": {
"email": {
"Host": "smtp.example.com", "Port": 587,
"User": "[email protected]", "Password": "***",
"From": "[email protected]"
},
"wechat_work": {
"Webhook": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=***"
},
"feishu": {
"Webhook": "https://open.feishu.cn/open-apis/bot/v2/hook/***"
},
"dingtalk": {
"Webhook": "https://oapi.dingtalk.com/robot/send?access_token=***",
"Secret": "***"
},
"sms": {
"Provider": "aliyun",
"AccessKey": "***", "AccessSecret": "***",
"SignName": "Darra", "TemplateCode": "SMS_xxx"
}
},
"Recipients": {
"Warn": ["ops-wechat"],
"Error": ["ops-wechat","shift-engineer-phone"],
"Fatal": ["ops-wechat","shift-engineer-phone","mgr-phone","factory-manager-email"]
}
}
}
PLC 可在报警触发时通过通讯 FB 调用 Service 的内部推送接口 (运行于 localhost, 无需外网):
PushAlarm(
Execute := AlarmJustRose,
Severity := AlarmRec.Severity,
Message := AlarmRec.Message,
Source := AlarmRec.Source
);
分级推送策略
| 级别 | 本地铃声 | 本地通知 | Web TTS | 邮件 | 微信/飞书/钉钉 | 短信 | 电话升级 | 自动停机 |
|---|---|---|---|---|---|---|---|---|
| Info | - | - | - | - | - | - | - | - |
| Warn | Yes | Yes | - | - | - | - | - | - |
| Error | Yes | Yes | - | Yes | Yes | - | - | - |
| Fatal | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
"电话升级"指语音机器人拨打当班工程师电话直到接听, 由第三方 SaaS 提供 (如阿里云语音通知)。
静默窗口 (防刷屏)
同一 Source + Code 的报警在 3 分钟内 第二次触发时, 只更新"最后时间"与"出现次数", 不再推送外部通道:
[10:00:15] 锅炉A 温度超标 → 推送到企业微信
[10:01:22] 锅炉A 温度超标 → 仅更新计数, 不推送
[10:02:55] 锅炉A 温度超标 → 仅更新计数, 不推送
[10:04:10] 锅炉A 温度超标 → 距首发 > 3 min, 重新推送, 消息里标注"5 次"
Service 侧实现 (伪代码):
var key = $"{a.Source}:{a.Code}";
var last = _lastPush.GetOrDefault(key);
if (last != null && (DateTime.Now - last.Time) < TimeSpan.FromMinutes(3)) {
last.Count++;
return; // 静默
}
_lastPush[key] = new PushRecord { Time = DateTime.Now, Count = 1 };
Push(a);
确认与升级
Fatal 报警必须在 5 分钟内被确认, 否则自动升级到下一联系人:
T+0:00 Fatal 报警触发 → 通知当班工程师
T+5:00 未确认 → 升级: 同时通知班长
T+10:00 仍未确认 → 升级: 通知车间主任 + 电话
T+15:00 仍未确认 → 升级: 通知工厂经理
确认动作: PLC 侧 Alm.Ack = TRUE 后, Service 收到变量变更事件即取消升级定时器。
升级联系人配置:
{
"Escalation": {
"Fatal": [
{ "delay": "0m", "group": "shift-engineer" },
{ "delay": "5m", "group": "shift-leader" },
{ "delay": "10m", "group": "workshop-director" },
{ "delay": "15m", "group": "plant-manager", "call": true }
]
}
}
夜间模式
22:00 - 06:00 仅 Fatal 级报警 推送外部通道 (白天 Warn 也推送), 避免夜间非严重报警骚扰休息中的工程师:
{
"QuietHours": {
"Start": "22:00", "End": "06:00",
"MinSeverity": "Fatal",
"ExceptGroups": ["night-shift-engineer"]
}
}
值班工程师 night-shift-engineer 组不受静默, 保留所有推送。
审计链
每条报警从生成到关闭的完整时间链写入 Historian:
| 字段 | 说明 |
|---|---|
| AlarmId | 报警唯一 ID |
| Code | 报警代码 |
| Severity | 级别 |
| Source | 来源 |
| Message | 消息文本 |
| FirstTime | 首次触发时间 |
| AckTime | 确认时间 |
| AckBy | 确认人 |
| EscalatedTo | 升级链 (数组) |
| ResetTime | 复位时间 |
| ResetBy | 复位人 |
| Duration | 持续时长 |
审计数据可导出为合规报表, 满足设备管理规范要求。
HMI 报警中心页面
推荐提供一个专用"报警中心"页面集中展示:
<div class="alarm-center" x-data="alarmCenter()">
<div class="kpi-bar">
<darra-numeric var="$.Alarm.ActiveCount" label="活动"></darra-numeric>
<darra-numeric var="$.Alarm.UnackedCount" label="未确认" big></darra-numeric>
<darra-numeric var="$.Alarm.TodayCount" label="今日合计"></darra-numeric>
</div>
<darra-alarm-list max-rows="50"
severity-filter="1,2,3"
columns="time,severity,source,code,message,acked"
allow-ack="true">
</darra-alarm-list>
<darra-alarm-chart period="24h" group-by="severity"></darra-alarm-chart>
</div>
$.Alarm.* 是 Service 提供的内置统计变量。
用户授权检查清单
- 登录成功后引导用户授权
Notification - 说明浏览器可能屏蔽声音播放 (Chrome 需用户先交互一次)
- 允许用户设置"推送免打扰时段" (个人偏好, 不影响系统级)
- 允许禁用 Web TTS 防骚扰同事
- 列出每个用户订阅的通道, 随时可退订
测试用例
每条报警规则上线前必测:
- 触发测试: 手工触发报警, 验证本地 + 外部推送都收到
- 静默测试: 连续触发 5 次, 3 分钟内只推送 1 次
- 升级测试: 触发 Fatal 不确认, 验证 5/10/15 分钟升级
- 确认终止: 升级链中途确认, 后续升级取消
- 夜间模式: 22:30 触发 Warn, 确认不推送外部, Fatal 正常推送
- 故障转移: 关 SMTP, Warn 降级为本地通知
- 审计完整: 每个事件均有 AlarmId / AckBy / ResetBy 记录
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| HMI 没响 | playSound 被浏览器拦截 | 首次登录后引导用户点击一次 |
| Notification 权限被永久拒绝 | 浏览器设置 | 浏览器设置里手动打开 |
| 微信推送收不到 | Webhook URL 失效 / IP 白名单 | 检查 appsettings.json |
| 钉钉推送失败 | 签名算法错 | Secret + timestamp HMAC-SHA256 |
| 短信乱码 | 模板未支持中文 | 改用 Unicode 模板 |
| 报警刷屏 | 静默窗口未启用 | 打开 QuietWindowMinutes |
| 升级未触发 | 确认事件未收到 | 检查 WebSocket 通路 |