融合SignalR的OPCClient实现环境参数实时监测

需要从现场设备读取数据?那就绕不开OPC这座大山(至少目前接触的几个项目都少不了OPC)。由于很多自动控制设备厂家已经提供了OPCServer,所以就不再花心思去开发了。只要是遵循OPC协议的OPCClient,就能从OPCServer读取写入数据。以下就是为解决项目实际需要开发OPCClient的过程:

针对本文中的OPCClient的功能改进参考下文:

基于OPC自定义接口的OPCClient功能改进


开发环境版本
操作系统Windows 10 Professional
编译器Visual Studio 2015 update 3
数据库SQLite

解决方案

针对项目实际需要有以下几点考虑:OPCClient独立运行,不嵌套在主业务程序中运行,只作为数据传输桥梁;能够支持Web应用、桌面应用和移动应用显示实时数据;实现自动配置DCOM;
基于以上考虑并结合以往的经验,考虑采用OPCClient结合SignalR进行实时数据传输的方案,整体结构如下图:
这里写图片描述

OPCClient从OPCServer读取数据传输至融合了SignalR的服务端,服务端将数据处理后转发至PC客户端和Web客户端。

服务端实现

服务端采用的是WebAPI与SignalR结合的形式,创建方式参考以前的文章:

WebAPI集成SignalR

Hub类中的方法如下:

        public static DataTable dtOPC;
        public async Task RefreshOPCData(OPCValueModel model)
        {

            try
            {
                if (dtOPC == null)
                {
                    dtOPC = new DataTable();

                    #region 测试代码

                    //测试代码
                    dtOPC.Columns.Add(new DataColumn("RowGuid", typeof(String)));
                    dtOPC.Columns.Add(new DataColumn("OPCServerName", typeof(String)));
                    dtOPC.Columns.Add(new DataColumn("OPCItemName", typeof(String)));
                    dtOPC.Columns.Add(new DataColumn("MonitorType", typeof(String)));
                    dtOPC.Columns.Add(new DataColumn("EquipmentGuid", typeof(String)));
                    dtOPC.Columns.Add(new DataColumn("AlarmMax", typeof(Double)));
                    dtOPC.Columns.Add(new DataColumn("AlarmMin", typeof(Double)));
                    //MatrikonOPC测试使用
                    dtOPC.Rows.Add("t1", "Matrikon.OPC.Simulation.1", "Random.Int1", "T", "", 10000, 0);
                    dtOPC.Rows.Add("h1", "Matrikon.OPC.Simulation.1", "Random.Int2", "H", "", 10000, 0);
                    dtOPC.Rows.Add("w1", "Matrikon.OPC.Simulation.1", "Random.Int4", "W", "", 10000, 0);
                    dtOPC.Rows.Add("o1", "Matrikon.OPC.Simulation.1", "Random.Real4", "O", "", 10000, 0);
                    dtOPC.Rows.Add("s1", "Matrikon.OPC.Simulation.1", "Random.Real8", "S", "", 10000, 0);
                    dtOPC.Rows.Add("c1", "Matrikon.OPC.Simulation.1", "Random.UInt1", "C", "", 10000, 0);
                    dtOPC.Rows.Add("s2", "Matrikon.OPC.Simulation.1", "Random.UInt2", "S", "", 10000, 0);
                    dtOPC.Rows.Add("h3", "Matrikon.OPC.Simulation.1", "Random.UInt4", "H", "", 10000, 0);

                    #endregion
                }
                else
                {
                    DataRow[] drs = dtOPC.Select("OPCServerName='" + model.ServerName + "' and OPCItemName='" + model.ItemName + "'");
                    if (drs.Length > 0)
                    {
                        string monitorType = drs[0]["MonitorType"].ToString();
                        string equipid = drs[0]["EquipmentGuid"].ToString();
                        double maxValue = Convert.ToDouble(drs[0]["AlarmMax"]);
                        double minValue = Convert.ToDouble(drs[0]["AlarmMin"]);
                        string describe = "";

                        #region 阈值判断
                        switch (monitorType)
                        {
                            case "T"://温度
                                if (Convert.ToDouble(model.ItemValue) > maxValue)
                                {
                                    describe = "温度超出最大限值";
                                }
                                else if (Convert.ToDouble(model.ItemValue) < minValue)
                                {
                                    describe = "温度低于最小限值";
                                }
                                break;
                            case "H"://湿度
                                if (Convert.ToDouble(model.ItemValue) > maxValue)
                                {
                                    describe = "湿度超出最大限值";
                                }
                                else if (Convert.ToDouble(model.ItemValue) < minValue)
                                {
                                    describe = "湿度低于最小限值";
                                }
                                break;
                            case "W"://水位
                                if (Convert.ToDouble(model.ItemValue) > maxValue)
                                {
                                    describe = "水位超出最大限值";
                                }
                                else if (Convert.ToDouble(model.ItemValue) < minValue)
                                {
                                    describe = "水位低于最小限值";
                                }
                                break;
                            case "O"://氧气
                                if (Convert.ToDouble(model.ItemValue) > maxValue)
                                {
                                    describe = "氧气浓度超出最大限值";
                                }
                                else if (Convert.ToDouble(model.ItemValue) < minValue)
                                {
                                    describe = "氧气浓度低于最小限值";
                                }
                                break;
                            case "S"://硫化氢
                                if (Convert.ToDouble(model.ItemValue) > maxValue)
                                {
                                    describe = "硫化氢浓度超出最大限值";
                                }

                                break;
                            case "C"://甲烷
                                if (Convert.ToDouble(model.ItemValue) > maxValue)
                                {
                                    describe = "甲烷浓度超出最大限值";
                                }

                                break;
                        }
                        #endregion

                        if (!string.IsNullOrEmpty(describe))
                        {
                            //报警信息推送
                            await Clients.Others.alarmData(DateTime.Now.ToString(), drs[0]["EquipmentGuid"].ToString(), drs[0]["MonitorType"].ToString(), model.ItemValue);

                        }

                        //数据更新推送
                        await Clients.Others.refreshOpcData(model, drs[0]["RowGuid"].ToString());
                    }
                }
            }
            catch (Exception ex)
            {
     

   #
       //异常数据推送
                await Clients.All.errorData(ex.Message);
            }
        }

定义OPC数据结构

    public class OPCItemModel
    {
        private string _serverId;

        public string ServerId
        {
            get { return _serverId; }
            set { _serverId = value; }
        }

        private string _itemId = "";

        public string ItemId
        {
            get { return _itemId; }
            set { _itemId = value; }
        }

        private string _serverName;

        public string ServerName
        {
            get { return _serverName; }
            set { _serverName = value; }
        }

        private string _groupName;

        public string GroupName
        {
            get { return _groupName; }
            set { _groupName = value; }
        }

        private string _itemName;

        public string ItemName
        {
            get { return _itemName; }
            set { _itemName = value; }
        }

        private object _itemHandle;

        public object ItemHandle
        {
            get { return _itemHandle; }
            set { _itemHandle = value; }
        }

        private string _itemRemrk;

        public string ItemRemark
        {
            get { return _itemRemrk; }
            set { _itemRemrk = value; }
        }

        private bool _isActive = false;

        public bool IsActive
        {
            get { return _isActive; }
            set { _isActive = value; }
        }


    }

    /// <summary>
    /// OPC值模型类
    /// </summary>
    public class OPCValueModel : OPCItemModel
    {
        private string _itemValue = "No Value";

        public string ItemValue
        {
            get { return _itemValue; }
            set { _itemValue = value; }
        }

        private string _itemQuality = "No Quality";

        public string ItemQuality
        {
            get { return _itemQuality; }
            set { _itemQuality = value; }
        }

        private int _itemRates = 0;

        public int ItemRates
        {
            get { return _itemRates; }
            set { _itemRates = value; }
        }

    }

OPC客户端实现

数据库

OPCClient的数据库采用轻量级的SQLite,无需驱动,可随处运行,用于保存OPCItem信息和历史数据。
这里写图片描述

根据OPC标准结构创建数据库表,分别创建了数据表OPCServer、OPCGroup、OPCItem。
这里写图片描述

OPC接口实现

OPC本质上来讲是一套标准,只要遵循这套标准实现的软件都能实现互联互通,同时OPC基金会也提供了一系列的开放接口供广大开发人员使用。本文使用的是自定义接口,包含OpcRcw.Da.dll、OpcRcw.Dx.dll、OpcRcw.Sec.dll、OpcRcw.Comn.dll等类库。

全局变量

        private OPCServer _opcServer = null;
        private OPCGroup _group = null;
        private int _handle = 0;
        private string _value = string.Empty;//写入值
        private System.Type _type;//写入值类型

        private string _serverId;//OPCServerID
        private string _serverName;//OPCServer名称
        private string _serverHost;//OPCServerIP地址
        private string _groupId;//组ID
        private string _groupName;//组名
        private static int _updateRate = int.Parse(ConfigurationManager.AppSdateRate"]);//新频率

连接OPCServer

        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            #region 连接
            if ((!string.IsNullOrEmpty(_serverId)) && (!string.IsNullOrEmpty(_groupId)))
            {
                try
                {
                    _opcServer = new OPCServer();
                    _opcServer.Connect(_serverName, _serverHost);
                    if (_opcServer.ServerState == (int)OPCServerState.OPCRunning)
                    {
                        _opcServer.OPCGroups.RemoveAll();
                        _group = _opcServer.OPCGroups.Add(_groupName);
                        #region group属性
                        _opcServer.OPCGroups.DefaultGroupIsActive = true;
                        _opcServer.OPCGroups.DefaultGroupDeadband = 0;
                        _group.UpdateRate = _updateRate;
                        _group.IsActive = true;
                        _group.IsSubscribed = true;
                        #endregion
                        Connector.Com.OperationFun operation = new Connector.Com.OperationFun();
                        DataTable dt = operation.GetItem(_serverId, _groupId);
                        int i = 1;
                        foreach (DataRow dr in dt.Rows)
                        {
                            OPCItem item = _group.OPCItems.AddItem(dr["OpcItemName"].ToString(), i);
                            OPCValueModel model = new OPCValueModel()
                            {
                                ServerId = _serverId,
                                ServerName = _serverName,
                                GroupName = _groupName,
                                ItemName = dr["OpcItemName"].ToString(),
                                ItemId = dr["RowGuid"].ToString(),
                                ItemRemark = dr["OpcItemAddress"].ToString(),
                                ItemValue = "No Value",
                                ItemQuality = "No Qulity",
                                AlarmType = dr["AlarmType"].ToString(),
                                AlarmLevel = dr["AlarmLevel"].ToString(),
                                MaxValue = dr["MaxValue"].ToString(),
                                MinValue = dr["MinValue"].ToString(),
                                IsActive = true,
                                ItemHandle = item
                            };
                            listValues.Add(model);
                            i++;
                        }
                        _group.DataChange += Group_DataChange;
                        dicServer.Add(_serverId, _opcServer);
                        BindItemValues();
                        WriteToStatus(_serverHost + "--" + _serverName + "已连接!");
                        isConnect = true;
                        btnStart.IsEnabled = false;
                        btnStop.IsEnabled = true;
                        txtStatus.Text = "连接";
                    }
                }
                catch (Exception ex)
                {
                    WriteToStatus("Server00" + ex.Message);
                }
            }
            else
            {
                WriteToStatus("请选择OPC服务器!");
            }
            #endregion
        }

读取OPC标签值,主要通过OPCGroup的DataChanged事件实现,需要注意的是只有当标签组内的任一标签值变化时才会触发此事件。

        private void Group_DataChange(int TransactionID, int NumItems, ref Array ClientHandles, ref Array ItemValues, ref Array Qualities, ref Array TimeStamps)
        {
            //WriteToStatus(Convert.ToString("Group_DataChange"));
            Array handles = ClientHandles;
            for (int i = 1; i <= NumItems; i++)
            {
                //WriteToStatus(Convert.ToString("for循环"));
                OPCValueModel model = listValues.Find(t => ((OPCItem)t.ItemHandle).ClientHandle == (int)handles.GetValue(i));
                try
                {
                    if (model != null)
                    {
                        Type t = ItemValues.GetValue(i).GetType();
                        switch (t.Name)
                        {
                            case "Double":
                                model.ItemValue = ((double)ItemValues.GetValue(i)).ToString("F2");
                                break;
                            case "Decimal":
                                model.ItemValue = ((decimal)ItemValues.GetValue(i)).ToString("F2");
                                break;
                            case "Single":
                                model.ItemValue = ((Single)ItemValues.GetValue(i)).ToString("F2");
                                break;
                            default:
                                model.ItemValue = ItemValues.GetValue(i).ToString();
                                break;
                        }
                        model.ItemQuality = Qualities.GetValue(i) == null ? "" : Qualities.GetValue(i).ToString();
                    }
                }
                catch (Exception ex)
                {
                    WriteToStatus(ex.Message);
  }


            }
        }

断开OPCServer

        private void btnStop_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                if (dicServer.ContainsKey(_serverId))
                {
                    dicServer.TryGetValue(_serverId, out _opcServer);
                    _opcServer.Disconnect();
                    _opcServer = null;
                    dicServer.Remove(_serverId);
                    WriteToStatus(_serverHost + "--" + _serverName + "已断开!");
                    isConnect = false;
                    btnStart.IsEnabled = true;
                    btnStop.IsEnabled = false;
                    int num = listValues.RemoveAll(t => t.ServerId == _serverId);

                }
                else
                {
                    _opcServer = null;
                }
                txtStatus.Text = "断开";
            }
            catch (Exception ex)
            {
                WriteToStatus(ex.Message);
            }

        }

实时数据传输

实时数据传输的关键在于当OPC标签值发生变化时,立即通过SignalR连接传输至服务端并转发至各个客户端,只需要对OPCGroup的DataChanged进行修改即可。

        private void Group_DataChange(int TransactionID, int NumItems, ref Array ClientHandles, ref Array ItemValues, ref Array Qualities, ref Array TimeStamps)
        {
            Array handles = ClientHandles;
            for (int i = 1; i <= NumItems; i++)
            {
                OPCValueModel model = listValues.Find(t => ((OPCItem)t.ItemHandle).ClientHandle == (int)handles.GetValue(i));
                try
                {
                    if (model != null)
                    {
                        Type t = ItemValues.GetValue(i).GetType();
                        switch (t.Name)
                        {
                            case "Double":
                                model.ItemValue = ((double)ItemValues.GetValue(i)).ToString("F2");
                                break;
                            case "Decimal":
                                model.ItemValue = ((decimal)ItemValues.GetValue(i)).ToString("F2");
                                break;
                            case "Single":
                                model.ItemValue = ((Single)ItemValues.GetValue(i)).ToString("F2");
                                break;
                            default:
                                model.ItemValue = ItemValues.GetValue(i).ToString();
                                break;
                        }
                        model.ItemQuality = Qualities.GetValue(i) == null ? "" : Qualities.GetValue(i).ToString();
                        if (isStart)
                        {
                            Sender("RefreshOPCData", model);//发送所有实时数据

                        }
                    }
                }
                catch (Exception ex)
                {
                    WriteToStatus(ex.Message);
                }

            }
        }

实时数据传输的其他相关代码如下:

        private static string _baseUri = ConfigurationManager.AppSettings["ApiUri"];//服务端基地址
        private static string _messageUri = _baseUri + "/MessageBus";//消息中心地址
        public HubConnection _connection = new HubConnection(_messageUri, useDefaultUrl: false);
        public IHubProxy _proxy;//通信代理

        private bool isStart = false;//是否启动数据传输
        private bool isConnect = false;//是否连接OPC

        #region 启动连接
        private async Task Connect()
        {
            _proxy = _connection.CreateHubProxy("MessageHub");
            try
            {
                await _connection.Start().ContinueWith(t =>
                {
                    isStart = true;
                    WriteToStatus("数据传输已启动!");
                });
            }
            catch (Exception ex)
            {
                WriteToStatus(ex.Message);
            }
        }

        #endregion

        #region 断开连接
        private void DisConnect()
        {
            if (_connection.State != Microsoft.AspNet.SignalR.Client.ConnectionState.Disconnected)
            {
                _connection.Stop();
                isStart = false;
                WriteToStatus("数据传输已停止!");
            }
        }
        #endregion

        #region 数据监听
        private void Listener()
        {
            _proxy.On<string, string, Type>("writeOPCItem", (itemId, itemValue, itemType) =>
            {
                OPCValueModel model = listValues.Find(t => t.ItemId == itemId);

                if (model != null)
                {
                    OPCItem item = model.ItemHandle as OPCItem;
                    WriteValueAsync(item, itemValue);
                    _value = itemValue;

                }
            });
            _proxy.On<string>("listen", data =>
            {
                WriteToStatus(data);
            });
        }
        #endregion

        #region 数据发送
        private void Sender(string action, object param, object attach = null)
        {
            try
            {
                if (attach == null)
                {
                    _proxy.Invoke(action, param);
                }
                else
                {
                    _proxy.Invoke(action, param, attach);
                }

            }
            catch (Exception ex)
            {
                WriteToStatus(Cnvert.ToString(ex.HResult));
            }
        }
        #endregion

PC、Web客户端实现

PC客户端和Web客户端的实现原理基本一致,通过SignalR与服务端建立连接并监听消息,客户端接收到数据后处理并显示在页面上。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js"></script>
    <script src="Scripts/jquery.signalR-2.2.1.min.js"></script>
    <script type="text/javascript">

        var msgUri = 'http://localhost:9600/MessageBus';
        var connection = $.hubConnection(msgUri);
        var msgHub = connection.createHubProxy("MessageHub");
        function ConnectAsync() {
            //消息监听
            msgHub.on('welcome', function (message) {
                console.log(message);
            });
            msgHub.on('errorData', function (message) {
                console.log(message);
            });
            msgHub.on('refreshOpcData', function (model,guid) {
                console.log(JSON.stringify(model) + ':' + guid);
                var l = document.getElementById(guid);
                if (l)
                {
                    console.log(model.ItemValue);
                    l.innerText = model.ItemValue;
                }
            });

            //启动连接
            connection.start().done(function () {
                alert('连接成功!');
            });
        }
        //发送消息
        function SendMessage() {
            msgHub.invoke('Welcome', $('#txtMsg').val());
        }
        function DisconnectAsync() {
            connection.stop();
        }
    </script>
  	<meta charset="utf-8" />    
</head>
<body>
    <div>
        <div>
            <button onclick="ConnectAsync()">连接</button>
            <button onclick="DisconnectAsync()">断开</button>
        </div>
        <table>
            <tr>
                <th style="width:240px;"><label>测点名称\监测类型</label></th>
                <td style="width:200px;"><label>温度(C)</label></td>
                <td style="width:200px;"><label>湿度(%)</label></td>
                <td style="width:200px;"><label>水位(m)</label></td>
                <td style="width:200px;"><label>O2(%)</label></td>
                <td style="width:200px;"><label>H2S(%)</label></td>
                <td style="width:200px;"><label>CH4(%)</label></td>
            </tr>
            <tr>
                <th><label>区域1</label></th>
                <td><label id="t1">--</label></td>
                <td><label id="h1">--</label></td>
                <td><label id="w1">--</label></td>
                <td><label id="o1">--</label></td>
                <td><label id="s1">--</label></td>
                <td><label id="c1">--</label></td>
            </tr>
            <tr>
                <th><label>区域2</label></th>
                <td><label id="t2">--</label></td>
                <td><label id="h2">--</label></td>
                <td><label id="w2">--</label></td>
                <td><label id="o2">--</label></td>
                <td><label id="s2">--</label></td>
                <td><label id="c2">--</label></td>
            </tr>
            <tr>
                <th><label>区域3</label></th>
                <td><label id="t3">--</label></td>
                <td><label id="h3">--</label></td>
                <td><label id="w3">--</label></td>
                <td><label id="o3">--</label></td>
                <td><label id="s3">--</label></td>
                <td><label id="c3">--</label></td>
            </tr>
        </table>
    </div>
</body>
</html>


由于服务端接收到OPCClient传输的数据后会以广播是形式发送出去,所以只要监听服务端相应方法的客户端都会收到数据,这样既解决了PC客户端、Web客户端跨平台显示数据的问题,也解决了多客户端同时显示数据的问题。

最后一个问题

最后一个问题不是很严重的问题,却是比较烦人的问题。现场部署时往往会出现OPCServer和OPCClient部署在不同计算机的情况,这是就必需配置DCOM了。但问题在于DCOM的配置不是每次都有用,辛辛苦苦按照教程配了半天,最后发现没用,依然连不上。鉴于配置十次DCOM只有一两次能用,果断将自己开发的OPCClient部署到OPCServer所在计算机上,这样就不需要配置DCOM了。而且有几个安装OPCServer的计算机就相应部署几个自己的OPCClient,目前这种结构也能支持多个OPCClient连接同一个服务端。至于自动实现DCOM配置的功能暂不考虑了。


实践出真知,想到就去做!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值