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
用於啟動伺服器後新增新的變數。
測試伺服器
比較好用的測試工具有:
-
UaExpert:Unified Automation公司提供的測試工具,需安裝,能用於連線OPC UA。
-
OpcExpert:opcti公司提供的免費測試工具,綠色版,能連線OPC和OPC UA。
以下用OpcExpert測試。
瀏覽本地計算機可發現OPC UA伺服器,可看到新增的Root節點和Test變數,Test變數的值會每秒更新。
原始碼地址:https://github.com/Yada-Yang/SampleOpcUaServer