设备报警实时监控功能,设备报警是按照二进制的位来操作的,在PLC中,一个字节(Byte)由8个位(bit)组成,如数字7对应的二进制为【00000111】,判断一个字节的某一位是否为1。可以使用与(&)操作符,判断字节第[0...7]位是否为1,使用判断:
(bufferData&(1<
报警状态只有两种:触发报警、清除报警。如表格所示:
M100.0 | A报警 |
M100.1 | B报警 |
M100.2 | C报警 |
M100.3 | D报警 |
M100.4 | E报警 |
M100.5 | F报警 |
M100.6 | G报警 |
M100.7 | H报警 |
如M100的值由0变成5,则为触发报警:A,C。
M100的值由5变成3,则为清除报警C,触发报警B,持续报警A。
一、新建窗体应用程序EquipmentAlarmDemo【.net framework 4.5】 ,将默认的Form1重命名为FormAlarm。
窗体设计如图:
二、新建报警实体类AlarmData.cs,源程序如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EquipmentAlarmDemo
{
///
/// 报警数据类
///
public class AlarmData
{
///
/// 报警状态:触发报警、清除报警
///
public string AlarmStatus { get; set; }
///
/// 报警时间
///
public DateTime AlarmTime { get; set; }
///
/// 报警PLC地址:如 M300.2
///
public string AlarmAddress { get; set; }
///
/// 报警内容:如 翻转上下电机报警
///
public string AlarmMessage { get; set; }
///
/// 报警工位:如 集流体工作台
///
public string AlarmPosition { get; set; }
}
}
三、新建全局变量类GlobalUtil.cs,源程序如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace EquipmentAlarmDemo
{
///
/// 登录成功后,保存为全局变量
///
public class GlobalUtil
{
///
/// 记录报警字典,按位描述
/// 键形如:M301.4【地址301的第四位是否为1。地址301共有8位 01234567】
/// 值形如:转盘一号气缸夹紧报警
///
public static Dictionary
///
/// 【报警工位名称】当前报警配置所在的Excel的工作簿sheet名称
///
public static string SheetName = "集流体焊接";
///
/// 设备报警的PLC开始地址
///
public static int StartAddr = 300;
}
}
四、新建保存报警信息到本地文件类SaveAlarmUtil.cs。源程序如下:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EquipmentAlarmDemo
{
///
/// Csv操作类
///
public class SaveAlarmUtil
{
private static Object thisLock = new Object();
///
/// 写入csv文件
///
/// 路径,如 @"D:\MESLog\ABC\"
/// 名称,不带.csv
/// 需要写入的数据
/// 是否拼接
///
public static bool WriteCsv(string _path, string _name, List
{
lock (thisLock)
{
try
{
//判断文件夹是否存在,不存在就创建
DirectoryInfo directoryInfo = new DirectoryInfo(_path);
if (!directoryInfo.Exists)
{
directoryInfo.Create();
}
string _tmPath = Path.Combine(_path, _name) + ".csv";
using (StreamWriter write = new StreamWriter(_tmPath, _append, Encoding.Default))
{
foreach (String[] strArr in _writeData)
{
write.WriteLine(String.Join(",", strArr));
}
}
return true;
}
catch (Exception e)
{
System.Windows.Forms.MessageBox.Show("CSV文件写入失败:" + e.Message);
return false;
}
}
}
///
/// 保存报警数据到csv文件
///
///
///
public static bool SaveAlarmData(AlarmData alarmData)
{
try
{
string path = AppDomain.CurrentDomain.BaseDirectory + "AlarmData\\";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
string fileName = DateTime.Now.ToString("yyyy-MM-dd");
//写字段名
List
listName.Add("报警状态");
listName.Add("报警时间");
listName.Add("报警地址");
listName.Add("报警内容");
listName.Add("报警工位");
List
iniList.Add(listName.ToArray());
bool ret = File.Exists(path + fileName + ".csv");
if (!ret)
{
bool ret2 = WriteCsv(path, fileName, iniList, false);
if (!ret2)
{
throw new Exception("报警日志字段名转换为csv格式失败,请检查文件是否被打开、被占用:" + path);
}
}
iniList.Clear();
List
listData.Add(alarmData.AlarmStatus);
listData.Add(alarmData.AlarmTime.ToString("yyyy-MM-dd HH:mm:ss"));
listData.Add(alarmData.AlarmAddress);
listData.Add(alarmData.AlarmMessage);
listData.Add(alarmData.AlarmPosition);
iniList.Add(listData.ToArray());
bool rtn = WriteCsv(path, fileName, iniList, true);
if (!rtn)
{
throw new Exception("报警日志具体信息转换为本地csv失败,请检查文件是否被打开、被占用:" + path);
}
return true;
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show(string.Format("保存报警日志出现异常:{0}", ex.Message), "出错");
return false;
}
}
}
}
五、新建主业务逻辑的处理报警状态变换的类AlarmMonitorUtil.cs。用于定义报警状态变化事件,以及相应的比较操作。源程序如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EquipmentAlarmDemo
{
///
/// PLC设备报警处理:
/// 每个字节都记录8个报警信息。
/// 监控数组的每一个元素是否与上一个数组的对应元素的值 是否不一致。
/// 如果不一致,就添加到报警状态更改的索引集合中。同时触发事件报警状态更改事件【触发报警、清除报警】
///
public class AlarmMonitorUtil
{
///
/// 触发报警状态改变事件:
/// 第一个参数:当前的报警状态。第二个参数:上一次的报警状态。第三个参数:数组的索引
///
public static event Action
///
/// 比较数组是否更改过,并返回更改后的数组的索引集合
///
///
///
///
public static bool CompareArrayChanged(byte[] buffer, byte[] bufferLast)
{
if (buffer.Length != bufferLast.Length)
{
return false;
}
bool isChanged = false;//是否已改变
for (int i = 0; i < buffer.Length; i++)
{
if (buffer[i] != bufferLast[i])
{
isChanged = true;
//触发报警状态改变事件
EventAlarmChanged?.Invoke(buffer[i], bufferLast[i], i);
}
}
return isChanged;
}
///
/// 读取指定PLC地址对应的字节值,
/// 该函数只是一个随机模拟器。正式环境下需要读取PLC指定报警地址的值
///
///
///
///
public static bool ReadByte(int startAddr, ref byte val)
{
val = (byte)new Random(Guid.NewGuid().GetHashCode()).Next(0, 256);
val = Convert.ToByte((startAddr + val) % 256);
return true;
}
///
/// 加载报警表格到内存字典中
///
public static void LoadAlarmDict()
{
GlobalUtil.DictAlarm.Add("M300.0", "正极盖板上料电机报警");
GlobalUtil.DictAlarm.Add("M300.1", "翻转上下电机报警");
GlobalUtil.DictAlarm.Add("M300.2", "短路测试电机报警");
GlobalUtil.DictAlarm.Add("M300.3", "正极盖板焊接X电机报警");
GlobalUtil.DictAlarm.Add("M300.4", "正极盖板焊接Y电机报警");
GlobalUtil.DictAlarm.Add("M300.5", "正极盖板焊接Z电机报警");
GlobalUtil.DictAlarm.Add("M300.6", "工位一定位气缸缩回位报警");
GlobalUtil.DictAlarm.Add("M300.7", "工位一定位气缸伸出位报警");
GlobalUtil.DictAlarm.Add("M301.0", "搬运正极盖板托盘破真空报警");
GlobalUtil.DictAlarm.Add("M301.1", "搬运正极盖板托盘真空到达报警");
GlobalUtil.DictAlarm.Add("M301.2", "搬运负极盖板托盘破真空报警");
GlobalUtil.DictAlarm.Add("M301.5", "搬运负极盖板托盘真空到达报警");
GlobalUtil.DictAlarm.Add("M301.6", "正极盖板上料侧无物料报警");
GlobalUtil.DictAlarm.Add("M301.7", "正极盖下料侧满料报警");
GlobalUtil.DictAlarm.Add("M302.0", "拍照轴报警");
GlobalUtil.DictAlarm.Add("M302.3", "线体翻转工位夹紧气缸松开位报警");
GlobalUtil.DictAlarm.Add("M302.4", "拍照NG报警");
}
}
}
六、窗体FormAlarm.cs的按钮事件以及相关界面的显示,源程序如下(忽略设计器自动生成的代码):
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace EquipmentAlarmDemo
{
public partial class FormAlarm : Form
{
///
/// 程序是否运行中
///
static bool isRun = false;
static int lockedValue = 0;
///
/// 上一次保存的报警信息,每个字节 都记录8个报警信息
///
byte[] bufferLast = new byte[3];
///
/// 监控线程
///
Thread thAlarm = null;
public FormAlarm()
{
InitializeComponent();
}
///
/// 窗体的Load事件
///
///
///
private void FormAlarm_Load(object sender, EventArgs e)
{
//加载报警地址 与 报警内容字典
AlarmMonitorUtil.LoadAlarmDict();
AlarmMonitorUtil.EventAlarmChanged += AlarmMonitorUtil_EventAlarmChanged;
btnStop.Enabled = false;
}
///
/// 报警状态改变事件
///
/// 当前报警状态代码【含有8个报警触发或清除信息】
/// 上一次报警状态代码【含有8个报警触发或清除信息】
private void AlarmMonitorUtil_EventAlarmChanged(byte current, byte last, int index)
{
int addr = GlobalUtil.StartAddr + index;
DisplayMessageAndRecord("触发报警状态变换的M区地址:" + addr);
StringBuilder sb = new StringBuilder();
//需要监控字节的8个位
for (int bitIndex = 0; bitIndex < 8; bitIndex++)
{
string alarmKey = string.Format("M{0}.{1}", addr, bitIndex);
//只考虑存在报警的地址
if (!GlobalUtil.DictAlarm.ContainsKey(alarmKey))
{
continue;
}
AlarmData alarmData = new AlarmData();
alarmData.AlarmAddress = alarmKey;
alarmData.AlarmMessage = GlobalUtil.DictAlarm[alarmKey];
alarmData.AlarmTime = DateTime.Now;
alarmData.AlarmPosition = GlobalUtil.SheetName;
//当前是否报警、上一次是否报警 共有4可能
if ((current & (1 << bitIndex)) != 0 && (last & (1 << bitIndex)) != 0)
{
//当前报警、上一次报警
sb.AppendFormat("持续报警_地址:【{0}】【{1}】;", alarmKey, GlobalUtil.DictAlarm[alarmKey]);
}
else if ((current & (1 << bitIndex)) != 0 && (last & (1 << bitIndex)) == 0)
{
//当前报警、上一次不报警
sb.AppendFormat("触发报警_地址:【{0}】【{1}】;", alarmKey, GlobalUtil.DictAlarm[alarmKey]);
alarmData.AlarmStatus = "触发报警";
SaveAlarmUtil.SaveAlarmData(alarmData);
}
else if ((current & (1 << bitIndex)) == 0 && (last & (1 << bitIndex)) != 0)
{
//当前不报警、上一次报警
sb.AppendFormat("清除报警_地址:【{0}】【{1}】;", alarmKey, GlobalUtil.DictAlarm[alarmKey]);
alarmData.AlarmStatus = "清除报警";
SaveAlarmUtil.SaveAlarmData(alarmData);
}
else
{
//当前不报警、上一次不报警
}
}
string sbInfo = sb.ToString();
if (sbInfo.Length > 0)
{
DisplayMessageAndRecord(sbInfo);
}
}
///
/// 设备报警监控线程
///
void AsyncMonitor()
{
try
{
while (isRun)
{
//添加锁
while (Interlocked.Exchange(ref lockedValue, 1) != 0)
{
Thread.Sleep(TimeSpan.FromSeconds(0.1));
//此循环用于等待当前捕获current的线程执行结束
}
//模拟读取当前的字节值
byte[] bufferTemp = new byte[3];
for (int i = 0; i < bufferTemp.Length; i++)
{
AlarmMonitorUtil.ReadByte(GlobalUtil.StartAddr + i, ref bufferTemp[i]);
}
byte[] buffer = new byte[3];
Array.Copy(bufferTemp, 0, buffer, 0, buffer.Length);
if (AlarmMonitorUtil.CompareArrayChanged(buffer, bufferLast))
{
DisplayMessageAndRecord($"【设备报警状态已变化】读取报警地址,打印操作结果字节:【{string.Join(",", buffer)}】", true);
}
bufferLast = buffer;
Thread.Sleep(1000);
//释放锁
Interlocked.Exchange(ref lockedValue, 0);//将current重置为0
}
}
catch (Exception ex)
{
DisplayMessageAndRecord("【设备报警状态】线程出现异常:" + ex.Message);
}
}
private void btnStart_Click(object sender, EventArgs e)
{
isRun = true;
btnStart.Enabled = false;
btnStop.Enabled = true;
thAlarm = new Thread(AsyncMonitor);
thAlarm.IsBackground = true;
thAlarm.Start();
DisplayMessageAndRecord("【开始监控】设备报警线程...");
}
private void btnStop_Click(object sender, EventArgs e)
{
isRun = false;
DisplayMessageAndRecord("【停止监控】设备报警线程...");
btnStart.Enabled = true;
btnStop.Enabled = false;
}
///
/// 显示内容并记录日志
///
///
/// 是否显示
public void DisplayMessageAndRecord(string message, bool isDisplay = true)
{
try
{
this.BeginInvoke(new Action(() =>
{
if (isDisplay)
{
if (rtxtDisplay.TextLength >= 10240)
{
rtxtDisplay.Clear();
}
rtxtDisplay.AppendText(DateTime.Now.ToString("yyyy-MM-dd_HH:mm:ss : ->") + message + "\n");
rtxtDisplay.ScrollToCaret();
}
}));
}
catch (Exception ex)
{
MessageBox.Show("记录日志时出现异常:" + ex.Message, "出现异常");
}
}
///
/// 窗体的FormClosing事件
///
///
///
private void FormAlarm_FormClosing(object sender, FormClosingEventArgs e)
{
DialogResult dialog = MessageBox.Show("是否关闭此程序,关闭将不能实时监控设备报警信息?", "警告", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (dialog != DialogResult.Yes)
{
e.Cancel = true;
return;
}
isRun = false;
Thread.Sleep(200);
try
{
thAlarm?.Join(2000);
}
catch { }
Application.ExitThread();
Environment.Exit(0);
}
}
}
七、程序运行效果如图:
①开启监控:
②停止监控