-
一、使用SIOME免费工具建模
从西门子官网下载软件SIOS,需要注册登录,下载安装版就行。下载后直接安装就可以用了,如图:

安装完成后打开,开始建模,如图左上角有新建模型的按钮。

新建了新工程后,需要添加命名空间,在右边Namespaces的位置,点击Add New Namespace,比如要创建一个Boiler的命名空间,则输入:http://opcfoundation.org/UA/Boiler

有了命名空间之后就要创建对象,但是创建对象之前需要先创建ObjectType,在左侧Types->ObjectTypes->BaseObjectType,右键Add New ObjectType添加对象类型,输入名称和选择命名空间就可以了,如图:

在BoilerType下面添加Child可以添加变量和方法,如图,DataType双击后,可以选择数据类型。NodeClass可以选择变量还是方法:


有了ObjectType后,就可以添加对象了,回到最上面的Objects节点,右键选中Add Instance。TypeDefinition需要双击后选择之前创建的类型。


以上完成后,保存,文件名为:Boiler.NodeSet2.xml
参考教程:https://blog.youkuaiyun.com/whahu1989/article/details/105079566
二、配置模型编译工具
从网站上下载https://github.com/OPCFoundation/UA-ModelCompiler源码,如果网站打不开直接用下载链接:https://codeload.github.com/OPCFoundation/UA-ModelCompiler/zip/refs/heads/master,下载后用visual studio打开如图:

这里需要用Release编译,编译后进入的运行文件路径下,然后将需要生成的xml拷贝过来(为了方便输入路径)。如图,xml的放置在Boiler中:

打开cmd,然后使用命令编译:
Opc.Ua.ModelCompiler.exe compile -d2 ./Boiler/Boiler.NodeSet2.xml -cg Boiler.csv -o2 NewBoiler -version v105
其中./Boiler/Boiler.NodeSet2.xml是需要被编译的xml,Boiler.csv有没有的没关系,NewBoiler是输出的文件夹。结果如下:


这里要注意,生成后的文件夹中包含了一个同名的xml。而我们一般使用的是uanodes二进制文件(因为直接使用xml,程序容易出现读取截断,字符无法识别等各种问题)。二进制文件我们不能直接使用,所以要判定生成的文件对不对,就使用生成后的xml。最简单的办法就是把生成后的xml重新导入到建模软件中查看。
参考教程链接:UA-ModelCompiler 使用教程 - GitCode博客
三、导入服务器验证
3.1 导入编译后的节点信息
需要从官网下载C#源代码,或者从开源网站下载C++源代码。这里先以C#为例子。下载地址:
https://github.com/OPCFoundation/UA-.NETStandard/

其中ConsoleReferenceServer就是示例服务器,可以直接用。服务器的地址空间配置放在Quickstarts.Servers工程中,其中Boiler文件夹是默认给的模型示例。我们需要做的事情就是仿照其中的BoilerNodeManager.cs写一个文件。
在UA-.NETStandard-master\Applications\Quickstarts.Servers路径下,创建NewBoiler文件夹,并把ModelCompiler编译工具生成的文件拷贝到文件夹中,然后导入到C#工程中,导入的方法是点击解决方案的显示所有文件按钮,然后找到新增的文件夹,右键包括到项目中。注意:记得新建一个BoilerNodeManager.cs文件。如图所示:


3.2 修改NodeManage代码
从示例代码中把nodeManage文件拷贝过来后,首先需要修改命名空间。打开文件Opc.Ua.Boiler.DataTypes.cs,可以看到ModelCompiler生成的命名空间:Opc.Ua.Boiler。在BoilerNodeManager文件中,涉及到命名空间的地方都需要修改,如图:

根据第一章我们创建的对象信息,我们没有创建Boilers以及相关的操作,所以示例程序中CreateBoiler这种函数都是不需要的。我们需要关注的是LoadPredefinedNodes函数,通过断点调试可以查看从二进制文件中加载进来的节点。其中LoadFromBinaryResource需要填写二进制文件所在的位置。如图所示,Boiler需要改成NewBoiler。把"Quickstarts.Servers.Boiler.Boiler.PredefinedNodes.uanodes"改成"Quickstarts.Servers\\NewBoiler\\Opc.Ua.Boiler.PredefinedNodes.uanodes"。

这里必须注意,程序运行的时候,读取资源文件是以可执行文件所在的位置做相对位置的,所以还需要把二进制文件(最好是整个文件夹)拷贝到执行目录下。这就是LoadFromBinaryResource读取的真实路径。

如下图,正确读取二进制文件后,断点调试可以看到读取的3个节点就是我们用西门子软件创建的3个节点。

四、使用客户端查看服务器信息
这里我们下在免费的UaExpert。下载和使用参考教程:使用UaExpert - 水滴大海 - 博客园。
点击加号添加服务后,可以看到我们新增的boiler1和变量var。也可以看到示例程序原来就有的Boilers。

五、方法调用
5.1 建模工具上创建方法
和创建变量一样,先找到BoilerType,然后AddChild,这里的NodeClass要选择Method。函数名这里用“func”。注意,完成后,还需在References中把ModelingRule中的√打上。


然后在对象节点下就可以看到创建的方法了。

如果需要入参和出参,在BoilerType的func下。可以看到InputArguments和OutputArguments。我们分别添加一个int型入参Arg1和int型出参returnVal。选择“Add New Argument”,把DataType改成我们需要的int类型即可。


5.2 代码中加入方法的执行函数
打开ModelComplier自动生成的文件,文件中有一个叫做“Opc.Ua.Boiler.Classes.cs”。在文件中可以看到,有一个继承自BaseObjectState类的子类BoilerTypeState。我们需要扩展这个类。我们需要做的是,覆盖父类的OnAfterCreate函数,然后把我们的方法通过这个函数注册进去。如下图,OnFunc是我们自定义用来执行func调用的函数,OnAfterCreate是覆盖BaseObjectState的方法。也是注册我们自己方法的执行函数OnFunc的入口。

完成后,还需要在我们第三章中建立的NodeManage文件中,完善AddBehaviourToPredefinedNode函数。主要是需要调用Call函数。如图,最重要的就是这三句代码,其他的照着写就行:
case ObjectTypes.BoilerType: // 我们创建的对象类型
MethodState func = activeNode.func; // 我们调用的函数
func.Call(context, activeNode.NodeId, inputArguments, errors, outputArguments); // 进入函数

完成后就可以编译调试了,现在UaExpert工具上,找到func然后选Call,弹出的界面中,Arg1参数填入1。点Call即可,在服务端代码的断点中,可以看到OnFunc函数的inputArguments中接收到了1。


六、客户端开发
这里的客户端是基于官方标准的C#源码基础上做开发。Librarys和Stack中的类都视为基础类,可以调用,但不能(也不需要)去修改它们。通过从官方示例客户端ConsoleReferenceClient中,提取最核心的代码组成客户端,来学习opcUA客户端。
客户端学习代码已经上传,绑定到优快云资源中。https://download.youkuaiyun.com/download/FHZZWZ/90921802?spm=1001.2101.3001.9500

6.1 开发session连接函数
① 先导入配置文件
需要用到的类为ApplicationInstance和ApplicationConfiguration。
如图,复制一个示例程序中的xml配置文件,并改成自己的名字。

然后修改配置文件中的ApplicationName为自己的名字。

代码写法如下,通过LoadApplicationConfiguration就把配置文件导入了。这里要把ApplicationName改成和刚才配置文件xml相同的名字。
string password = null;
CertificatePasswordProvider PasswordProvider = new CertificatePasswordProvider(password);
ApplicationInstance application = new ApplicationInstance {
ApplicationName = "zhanzhiClient",
ApplicationType = ApplicationType.Client,
ConfigSectionName = "zhanzhiClient",
CertificatePasswordProvider = PasswordProvider
};
var config = await application.LoadApplicationConfiguration(false).ConfigureAwait(false);
m_configuration = application.ApplicationConfiguration;
② 获取endpoint
这里的serverUrl,是服务器的url,例如:"opc.tcp://DESKTOP-LV5DIJF:53530/OPCUA/SimulationServer"
EndpointDescription endpointDescription = null;
endpointDescription = CoreClientUtils.SelectEndpoint(m_configuration, serverUrl, useSecurity);
EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration);
ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
③ 创建一个session
首先需要注意,CancellationToken ct = default,回调函数为:
private void Session_KeepAlive(ISession session, KeepAliveEventArgs e)
{
if (e.CurrentState != ServerState.Running)
{
m_session = null;
}
}
创建session的代码如下:
var sessionFactory = TraceableSessionFactory.Instance;
ITransportWaitingConnection connection = null;
var session = await sessionFactory.CreateAsync(
m_configuration,
connection,
endpoint,
connection == null,
false,
m_configuration.ApplicationName,
10000, // 10秒
UserIdentity,
null,
ct
).ConfigureAwait(false);
if (session != null && session.Connected)
{
m_session = session;
m_session.KeepAliveInterval = 5000;
m_session.DeleteSubscriptionsOnClose = false;
m_session.TransferSubscriptionsOnReconnect = true;
m_session.KeepAlive += Session_KeepAlive;
}
④ ClientAPI.cs文件完整内容如下
把这个类ClientAPI拷贝过去,通过调用ConnectAsync就可以验证连接了。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using static Org.BouncyCastle.Math.EC.ECCurve;
namespace zhanzhiClient
{
public class ClientAPI
{
public async Task<bool> ConnectAsync(string serverUrl, bool useSecurity = true, CancellationToken ct = default)
{
try
{
if (m_session != null && m_session.Connected == true)
{
// 已连接
}
else
{
string password = null;
CertificatePasswordProvider PasswordProvider = new CertificatePasswordProvider(password);
ApplicationInstance application = new ApplicationInstance {
ApplicationName = "zhanzhiClient",
ApplicationType = ApplicationType.Client,
ConfigSectionName = "zhanzhiClient",
CertificatePasswordProvider = PasswordProvider
};
var config = await application.LoadApplicationConfiguration(false).ConfigureAwait(false);
m_configuration = application.ApplicationConfiguration;
EndpointDescription endpointDescription = null;
endpointDescription = CoreClientUtils.SelectEndpoint(m_configuration, serverUrl, useSecurity);
EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration);
ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
var sessionFactory = TraceableSessionFactory.Instance;
ITransportWaitingConnection connection = null;
var session = await sessionFactory.CreateAsync(
m_configuration,
connection,
endpoint,
connection == null,
false,
m_configuration.ApplicationName,
10000, // 10秒
UserIdentity,
null,
ct
).ConfigureAwait(false);
// Assign the created session
if (session != null && session.Connected)
{
m_session = session;
// override keep alive interval
m_session.KeepAliveInterval = 5000;
// support transfer
m_session.DeleteSubscriptionsOnClose = false;
m_session.TransferSubscriptionsOnReconnect = true;
// set up keep alive callback.
m_session.KeepAlive += Session_KeepAlive;
}
}
return true;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
}
private void Session_KeepAlive(ISession session, KeepAliveEventArgs e)
{
if (e.CurrentState != ServerState.Running)
{
m_session = null;
}
}
public ISession getSession()
{
return m_session;
}
private bool m_disposed = false;
private ISession m_session = null;
private ReverseConnectManager m_reverseConnectManager = null;
private ApplicationConfiguration m_configuration = null;
public IUserIdentity UserIdentity { get; set; } = new UserIdentity();
}
}
6.2 Certificate 证书问题
CheckApplicationInstanceCertificates这个函数会在C:\Users\xxxxx\AppData\Local\OPC Foundation\pki\own\certs文件夹下产生证书。如图:

注意:linux下这个路径为/home/xxxxx/.local/share/OPC Foundation/pki/rejected
路径配置是在6.1中提到的xml中,StorePath节点。

因此客户端代码中,需要在CreateAsync加入下面这段代码才能产生证书:
// check the application certificate.
bool haveAppCertificate = await application.CheckApplicationInstanceCertificates(false).ConfigureAwait(false);
客户端如果没有产生证书的话,会报错:
ApplicationCertificate for the security profile http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss cannot be found.
产生证书后,在连接完成一次服务端(这里用模拟服务器Prosys做示例), 服务端会把证书放到不信任列表,并返回错误:Error establishing a connection: Error received from remote host: BadSecurityChecksFailed。因此需要在证书列表上,把Rejecte改为Trust。

服务端信任了客户端证书后,客户端也要信任服务端的证书。在pki下面创建trusted文件夹,把rejected里面的certs文件夹remove到trusted中。
6.3 获取服务器数据
① 首先要连接,直接调用上一小节写好的函数。
bool connected = await m_clientAPI.ConnectAsync(comboUrl.Text, false).ConfigureAwait(false);
② 读取服务器基础信息
如下,读取了服务器状态和时间,其中关键的就是session的Read函数,最终结果格式如下:
ServerStatus: {2025/5/27 2:25:50 | 2025/5/27 9:17:22 | Running | Opc.Ua.BuildInfo | 0 | }
StartTime: 2025/5/27 2:25:50
代码如下:
ReadValueIdCollection nodesToRead = new ReadValueIdCollection() {
// Value of ServerStatus
new ReadValueId() { NodeId = Variables.Server_ServerStatus, AttributeId = Attributes.Value },
// BrowseName of ServerStatus_StartTime
new ReadValueId() { NodeId = Variables.Server_ServerStatus_StartTime, AttributeId = Attributes.BrowseName },
// Value of ServerStatus_StartTime
new ReadValueId() { NodeId = Variables.Server_ServerStatus_StartTime, AttributeId = Attributes.Value }
};
m_clientAPI.getSession().Read(
null,
0,
TimestampsToReturn.Both,
nodesToRead,
out DataValueCollection resultsValues,
out DiagnosticInfoCollection diagnosticInfos);
{
string strTxt = "ServerStatus: ";
strTxt += resultsValues[0].Value;
strTxt += "\r\n";
strTxt += resultsValues[1].Value;
strTxt += ": ";
strTxt += resultsValues[2].Value;
}
③ 节点Browse
首先,opcUA标准协议定义了一些特殊节点的nodeID,在文件UA-.NETStandard-master\Stack\Opc.Ua.Core\Stack\GeneratedOpc.Ua.Constants.cs中,或者看这里:
Standard-Defined Constants — open62541 0.3.1 documentation
比如object文件夹的nodeID是85。
我们用Stack中的命名空间,就可以直接取到ID。代码如下:
Browser browser = new Browser(m_clientAPI.getSession());
browser.IncludeSubtypes = true;
NodeId nodeId = ObjectIds.ObjectsFolder;
ReferenceDescriptionCollection browseObjectsResults = browser.Browse(nodeId);
nodeId = ObjectIds.TypesFolder;
ReferenceDescriptionCollection browseTypesResults = browser.Browse(nodeId);
nodeId = ObjectIds.ViewsFolder;
ReferenceDescriptionCollection browseViewsResults = browser.Browse(nodeId);
1857

被折叠的 条评论
为什么被折叠?



