如何用C# OPC UA Client读写服务器自定义对象并映射至C#类
一次性读取OPC UA对象节点并映射到C#自定义类
问题背景
拥有一台搭载OPC UA Server的西门子PLC,已通过OPC Foundation ReferenceClient定位到名为Data的对象节点。当前仅能逐个读取其下子节点(如pressure),尝试直接读取Data对象节点时触发错误:Opc.Ua.ServiceResultException: 'BadAttributeIdInvalid',需求是一次性读取整个Data对象并映射到自定义C#类DataClass。
错误原因
OPC UA的对象节点(Object Node)本身没有Value属性,session.ReadValue()方法默认请求读取节点的Value属性(AttributeId=13),因此直接读取对象节点会返回属性ID无效的错误。
解决方案
要实现批量读取并映射,需分三步操作:
- 遍历
Data对象节点的所有子变量节点,收集它们的NodeId - 批量读取这些变量节点的Value值
- 将读取结果映射到自定义C#类实例中
步骤1:遍历对象节点的子变量节点
编写方法获取指定对象下的所有变量节点(包含嵌套节点,比如Startup_Time下的子节点):
using Opc.Ua; using Opc.Ua.Client; using System.Collections.Generic; private List<NodeId> GetAllVariableNodeIds(Session session, NodeId parentNodeId) { var variableNodeIds = new List<NodeId>(); var browseDesc = new BrowseDescription { NodeId = parentNodeId, BrowseDirection = BrowseDirection.Forward, ReferenceTypeId = ReferenceTypeIds.HasComponent, IncludeSubtypes = true, NodeClassMask = (uint)NodeClass.Variable | (uint)NodeClass.Object, ResultMask = (uint)BrowseResultMask.NodeId | (uint)BrowseResultMask.NodeClass }; var browseResult = session.Browse(null, null, 0, browseDesc); foreach (var reference in browseResult.References) { if (reference.NodeClass == NodeClass.Variable) { variableNodeIds.Add(reference.NodeId); } else if (reference.NodeClass == NodeClass.Object) { // 递归遍历嵌套对象节点 variableNodeIds.AddRange(GetAllVariableNodeIds(session, reference.NodeId)); } } return variableNodeIds; }
步骤2:批量读取变量节点值
使用session.Read()方法批量读取所有收集到的节点值:
var dataNodeId = new NodeId("ns=4;i=2"); var variableNodeIds = GetAllVariableNodeIds(session, dataNodeId); // 构造读取请求 var readValues = new ReadValueIdCollection(); foreach (var nodeId in variableNodeIds) { readValues.Add(new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }); } // 执行批量读取 DataValueCollection results; DiagnosticInfoCollection diagnosticInfos; session.Read(null, 0, TimestampsToReturn.Both, readValues, out results, out diagnosticInfos);
步骤3:映射到自定义C#类
将读取结果映射到DataClass,注意处理嵌套的Startup_Time类型:
public DataClass MapToDataClass(DataValueCollection results, List<NodeId> nodeIds) { var data = new DataClass(); var nodeValueMap = new Dictionary<NodeId, object>(); // 先把所有节点值存入字典,方便查找 for (int i = 0; i < results.Count; i++) { nodeValueMap[nodeIds[i]] = results[i].GetValue(results[i].WrappedValue.TypeInfo); } // 替换为实际通过ReferenceClient查询到的各字段对应NodeId data.pressure = (short)nodeValueMap[new NodeId("ns=4;i=17")]; data.Startup_Counter = (short)nodeValueMap[new NodeId("ns=4;i=18")]; // 处理嵌套的Startup_Time var startupTime = new Startup_Time(); startupTime.DAY = (byte)nodeValueMap[new NodeId("ns=4;i=19")]; startupTime.HOUR = (byte)nodeValueMap[new NodeId("ns=4;i=20")]; startupTime.MINUTE = (byte)nodeValueMap[new NodeId("ns=4;i=21")]; startupTime.MONTH = (byte)nodeValueMap[new NodeId("ns=4;i=22")]; startupTime.NANOSECOND = (uint)nodeValueMap[new NodeId("ns=4;i=23")]; startupTime.SECOND = (byte)nodeValueMap[new NodeId("ns=4;i=24")]; startupTime.WEEKDAY = (byte)nodeValueMap[new NodeId("ns=4;i=25")]; startupTime.YEAR = (ushort)nodeValueMap[new NodeId("ns=4;i=26")]; data.Startup_Time = startupTime; data.temperature = (short)nodeValueMap[new NodeId("ns=4;i=27")]; data.Value1 = (short)nodeValueMap[new NodeId("ns=4;i=28")]; return data; }
完整调用示例
// 假设session已成功建立并连接 var dataNodeId = new NodeId("ns=4;i=2"); var variableNodeIds = GetAllVariableNodeIds(session, dataNodeId); DataValueCollection results; DiagnosticInfoCollection diagnosticInfos; session.Read(null, 0, TimestampsToReturn.Both, variableNodeIds.Select(n => new ReadValueId { NodeId = n, AttributeId = Attributes.Value }).ToList(), out results, out diagnosticInfos); DataClass data = MapToDataClass(results, variableNodeIds);
优化建议
- 提前将各字段对应的NodeId存储在配置文件中,避免硬编码
- 对于复杂类型映射,可以使用反射实现自动化映射,减少重复代码
- 批量读取时注意节点数量限制(部分OPC UA服务器对单次读取的节点数有上限)
内容的提问来源于stack exchange,提问作者Caleb McKay
相关产品推荐
相关产品推荐

