需要从现场设备读取数据?那就绕不开OPC这座大山(至少目前接触的几个项目都少不了OPC)。由于很多自动控制设备厂家已经提供了OPCServer,所以就不再花心思去开发了。只要是遵循OPC协议的OPCClient,就能从OPCServer读取写入数据。以下就是为解决项目实际需要开发OPCClient的过程:
针对本文中的OPCClient的功能改进参考下文:
开发环境 | 版本 |
---|---|
操作系统 | Windows 10 Professional |
编译器 | Visual Studio 2015 update 3 |
数据库 | SQLite |
解决方案
针对项目实际需要有以下几点考虑:OPCClient独立运行,不嵌套在主业务程序中运行,只作为数据传输桥梁;能够支持Web应用、桌面应用和移动应用显示实时数据;实现自动配置DCOM;
基于以上考虑并结合以往的经验,考虑采用OPCClient结合SignalR进行实时数据传输的方案,整体结构如下图:
OPCClient从OPCServer读取数据传输至融合了SignalR的服务端,服务端将数据处理后转发至PC客户端和Web客户端。
服务端实现
服务端采用的是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配置的功能暂不考虑了。
实践出真知,想到就去做!