上位机报警功能仿真模拟器【设备实时报警保存】

设备报警实时监控功能,设备报警是按照二进制的位来操作的,在PLC中,一个字节(Byte)由8个位(bit)组成,如数字7对应的二进制为【00000111】,判断一个字节的某一位是否为1。可以使用与(&)操作符,判断字节第[0...7]位是否为1,使用判断:

(bufferData&(1<<bitIndex))!=0  如果表达式结果是true,说明该位为1,表达式结果是false,说明该位为1。

报警状态只有两种:触发报警、清除报警。如表格所示:

M100.0A报警
M100.1B报警
M100.2C报警
M100.3D报警
M100.4E报警
M100.5F报警
M100.6G报警
M100.7H报警

如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
{
    /// <summary>
    /// 报警数据类
    /// </summary>
    public class AlarmData
    {
        /// <summary>
        /// 报警状态:触发报警、清除报警 
        /// </summary>
        public string AlarmStatus { get; set; }
        /// <summary>
        /// 报警时间
        /// </summary>
        public DateTime AlarmTime { get; set; }
        /// <summary>
        /// 报警PLC地址:如 M300.2
        /// </summary>
        public string AlarmAddress { get; set; }
        /// <summary>
        /// 报警内容:如 翻转上下电机报警
        /// </summary>
        public string AlarmMessage { get; set; }
        /// <summary>
        /// 报警工位:如 集流体工作台
        /// </summary>
        public string AlarmPosition { get; set; }
    }
}
三、新建全局变量类GlobalUtil.cs,源程序如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace EquipmentAlarmDemo
{
    /// <summary>
    /// 登录成功后,保存为全局变量
    /// </summary>
    public class GlobalUtil
    {
        /// <summary>
        /// 记录报警字典,按位描述
        /// 键形如:M301.4【地址301的第四位是否为1。地址301共有8位 01234567】
        /// 值形如:转盘一号气缸夹紧报警
        /// </summary>
        public static Dictionary<string, string> DictAlarm = new Dictionary<string, string>();

        /// <summary>
        /// 【报警工位名称】当前报警配置所在的Excel的工作簿sheet名称
        /// </summary>
        public static string SheetName = "集流体焊接";
        /// <summary>
        /// 设备报警的PLC开始地址
        /// </summary>
        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
{
    /// <summary>
    /// Csv操作类
    /// </summary>
    public class SaveAlarmUtil
    {
        private static Object thisLock = new Object();
        /// <summary>
        /// 写入csv文件
        /// </summary>
        /// <param name="_path">路径,如 @"D:\MESLog\ABC\"</param>
        /// <param name="_name">名称,不带.csv</param>
        /// <param name="_writeData">需要写入的数据</param>
        /// <param name="_append">是否拼接</param>
        /// <returns></returns>
        public static bool WriteCsv(string _path, string _name, List<string[]> _writeData, bool _append)
        {
            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;
                }
            }
        }

        /// <summary>
        /// 保存报警数据到csv文件
        /// </summary>
        /// <param name="alarmData"></param>
        /// <returns></returns>
        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<string> listName = new List<string>();
                listName.Add("报警状态");
                listName.Add("报警时间");       
                listName.Add("报警地址");
                listName.Add("报警内容");
                listName.Add("报警工位");
                List<string[]> iniList = new List<string[]>();
                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<string> listData = new List<string>();
                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
{
    /// <summary>
    /// PLC设备报警处理:
    /// 每个字节都记录8个报警信息。
    /// 监控数组的每一个元素是否与上一个数组的对应元素的值 是否不一致。
    /// 如果不一致,就添加到报警状态更改的索引集合中。同时触发事件报警状态更改事件【触发报警、清除报警】
    /// </summary>
    public class AlarmMonitorUtil
    {
        /// <summary>
        /// 触发报警状态改变事件:
        /// 第一个参数:当前的报警状态。第二个参数:上一次的报警状态。第三个参数:数组的索引
        /// </summary>
        public static event Action<byte, byte, int> EventAlarmChanged;
        /// <summary>
        /// 比较数组是否更改过,并返回更改后的数组的索引集合
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="bufferLast"></param>
        /// <returns></returns>
        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;
        }

        /// <summary>
        /// 读取指定PLC地址对应的字节值,
        /// 该函数只是一个随机模拟器。正式环境下需要读取PLC指定报警地址的值
        /// </summary>
        /// <param name="startAddr"></param>
        /// <param name="val"></param>
        /// <returns></returns>
        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;
        }

        /// <summary>
        /// 加载报警表格到内存字典中
        /// </summary>
        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
    {
        /// <summary>
        /// 程序是否运行中
        /// </summary>
        static bool isRun = false;

        static int lockedValue = 0;
        /// <summary>
        /// 上一次保存的报警信息,每个字节 都记录8个报警信息
        /// </summary>
        byte[] bufferLast = new byte[3];
        /// <summary>
        /// 监控线程
        /// </summary>
        Thread thAlarm = null;
        public FormAlarm()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 窗体的Load事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void FormAlarm_Load(object sender, EventArgs e)
        {
            //加载报警地址 与 报警内容字典
            AlarmMonitorUtil.LoadAlarmDict();
            AlarmMonitorUtil.EventAlarmChanged += AlarmMonitorUtil_EventAlarmChanged;
            btnStop.Enabled = false;
        }

        /// <summary>
        /// 报警状态改变事件
        /// </summary>
        /// <param name="current">当前报警状态代码【含有8个报警触发或清除信息】</param>
        /// <param name="last">上一次报警状态代码【含有8个报警触发或清除信息】</param>
        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);
            }
        }

        /// <summary>
        /// 设备报警监控线程
        /// </summary>
        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;
        }

        /// <summary>
        /// 显示内容并记录日志
        /// </summary>
        /// <param name="message"></param>
        /// <param name="isDisplay">是否显示</param>
        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, "出现异常");
            }
        }

        /// <summary>
        /// 窗体的FormClosing事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        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);
        }
    }
}
 

七、程序运行效果如图:

①开启监控:

②停止监控

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

斯内科

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值