黎俊刚 发表于 2024-6-20 02:13:29

使用C#开发OPC UA服务器

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>

               
                <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>

               
                <TrustedIssuerCertificates>
                        <StoreType>Directory</StoreType>
                        <StorePath>%CommonApplicationData%\OPC Foundation\pki\issuer</StorePath>
                </TrustedIssuerCertificates>

               
                <TrustedPeerCertificates>
                        <StoreType>Directory</StoreType>
                        <StorePath>%CommonApplicationData%\OPC Foundation\pki\trusted</StorePath>
                </TrustedPeerCertificates>

               
                <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>
                       
                </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>
               
               
               
               
               
                <TraceMasks>515</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用于启动服务器后添加新的变量。
测试服务器

比较好用的测试工具有:

[*]UaExpert:Unified Automation公司提供的测试工具,需安装,能用于连接OPC UA。
[*]OpcExpert:opcti公司提供的免费测试工具,绿色版,能连接OPC和OPC UA。
以下用OpcExpert测试。
浏览本地计算机可发现OPC UA服务器,可看到添加的Root节点和Test变量,Test变量的值会每秒更新。

源码地址:https://github.com/Yada-Yang/SampleOpcUaServer

来源:https://www.cnblogs.com/yada/p/18257593
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 使用C#开发OPC UA服务器