编辑
2025-02-13
C# 应用
00
请注意,本文编写于 101 天前,最后修改于 101 天前,其中某些信息可能已经过时。

目录

项目准备
完整代码示例
Program.cs
MyServer.cs
MyNodeManager.cs
3. 注意事项
结论

本文将尝试说明如何使用C#创建一个完整的OPC UA服务器。我们将使用开源的OPC UA .NET Standard库,感觉要写一个完整的服务还是太麻烦了,要考虑的太多了,只能给有需要的朋友一点点意见吧。

项目准备

首先需要安装必要的NuGet包。在Visual Studio中,通过NuGet包管理器安装:

C#
OPCFoundation.NetStandard.Opc.Ua

完整代码示例

Program.cs

C#
class Program { static async Task Main(string[] args) { MyServer server = new MyServer(); await server.Start(); Console.WriteLine("OPC UA Server started. Press Enter to exit."); Console.ReadLine(); server.Stop(); } }

MyServer.cs

C#
public class MyServer { private ApplicationInstance application; private StandardServer server; private MyNodeManager nodeManager; public async Task Start() { try { // 步骤1:创建应用程序配置 application = new ApplicationInstance { ApplicationName = "My OPC UA Server", ApplicationType = ApplicationType.Server, ConfigSectionName = "MyOpcUaServer" }; // 加载应用程序配置 var config = await LoadApplicationConfiguration(); // 步骤2:检查证书 await application.CheckApplicationInstanceCertificate(false, 2048); // 步骤3:创建自定义服务器实例 server = new CustomStandardServer(config); // 步骤4:启动服务器 await application.Start(server); Console.WriteLine( $"Server started at: {config.ServerConfiguration.BaseAddresses[0]}"); } catch (Exception ex) { Console.WriteLine($"Error starting server: {ex.Message}"); throw; } } public void Stop() { if (server != null) { server.Stop(); } } private async Task<ApplicationConfiguration> LoadApplicationConfiguration() { var config = new ApplicationConfiguration() { ApplicationName = "My OPC UA Server", ApplicationUri = "urn:MyOpcUaServer", ApplicationType = ApplicationType.Server, SecurityConfiguration = new SecurityConfiguration { ApplicationCertificate = new CertificateIdentifier(), TrustedPeerCertificates = new CertificateTrustList(), TrustedIssuerCertificates = new CertificateTrustList(), RejectedCertificateStore = new CertificateTrustList(), AutoAcceptUntrustedCertificates = true }, TransportConfigurations = new TransportConfigurationCollection(), TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }, ServerConfiguration = new ServerConfiguration { BaseAddresses = new StringCollection { "opc.tcp://localhost:4840/MyOpcUaServer" }, MinRequestThreadCount = 5, MaxRequestThreadCount = 100, MaxQueuedRequestCount = 200, }, TraceConfiguration = new TraceConfiguration() }; await config.Validate(ApplicationType.Server); return config; } } // 自定义服务器类,用于初始化节点管理器 public class CustomStandardServer : StandardServer { private ApplicationConfiguration configuration; public CustomStandardServer(ApplicationConfiguration configuration) { this.configuration = configuration; } protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration { List<INodeManager> nodeManagers = new List<INodeManager>(); // 创建自定义节点管理器并添加到列表 nodeManagers.Add(new MyNodeManager(server, configuration)); // 创建主节点管理器 return new MasterNodeManager(server, configuration, null, nodeManagers.ToArray()); } }

MyNodeManager.cs

C#
public class MyNodeManager : CustomNodeManager2 { private IServerInternal server; private ApplicationConfiguration configuration; public MyNodeManager(IServerInternal server, ApplicationConfiguration configuration) : base(server, configuration, "http://myopcuaserver.com") { this.server = server; this.configuration = configuration; SystemContext.NodeIdFactory = this; } public override void CreateAddressSpace(IDictionary<NodeId, IList<IReference>> externalReferences) { lock (Lock) { IList<IReference> references = null; if (!externalReferences.TryGetValue(ObjectIds.ObjectsFolder, out references)) { references = new List<IReference>(); externalReferences[ObjectIds.ObjectsFolder] = references; } // 创建 MyDevices 文件夹 FolderState myDevicesFolder = CreateFolder(null, "MyDevices", "设备"); // 将文件夹组织到 Objects 文件夹下 myDevicesFolder.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder); references.Add(new NodeStateReference(ReferenceTypes.Organizes, false, myDevicesFolder.NodeId)); // 设置文件夹属性 myDevicesFolder.EventNotifier = EventNotifiers.SubscribeToEvents; AddPredefinedNode(SystemContext, myDevicesFolder); // 创建温度变量 BaseDataVariableState temperatureVariable = CreateVariable( myDevicesFolder, "Temperature", "温度", DataTypeIds.Double, ValueRanks.Scalar); // 设置温度变量的属性 temperatureVariable.Value = 25.0; temperatureVariable.AccessLevel = AccessLevels.CurrentRead | AccessLevels.CurrentWrite; temperatureVariable.UserAccessLevel = AccessLevels.CurrentRead | AccessLevels.CurrentWrite; temperatureVariable.Historizing = false; temperatureVariable.StatusCode = StatusCodes.Good; temperatureVariable.Timestamp = DateTime.Now; // 添加工程单位 AddEngineeringUnit(temperatureVariable, "°C", "摄氏度"); // 添加描述 temperatureVariable.Description = new LocalizedText("当前温度值"); // 添加范围 AddAnalogRange(temperatureVariable, -100.0, 100.0); AddPredefinedNode(SystemContext, temperatureVariable); // 创建湿度变量 BaseDataVariableState humidityVariable = CreateVariable( myDevicesFolder, "Humidity", "湿度", DataTypeIds.Double, ValueRanks.Scalar); // 设置湿度变量的属性 humidityVariable.Value = 60.0; humidityVariable.AccessLevel = AccessLevels.CurrentRead | AccessLevels.CurrentWrite; humidityVariable.UserAccessLevel = AccessLevels.CurrentRead | AccessLevels.CurrentWrite; humidityVariable.Historizing = false; humidityVariable.StatusCode = StatusCodes.Good; humidityVariable.Timestamp = DateTime.Now; // 添加工程单位 AddEngineeringUnit(humidityVariable, "%RH", "相对湿度"); // 添加描述 humidityVariable.Description = new LocalizedText("当前湿度值"); // 添加范围 AddAnalogRange(humidityVariable, 0.0, 100.0); AddPredefinedNode(SystemContext, humidityVariable); } } private FolderState CreateFolder(NodeState parent, string name, string displayName) { FolderState folder = new FolderState(parent); folder.SymbolicName = name; folder.ReferenceTypeId = ReferenceTypes.Organizes; folder.TypeDefinitionId = ObjectTypeIds.FolderType; folder.NodeId = new NodeId(name, NamespaceIndex); folder.BrowseName = new QualifiedName(name, NamespaceIndex); folder.DisplayName = new LocalizedText(displayName); folder.WriteMask = AttributeWriteMask.None; folder.UserWriteMask = AttributeWriteMask.None; folder.EventNotifier = EventNotifiers.None; if (parent != null) { parent.AddChild(folder); } return folder; } private BaseDataVariableState CreateVariable( NodeState parent, string name, string displayName, NodeId dataType, int valueRank) { BaseDataVariableState variable = new BaseDataVariableState(parent); variable.SymbolicName = name; variable.ReferenceTypeId = ReferenceTypes.Organizes; variable.TypeDefinitionId = VariableTypeIds.BaseDataVariableType; variable.NodeId = new NodeId(name, NamespaceIndex); variable.BrowseName = new QualifiedName(name, NamespaceIndex); variable.DisplayName = new LocalizedText(displayName); variable.WriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description; variable.UserWriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description; variable.DataType = dataType; variable.ValueRank = valueRank; variable.AccessLevel = AccessLevels.CurrentRead | AccessLevels.CurrentWrite; variable.UserAccessLevel = AccessLevels.CurrentRead | AccessLevels.CurrentWrite; variable.MinimumSamplingInterval = 100; if (parent != null) { parent.AddChild(variable); } return variable; } private void AddEngineeringUnit(BaseVariableState variable, string unitId, string displayName) { ExtensionObject engineeringUnit = new ExtensionObject( new EUInformation( unitId, // UnitId displayName, // DisplayName string.Empty // Description )); PropertyState property = new PropertyState(variable); property.ReferenceTypeId = ReferenceTypes.HasProperty; property.TypeDefinitionId = VariableTypeIds.PropertyType; property.NodeId = new NodeId(variable.SymbolicName + "_EURange", NamespaceIndex); property.BrowseName = BrowseNames.EngineeringUnits; property.DisplayName = BrowseNames.EngineeringUnits; property.DataType = DataTypeIds.EUInformation; property.ValueRank = ValueRanks.Scalar; property.AccessLevel = AccessLevels.CurrentRead; property.UserAccessLevel = AccessLevels.CurrentRead; property.Value = engineeringUnit; variable.AddChild(property); } private void AddAnalogRange(BaseDataVariableState variable, double min, double max) { Range range = new Range(min, max); ExtensionObject extensionObject = new ExtensionObject(range); PropertyState property = new PropertyState(variable); property.ReferenceTypeId = ReferenceTypes.HasProperty; property.TypeDefinitionId = VariableTypeIds.PropertyType; property.NodeId = new NodeId(variable.SymbolicName + "_Range", NamespaceIndex); property.BrowseName = BrowseNames.EURange; property.DisplayName = BrowseNames.EURange; property.DataType = DataTypeIds.Range; property.ValueRank = ValueRanks.Scalar; property.AccessLevel = AccessLevels.CurrentRead; property.UserAccessLevel = AccessLevels.CurrentRead; property.Value = extensionObject; variable.AddChild(property); } protected override NodeStateCollection LoadPredefinedNodes(ISystemContext context) { NodeStateCollection predefinedNodes = new NodeStateCollection(); return predefinedNodes; } }

image.png

使用UaExpert 简单测试

image.png

3. 注意事项

  1. 本示例使用了自签名证书,生产环境中应使用正式的安全证书
  2. 为了简化示例,启用了自动接受不受信任的证书(AutoAcceptUntrustedCertificates = true)
  3. 生产环境中应该添加适当的错误处理和日志记录
  4. 可以根据需要添加更多的节点和功能

结论

这个示例提供了一个基础但完整的OPC UA服务器实现。它可以作为开发更复杂OPC UA应用程序的起点。根据具体需求,你可以扩展功能,添加更多的节点类型,实现数据持久化等高级特性。

本文作者:rick

本文链接:https://www.idiosoft.com/post/196

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

评论
  • 按正序
  • 按倒序
  • 按热度
来发评论吧~
Powered by Waline v2.14.8