网上OPC UA服务端的介绍非常少,网关的更是找不到,找到的几乎都是收费的,最近静下心把OPC基金会的代码学习了一遍,结合一些大牛的文章,写了一个简单的OPC UA网关。目前网关的设备只支持西门子,因为只有西门子的仿真器足够强大(S7协议可以仿真),后续根据条件会加上三菱、欧姆龙等等。人过中年,总想踏踏实实做点事情,如果有好的工作平台希望大家能介绍一下,感谢!
OPC是应用于工业通信的,在windows环境的下一种通讯技术,原有的通信技术难以满足日益复杂的环境,在可扩展性,安全性,跨平台性方面的不足日益明显,所以OPC基金会在几年前提出了面向未来的架构设计的OPC 统一架构,简称OPC UA,截止目前为止,越来越多公司将OPC UA作为开放的数据标准.
OPC UA 基于Web服务(windows的WCF)开发的,同时又具有MQTT 可订阅的特性,部署方便、使用简单,容易被大家广泛的接受,于是随着工业4.0的发展逐渐普及
一、测试平台搭建
1.软件配置
仿真采用:S7-PLCSIM Advanced V4.0 SP1
编程采用:TIA V15.1
2.编写简单程序,提供基础变量,如布尔,整型,实数等供测试
3.仿真注意事项
1)PLC组态地址,仿真PLC地址必须和虚拟机一个网段
2)项目属性要选择块编译时支持仿真、数据块优化项去掉,通信属性中勾选允许远程PUT/GET
二.OPC UA网关架构流程
三、OPC UA 编译环境
编译软件:Visual Studio 2022
编译框架:Microsoft .NET Framework 4.6.2
四、OPC UA 网关主要部分
节点配置
服务配置
PLC变量监控,读取和写入
部分代码展示
//节点的类
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; }
}
OPC UA的主要数据类型,注意和PLC变量对应
-<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>
读取节点配置文件加载到树形控件,同时给节点类赋值,详细见附件源码
connectPlc();
//判断PLC连接状态
if (lianjie == 1&&server==null)
{
//清空节点列表
opcuaNodes.Clear();
//清空列表
treeView1.Nodes.Clear();
//加载文件
doc.Load(xmlpath);
//遍历节点
RecursionTreeControl1(doc.DocumentElement, treeView1.Nodes);
//展开视图
treeView1.ExpandAll();
服务启动初始化,读取服务配置,包括服务地址、端口、证书、签名或密钥的存放路径,是否启用匿名登录、是否启用非安全登录等等,详细见附件源码
//声明应用实例
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);
}
}
PLC 变量监控分读和写2个部分,读是开一个定时器循环读取PLC的变量的值,不断刷新,写是绑定OPC UA的一个变量写入事件,写入动作时才会触发,具体见附件源码
定时读取
//CreateAddressSpace中开启定时器,读PLC
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;
}
写的事件绑定
/// <summary>
/// Creates a new variable.
/// </summary>
private BaseDataVariableState CreateVariable(NodeState parent, string path, string name, NodeId dataType, int valueRank)
{
//绑定客户端写入事件
variable.OnWriteValue = ToWritePLC;
}
具体见附件吧,已经超出字数限制了!
为了数据的安全,防止误操作,我加了一个写入保护开关,需要时开启
五、运行测试,读写正常