OPC概论
网上OPC UA服务端的介绍非常少,网关的更是找不到,找到的几乎都是收费的,最近静下心把OPC基金会的代码学习了一遍,结合一些大牛的文章,写了一个简单的OPC UA网关。目前网关的设备只支持西门子,因为只有西门子的仿真器足够强大(S7协议可以仿真),后续根据条件会加上三菱、欧姆龙等等。人过中年,总想踏踏实实做点事情,如果有好的工作平台希望大家能介绍一下,感谢!
OPC是应用于工业通信的,在windows环境的下一种通讯技术,原有的通信技术难以满足日益复杂的环境,在可扩展性,安全性,跨平台性方面的不足日益明显,所以OPC基金会在几年前提出了面向未来的架构设计的OPC 统一架构,简称OPC UA,截止目前为止,越来越多公司将OPC UA作为开放的数据标准.
OPC UA 基于Web服务(windows的WCF)开发的,同时又具有MQTT 可订阅的特性,部署方便、使用简单,容易被大家广泛的接受,于是随着工业4.0的发展逐渐普及
网关结构
1.读取配置文件并创建节点
(目前所有配置文件均为手动设置修改,比较下来还是手动配置快点)
节点配置包含一下字段
节点名称、父节点、节点类型,节点数据类型、PLC变量的地址
下面是节点的配置的文件,xml格式
<?xml version="1.0" encoding="UTF-8"?>
<!-- 根目录-->
<Root>
<!-- 目录-->
<Node Name="Motor1" ParentNode="Root" NodeType="Folder" >
<!-- 变量-->
<Node Name="Start1" ParentNode="Motor1" NodeType="Variable" DataType="DataTypeIds.Boolean" Adress="M10.0" >
</Node>
<Node Name="Stop1" ParentNode="Motor1" NodeType="Variable" DataType="DataTypeIds.Boolean" Adress="M10.1" >
</Node>
<Node Name="Speed1" ParentNode="Motor1" NodeType="Variable" DataType="DataTypeIds.Float" Adress="MD12" >
</Node>
</Node>
<!-- 目录-->
<Node Name="Motor2" ParentNode="Root" NodeType="Folder" >
<!-- 变量-->
<Node Name="Start2" ParentNode="Motor2" NodeType="Variable" DataType="DataTypeIds.Boolean" Adress="M20.0" >
</Node>
<Node Name="Stop2" ParentNode="Motor2" NodeType="Variable" DataType="DataTypeIds.Boolean" Adress="M20.1" >
</Node>
<Node Name="Speed2" ParentNode="Motor2" NodeType="Variable" DataType="DataTypeIds.Float" Adress="MD22" >
</Node>
</Node>
<!-- 目录-->
<Node Name="DBTest" ParentNode="Root" NodeType="Folder" >
<!-- 变量-->
<Node Name="Name" ParentNode="DBTest" NodeType="Variable" DataType="DataTypeIds.String" Adress="DB1.DBD0" >
</Node>
<Node Name="Age" ParentNode="DBTest" NodeType="Variable" DataType="DataTypeIds.Int16" Adress="DB1.DBW256" >
</Node>
<Node Name="High" ParentNode="DBTest" NodeType="Variable" DataType="DataTypeIds.Float" Adress="DB1.DBD258" >
</Node>
<Node Name="Good" ParentNode="DBTest" NodeType="Variable" DataType="DataTypeIds.Boolean" Adress="DB1.DBX262.0" >
</Node>
<Node Name="SintTest" ParentNode="DBTest" NodeType="Variable" DataType="DataTypeIds.Int16" Adress="DB1.DBW264" >
</Node>
<Node Name="CharTest" ParentNode="DBTest" NodeType="Variable" DataType="DataTypeIds.ByteString" Adress="DB1.DBB266" >
</Node>
</Node>
</Root>
程序里定义了一个类,对应xml文件的格式,这样方便在OPC网关里创建地址空间和变量
//节点的类
public class OpcuaNode
{
//节点名称
public string NodeName { get; set; }
//父节点
public string ParentNode { get; set; }
//节点数据类型
public string DataType { get; set; }
//节点类型
public string NodeType { get; set; }
//节点的值
public string NodeValue { get; set; }
//节点的地址
public string Adress { get; set; }
}
2…读取服务配置并启动服务实例
配置是OPC基金会的代码里自带的,除了服务地址外其它不作修改
##变量监控、PLC变量的读取和写入(S7net开源代码)
判断变量的数据类型读取或者写入,字符串和实数等单独处理
S7net支持的主要数据类型
public static byte[] SerializeValue(object value)
{
switch (value.GetType().Name)
{
case "Boolean":
return new[] { (byte)((bool)value ? 1 : 0) };
case "Byte":
return Types.Byte.ToByteArray((byte)value);
case "Int16":
return Types.Int.ToByteArray((Int16)value);
case "UInt16":
return Types.Word.ToByteArray((UInt16)value);
case "Int32":
return Types.DInt.ToByteArray((Int32)value);
case "UInt32":
return Types.DWord.ToByteArray((UInt32)value);
case "Single":
return Types.Real.ToByteArray((float)value);
case "Double":
return Types.LReal.ToByteArray((double)value);
case "DateTime":
return Types.DateTime.ToByteArray((System.DateTime)value);
case "Byte[]":
return (byte[])value;
case "Int16[]":
return Types.Int.ToByteArray((Int16[])value);
case "UInt16[]":
return Types.Word.ToByteArray((UInt16[])value);
case "Int32[]":
return Types.DInt.ToByteArray((Int32[])value);
case "UInt32[]":
return Types.DWord.ToByteArray((UInt32[])value);
case "Single[]":
return Types.Real.ToByteArray((float[])value);
case "Double[]":
return Types.LReal.ToByteArray((double[])value);
case "String":
// Hack: This is backwards compatible with the old code, but functionally it's broken
// if the consumer does not pay attention to string length.
var stringVal = (string)value;
return Types.String.ToByteArray(stringVal, stringVal.Length);
case "DateTime[]":
return Types.DateTime.ToByteArray((System.DateTime[])value);
case "DateTimeLong[]":
return Types.DateTimeLong.ToByteArray((System.DateTime[])value);
default:
throw new InvalidVariableTypeException();
}
}
OPC UA主要的数据类型
-<Aliases>
<Alias Alias="Boolean">i=1</Alias>
<Alias Alias="SByte">i=2</Alias>
<Alias Alias="Byte">i=3</Alias>
<Alias Alias="Int16">i=4</Alias>
<Alias Alias="UInt16">i=5</Alias>
<Alias Alias="Int32">i=6</Alias>
<Alias Alias="UInt32">i=7</Alias>
<Alias Alias="Int64">i=8</Alias>
<Alias Alias="UInt64">i=9</Alias>
<Alias Alias="Float">i=10</Alias>
<Alias Alias="Double">i=11</Alias>
<Alias Alias="DateTime">i=13</Alias>
<Alias Alias="String">i=12</Alias>
</Aliases>
编程时注意数据格式要对应,上面的数据类型是常用的
具体代码
1.节点读取和创建
读取文件并加载到树形控件
//连接PLC
connectPlc();
//判断PLC连接状态
if (lianjie == 1&&server==null)
{
//清空节点列表
opcuaNodes.Clear();
//清空列表
treeView1.Nodes.Clear();
//加载文件
doc.Load(xmlpath);
//遍历节点
RecursionTreeControl1(doc.DocumentElement, treeView1.Nodes);
//展开视图
treeView1.ExpandAll();
遍历的目录,同时创建节点类并依次赋值
//遍历目录
private void RecursionTreeControl1(XmlNode xmlNode, TreeNodeCollection nodes)
{
foreach (XmlNode node in xmlNode.ChildNodes)//循环遍历当前元素的子元素集合
{
if (node.NodeType.ToString() != "Comment")
{
OpcuaNode node1 = new OpcuaNode();
TreeNode new_child = new TreeNode();
new_child.Text = node.Attributes["Name"].Value;
node1.NodeName = node.Attributes["Name"].Value;
node1.ParentNode = node.Attributes["ParentNode"].Value;
node1.NodeType = node.Attributes["NodeType"].Value;
//node1.DataType=node.Attributes["DataType"].Value;
//MessageBox.Show(node.InnerText);
opcuaNodes.Add(node1);
nodes.Add(new_child);
RecursionTreeControl2(node, new_child.Nodes);//遍历子节点
}
}
}
遍历变量节点
//遍历子节点
private void RecursionTreeControl2(XmlNode xmlNode, TreeNodeCollection nodes)
{
foreach (XmlNode node in xmlNode.ChildNodes)
{
if (node.NodeType.ToString() != "Comment")
{
OpcuaNode node1 = new OpcuaNode();
TreeNode new_child = new TreeNode();
new_child.Text = node.Attributes["Name"].Value;
node1.NodeName = node.Attributes["Name"].Value;
node1.ParentNode = node.Attributes["ParentNode"].Value;
node1.NodeType = node.Attributes["NodeType"].Value;
node1.DataType = node.Attributes["DataType"].Value;
node1.Adress = node.Attributes["Adress"].Value;
//MessageBox.Show(node.InnerText);
opcuaNodes.Add(node1);
nodes.Add(new_child);//向当前TreeNodeCollection集合中添加当前节点
}
}
}
创建OPC UA地址空间和对应的变量节点
public override void CreateAddressSpace(IDictionary<NodeId, IList<IReference>> externalReferences)
{
lock (Lock)
{
IList<IReference> references = null;
if (!externalReferences.TryGetValue(ObjectIds.ObjectsFolder, out references))
{
externalReferences[ObjectIds.ObjectsFolder] = references = new List<IReference>();
}
FolderState root = CreateFolder(null, "MyOPC", "MyOPC");
root.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder);
references.Add(new NodeStateReference(ReferenceTypes.Organizes, false, root.NodeId));
root.EventNotifier = EventNotifiers.SubscribeToEvents;
AddRootNotifier(root);
List<BaseDataVariableState> variables = new List<BaseDataVariableState>();
FolderState simulationFolder = null;
try
{
#region Scalar_Simulation
NodeId nodetype = DataTypeIds.Boolean;
for (int i = 0; i < Form1.opcuaNodes.Count; i++)
{
if (Form1.opcuaNodes[i].NodeType == "Folder")
{
simulationFolder = CreateFolder(root, Form1.opcuaNodes[i].NodeName, Form1.opcuaNodes[i].NodeName);
}
else
{
switch (Form1.opcuaNodes[i].DataType)
{
case "DataTypeIds.Boolean":
nodetype = DataTypeIds.Boolean;
break;
case "DataTypeIds.String":
nodetype = DataTypeIds.String;
break;
case "DataTypeIds.Double":
nodetype = DataTypeIds.Double;
break;
case "DataTypeIds.Float":
nodetype = DataTypeIds.Float;
break;
case "DataTypeIds.Int16":
nodetype = DataTypeIds.Int16;
break;
case "DataTypeIds.Int32":
nodetype = DataTypeIds.Int32;
break;
//case "DataTypeIds.Int64":
// nodetype = DataTypeIds.Int64;
// break;
case "DataTypeIds.UInt16":
nodetype = DataTypeIds.UInt16;
break;
case "DataTypeIds.UInt32":
nodetype = DataTypeIds.UInt32;
break;
//case "DataTypeIds.UInt64":
// nodetype = DataTypeIds.UInt64;
// break;
case "DataTypeIds.ByteString":
nodetype = DataTypeIds.ByteString;
break;
case "DataTypeIds.Byte":
nodetype = DataTypeIds.Byte;
break;
case "DataTypeIds.DateTime":
nodetype = DataTypeIds.DateTime;
break;
//case "DataTypeIds.Integer":
// nodetype = DataTypeIds.Integer;
// break;
//case "DataTypeIds.UInteger":
// nodetype = DataTypeIds.UInteger;
// break;
//case "DataTypeIds.Number":
// nodetype = DataTypeIds.Number;
// break;
}
CreateDynamicVariable(simulationFolder, Form1.opcuaNodes[i].NodeName, Form1.opcuaNodes[i].NodeName, nodetype, ValueRanks.Scalar, Form1.opcuaNodes[i].Adress);
}
}
#endregion
}
catch (Exception e)
{
Utils.LogError(e, "Error creating the ReferenceNodeManager address space.");
}
AddPredefinedNode(SystemContext, root);
//开启定时器,读PLC
m_simulationTimer = new Timer(DoSimulation, null, 1000, 1000);
}
}
2.读取服务配置,启动服务
服务初始化
//声明应用实例
ApplicationInstance application;
//声明服务
ServerBase server;
//声明服务配置
ApplicationConfiguration config;
private void UaServerInit()
{
try
{
// Initialize the user interface.
Application.EnableVisualStyles();
ApplicationInstance.MessageDlg = new ApplicationMessageDlg();
application = new ApplicationInstance();
application.ApplicationType = ApplicationType.Server;
application.ConfigSectionName = "MyOPC.UA.Server";
//读取服务配置
config = application.LoadApplicationConfiguration(false).Result;
//读取服务地址和端口
textBox1.Text = config.ServerConfiguration.BaseAddresses.ElementAt(1).ToString();
}
catch (Exception ex)
{
ExceptionDlg.Show(application.ApplicationName, ex);
}
}
启动服务
// check the application certificate.
bool certOk = application.CheckApplicationInstanceCertificate(false, 0).Result;
if (!certOk)
{
throw new Exception("Application instance certificate invalid!");
}
// Create server, add additional node managers
//服务实例化
server = new ReferenceServer();
//启动服务
// start the server.
application.Start(server).Wait();
// check whether the invalid certificates dialog should be displayed.
bool showCertificateValidationDialog = false;
ReferenceServerConfiguration refServerconfiguration = application.ApplicationConfiguration.ParseExtension<ReferenceServerConfiguration>();
if (refServerconfiguration != null)
{
showCertificateValidationDialog = refServerconfiguration.ShowCertificateValidationDialog;
}
// run the application interactively.
//绑定证书验证事件
if (showCertificateValidationDialog &&
!application.ApplicationConfiguration.SecurityConfiguration.AutoAcceptUntrustedCertificates)
{
application.ApplicationConfiguration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_CertificateValidation);
}
证书验证对话框
/// <summary>
/// Handles a certificate validation error.
/// </summary>
void CertificateValidator_CertificateValidation(CertificateValidator validator, CertificateValidationEventArgs e)
{
try
{
HandleCertificateValidationError(this, validator, e);
}
catch (Exception exception)
{
HandleException(this.Text, MethodBase.GetCurrentMethod(), exception);
}
}
/// <summary>
/// Handles a certificate validation error.
/// </summary>
/// <param name="caller">The caller's text is used as the caption of the <see cref="MessageBox"/> shown to provide details about the error.</param>
/// <param name="validator">The validator (not used).</param>
/// <param name="e">The <see cref="Opc.Ua.CertificateValidationEventArgs"/> instance event arguments provided when a certificate validation error occurs.</param>
public static void HandleCertificateValidationError(Form caller, CertificateValidator validator, CertificateValidationEventArgs e)
{
StringBuilder buffer = new StringBuilder();
buffer.AppendLine("Certificate could not be validated!");
buffer.AppendLine("Validation error(s):");
ServiceResult error = e.Error;
while (error != null)
{
buffer.AppendFormat("- {0}\r\n", error.ToString().Split('\r', '\n').FirstOrDefault());
error = error.InnerResult;
}
buffer.AppendFormat("\r\nSubject: {0}\r\n", e.Certificate.Subject);
buffer.AppendFormat("Issuer: {0}\r\n", X509Utils.CompareDistinguishedName(e.Certificate.Subject, e.Certificate.Issuer)
? "Self-signed" : e.Certificate.Issuer);
buffer.AppendFormat("Valid From: {0}\r\n", e.Certificate.NotBefore);
buffer.AppendFormat("Valid To: {0}\r\n", e.Certificate.NotAfter);
buffer.AppendFormat("Thumbprint: {0}\r\n\r\n", e.Certificate.Thumbprint);
buffer.Append("Security certificate problems may indicate an attempt to intercept any data you send ");
buffer.Append("to a server or to allow an untrusted client to connect to your server.");
buffer.Append("\r\n\r\nAccept anyway?");
if (MessageBox.Show(buffer.ToString(), caller.Text, MessageBoxButtons.YesNo) == DialogResult.Yes)
{
e.AcceptAll = true;
}
}
3. 监控变量(PLC变量的读取和写入)
1.定时读取
CreateAddressSpace 方法里开个定时器,代码见上面
m_simulationTimer = new Timer(DoSimulation, null, 1000, 1000);
//定时读取PLC并更新数据
private void DoSimulation(object state)
{
try
{
lock (Lock)
{
var timeStamp = DateTime.UtcNow;
foreach (BaseDataVariableState variable in m_dynamicNodes)
{
variable.Value = GetNewValue(variable);
variable.Timestamp = timeStamp;
variable.ClearChangeMasks(SystemContext, false);
}
}
}
catch (Exception e)
{
Utils.LogError(e, "Unexpected error doing simulation.");
}
}
读取的具体代码
//读取PLC
private object GetNewValue(BaseVariableState variable)
{
object value = null;
//判断变量地址是否为空
if (variable.Description.Text.Length > 0)
{
//实数处理
//if (variable.DataType == DataTypeIds.Float)
if (variable.DataType == DataTypeIds.Float || variable.DataType == DataTypeIds.Double)
{
value = Form1.plc.Read(variable.Description.Text);
var buffer = new byte[4];
buffer[3] = (byte)((uint)value >> 24);
buffer[2] = (byte)((uint)value >> 16);
buffer[1] = (byte)((uint)value >> 8);
buffer[0] = (byte)((uint)value >> 0);
value = (BitConverter.ToSingle(buffer, 0)).ToString();
return value;
}
//字符串处理
//if (variable.DataType == DataTypeIds.Float)
else if (variable.DataType == DataTypeIds.String)
{
int tt = variable.Description.Text.IndexOf(".");
int dbnum = int.Parse(variable.Description.Text.Substring(2,1));
int dbcount = int.Parse(variable.Description.Text.Substring(tt+4));
var count = (byte)Form1.plc.Read(DataType.DataBlock, dbnum, dbcount, VarType.Byte, 1);
value = Form1.plc.Read(DataType.DataBlock, dbnum, dbcount + 1, VarType.String, count+1).ToString().Substring(1);
return value;
}
//字符处理
else if (variable.DataType == DataTypeIds.ByteString)
{
value = Form1.plc.Read(variable.Description.Text);
byte[] array = new byte[1];
array[0] = (byte)(Convert.ToInt32(value)); //ASCII码强制转换二进制
value = Convert.ToString(System.Text.Encoding.ASCII.GetString(array));//str为ASCII码对应的字符
return value;
}
else
{
//读取变量
value = Form1.plc.Read(variable.Description.Text);
return value;
}
}
else
{
value = null;
}
// skip Variant Null
if (value is Variant variant)
{
if (variant.Value == null)
{
value = null;
}
}
return value;
}
2.客户端写入监控
为了数据的安全性,防止误写入,程序加了写入开关
/// <summary>
/// Creates a new variable.
/// </summary>
private BaseDataVariableState CreateVariable(NodeState parent, string path, string name, NodeId dataType, int valueRank)
{
BaseDataVariableState variable = new BaseDataVariableState(parent);
variable.SymbolicName = name;
variable.ReferenceTypeId = ReferenceTypes.Organizes;
variable.TypeDefinitionId = VariableTypeIds.BaseDataVariableType;
variable.NodeId = new NodeId(path, NamespaceIndex);
variable.BrowseName = new QualifiedName(path, NamespaceIndex);
variable.DisplayName = new LocalizedText("en", name);
variable.WriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description;
variable.UserWriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description;
variable.DataType = dataType;
variable.ValueRank = valueRank;
variable.AccessLevel = AccessLevels.CurrentReadOrWrite;
variable.UserAccessLevel = AccessLevels.CurrentReadOrWrite;
variable.Historizing = false;
//variable.Value = GetNewValue(variable);
variable.StatusCode = StatusCodes.Good;
variable.Timestamp = DateTime.UtcNow;
//绑定客户端写入事件
variable.OnWriteValue = ToWritePLC;
if (valueRank == ValueRanks.OneDimension)
{
variable.ArrayDimensions = new ReadOnlyList<uint>(new List<uint> { 0 });
}
else if (valueRank == ValueRanks.TwoDimensions)
{
variable.ArrayDimensions = new ReadOnlyList<uint>(new List<uint> { 0, 0 });
}
if (parent != null)
{
parent.AddChild(variable);
}
return variable;
}
写入的具体代码
//写入PLC
private ServiceResult ToWritePLC(
ISystemContext context,
NodeState node,
NumericRange indexRange,
QualifiedName dataEncoding,
ref object value,
ref StatusCode statusCode,
ref DateTime timestamp)
{
if (!Form1.WriteProtect)
{
BaseVariableState variable = node as BaseVariableState;
switch (variable.DataType.ToString())
{
case "i=1"://Boolean
Form1.plc.Write(variable.Description.Text, value);
break;
case "i=12"://String
int tt = variable.Description.Text.IndexOf(".");
int dbnum = int.Parse(variable.Description.Text.Substring(2, 1));
int dbcount = int.Parse(variable.Description.Text.Substring(tt + 4));
var temp = Encoding.ASCII.GetBytes(value.ToString());
var bytes = S7.Net.Types.S7String.ToByteArray(value.ToString(), temp.Length);
Form1.plc.WriteBytes(DataType.DataBlock, dbnum, dbcount, bytes);
break;
case "i=11"://Double
Form1.plc.Write(variable.Description.Text, Convert.ToDouble(value));
break;
case "i=10"://Float
Form1.plc.Write(variable.Description.Text, Convert.ToSingle(value));
break;
case "i=4"://Int16
Form1.plc.Write(variable.Description.Text, Convert.ToInt16(value));
break;
case "i=6"://Int32
Form1.plc.Write(variable.Description.Text, Convert.ToInt32(value));
break;
//case "i=8"://Int64
// Form1.plc.Write(variable.Description.Text, Convert.ToInt64(value));
// break;
case "i=5"://UInt16
Form1.plc.Write(variable.Description.Text, Convert.ToUInt16(value));
break;
case "i=7"://UInt32
Form1.plc.Write(variable.Description.Text, Convert.ToUInt32(value));
break;
//case "i=9"://UInt64
// Form1.plc.Write(variable.Description.Text, Convert.ToUInt64(value));
// break;
case "i=15"://ByteString
var temp2 = Encoding.ASCII.GetBytes(value.ToString());
Form1.plc.Write(variable.Description.Text, temp2);
break;
case "i=3"://Byte
Form1.plc.Write(variable.Description.Text, Convert.ToByte(value));
break;
case "i=13"://DateTime
Form1.plc.Write(variable.Description.Text, Convert.ToDateTime(value));
break;
case "i=27"://Integer
Form1.plc.Write(variable.Description.Text, Convert.ToInt32(value));
break;
case "i=28"://UInteger
Form1.plc.Write(variable.Description.Text, Convert.ToInt32(value));
break;
//case "i=26"://Number
// Form1.plc.Write(variable.Description.Text, value);
//break;
}
}
return ServiceResult.Good;
}
注意数据类型,一定不要搞错,否则写入的值会越界
运行调试
虚拟机采用S7-PLCSIM Advanced V4.0 SP1,IP设置很重要,否则连接不上
见图片
然后在虚拟机里运行仿真器,编写简单程序,下载监控运行,在本机运行OPC UA网关,打开OPC客户端监控或者修改PLC变量,测试正常!
源码下载地址:https://gitee.com/chenzunzhi/opcua