前端驱动工业报警:基于 WebSocket 与网关的三色蜂鸣灯实时报警系统实战

前言

在智能制造和工业 4.0 的浪潮下,越来越多的工厂车间需要实时监控设备的运行状态。三色报警灯(红、黄、绿)作为工业现场最直观的"视觉语言",配合蜂鸣器发出的声音告警,能够第一时间通知现场人员设备出现了什么问题。

传统的三色灯通常直接连接到 PLC 的数字输出点,由 PLC 根据设备状态直接控制。但在很多智能化改造场景中,我们希望前端 Web 应用能够实时采集设备运行参数,并根据预设的阈值规则远程控制三色蜂鸣灯的报警状态。这就面临一个问题:三色灯不与设备机器直接通信,而是通过网关接入网络

本文将从前端工程师的视角,详细介绍如何通过 WebSocket 实时接收设备采集数据,并结合网关的 Modbus TCP 协议实现三色蜂鸣灯的远程报警控制。全文涵盖系统架构设计、前端核心代码实现、协议解析和性能优化,力求提供一套可落地的技术方案。

一、系统架构概览

在正式写代码之前,先理清整个系统的数据流向:

scss 复制代码
┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  设备传感器  │────▶│   边缘网关   │────▶│   后端服务   │────▶│  前端 Web   │
│ (温度/振动等)│     │ (Modbus/MQTT)│     │ (WebSocket)  │     │  (实时监控)  │
└─────────────┘     └──────┬──────┘     └──────┬──────┘     └──────┬──────┘
                           │                    │                    │
                           ▼                    ▼                    ▼
                    ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
                    │ 三色蜂鸣灯  │◀────│  控制指令   │◀────│  阈值判断   │
                    │(Modbus TCP) │     │  (HTTP/WS)  │     │  (前端逻辑)  │
                    └─────────────┘     └─────────────┘     └─────────────┘

核心组件说明:

  1. 设备传感器:采集设备运行参数,如温度、振动频率、转速、电流等,通过串口、网口或无线方式上报给网关。
  2. 边缘网关:负责协议转换和数据汇聚,将传感器数据统一封装后上报后端,同时接收后端下发的控制指令,通过 Modbus TCP 协议驱动三色蜂鸣灯。
  3. 后端服务:提供 WebSocket 服务端,负责实时推送设备数据到前端,并接收前端的报警控制指令。
  4. 前端 Web 应用:接收 WebSocket 实时数据流,与预设的标准阈值对比,当参数超限时触发报警逻辑,向网关发送三色蜂鸣灯的控制指令。
  5. 三色蜂鸣灯:通过 Modbus TCP 协议接入网关,根据指令显示红/黄/绿状态并触发蜂鸣器。

二、通信协议选型

2.1 数据上报:WebSocket 实时推送

设备监控场景对实时性要求极高,轮询方式不仅浪费带宽,还存在延迟。WebSocket 建立长连接后,服务端可以主动向客户端推送数据,实现毫秒级的实时响应。

选型要点:

  • 原生 WebSocket 协议简单,前端 new WebSocket() 即可使用,无需额外库。
  • 需要心跳机制保持连接,防止防火墙或代理自动断开。
  • 在复杂场景下可以考虑 Socket.IO 或 Netty-SocketIO,它们提供了自动重连、房间管理、降级轮询等高级特性。

2.2 设备控制:Modbus TCP 协议

三色蜂鸣灯通过网关接入网络后,通常支持 Modbus TCP 协议进行远程控制。泓格科技的 ALM-Horn-RGB 等工业声光警报器就是典型的 Modbus TCP 设备,支持通过网络远程设置 LED 颜色和蜂鸣器状态。

Modbus TCP 的核心概念:

概念 说明
从站地址(Unit ID) 设备的唯一标识,通常为 1~247
功能码 03 读保持寄存器,06 写单个寄存器,16 写多个寄存器
寄存器地址 每个控制功能对应一个寄存器地址,如颜色控制寄存器、蜂鸣器控制寄存器
数据值 写入寄存器的具体值,如 0x01=红色、0x02=黄色、0x03=绿色

例如,向某个三色灯发送红色常亮指令,可能对应功能码 06(写单个寄存器),寄存器地址 0x0000,数据值 0x0001。

三、前端核心实现

3.1 建立 WebSocket 连接

javascript 复制代码
class DeviceMonitor {
  constructor(options) {
    this.wsUrl = options.wsUrl || 'ws://localhost:8080/device-data';
    this.reconnectInterval = options.reconnectInterval || 3000;
    this.heartbeatInterval = options.heartbeatInterval || 30000;
    this.thresholds = options.thresholds || {}; // 阈值配置
    this.onAlarm = options.onAlarm; // 报警回调
    this.init();
  }

  init() {
    this.connect();
    this.bindEvents();
  }

  connect() {
    this.ws = new WebSocket(this.wsUrl);
    
    this.ws.onopen = () => {
      console.log('[DeviceMonitor] WebSocket 连接成功');
      this.startHeartbeat();
      // 连接成功后可以发送订阅消息
      this.send({
        type: 'subscribe',
        devices: this.deviceIds
      });
    };

    this.ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        this.handleDeviceData(data);
      } catch (err) {
        console.error('[DeviceMonitor] 数据解析失败', err);
      }
    };

    this.ws.onerror = (error) => {
      console.error('[DeviceMonitor] WebSocket 错误', error);
    };

    this.ws.onclose = () => {
      console.log('[DeviceMonitor] WebSocket 连接关闭,尝试重连...');
      this.stopHeartbeat();
      this.reconnect();
    };
  }

  send(data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    }
  }

  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      this.send({ type: 'ping', timestamp: Date.now() });
    }, this.heartbeatInterval);
  }

  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  reconnect() {
    if (this.reconnectTimer) return;
    this.reconnectTimer = setTimeout(() => {
      this.reconnectTimer = null;
      this.connect();
    }, this.reconnectInterval);
  }
}

3.2 数据处理与阈值判断

接收到设备数据后,需要与预设的标准阈值进行对比,决定是否触发报警:

javascript 复制代码
// 报警等级定义
const AlarmLevel = {
  NORMAL: { level: 0, color: 'green', buzzer: false },
  WARNING: { level: 1, color: 'yellow', buzzer: false },
  CRITICAL: { level: 2, color: 'red', buzzer: true }
};

// 阈值配置示例
const THRESHOLD_CONFIG = {
  temperature: {
    warning: { min: 60, max: 75 },
    critical: { min: 75, max: 100 }
  },
  vibration: {
    warning: { min: 4.5, max: 8.0 },
    critical: { min: 8.0, max: 20.0 }
  },
  current: {
    warning: { min: 15, max: 25 },
    critical: { min: 25, max: 50 }
  }
};

class AlarmEngine {
  constructor(thresholds) {
    this.thresholds = thresholds;
    this.currentAlarmLevel = AlarmLevel.NORMAL;
    this.alarmHistory = [];
  }

  evaluate(deviceData) {
    const { temperature, vibration, current } = deviceData;
    
    // 检查是否有任何参数达到严重告警
    if (this.isCritical(temperature, 'temperature') ||
        this.isCritical(vibration, 'vibration') ||
        this.isCritical(current, 'current')) {
      return AlarmLevel.CRITICAL;
    }
    
    // 检查是否有任何参数达到警告告警
    if (this.isWarning(temperature, 'temperature') ||
        this.isWarning(vibration, 'vibration') ||
        this.isWarning(current, 'current')) {
      return AlarmLevel.WARNING;
    }
    
    return AlarmLevel.NORMAL;
  }

  isWarning(value, param) {
    const config = this.thresholds[param];
    if (!config || !config.warning) return false;
    const { min, max } = config.warning;
    return value >= min && value < max;
  }

  isCritical(value, param) {
    const config = this.thresholds[param];
    if (!config || !config.critical) return false;
    const { min, max } = config.critical;
    return value >= min && value < max;
  }

  // 避免告警抖动,增加防抖逻辑
  shouldTriggerAlarm(newLevel) {
    // 如果告警等级上升,立即触发
    if (newLevel.level > this.currentAlarmLevel.level) {
      return true;
    }
    // 如果告警等级下降,需要连续 N 次确认后才恢复
    if (newLevel.level < this.currentAlarmLevel.level) {
      // 这里可以实现连续确认逻辑
      return false;
    }
    return false;
  }
}

3.3 触发报警控制

当检测到参数超限时,前端通过 HTTP 请求向后端发送报警控制指令,后端再通过网关的 Modbus TCP 接口驱动三色蜂鸣灯:

javascript 复制代码
class AlarmController {
  constructor(apiBase) {
    this.apiBase = apiBase;
    this.currentState = null;
  }

  async triggerAlarm(deviceId, level) {
    // 防止重复发送相同状态
    if (this.currentState && 
        this.currentState.deviceId === deviceId && 
        this.currentState.level === level) {
      return;
    }

    const command = this.buildAlarmCommand(level);
    
    try {
      const response = await fetch(`${this.apiBase}/alarm/trigger`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          deviceId,
          color: command.color,
          pattern: command.pattern, // 'on' | 'blink_slow' | 'blink_fast'
          buzzer: command.buzzer,
          buzzerPattern: command.buzzerPattern
        })
      });

      const result = await response.json();
      if (result.success) {
        this.currentState = { deviceId, level };
        console.log(`[Alarm] 报警已触发,设备: ${deviceId},等级: ${level}`);
        
        // 记录告警日志
        this.logAlarm(deviceId, level);
      }
    } catch (error) {
      console.error('[Alarm] 报警触发失败', error);
      // 可以考虑加入重试机制
      this.retryTrigger(deviceId, level);
    }
  }

  buildAlarmCommand(level) {
    switch (level) {
      case AlarmLevel.CRITICAL:
        return {
          color: 'red',
          pattern: 'blink_fast',
          buzzer: true,
          buzzerPattern: 'intermittent'
        };
      case AlarmLevel.WARNING:
        return {
          color: 'yellow',
          pattern: 'on',
          buzzer: false
        };
      default:
        return {
          color: 'green',
          pattern: 'on',
          buzzer: false
        };
    }
  }

  async resetAlarm(deviceId) {
    return this.triggerAlarm(deviceId, AlarmLevel.NORMAL);
  }

  logAlarm(deviceId, level) {
    // 上报告警事件到日志系统
    console.log(`[AlarmLog] ${new Date().toISOString()} | Device: ${deviceId} | Level: ${level}`);
  }
}

3.4 完整监控组件整合

javascript 复制代码
// 使用示例:React 组件中的整合
import { useState, useEffect, useRef } from 'react';

function DeviceMonitorDashboard({ deviceId, thresholds }) {
  const [deviceData, setDeviceData] = useState(null);
  const [alarmLevel, setAlarmLevel] = useState(AlarmLevel.NORMAL);
  const [connectionStatus, setConnectionStatus] = useState('disconnected');
  
  const monitorRef = useRef(null);
  const alarmControllerRef = useRef(null);
  const alarmEngineRef = useRef(null);

  useEffect(() => {
    // 初始化报警引擎
    alarmEngineRef.current = new AlarmEngine(thresholds);
    alarmControllerRef.current = new AlarmController('/api/v1');
    
    // 初始化设备监控
    monitorRef.current = new DeviceMonitor({
      wsUrl: `ws://gateway.local:8080/device/${deviceId}/stream`,
      thresholds,
      onAlarm: (level) => {
        setAlarmLevel(level);
        alarmControllerRef.current.triggerAlarm(deviceId, level);
      }
    });

    // 扩展 onmessage 处理
    const originalOnMessage = monitorRef.current.ws.onmessage;
    monitorRef.current.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setDeviceData(data);
      
      // 阈值判断
      const newLevel = alarmEngineRef.current.evaluate(data);
      if (alarmEngineRef.current.shouldTriggerAlarm(newLevel)) {
        setAlarmLevel(newLevel);
        alarmControllerRef.current.triggerAlarm(deviceId, newLevel);
      }
      
      // 调用原始处理
      if (originalOnMessage) originalOnMessage(event);
    };

    return () => {
      // 清理连接
      monitorRef.current?.ws?.close();
    };
  }, [deviceId, thresholds]);

  return (
    <div className="device-monitor">
      <div className={`status-indicator ${alarmLevel.color}`}>
        <span>当前状态: {alarmLevel.level === 0 ? '正常' : alarmLevel.level === 1 ? '警告' : '严重告警'}</span>
      </div>
      
      {deviceData && (
        <div className="data-panel">
          <div>温度: {deviceData.temperature}°C</div>
          <div>振动: {deviceData.vibration} mm/s</div>
          <div>电流: {deviceData.current} A</div>
        </div>
      )}
      
      <div className="connection-status">
        连接状态: {connectionStatus}
      </div>
    </div>
  );
}

四、后端网关通信实现(简述)

虽然本文聚焦前端,但为了让整体方案更完整,这里简述后端如何与网关交互控制三色蜂鸣灯:

javascript 复制代码
// Node.js 后端示例:通过 Modbus TCP 控制三色灯
const ModbusRTU = require('modbus-serial');

class ModbusAlarmController {
  constructor(options) {
    this.host = options.host;
    this.port = options.port || 502; // Modbus TCP 默认端口
    this.unitId = options.unitId || 1;
    this.client = new ModbusRTU();
  }

  async connect() {
    await this.client.connectTCP(this.host, { port: this.port });
    this.client.setID(this.unitId);
    console.log(`[Modbus] 已连接到 ${this.host}:${this.port}`);
  }

  // 设置 LED 颜色
  // 假设寄存器 0x0000 控制颜色:0=关, 1=红, 2=黄, 3=绿
  async setColor(color) {
    const colorMap = { red: 1, yellow: 2, green: 3, off: 0 };
    const value = colorMap[color] || 0;
    await this.client.writeRegister(0x0000, value);
    console.log(`[Modbus] 设置颜色: ${color} (${value})`);
  }

  // 设置闪烁模式
  // 假设寄存器 0x0001 控制闪烁:0=常亮, 1=慢闪, 2=快闪
  async setBlinkPattern(pattern) {
    const patternMap = { on: 0, blink_slow: 1, blink_fast: 2 };
    const value = patternMap[pattern] || 0;
    await this.client.writeRegister(0x0001, value);
  }

  // 控制蜂鸣器
  // 假设寄存器 0x0002 控制蜂鸣器:0=关, 1=开
  async setBuzzer(enabled) {
    await this.client.writeRegister(0x0002, enabled ? 1 : 0);
  }

  async close() {
    this.client.close();
  }
}

// Express 路由示例
app.post('/api/v1/alarm/trigger', async (req, res) => {
  const { deviceId, color, pattern, buzzer } = req.body;
  
  try {
    const controller = getModbusController(deviceId);
    await controller.setColor(color);
    await controller.setBlinkPattern(pattern);
    await controller.setBuzzer(buzzer);
    
    res.json({ success: true, message: '报警指令已下发' });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

五、WebSocket 连接优化策略

工业现场网络环境复杂,WebSocket 连接的稳定性直接影响报警系统的可靠性。以下是一些经过验证的优化策略:

5.1 心跳保活

WebSocket 连接可能因网络波动、防火墙策略等原因被中断。心跳机制是保持连接的有效手段:

javascript 复制代码
class HeartbeatManager {
  constructor(ws, options = {}) {
    this.ws = ws;
    this.pingInterval = options.pingInterval || 30000;
    this.pongTimeout = options.pongTimeout || 10000;
    this.timer = null;
    this.pongTimer = null;
  }

  start() {
    this.timer = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ type: 'ping' }));
        
        // 设置 pong 超时检测
        this.pongTimer = setTimeout(() => {
          console.warn('[Heartbeat] Pong 超时,主动断开连接');
          this.ws.close();
        }, this.pongTimeout);
      }
    }, this.pingInterval);
  }

  onPong() {
    if (this.pongTimer) {
      clearTimeout(this.pongTimer);
      this.pongTimer = null;
    }
  }

  stop() {
    if (this.timer) clearInterval(this.timer);
    if (this.pongTimer) clearTimeout(this.pongTimer);
    this.timer = null;
    this.pongTimer = null;
  }
}

5.2 指数退避重连

网络断开后,不宜立即高频重连,应采用指数退避策略:

javascript 复制代码
class ReconnectScheduler {
  constructor(maxDelay = 60000) {
    this.attempt = 0;
    this.maxDelay = maxDelay;
  }

  nextDelay() {
    // 指数退避:2^attempt * 1000,最大 60 秒
    const delay = Math.min(1000 * Math.pow(2, this.attempt), this.maxDelay);
    this.attempt++;
    return delay;
  }

  reset() {
    this.attempt = 0;
  }
}

5.3 数据去重与乱序处理

WebSocket 数据推送可能在重连后出现重复或乱序。可以在数据包中加入序列号:

javascript 复制代码
class DataStreamHandler {
  constructor() {
    this.lastSeq = 0;
    this.dataBuffer = new Map();
  }

  handle(data) {
    const { seq, timestamp, payload } = data;
    
    // 丢弃旧数据
    if (seq <= this.lastSeq) {
      console.warn(`[Stream] 丢弃重复/过期数据 seq=${seq}`);
      return null;
    }
    
    this.lastSeq = seq;
    return payload;
  }
}

六、常见问题与解决方案

6.1 浏览器兼容性

WebSocket 在所有现代浏览器中都有良好支持。但如果需要兼容 IE 等老旧浏览器,可以考虑使用 Socket.IO,它提供了自动降级到长轮询的能力。

6.2 网关指令下发延迟

Modbus TCP 通信本身存在网络延迟,加上网关处理时间,指令下发到三色灯亮起可能有 100~500ms 的延迟。在阈值判断时应考虑这一延迟,可以适当设置报警触发缓冲时间,避免因瞬时波动频繁触发。

6.3 多设备并发控制

当监控多个设备时,需要注意网关的并发处理能力。建议在前端实现指令队列,避免短时间大量指令同时下发导致网关过载:

javascript 复制代码
class CommandQueue {
  constructor(interval = 100) {
    this.queue = [];
    this.interval = interval;
    this.running = false;
  }

  enqueue(command) {
    return new Promise((resolve, reject) => {
      this.queue.push({ command, resolve, reject });
      this.run();
    });
  }

  async run() {
    if (this.running) return;
    this.running = true;
    
    while (this.queue.length > 0) {
      const { command, resolve, reject } = this.queue.shift();
      try {
        const result = await this.execute(command);
        resolve(result);
      } catch (err) {
        reject(err);
      }
      await this.sleep(this.interval);
    }
    
    this.running = false;
  }
}

6.4 报警状态持久化

当 WebSocket 断连时,前端无法接收到最新数据,但三色灯应该保持最后的状态。建议在后端维护报警状态的持久化记录,前端重连后同步最新状态。

七、总结

本文从前端工程师的视角,详细介绍了如何通过 WebSocket 实时接收设备采集数据,并结合网关的 Modbus TCP 协议驱动三色蜂鸣灯实现远程报警。核心要点回顾:

  1. 通信架构:设备数据通过 WebSocket 推送到前端,前端判断阈值后通过 HTTP 下发报警指令,后端再通过 Modbus TCP 控制三色灯。
  2. 协议选型:WebSocket 保证数据实时性,Modbus TCP 实现工业设备的标准化控制。
  3. 前端核心逻辑:阈值判断 + 告警去抖 + 状态防重复,保证报警的准确性和稳定性。
  4. 可靠性保障:心跳保活、指数退避重连、数据去重、指令队列,应对工业现场复杂的网络环境。

随着工业物联网的不断发展,前端工程师在工业智能化场景中的参与度会越来越高。掌握 WebSocket 实时通信和工业协议的基本概念,将帮助前端工程师更好地融入智能制造的技术生态。

希望本文能为正在做工业监控项目的同学提供一些参考和启发。欢迎在评论区交流讨论!

相关推荐
彭于晏Yan2 小时前
Spring Boot整合WebSocket入门(一)
spring boot·后端·websocket
狗都不学爬虫_2 小时前
小程序逆向 - Hai尔(AliV3拖动物品)
javascript·爬虫·python·网络爬虫
We་ct2 小时前
HTML5 原生拖拽 API 基础原理与核心机制
前端·javascript·html·api·html5·浏览器·拖拽
qq_12084093713 小时前
Three.js 工程向:后处理性能预算与多 Pass 链路优化
前端·javascript
南棱笑笑生3 小时前
20260422给万象奥科的开发板HD-RK3576-PI适配瑞芯微原厂的Buildroot时使用mpg123播放mp3音频
前端·javascript·音视频·rockchip
空中海3 小时前
第一章:Vue 基础与模板语法
前端·javascript·vue.js
mCell3 小时前
为什么我不建议初学者一上来就用框架学 Agent
javascript·langchain·agent
每天吃饭的羊4 小时前
水平,垂直居中
前端·javascript·html