OPCUA探讨(四)——客户端代码解读2
本系列文章:
OPCUA 探讨(一)——测试与开发环境搭建
OPCUA 探讨(二)——服务器节点初探
OPCUA 探讨(三)——客户端代码解读
OPCUA 探讨(四)——客户端代码解读2
前文中我们探讨了OPCUA客户端应用的基本配置,以及如何与OPCUA服务器建立会话(Session)。
OPCUA探讨(三)——客户端代码解读1
该项目源码地址:https://gitee.com/zuoquangong/opcuaapi
本文我们将讨论如何实现服务器节点浏览(Browse)功能。
一、浏览(Browse)
1.1 节点(Node)与引用(Reference)
首先我们要了解OPCUA服务器上的内容是怎样组织的。下图是一个简化示例:
可以看到这个示例展示的是一个树状结构。
节点(Node) 是基本单位,节点内部包含了多个属性,不同节点内部属性数目因 节点类(NodeClass)不同而有所差异。
引用(Reference)理解为节点之间的关系,用图的角度来说是“边”。引用也分多个种类,例如我们在Prosys的OPCUA服务器上可以看到:
引用的不同类型标志着节点之间的不同关系(HasTypeDefinition、Organizes等)。可以暂不关心这些引用具体有哪些类型,因为不影响我们浏览。想详细了解引用的定义和类型,请查看官方文档标准引用类型(Standard Reference Types)
1.2 浏览函数
使用浏览函数,则需了解要传哪些参数。
浏览函数的参数:
点击查看代码
public virtual ResponseHeader Browse( RequestHeader requestHeader, //*客户端请求报文的头部,可为null ViewDescription view, //*所浏览的视图的ID,可为null NodeId nodeToBrowse, //所浏览的节点ID uint maxResultsToReturn, //最大返回结果数目 BrowseDirection browseDirection, //浏览方向(正向浏览子节点/反向浏览父节点) NodeId referenceTypeId, //浏览的引用类型ID,即要浏览哪类引用 bool includeSubtypes, //是否包括子类型 uint nodeClassMask, //对查询结果的节点类进行筛选 out byte[] continuationPoint, //接续点,由于OPCUA报文长度限制,服务器可能不会一次返回所有浏览结果(太长),通过接续点再次发送请求可以继续获取剩余的浏览结果(服务器返回) out ReferenceDescriptionCollection references //返回浏览到的引用信息集合(服务器返回) )
点击查看代码
/// <summary> /// 浏览节点 /// 通过nodeId遍历其子节点(根节点nodeId="") /// 反向获取父节点时inverse=true /// </summary> /// <param name="nodeId">想要浏览的节点的ID</param> /// <param name="inverse">是否反向浏览</param> /// <returns></returns> public ReferenceDescriptionCollection Browse(string nodeId="",bool inverse=false) { if (current_session == null) //浏览功能建立在会话的基础上 return null; if (current_session.Connected == false)//浏览功能建立在会话连接的基础上 return null; ReferenceDescriptionCollection referenceDescriptionCollection; //当前获取到的子节点的信息 byte[] continuationPoint; //接续点。由于OPCUA报文长度限制,服务器可能不会一次返回所有浏览结果(太长),通过接续点再次发送请求可以继续获取剩余的浏览结果 if (nodeId == "") //nodeId为空时,默认浏览根节点 { current_session.Browse( null, //requestHeader null, //ViewDescription view ObjectIds.RootFolder, //NodeId-根目录 0u, //maxResultToReturn BrowseDirection.Forward, //正向-遍历子节点(反向Inverse-回溯父节点) ReferenceTypeIds.HierarchicalReferences, //NodeId ReferenceTypeId true, //includeSubtypes (uint)NodeClass.Variable | (uint)NodeClass.Object | (uint)NodeClass.Method, //nodeClassMask out continuationPoint, //byte[] continuationPoint(用于获取后续结果) out referenceDescriptionCollection ); //references } else if(inverse == true) //反向查询父节点 { //MessageBox.Show(nodeId); current_session.Browse(null, null, nodeId, 1, //仅返回一个引用(父节点就一个) BrowseDirection.Inverse, //反向查询标志 ReferenceTypeIds.HierarchicalReferences, true, 0, out continuationPoint, out referenceDescriptionCollection); } else //正向查询子节点 { ReferenceDescriptionCollection nextreferenceDescriptionCollection; //当前获取到的子节点的信息 byte[] revisedContinuationPoint; //接续点,用于获取后续结果 current_session.Browse( null, null, nodeId, 0u, BrowseDirection.Forward, ReferenceTypeIds.HierarchicalReferences, true, 0,//不进行nodeClass筛选 out continuationPoint, out referenceDescriptionCollection ); while (continuationPoint != null)//循着起始点遍历全部子节点 { current_session.BrowseNext(null, false, continuationPoint, out revisedContinuationPoint, out nextreferenceDescriptionCollection);//继续浏览下一个节点,返回的nextreferenceDescriptionCollection为所需信息 referenceDescriptionCollection.AddRange(nextreferenceDescriptionCollection); continuationPoint = revisedContinuationPoint;//更新 } } return referenceDescriptionCollection; }
1.3 浏览结果的解析
可以看到浏览(Browse)函数返回的是一个类型为ReferenceDescriptionCollection
的结果,即ReferenceDescription
的集合。
一个ReferenceDescription
对象可包含以下信息(成员):
通过其中的NodeId
可以获取更详细的节点信息。
二、获取某个节点的详细信息
已知以下两个函数的功能:
1.调用ReadNode
函数可以获取除了节点值之外的全部属性信息。
2.调用ReadValue
函数可以获得节点值。
因此,通过调用上述两个函数可以获得节点所有属性信息。以下为示例:
点击查看代码
/// <summary> /// 获取某个节点的详细信息 /// </summary> /// <param name="refDesc">节点描述信息,用于读取详细信息</param> /// <returns></returns> public object[] GetNodeInfo(ReferenceDescription refDesc) { if(refDesc==null) { return null; } object[] rows=null; Node node = current_session.ReadNode(refDesc.NodeId.ToString()); //节点值需要单独获取,使用ReadValue函数 string value = this.ReadValue(refDesc.NodeId.ToString()); try { VariableNode variableNode = new VariableNode(); string[] rowx = new string[] { "节点值", value }; string[] row1 = new string[] { "节点类", refDesc.NodeClass.ToString() }; string[] row2 = new string[] { "节点ID", refDesc.NodeId.ToString() }; string[] row3 = new string[] { "命名空间索引", refDesc.NodeId.NamespaceIndex.ToString() }; string[] row4 = new string[] { "Identifier Type", refDesc.NodeId.IdType.ToString() }; string[] row5 = new string[] { "Identifier", refDesc.NodeId.Identifier.ToString() }; string[] row6 = new string[] { "浏览名称", refDesc.BrowseName.ToString() }; string[] row7 = new string[] { "显示名称", refDesc.DisplayName.ToString() }; string[] row8 = new string[] { "描述", "null" }; if (node.Description != null) { try { row8 = new string[] { "Description", node.Description.ToString() }; } catch { row8 = new string[] { "Description", "null" }; } } string typeDefinition = ""; if ((NodeId)refDesc.TypeDefinition.NamespaceIndex == 0) { typeDefinition = refDesc.TypeDefinition.ToString(); } else { typeDefinition = "Struct/UDT: " + refDesc.TypeDefinition.ToString(); //类型为结构体或数据表 } string[] row9 = new string[] { "类型定义", typeDefinition }; string[] row10 = new string[] { "Write Mask", node.WriteMask.ToString() }; string[] row11 = new string[] { "User Write Mask", node.UserWriteMask.ToString() }; if (node.NodeClass == NodeClass.Variable) { variableNode = (VariableNode)node.DataLock; List<NodeId> nodeIds = new List<NodeId>(); IList<string> displayNames = new List<string>(); IList<ServiceResult> errors = new List<ServiceResult>(); NodeId nodeId = new NodeId(variableNode.DataType); nodeIds.Add(nodeId); current_session.ReadDisplayName(nodeIds, out displayNames, out errors); int valueRank = variableNode.ValueRank; List<string> arrayDimension = new List<string>(); string[] row12 = new string[] { "数据类型", displayNames[0] }; string[] row13 = new string[] { "Value Rank", valueRank.ToString() }; //Define array dimensions depending on the value rank if (valueRank > 0) //More dimensional arrays { for (int i = 0; i < valueRank; i++) { arrayDimension.Add(variableNode.ArrayDimensions.ElementAtOrDefault(i).ToString()); } } else { arrayDimension.Add("标量"); } string[] row14 = new string[] { "数组维数", String.Join(";", arrayDimension.ToArray()) }; string[] row15 = new string[] { "访问级别", variableNode.AccessLevel.ToString() }; string[] row16 = new string[] { "最小采样间隔", variableNode.MinimumSamplingInterval.ToString() }; string[] row17 = new string[] { "历史化", variableNode.Historizing.ToString() }; rows = new object[] { rowx, row1, row2, row3, row4, row5, row6, row7, row8, row9, row10, row11, row12, row13, row14, row15, row16, row17 }; } else { rows = new object[] { rowx, row1, row2, row3, row4, row5, row6, row7, row8, row9, row10, row11 }; } //MessageBox.Show(refDesc.ToString()); } catch (Exception ex) { MessageBox.Show(ex.Message, "Error"); } return rows; } /// <summary> /// 读取节点的值(Value) /// 根据节点ID,读取节点变量值 /// </summary> /// <param name="nodeIdString"></param> /// <returns></returns> public string ReadValue(string nodeIdString) { NodeId nodeId=new NodeId(nodeIdString); //MessageBox.Show(nodeIdString); try { var res = current_session.ReadValue(nodeId); if (res != null) { if (res.Value != null) { return res.Value.ToString(); } } } catch(Exception ex) { return "null"; } return ""; }
附录
OPCUA官方文档——地址空间
.NET Based OPC UA Client/Server/PubSub SDK 4.0.2.550
总结
本文介绍了如何浏览OPCUA服务器上的节点以及如何获取节点详细信息。
*附言
由于作者水平有限,可能在文章中出现错误或不当描述,如有发现此类情况希望您能及时提供反馈,非常感谢!
如果感觉本文对您有所帮助,希望为文章点个推荐,谢谢。
作者联系方式,163邮箱:zuoquangong@163.com
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端
· 10亿数据,如何做迁移?
· 推荐几款开源且免费的 .NET MAUI 组件库
· c# 半导体/led行业 晶圆片WaferMap实现 map图实现入门篇