首页 > 编程语言 >使用C#开发OPC UA服务器

使用C#开发OPC UA服务器

时间:2024-06-19 22:22:02浏览次数:11  
标签:name C# 服务器 OPC variable new UA

OPC基金会提供了OPC UA .NET标准库以及示例程序,但官方文档过于简单,光看官方文档和示例程序很难弄懂OPC UA .NET标准库怎么用,花了不少时间摸索才略微弄懂如何使用,以下记录如何从一个控制台程序开发一个OPC UA服务器。

安装Nuget包

安装OPCFoundation.NetStandard.Opc.Ua
image

主程序

修改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赋值。
  • StandardServerReverseConnectServer两种作为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文件。

image

在属性中设为“始终赋值”。

image

内容如下:

<?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用于启动服务器后添加新的变量。

测试服务器

比较好用的测试工具有:

  • UaExpert:Unified Automation公司提供的测试工具,需安装,能用于连接OPC UA。

  • OpcExpert:opcti公司提供的免费测试工具,绿色版,能连接OPC和OPC UA。

以下用OpcExpert测试。

浏览本地计算机可发现OPC UA服务器,可看到添加的Root节点和Test变量,Test变量的值会每秒更新。

image

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

标签:name,C#,服务器,OPC,variable,new,UA
From: https://www.cnblogs.com/yada/p/18257593

相关文章

  • “detached HEAD” 状态
    当前处于一个“detachedHEAD”状态,这意味着你当前的HEAD(当前检出的提交)没有绑定到任何分支。一般情况下,这种情况出现在你检出一个特定的提交(而不是分支的最新提交)时。在这种状态下,你无法使用常规的gitpush命令,因为你不在任何分支上。因此,Git提示你可以使用特定的命令将当......
  • 使用docker离线制作es镜像,方便内网环境部署
    1、自己在本地安装docker以及docker-compose2、拉取elasticsearch镜像dockerpullelasticsearch:7.14.0dockerpullkibana:7.14.03、将拉取到的镜像打包到本地目录dockersaveelasticsearch:7.14.0-o/Users/yanjun.hou/es/elasticsearch-7.14.0.tardockersav......
  • CSP历年复赛题-P8815 [CSP-J 2022] 逻辑表达式
    原题链接:https://www.luogu.com.cn/problem/P8815题意解读:计算逻辑表达式的值以及&,|短路操作的次数。解题思路:又是一道经典的中缀表达式的变形问题,如果对中缀表示式如何求值不理解,移步https://www.acwing.com/problem/content/3305/进行复习如果对表示式如何构建树形结构以及......
  • CSS 属性计算
    CSS属性计算过程你是否了解CSS的属性计算过程呢?有的同学可能会讲,CSS属性我倒是知道,例如:p{color:red;}上面的CSS代码中,p是元素选择器,color就是其中的一个CSS属性。但是要说CSS属性的计算过程,还真的不是很清楚。没关系,通过此篇文章,能够让你彻底明白什么是C......
  • PhantomReference 和 WeakReference 究竟有何不同
    本文基于OpenJDK17进行讨论,垃圾回收器为ZGC。提示:为了方便大家索引,特将在上篇文章《以ZGC为例,谈一谈JVM是如何实现Reference语义的》中讨论的众多主题独立出来。PhantomReference和WeakReference如果仅仅从概念上来说其实很难区别出他们之间究竟有何不同,比如,......
  • SEETF-2023 express-javascript-security ejs相关漏洞
    今天做个ejs相关题目。进入页面只发现一个输入框,题目标签是ejs相关,去github看看源码,发现ejs版本为3.1.9,可以确定地是rce漏洞。接下来说说这个rce漏洞。3.1.9版本的rce漏洞主要是因为使用了这个模板来构建网页逻辑导致的。点击查看代码//index.jsconstexpress=require('e......
  • Controller 注解
    @Controller注解的原理在SpringMVC中,@Controller注解用于标识一个Java类是一个控制器。控制器负责接收请求、处理请求,并返回响应。具体来说,使用@Controller注解的类将会被SpringMVC自动扫描,并注册为一个控制器。@Controller注解的作用和@Component注解类似,都是将......
  • 推荐一款集成化的通用结构分析与设计软件:CSI SAP2000
    CSISAP2000是一款高级的结构分析和设计软件,由ComputersandStructures,Inc.开发。该软件以其强大的分析能力和直观的用户界面而闻名于世,适用于各种工程领域,包括交通、工业、公共工程、体育设施等。SAP2000已经有30多年的历史,它继续沿用其传统,提供先进的分析方法和无与伦比的......
  • C#贪吃蛇小游戏源码
    usingSystem;usingSystem.Collections.Generic;usingSystem.ComponentModel;usingSystem.Data;usingSystem.Drawing;usingSystem.Linq;usingSystem.Text;usingSystem.Threading.Tasks;usingSystem.Windows.Forms;namespace贪吃蛇游戏{publicp......
  • CSS(4)盒子模型
    盒子模型(CSS重点)其实,CSS就三个大模块:盒子模型、浮动、定位,其余的都是细节。要求这三部分,无论如何也要学的非常精通。1.看透网页布局的本质网页布局中,我们是如何把里面的文字,图片,按照美工给我们的效果图排列的整齐有序呢?看透网页布局的本质:把网页元素比如文字图片等......