OPC基金会提供了OPC UA .NET标准库以及示例程序,但官方文档过于简单,光看官方文档和示例程序很难弄懂OPC UA .NET标准库怎么用,花了不少时间摸索才略微弄懂如何使用,以下记录如何从一个控制台程序开发一个OPC UA服务器。
安装Nuget包
安装OPCFoundation.NetStandard.Opc.Ua
主程序
修改Program.cs
代码如下:
using Opc.Ua;
using Opc.Ua.Configuration;
using Opc.Ua.Server;
namespace SampleOpcUaServer
{
internal class Program
{
static void Main(string[] args)
{
// 启动OPC UA服务器
ApplicationInstance application = new ApplicationInstance();
application.ConfigSectionName = "OpcUaServer";
application.LoadApplicationConfiguration(false).Wait();
application.CheckApplicationInstanceCertificate(false, 0).Wait();
var server = new StandardServer();
var nodeManagerFactory = new NodeManagerFactory();
server.AddNodeManager(nodeManagerFactory);
application.Start(server).Wait();
// 模拟数据
var nodeManager = nodeManagerFactory.NodeManager;
var simulationTimer = new System.Timers.Timer(1000);
var random = new Random();
simulationTimer.Elapsed += (sender, EventArgs) =>
{
nodeManager?.UpdateValue("ns=2;s=Root_Test", random.NextInt64());
};
simulationTimer.Start();
// 输出OPC UA Endpoint
Console.WriteLine("Endpoints:");
foreach (var endpoint in server.GetEndpoints().DistinctBy(x => x.EndpointUrl))
{
Console.WriteLine(endpoint.EndpointUrl);
}
Console.WriteLine("按Enter添加新变量");
Console.ReadLine();
// 添加新变量
nodeManager?.AddVariable("ns=2;s=Root", "Test2", (int)BuiltInType.Int16, ValueRanks.Scalar);
Console.WriteLine("已添加变量");
Console.ReadLine();
}
}
}
上述代码中:
ApplicationInstance
是OPC UA标准库中用于配置OPC UA Server和检查证书的类。application.ConfigSectionName
指定了配置文件的名称,配置文件是xml文件,将会在程序文件夹查找名为OpcUaServer.Config.xml
的配置文件。配置文件内容见后文。application.LoadApplicationConfiguration
加载前面指定的配置文件。如果不想使用配置文件,也可通过代码给application.ApplicationConfiguration
赋值。- 有
StandardServer
和ReverseConnectServer
两种作为OPC UA服务器的类,ReverseConnectServer
派生于StandardServer
,这两种类的区别未深入研究,用StandardServer
可满足基本的需求。 - OPC UA的地址空间由节点组成,简单理解节点就是提供给OPC UA客户端访问的变量和文件夹。通过
server.AddNodeManager
方法添加节点管理工厂类,NodeManagerFactory
类定义见后文。 - 调用
application.Start(server)
方法后,OPC UA Server就会开始运行,并不会阻塞代码,为了保持在控制台程序中运行,所以使用Console.ReadLine()
阻塞程序。 nodeManager?.UpdateValue
是自定义的更新OPC UA地址空间中变量值的方法。nodeManager?.AddVariable
在此演示动态添加一个新的变量。
OPC UA配置文件
新建OpcUaServer.Config.xml
文件。
在属性中设为“始终赋值”。
内容如下:
<?xml version="1.0" encoding="utf-8"?>
<ApplicationConfiguration
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ua="http://opcfoundation.org/UA/2008/02/Types.xsd"
xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd"
>
<ApplicationName>Sample OPC UA Server</ApplicationName>
<ApplicationUri>urn:localhost:UA:OpcUaServer</ApplicationUri>
<ProductUri>uri:opcfoundation.org:OpcUaServer</ProductUri>
<ApplicationType>Server_0</ApplicationType>
<SecurityConfiguration>
<!-- Where the application instance certificate is stored (MachineDefault) -->
<ApplicationCertificate>
<StoreType>Directory</StoreType>
<StorePath>%CommonApplicationData%\OPC Foundation\pki\own</StorePath>
<SubjectName>CN=Sample Opc Ua Server, C=US, S=Arizona, O=SomeCompany, DC=localhost</SubjectName>
</ApplicationCertificate>
<!-- Where the issuer certificate are stored (certificate authorities) -->
<TrustedIssuerCertificates>
<StoreType>Directory</StoreType>
<StorePath>%CommonApplicationData%\OPC Foundation\pki\issuer</StorePath>
</TrustedIssuerCertificates>
<!-- Where the trust list is stored -->
<TrustedPeerCertificates>
<StoreType>Directory</StoreType>
<StorePath>%CommonApplicationData%\OPC Foundation\pki\trusted</StorePath>
</TrustedPeerCertificates>
<!-- The directory used to store invalid certficates for later review by the administrator. -->
<RejectedCertificateStore>
<StoreType>Directory</StoreType>
<StorePath>%CommonApplicationData%\OPC Foundation\pki\rejected</StorePath>
</RejectedCertificateStore>
</SecurityConfiguration>
<TransportConfigurations></TransportConfigurations>
<TransportQuotas>
<OperationTimeout>600000</OperationTimeout>
<MaxStringLength>1048576</MaxStringLength>
<MaxByteStringLength>1048576</MaxByteStringLength>
<MaxArrayLength>65535</MaxArrayLength>
<MaxMessageSize>4194304</MaxMessageSize>
<MaxBufferSize>65535</MaxBufferSize>
<ChannelLifetime>300000</ChannelLifetime>
<SecurityTokenLifetime>3600000</SecurityTokenLifetime>
</TransportQuotas>
<ServerConfiguration>
<BaseAddresses>
<ua:String>https://localhost:62545/OpcUaServer/</ua:String>
<ua:String>opc.tcp://localhost:62546/OpcUaServer</ua:String>
</BaseAddresses>
<SecurityPolicies>
<ServerSecurityPolicy>
<SecurityMode>SignAndEncrypt_3</SecurityMode>
<SecurityPolicyUri>http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256</SecurityPolicyUri>
</ServerSecurityPolicy>
<ServerSecurityPolicy>
<SecurityMode>None_1</SecurityMode>
<SecurityPolicyUri>http://opcfoundation.org/UA/SecurityPolicy#None</SecurityPolicyUri>
</ServerSecurityPolicy>
<ServerSecurityPolicy>
<SecurityMode>Sign_2</SecurityMode>
<SecurityPolicyUri></SecurityPolicyUri>
</ServerSecurityPolicy>
<ServerSecurityPolicy>
<SecurityMode>SignAndEncrypt_3</SecurityMode>
<SecurityPolicyUri></SecurityPolicyUri>
</ServerSecurityPolicy>
</SecurityPolicies>
<UserTokenPolicies>
<ua:UserTokenPolicy>
<ua:TokenType>Anonymous_0</ua:TokenType>
</ua:UserTokenPolicy>
<ua:UserTokenPolicy>
<ua:TokenType>UserName_1</ua:TokenType>
</ua:UserTokenPolicy>
<ua:UserTokenPolicy>
<ua:TokenType>Certificate_2</ua:TokenType>
</ua:UserTokenPolicy>
<!--
<ua:UserTokenPolicy>
<ua:TokenType>IssuedToken_3</ua:TokenType>
<ua:IssuedTokenType>urn:oasis:names:tc:SAML:1.0:assertion:Assertion</ua:IssuedTokenType>
</ua:UserTokenPolicy>
-->
</UserTokenPolicies>
<DiagnosticsEnabled>false</DiagnosticsEnabled>
<MaxSessionCount>100</MaxSessionCount>
<MinSessionTimeout>10000</MinSessionTimeout>
<MaxSessionTimeout>3600000</MaxSessionTimeout>
<MaxBrowseContinuationPoints>10</MaxBrowseContinuationPoints>
<MaxQueryContinuationPoints>10</MaxQueryContinuationPoints>
<MaxHistoryContinuationPoints>100</MaxHistoryContinuationPoints>
<MaxRequestAge>600000</MaxRequestAge>
<MinPublishingInterval>100</MinPublishingInterval>
<MaxPublishingInterval>3600000</MaxPublishingInterval>
<PublishingResolution>50</PublishingResolution>
<MaxSubscriptionLifetime>3600000</MaxSubscriptionLifetime>
<MaxMessageQueueSize>10</MaxMessageQueueSize>
<MaxNotificationQueueSize>100</MaxNotificationQueueSize>
<MaxNotificationsPerPublish>1000</MaxNotificationsPerPublish>
<MinMetadataSamplingInterval>1000</MinMetadataSamplingInterval>
<AvailableSamplingRates>
<SamplingRateGroup>
<Start>5</Start>
<Increment>5</Increment>
<Count>20</Count>
</SamplingRateGroup>
<SamplingRateGroup>
<Start>100</Start>
<Increment>100</Increment>
<Count>4</Count>
</SamplingRateGroup>
<SamplingRateGroup>
<Start>500</Start>
<Increment>250</Increment>
<Count>2</Count>
</SamplingRateGroup>
<SamplingRateGroup>
<Start>1000</Start>
<Increment>500</Increment>
<Count>20</Count>
</SamplingRateGroup>
</AvailableSamplingRates>
<MaxRegistrationInterval>30000</MaxRegistrationInterval>
<NodeManagerSaveFile>OpcUaServer.nodes.xml</NodeManagerSaveFile>
</ServerConfiguration>
<TraceConfiguration>
<OutputFilePath>Logs\SampleOpcUaServer.log</OutputFilePath>
<DeleteOnLoad>true</DeleteOnLoad>
<!-- Show Only Errors -->
<!-- <TraceMasks>1</TraceMasks> -->
<!-- Show Only Security and Errors -->
<!-- <TraceMasks>513</TraceMasks> -->
<!-- Show Only Security, Errors and Trace -->
<TraceMasks>515</TraceMasks>
<!-- Show Only Security, COM Calls, Errors and Trace -->
<!-- <TraceMasks>771</TraceMasks> -->
<!-- Show Only Security, Service Calls, Errors and Trace -->
<!-- <TraceMasks>523</TraceMasks> -->
<!-- Show Only Security, ServiceResultExceptions, Errors and Trace -->
<!-- <TraceMasks>519</TraceMasks> -->
</TraceConfiguration>
</ApplicationConfiguration>
需要关注的内容有:
-
ApplicationName:在通过OPC UA工具连接此服务器时,显示的服务器名称就是该值。
-
ApplicationType:应用类型,可用的值有:
- Server_0:服务器
- Client_1:客户端
- ClientAndServer_2:客户机和服务器
- DisconveryServer_3:发现服务器。发现服务器用于注册OPC UA服务器,然后提供OPC UA客户端搜索到服务器。
-
SecurityConfiguration:该节点中指定了OPC UA的证书存储路径,一般保持默认,不需修改。
-
ServerConfiguration.BaseAddresses
:该节点指定OPC UA服务器的url地址。 -
ServerConfiguration.SecurityPolicies
:该节点配置允许的服务器安全策略,配置通讯是否要签名和加密。 -
ServerConfiguration.UserTokenPolicies
:该节点配置允许的用户Token策略,例如是否允许匿名访问。 -
AvailableSamplingRates
:配置支持的变量采样率。 -
TraceConfiguration
:配置OPC UA服务器的日志记录,设定日志记录路径,配置的路径是在系统临时文件夹下的路径,日志文件的完整路径是在%TEMP%\Logs\SampleOpcUaServer.log
。
NodeManagerFactory
新建NodeManagerFactory
类,OPC UA server将调用该类的Create
方法创建INodeManager
实现类,而INodeManager
实现类用于管理OPC UA地址空间。内容如下:
using Opc.Ua;
using Opc.Ua.Server;
namespace SampleOpcUaServer
{
internal class NodeManagerFactory : INodeManagerFactory
{
public NodeManager? NodeManager { get; private set; }
public StringCollection NamespacesUris => new StringCollection() { "http://opcfoundation.org/OpcUaServer" };
public INodeManager Create(IServerInternal server, ApplicationConfiguration configuration)
{
if (NodeManager != null)
return NodeManager;
NodeManager = new NodeManager(server, configuration, NamespacesUris.ToArray());
return NodeManager;
}
}
}
- 实现
INodeManagerFactory
接口,需实现NamespacesUris
属性和Create
方法。 NodeManager
类是自定义的类,定义见后文。- 为了获取
Create
方法返回的NodeManager
类,定义了NodeManager
属性。
NodeManager
新建NodeManager
类:
using Opc.Ua;
using Opc.Ua.Server;
namespace SampleOpcUaServer
{
internal class NodeManager : CustomNodeManager2
{
public NodeManager(IServerInternal server, params string[] namespaceUris)
: base(server, namespaceUris)
{
}
public NodeManager(IServerInternal server, ApplicationConfiguration configuration, params string[] namespaceUris)
: base(server, configuration, namespaceUris)
{
}
protected override NodeStateCollection LoadPredefinedNodes(ISystemContext context)
{
FolderState root = CreateFolder(null, "Root");
root.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder); // 将节点添加到服务器根节点
root.EventNotifier = EventNotifiers.SubscribeToEvents;
AddRootNotifier(root);
CreateVariable(root, "Test", BuiltInType.Int64, ValueRanks.Scalar);
return new NodeStateCollection(new List<NodeState> { root });
}
protected virtual FolderState CreateFolder(NodeState? parent, string name)
{
string path = parent?.NodeId.Identifier is string id ? id + "_" + name : name;
FolderState folder = new FolderState(parent);
folder.SymbolicName = name;
folder.ReferenceTypeId = ReferenceTypes.Organizes;
folder.TypeDefinitionId = ObjectTypeIds.FolderType;
folder.NodeId = new NodeId(path, NamespaceIndex);
folder.BrowseName = new QualifiedName(path, NamespaceIndex);
folder.DisplayName = new LocalizedText("en", name);
folder.WriteMask = AttributeWriteMask.None;
folder.UserWriteMask = AttributeWriteMask.None;
folder.EventNotifier = EventNotifiers.None;
if (parent != null)
{
parent.AddChild(folder);
}
return folder;
}
protected virtual BaseDataVariableState CreateVariable(NodeState? parent, string name, BuiltInType dataType, int valueRank)
{
return CreateVariable(parent, name, (uint)dataType, valueRank);
}
protected virtual BaseDataVariableState CreateVariable(NodeState? parent, string name, NodeId dataType, int valueRank)
{
string path = parent?.NodeId.Identifier is string id ? id + "_" + name : name;
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.None;
variable.UserWriteMask = AttributeWriteMask.None;
variable.DataType = dataType;
variable.ValueRank = valueRank;
variable.AccessLevel = AccessLevels.CurrentReadOrWrite;
variable.UserAccessLevel = AccessLevels.CurrentReadOrWrite;
variable.Historizing = false;
variable.Value = Opc.Ua.TypeInfo.GetDefaultValue(dataType, valueRank, Server.TypeTree);
variable.StatusCode = StatusCodes.Good;
variable.Timestamp = DateTime.UtcNow;
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;
}
public void UpdateValue(NodeId nodeId, object value)
{
var variable = (BaseDataVariableState)FindPredefinedNode(nodeId, typeof(BaseDataVariableState));
if (variable != null)
{
variable.Value = value;
variable.Timestamp = DateTime.UtcNow;
variable.ClearChangeMasks(SystemContext, false);
}
}
public void AddFolder(NodeId parentId, string name)
{
var node = Find(parentId);
if (node != null)
{
CreateFolder(node, name);
AddPredefinedNode(SystemContext, node);
}
}
public void AddVariable(NodeId parentId, string name, BuiltInType dataType, int valueRank)
{
AddVariable(parentId, name, (uint)dataType, valueRank);
}
public void AddVariable(NodeId parentId, string name, NodeId dataType, int valueRank)
{
var node = Find(parentId);
if (node != null)
{
CreateVariable(node, name, dataType, valueRank);
AddPredefinedNode(SystemContext, node);
}
}
}
}
上述代码中:
- 需继承
CustomNodeManager2
,这是OPC UA标准库中提供的类。 - 重写
LoadPredefinedNodes
方法,在该方法中配置预定义节点。其中创建了一个Root文件夹,Root文件夹中添加了Test变量。 root.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder)
该语句将节点添加到OPC UA服务器根节点,如果不使用该语句,可在Server
节点下看到添加的节点。CreateFolder
是定义的方法,用于简化创建文件夹节点。CreateVariable
是自定义的方法,用于简化创建变量节点。UpdateValue
是用于更新变量节点值的方法。其中修改值后,需调用ClearChangeMasks
方法,才能通知客户端更新值。AddFolder
用于启动服务器后添加新的文件夹。AddVariable
用于启动服务器后添加新的变量。
测试服务器
比较好用的测试工具有:
以下用OpcExpert测试。
浏览本地计算机可发现OPC UA服务器,可看到添加的Root节点和Test变量,Test变量的值会每秒更新。
源码地址:https://github.com/Yada-Yang/SampleOpcUaServer
标签:name,C#,服务器,OPC,variable,new,UA From: https://www.cnblogs.com/yada/p/18257593