From fe42bedf56013c6ced57b8073d0fb3f9c5a7ce43 Mon Sep 17 00:00:00 2001 From: zhaoxi <535394140@qq.com> Date: Sat, 6 Jul 2024 09:43:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=8D=E5=8D=A0=E6=9C=89?= =?UTF-8?q?=E6=89=80=E6=9C=89=E6=9D=83=E7=9A=84=20ServerView?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmake/RMVLCompilerOptions.cmake | 24 ++++++ cmake/templates/cmake_uninstall.cmake.in | 6 +- doc/tutorials/modules/tools/opcua.md | 64 ++++++++++----- modules/opcua/include/rmvl/opcua/client.hpp | 26 +++--- modules/opcua/include/rmvl/opcua/server.hpp | 85 ++++++++++++++++---- modules/opcua/src/client.cpp | 31 ++++--- modules/opcua/src/helper.cpp | 2 +- modules/opcua/src/server.cpp | 84 ++++++++++--------- modules/opcua/test/test_opcua_server.cpp | 46 ++++------- modules/opcua/test/test_opcua_serverview.cpp | 65 +++++++++++++++ 10 files changed, 297 insertions(+), 136 deletions(-) create mode 100644 modules/opcua/test/test_opcua_serverview.cpp diff --git a/cmake/RMVLCompilerOptions.cmake b/cmake/RMVLCompilerOptions.cmake index 476ebbd1..2ce3f738 100755 --- a/cmake/RMVLCompilerOptions.cmake +++ b/cmake/RMVLCompilerOptions.cmake @@ -100,6 +100,30 @@ if(ENABLE_PIC) set(CMAKE_POSITION_INDEPENDENT_CODE ${ENABLE_PIC}) endif() +option(ENABLE_LTO "Enable Link Time Optimization" OFF) +if(ENABLE_LTO) + include(CheckCXXCompilerFlag) + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + check_cxx_compiler_flag("-flto=auto" HAS_LTO_AUTO_FLAG) + if(HAS_LTO_AUTO_FLAG) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto=auto") + else() + check_cxx_compiler_flag("-flto" HAS_LTO_FLAG) + if(HAS_LTO_FLAG) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto") + endif() + endif() + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + check_cxx_compiler_flag("/GL" COMPILER_SUPPORTS_LTO) + if(COMPILER_SUPPORTS_LTO) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /GL") + set(CMAKE_STATIC_LINKER_FLAGS "${CMAKE_STATIC_LINKER_FLAGS} /LTCG") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /LTCG") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /LTCG") + endif() + endif() +endif() + # ---------------------------------------------------------------------------- # Develop options # ---------------------------------------------------------------------------- diff --git a/cmake/templates/cmake_uninstall.cmake.in b/cmake/templates/cmake_uninstall.cmake.in index c84027a6..c693259c 100644 --- a/cmake/templates/cmake_uninstall.cmake.in +++ b/cmake/templates/cmake_uninstall.cmake.in @@ -14,10 +14,10 @@ string(REGEX REPLACE "\n" ";" files "${files}") foreach(file ${files}) message(STATUS "Uninstalling: $ENV{DESTDIR}${file}") if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") - exec_program( - "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" + execute_process( + "@CMAKE_COMMAND@" "-E" "remove" "$ENV{DESTDIR}${file}" OUTPUT_VARIABLE rm_out - RETURN_VALUE rm_retval + RESULT_VARIABLE rm_retval ) if(NOT "${rm_retval}" STREQUAL 0) message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}") diff --git a/doc/tutorials/modules/tools/opcua.md b/doc/tutorials/modules/tools/opcua.md index a3406556..2e802103 100644 --- a/doc/tutorials/modules/tools/opcua.md +++ b/doc/tutorials/modules/tools/opcua.md @@ -16,9 +16,9 @@ 相关模块: @ref opcua -### 简介 +## 1. 简介 -#### OPC UA 是什么 +### 1.1 OPC UA 是什么 [OPC UA](https://opcfoundation.org/about/opc-technologies/opc-ua/)(全称为 Open Platform Communications Unified Architecture)是一种用于工业和物联网(IoT)应用的开放通信协议和架构。它提供了一种统一的框架,用于在不同设备和系统之间实现数据传输、通信和集成。 @@ -32,7 +32,7 @@ OPC UA 的设计目标是建立一种通用的、独立于厂商和平台的通 | 信息建模 | OPC UA 使用统一的信息模型,将数据和功能以标准化的方式表示和描述,使不同系统之间的数据交换更加简化和一致 | | 可靠性 | OPC UA 提供了可靠的通信机制,包括消息确认、重试和错误处理,以确保数据的可靠传输 | -#### 地址空间 +### 1.2 地址空间 在 OPC UA 中,所有的数据都被组织成一个地址空间,地址空间中的每一个元素都被称为一个节点。每个节点都有一个唯一的节点号,在 @ref opcua 中表示为 rm::NodeId 。 @@ -63,11 +63,11 @@ OPC UA 的设计目标是建立一种通用的、独立于厂商和平台的通 8. 视图节点 rm::View :视图节点可将地址空间中感兴趣的节点提取出来,作为一个子集,视图节点作为该子集的入口,方便客户端浏览。 -### 服务器/客户端 {#opcua_server_client} +## 2. 服务器/客户端 {#opcua_server_client} 基于服务器/客户端的方式是 OPC UA 最基本的一种通信方式,上文的地址空间在服务器/客户端通信的过程中完全展现出来。下面列举一些 opcua 模块中常用的服务器与客户端的成员方法。 -#### 初始化 +### 2.1 初始化 **服务器** @@ -104,7 +104,7 @@ int main() } ``` -#### 变量 +### 2.2 变量 在上文介绍了变量的 3 种访问方式,这里使用最简单的直接读写的方式。首先在服务器中添加变量节点。 @@ -158,7 +158,7 @@ int main() } ``` -#### 方法 +### 2.3 方法 在服务器中添加两数之和的方法节点,供客户端调用。 @@ -227,7 +227,7 @@ int main() } ``` -#### 对象 +### 2.4 对象 在服务器中添加对象节点: @@ -302,7 +302,7 @@ int main() } ``` -#### 视图 +### 2.5 视图 在 `nodeObjectsFolder` 中先添加 `A/num1`、`num2` 2 个变量节点,并将 `num1` 和 `num2` 加入视图,下面的示例演示在 **服务器** 中创建并添加视图节点。若要在客户端中进行此操作,创建并添加视图节点的步骤基本一致,这里不做展示。需要注意的是,在客户端中创建并添加视图节点,需要提前在服务器中加入对应的(变量、方法、对象……)节点 @@ -336,7 +336,7 @@ int main() } ``` -#### 监视 +### 2.6 监视 在服务器中添加待监视的变量节点 @@ -407,7 +407,7 @@ int main() } ``` -### 发布/订阅 {#opcua_pub_sub} +## 3. 发布/订阅 {#opcua_pub_sub} 这是一段来自 [open62541 手册](https://www.open62541.org)中有关 PubSub 的介绍。 @@ -437,7 +437,7 @@ int main() 有关 API 使用的更多详细信息,请查看 [PubSub 教程](https://www.open62541.org/doc/master/pubsub.html)。 -#### 无代理 Pub/Sub +### 3.1 无代理 Pub/Sub RMVL 提供了基于 `UDP` 传输协议的 Broker-less 即无代理的发布订阅机制,目前支持 `UADP` 的消息映射方式,对应的枚举类型是 `TransportID::UDP_UADP`。 @@ -513,11 +513,15 @@ int main() } ``` -#### 有代理 Pub/Sub +### 3.2 有代理 Pub/Sub @warning RMVL 目前暂不支持有代理的发布订阅机制。 -### 参数加载 {#opcua_parameters} +## 4. 使用技巧 + +以下是 @ref opcua 的使用技巧。 + +### 4.1 参数加载 {#opcua_parameters} @ref opcua 中提供了以下几个运行时可调节参数 @@ -533,9 +537,9 @@ int main() 具体调节方式可参考引言中的 @ref intro_parameters_manager 部分。 -### 从 XML 配置 OPC UA {#opcua_nodeset_compiler} +### 4.2 从 XML 配置 OPC UA {#opcua_nodeset_compiler} -#### 安装 UaModeler +#### 4.2.1 安装 UaModeler 可使用 UaModeler 等软件进行可视化信息模型的建立,构建后可以导出为一个 `*.xml` 文件,首先先安装 UaModeler。 @@ -555,7 +559,7 @@ int main() 具体安装细节可参考 [opcua-modeler on Github](https://github.com/FreeOpcUa/opcua-modeler) 的 README。 -#### 可视化配置 OPC UA 信息模型 +#### 4.2.2 可视化配置 OPC UA 信息模型 对于项目创建或导出等内容,此处不做过多介绍,可参考[此博客](https://wanghao1314.blog.csdn.net/article/details/104092781)了解上述内容。 @@ -563,7 +567,7 @@ int main() - 一般的,定义对象、变量、方法等内容均按照在代码中的顺序进行定义即可,但需要注意的是,添加了方法节点后,还需要在代码中设置该方法节点执行的回调函数,可参见 `rm::Server::setMethodNodeCallBack`。 - `NamespaceArray` 的 `[1]` 的字符串需要更改为 `urn:open62541.server.application` -#### 生成 \*.c/\*.h 文件 +#### 4.2.3 生成 \*.c/\*.h 文件 @note 以下生成 C/C++ 文件的介绍来自 [open62541 nodeset-compiler](https://www.open62541.org/doc/master/nodeset_compiler.html#getting-started)。 @@ -582,8 +586,30 @@ python3 ./nodeset_compiler.py \ myNodeSet # myNodeSet 是要生成的文件名,包含 myNodeSet.h 和 myNodeSet.c,请自行设置 ``` +### 4.3 不占有所有权的服务器视图 + +`rm::Server` 使用 RAII 进行设计,一个对象占有了服务器的所有权和生命周期,当对象析构时,会自动停止并结束服务器。使用 `rm::ServerView` 来获取不占有所有权的服务器视图,并进行变量读写、路径搜索的操作,下面用服务器视图的单元测试作为示例。 + +```cpp +rm::Method method; +method.browse_name = "plus"; +method.display_name = "Input + Number"; +method.description = "输入值加数"; +method.func = [](UA_Server *p_server, const UA_NodeId *, void *, const UA_NodeId *, void *, const UA_NodeId *, + void *, size_t, const UA_Variant *inputs, size_t, UA_Variant *) -> UA_StatusCode { + rm::ServerView sv = p_server; + auto num_node = nodeObjectsFolder | sv.find("num"); + int num = sv.read(num_node).cast(); + rm::Variable dst = *reinterpret_cast(inputs->data) + num; + sv.write(num_node, dst); + return UA_STATUSCODE_GOOD; +}; +method.iargs = {{"input", UA_TYPES_INT32, 1, "输入值"}}; +svr.addMethodNode(method); +``` + --- -### 引用 +## 5. 引用 @cite ua-modeler UaModeler · FreeOpcUa/opcua-modeler · Github \ No newline at end of file diff --git a/modules/opcua/include/rmvl/opcua/client.hpp b/modules/opcua/include/rmvl/opcua/client.hpp index a2f5b844..6d066b06 100644 --- a/modules/opcua/include/rmvl/opcua/client.hpp +++ b/modules/opcua/include/rmvl/opcua/client.hpp @@ -59,7 +59,7 @@ class Client * @return 目标节点信息 * @retval fnic `[_client, browse_name]` 元组 */ - inline FindNodeInClient find(const std::string &browse_name, uint16_t ns = 1U) { return {_client, browse_name, ns}; } + inline FindNodeInClient find(const std::string &browse_name, uint16_t ns = 1U) const { return {_client, browse_name, ns}; } /****************************** 功能配置 ******************************/ @@ -71,14 +71,14 @@ class Client * @brief * - 执行事件循环,等效于 ROS/ROS2 工具包中的 `ros::spin()` 以及 `rclcpp::spin()` */ - void spin(); + void spin() const; /** * @brief 在网络上监听并处理到达的异步响应,同时进行内部维护、安全通道的更新和订阅管理 * @brief * - 处理当前已到来的事件,等效于 ROS/ROS2 工具包中的 `ros::spinOnce()` 以及 `rclcpp::spin_some()` */ - void spinOnce(); + void spinOnce() const; /** * @brief 从指定的变量节点读数据 @@ -86,7 +86,7 @@ class Client * @param[in] node 既存的变量节点的 `NodeId` * @return 读出的用 `rm::Variable` 表示的数据,未成功读取则返回空 */ - Variable read(const NodeId &node); + Variable read(const NodeId &node) const; /** * @brief 给指定的变量节点写数据 @@ -95,7 +95,7 @@ class Client * @param[in] val 待写入的数据 * @return 是否写入成功 */ - bool write(const NodeId &node, const Variable &val); + bool write(const NodeId &node, const Variable &val) const; /** * @brief 在客户端调用指定对象节点中的方法 @@ -106,25 +106,25 @@ class Client * @param[out] outputs 输出参数列表 * @return 是否成功完成当前操作 */ - bool call(const NodeId &obj_node, const std::string &name, const std::vector &inputs, std::vector &outputs); + bool call(const NodeId &obj_node, const std::string &name, const std::vector &inputs, std::vector &outputs) const; /** * @brief 在客户端调用 ObjectsFolder 中的方法 * - * @param[in] name 方法名 + * @param[in] name 方法名 `browse_name` * @param[in] inputs 输入参数列表 * @param[out] outputs 输出参数列表 * @return 是否成功完成当前操作 */ - inline bool call(const std::string &name, const std::vector &inputs, std::vector &outputs) { return call(nodeObjectsFolder, name, inputs, outputs); } + inline bool call(const std::string &name, const std::vector &inputs, std::vector &outputs) const { return call(nodeObjectsFolder, name, inputs, outputs); } /** - * @brief 添加视图节点 ViewNode 至 `ViewsFolder` 中 + * @brief 添加 OPC UA 视图节点 ViewNode 至 `ViewsFolder` 中 * * @param[in] view `rm::View` 表示的视图 * @return 添加至服务器后,对应视图节点的唯一标识 `NodeId` */ - NodeId addViewNode(const View &view); + NodeId addViewNode(const View &view) const; /** * @brief 创建变量节点监视项,以实现订阅节点的功能 @@ -151,7 +151,7 @@ class Client * @param[in] queue_size 通知存放的队列大小,若队列已满,新的通知会覆盖旧的通知,默认为 `10` * @return 变量节点监视创建成功? */ - bool monitor(NodeId node, UA_Client_DataChangeNotificationCallback on_change, uint32_t queue_size = 10); + bool monitor(NodeId node, UA_Client_DataChangeNotificationCallback on_change, uint32_t queue_size = 10) const; /** * @brief 创建事件监视项,以实现事件的订阅功能 @@ -161,7 +161,7 @@ class Client * @param[in] on_event 事件回调函数 * @return 事件监视创建成功? */ - bool monitor(NodeId node, const std::vector &names, UA_Client_EventNotificationCallback on_event); + bool monitor(NodeId node, const std::vector &names, UA_Client_EventNotificationCallback on_event) const; private: /** @@ -170,7 +170,7 @@ class Client * @param[out] response 订阅请求的响应 * @return 是否成功完成当前操作 */ - bool createSubscription(UA_CreateSubscriptionResponse &responce); + bool createSubscription(UA_CreateSubscriptionResponse &responce) const; }; //! @} opcua diff --git a/modules/opcua/include/rmvl/opcua/server.hpp b/modules/opcua/include/rmvl/opcua/server.hpp index 7d4922b6..470b524d 100644 --- a/modules/opcua/include/rmvl/opcua/server.hpp +++ b/modules/opcua/include/rmvl/opcua/server.hpp @@ -43,6 +43,59 @@ using DataSourceWrite = UA_StatusCode (*)(UA_Server *, const UA_NodeId *, void * //! 服务器配置函数指针,由 `nodeset_compiler` 生成 using ServerUserConfig = UA_StatusCode (*)(UA_Server *); +//! OPC UA 服务器视图 +class ServerView final +{ + UA_Server *_server{nullptr}; //!< OPC UA 服务器指针 + +public: + ServerView() = default; + + /** + * @brief 创建不占有生命周期的 OPC UA 服务器视图,在 OPC UA 方法节点中使用特别有效 + * + * @param[in] server OPC UA 服务器指针 + */ + ServerView(UA_Server *server) : _server(server) {} + + ServerView &operator=(UA_Server *const server) + { + _server = server; + return *this; + } + + /** + * @brief 获取路径搜索必要信息 + * @brief 需要配合管道运算符 `|` 完成路径搜索 + * @code{.cpp} + * auto dst_mode = src_node | svr.find("person") | svr.find("name"); + * @endcode + * + * @param[in] browse_name 浏览名 + * @param[in] ns 命名空间索引,默认为 `1` + * @return 目标节点信息 + * @retval fnis `[_client, browse_name]` 元组 + */ + inline FindNodeInServer find(const std::string &browse_name, uint16_t ns = 1U) const { return {_server, browse_name, ns}; } + + /** + * @brief 从指定的变量节点读数据 + * + * @param[in] node 既存的变量节点的 `NodeId` + * @return 读出的用 `rm::Variable` 表示的数据,未成功读取则返回空 + */ + Variable read(const NodeId &node) const; + + /** + * @brief 给指定的变量节点写数据 + * + * @param[in] node 既存的变量节点的 `NodeId` + * @param[in] val 待写入的数据 + * @return 是否写入成功 + */ + bool write(const NodeId &node, const Variable &val) const; +}; + //! OPC UA 服务器 class Server { @@ -77,7 +130,9 @@ class Server Server(ServerUserConfig on_config, uint16_t port, std::string_view name = {}, const std::vector &users = {}); Server(const Server &) = delete; - Server(Server &&svr) = default; + Server(Server &&svr) = delete; + + operator ServerView() const { return _server; } //! 运行服务器,调用方线程不阻塞 void start(); @@ -108,7 +163,7 @@ class Server * @return 目标节点信息 * @retval fnis `[_client, browse_name]` 元组 */ - inline FindNodeInServer find(const std::string &browse_name, uint16_t ns = 1U) { return {_server, browse_name, ns}; } + inline FindNodeInServer find(const std::string &browse_name, uint16_t ns = 1U) const { return {_server, browse_name, ns}; } /****************************** 功能配置 ******************************/ @@ -118,7 +173,7 @@ class Server * @param[in] vtype `rm::VariableType` 表示的变量 * @return 添加至服务器后,对应变量类型节点的唯一标识 `NodeId` */ - NodeId addVariableTypeNode(const VariableType &vtype); + NodeId addVariableTypeNode(const VariableType &vtype) const; /** * @brief 添加变量节点 VariableNode 至指定父节点中,并指定引用类型 @@ -127,7 +182,7 @@ class Server * @param[in] parent_id 指定父节点的 `NodeId`,默认为 `rm::nodeObjectsFolder` * @return 添加至服务器后,对应变量节点的唯一标识 `NodeId` */ - NodeId addVariableNode(const Variable &val, const NodeId &parent_id = nodeObjectsFolder); + NodeId addVariableNode(const Variable &val, const NodeId &parent_id = nodeObjectsFolder) const; /** * @brief 为既有的变量节点 VariableNode 添加值回调 @@ -138,7 +193,7 @@ class Server * @param[in] after_write 可隐式转换为 `ValueCallBackAfterWrite` 函数指针类型的可调用对象 * @return 是否添加成功 */ - bool addVariableNodeValueCallBack(NodeId id, ValueCallBackBeforeRead before_read, ValueCallBackAfterWrite after_write); + bool addVariableNodeValueCallBack(NodeId id, ValueCallBackBeforeRead before_read, ValueCallBackAfterWrite after_write) const; /** * @brief 添加数据源变量节点 VariableNode 至指定父节点中 @@ -155,7 +210,7 @@ class Server * @param[in] parent_id 指定父节点的 `NodeId`,默认为 `rm::nodeObjectsFolder` * @return 添加至服务器后,对应数据源变量节点的唯一标识 `NodeId` */ - NodeId addDataSourceVariableNode(const Variable &val, DataSourceRead on_read, DataSourceWrite on_write, NodeId parent_id = nodeObjectsFolder); + NodeId addDataSourceVariableNode(const Variable &val, DataSourceRead on_read, DataSourceWrite on_write, NodeId parent_id = nodeObjectsFolder) const; /** * @brief 从指定的变量节点读数据 @@ -163,7 +218,7 @@ class Server * @param[in] node 既存的变量节点的 `NodeId` * @return 读出的用 `rm::Variable` 表示的数据,未成功读取则返回空 */ - Variable read(const NodeId &node); + Variable read(const NodeId &node) const; /** * @brief 给指定的变量节点写数据 @@ -172,7 +227,7 @@ class Server * @param[in] val 待写入的数据 * @return 是否写入成功 */ - bool write(const NodeId &node, const Variable &val); + bool write(const NodeId &node, const Variable &val) const; /** * @brief 添加方法节点 MethodNode 至指定父节点中 @@ -181,7 +236,7 @@ class Server * @param[in] parent_id 指定父节点的 `NodeId`,默认为 `rm::nodeObjectsFolder` * @return 添加至服务器后,对应方法节点的唯一标识 `NodeId` */ - NodeId addMethodNode(const Method &method, const NodeId &parent_id = nodeObjectsFolder); + NodeId addMethodNode(const Method &method, const NodeId &parent_id = nodeObjectsFolder) const; /** * @brief 为既有的方法节点 MethodNode 设置方法的回调函数 @@ -189,7 +244,7 @@ class Server * @param[in] id 既有的方法节点的 `NodeId`,因方法节点可能位于任意一个父节点下,因此可以使用 **路径搜索** 进行查找 * @param[in] on_method 可隐式转换为 `UA_MethodCallback` 函数指针类型的可调用对象 */ - void setMethodNodeCallBack(const NodeId &id, UA_MethodCallback on_method); + void setMethodNodeCallBack(const NodeId &id, UA_MethodCallback on_method) const; /** * @brief 添加对象类型节点 ObjectTypeNode 至 `rm::nodeBaseObjectType` 中 @@ -197,7 +252,7 @@ class Server * @param[in] otype `rm::ObjectType` 表示的对象类型 * @return 添加至服务器后,对应对象类型节点的唯一标识 `NodeId` */ - NodeId addObjectTypeNode(const ObjectType &otype); + NodeId addObjectTypeNode(const ObjectType &otype) const; /** * @brief 添加对象节点 ObjectNode 至指定的父节点中 @@ -206,7 +261,7 @@ class Server * @param[in] parent_id 指定的父节点 `NodeId`,默认为 `rm::nodeObjectsFolder` * @return 添加至服务器后,对应对象节点的唯一标识 `NodeId` */ - NodeId addObjectNode(const Object &obj, NodeId parent_id = nodeObjectsFolder); + NodeId addObjectNode(const Object &obj, NodeId parent_id = nodeObjectsFolder) const; /** * @brief 添加视图节点 ViewNode 至 `rm::nodeViewsFolder` 中 @@ -214,7 +269,7 @@ class Server * @param[in] view `rm::View` 表示的视图 * @return 添加至服务器后,对应视图节点的唯一标识 `NodeId` */ - NodeId addViewNode(const View &view); + NodeId addViewNode(const View &view) const; /** * @brief 添加事件类型至 `BaseEventType` 中 @@ -222,7 +277,7 @@ class Server * @param[in] etype `rm::EventType` 表示的事件类型 * @return 添加至服务器后,对应事件类型的唯一标识 `NodeId` */ - NodeId addEventTypeNode(const EventType &etype); + NodeId addEventTypeNode(const EventType &etype) const; /** * @brief 创建并触发事件 @@ -232,7 +287,7 @@ class Server * @note `Server` 的 `node_id` 是 `rm::nodeServer` * @return 是否创建并触发成功? */ - bool triggerEvent(const NodeId &node_id, const Event &event); + bool triggerEvent(const NodeId &node_id, const Event &event) const; }; //! @} opcua diff --git a/modules/opcua/src/client.cpp b/modules/opcua/src/client.cpp index bf0e28c4..63982ece 100644 --- a/modules/opcua/src/client.cpp +++ b/modules/opcua/src/client.cpp @@ -22,9 +22,7 @@ namespace rm { -////////////////////////// Public ////////////////////////// - -// ****************** 通用配置 ****************** +////////////////////////// 通用配置 ////////////////////////// Client::Client(std::string_view address, UserConfig usr) { @@ -64,9 +62,7 @@ Client::~Client() _client = nullptr; } -// ****************** 功能配置 ****************** - -void Client::spin() +void Client::spin() const { bool warning{}; while (true) @@ -81,9 +77,14 @@ void Client::spin() } } -void Client::spinOnce() { UA_Client_run_iterate(_client, para::opcua_param.SPIN_TIMEOUT); } +void Client::spinOnce() const +{ + UA_Client_run_iterate(_client, para::opcua_param.SPIN_TIMEOUT); +} + +////////////////////////// 功能配置 ////////////////////////// -Variable Client::read(const NodeId &node) +Variable Client::read(const NodeId &node) const { RMVL_DbgAssert(_client != nullptr); @@ -97,7 +98,7 @@ Variable Client::read(const NodeId &node) return retval; } -bool Client::write(const NodeId &node, const Variable &val) +bool Client::write(const NodeId &node, const Variable &val) const { RMVL_DbgAssert(_client != nullptr); @@ -112,7 +113,7 @@ bool Client::write(const NodeId &node, const Variable &val) return true; } -bool Client::call(const NodeId &obj_node, const std::string &name, const std::vector &inputs, std::vector &outputs) +bool Client::call(const NodeId &obj_node, const std::string &name, const std::vector &inputs, std::vector &outputs) const { RMVL_DbgAssert(_client != nullptr); @@ -149,7 +150,7 @@ bool Client::call(const NodeId &obj_node, const std::string &name, const std::ve return true; } -NodeId Client::addViewNode(const View &view) +NodeId Client::addViewNode(const View &view) const { RMVL_DbgAssert(_client != nullptr); @@ -183,7 +184,7 @@ NodeId Client::addViewNode(const View &view) return retval; } -bool Client::monitor(NodeId node, UA_Client_DataChangeNotificationCallback on_change, uint32_t queue_size) +bool Client::monitor(NodeId node, UA_Client_DataChangeNotificationCallback on_change, uint32_t queue_size) const { RMVL_DbgAssert(_client != nullptr); @@ -208,7 +209,7 @@ bool Client::monitor(NodeId node, UA_Client_DataChangeNotificationCallback on_ch return true; } -bool Client::monitor(NodeId node, const std::vector &names, UA_Client_EventNotificationCallback on_event) +bool Client::monitor(NodeId node, const std::vector &names, UA_Client_EventNotificationCallback on_event) const { RMVL_DbgAssert(_client != nullptr); // 创建订阅 @@ -258,9 +259,7 @@ bool Client::monitor(NodeId node, const std::vector &names, UA_Clie return true; } -////////////////////////// Private ////////////////////////// - -bool Client::createSubscription(UA_CreateSubscriptionResponse &response) +bool Client::createSubscription(UA_CreateSubscriptionResponse &response) const { UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default(); request.requestedPublishingInterval = para::opcua_param.PUBLISHING_INTERVAL; diff --git a/modules/opcua/src/helper.cpp b/modules/opcua/src/helper.cpp index 40cf0654..9f93ee07 100644 --- a/modules/opcua/src/helper.cpp +++ b/modules/opcua/src/helper.cpp @@ -285,7 +285,7 @@ Variable cvtVariable(const UA_Variant &p_val) switch (type_flag) { case UA_TYPES_STRING: - return reinterpret_cast(data); + return reinterpret_cast(reinterpret_cast(data)->data); case UA_TYPES_BOOLEAN: return *reinterpret_cast(data); case UA_TYPES_SBYTE: diff --git a/modules/opcua/src/server.cpp b/modules/opcua/src/server.cpp index 3a8cbac2..e7d35815 100644 --- a/modules/opcua/src/server.cpp +++ b/modules/opcua/src/server.cpp @@ -22,7 +22,7 @@ namespace rm { -// ============================= 基本配置 ============================= +///////////////////////// 基本配置 ///////////////////////// Server::Server(uint16_t port, std::string_view name, const std::vector &users) { @@ -102,9 +102,35 @@ Server::~Server() UA_Server_delete(_server); } -// ============================= 节点配置 ============================= +static Variable serverRead(UA_Server *p_server, const NodeId &node) +{ + RMVL_DbgAssert(p_server != nullptr); + + UA_Variant p_val; + UA_Variant_init(&p_val); + auto status = UA_Server_readValue(p_server, node, &p_val); + if (status != UA_STATUSCODE_GOOD) + return {}; + Variable retval = helper::cvtVariable(p_val); + UA_Variant_clear(&p_val); + return retval; +} + +static bool serverWrite(UA_Server *p_server, const NodeId &node, const Variable &val) +{ + RMVL_DbgAssert(p_server != nullptr); -NodeId Server::addVariableTypeNode(const VariableType &vtype) + auto variant = helper::cvtVariable(val); + auto status = UA_Server_writeValue(p_server, node, variant); + UA_Variant_clear(&variant); + if (status != UA_STATUSCODE_GOOD) + ERROR_("Failed to write variable, error code: %s", UA_StatusCode_name(status)); + return status == UA_STATUSCODE_GOOD; +} + +///////////////////////// 节点配置 ///////////////////////// + +NodeId Server::addVariableTypeNode(const VariableType &vtype) const { RMVL_DbgAssert(_server != nullptr); @@ -135,7 +161,7 @@ NodeId Server::addVariableTypeNode(const VariableType &vtype) return retval; } -NodeId Server::addVariableNode(const Variable &val, const NodeId &parent_id) +NodeId Server::addVariableNode(const Variable &val, const NodeId &parent_id) const { RMVL_DbgAssert(_server != nullptr); @@ -182,33 +208,10 @@ NodeId Server::addVariableNode(const Variable &val, const NodeId &parent_id) return retval; } -Variable Server::read(const NodeId &node) -{ - RMVL_DbgAssert(_server != nullptr); +Variable Server::read(const NodeId &node) const { return serverRead(_server, node); } +bool Server::write(const NodeId &node, const Variable &val) const { return serverWrite(_server, node, val); } - UA_Variant p_val; - UA_Variant_init(&p_val); - auto status = UA_Server_readValue(_server, node, &p_val); - if (status != UA_STATUSCODE_GOOD) - return {}; - Variable retval = helper::cvtVariable(p_val); - UA_Variant_clear(&p_val); - return retval; -} - -bool Server::write(const NodeId &node, const Variable &val) -{ - RMVL_DbgAssert(_server != nullptr); - - auto variant = helper::cvtVariable(val); - auto status = UA_Server_writeValue(_server, node, variant); - UA_Variant_clear(&variant); - if (status != UA_STATUSCODE_GOOD) - ERROR_("Failed to write variable, error code: %s", UA_StatusCode_name(status)); - return status == UA_STATUSCODE_GOOD; -} - -bool Server::addVariableNodeValueCallBack(NodeId id, ValueCallBackBeforeRead before_read, ValueCallBackAfterWrite after_write) +bool Server::addVariableNodeValueCallBack(NodeId id, ValueCallBackBeforeRead before_read, ValueCallBackAfterWrite after_write) const { RMVL_DbgAssert(_server != nullptr); @@ -219,7 +222,7 @@ bool Server::addVariableNodeValueCallBack(NodeId id, ValueCallBackBeforeRead bef return status == UA_STATUSCODE_GOOD; } -NodeId Server::addDataSourceVariableNode(const Variable &val, DataSourceRead on_read, DataSourceWrite on_write, NodeId parent_id) +NodeId Server::addDataSourceVariableNode(const Variable &val, DataSourceRead on_read, DataSourceWrite on_write, NodeId parent_id) const { RMVL_DbgAssert(_server != nullptr); @@ -258,7 +261,7 @@ NodeId Server::addDataSourceVariableNode(const Variable &val, DataSourceRead on_ return retval; } -NodeId Server::addMethodNode(const Method &method, const NodeId &parent_id) +NodeId Server::addMethodNode(const Method &method, const NodeId &parent_id) const { RMVL_DbgAssert(_server != nullptr); @@ -297,13 +300,13 @@ NodeId Server::addMethodNode(const Method &method, const NodeId &parent_id) return retval; } -void Server::setMethodNodeCallBack(const NodeId &id, UA_MethodCallback on_method) +void Server::setMethodNodeCallBack(const NodeId &id, UA_MethodCallback on_method) const { RMVL_DbgAssert(_server != nullptr); UA_Server_setMethodNodeCallback(_server, id, on_method); } -NodeId Server::addObjectTypeNode(const ObjectType &otype) +NodeId Server::addObjectTypeNode(const ObjectType &otype) const { RMVL_DbgAssert(_server != nullptr); @@ -362,7 +365,7 @@ NodeId Server::addObjectTypeNode(const ObjectType &otype) return retval; } -NodeId Server::addObjectNode(const Object &obj, NodeId parent_id) +NodeId Server::addObjectNode(const Object &obj, NodeId parent_id) const { RMVL_DbgAssert(_server != nullptr); @@ -419,7 +422,7 @@ NodeId Server::addObjectNode(const Object &obj, NodeId parent_id) return retval; } -NodeId Server::addViewNode(const View &view) +NodeId Server::addViewNode(const View &view) const { RMVL_DbgAssert(_server != nullptr); @@ -453,7 +456,7 @@ NodeId Server::addViewNode(const View &view) return retval; } -NodeId Server::addEventTypeNode(const EventType &etype) +NodeId Server::addEventTypeNode(const EventType &etype) const { RMVL_DbgAssert(_server != nullptr); @@ -503,7 +506,7 @@ NodeId Server::addEventTypeNode(const EventType &etype) return retval; } -bool Server::triggerEvent(const NodeId &node_id, const Event &event) +bool Server::triggerEvent(const NodeId &node_id, const Event &event) const { RMVL_DbgAssert(_server != nullptr); @@ -549,4 +552,9 @@ bool Server::triggerEvent(const NodeId &node_id, const Event &event) return true; } +//////////////////////// 服务端视图 //////////////////////// + +Variable ServerView::read(const NodeId &node) const { return serverRead(_server, node); } +bool ServerView::write(const NodeId &node, const Variable &val) const { return serverWrite(_server, node, val); } + } // namespace rm diff --git a/modules/opcua/test/test_opcua_server.cpp b/modules/opcua/test/test_opcua_server.cpp index 475e7c43..4a610b29 100644 --- a/modules/opcua/test/test_opcua_server.cpp +++ b/modules/opcua/test/test_opcua_server.cpp @@ -38,7 +38,7 @@ TEST(OPC_UA_Server, variable_config) // 服务器添加变量节点 TEST(OPC_UA_Server, add_variable_node) { - rm::Server svr(4820, "TestServer"); + rm::Server svr(4810, "TestServer"); rm::Variable variable{3.1415}; variable.browse_name = "test_double"; variable.description = "this is test double"; @@ -46,8 +46,17 @@ TEST(OPC_UA_Server, add_variable_node) auto node = svr.addVariableNode(variable); EXPECT_FALSE(UA_NodeId_isNull(&node)); svr.start(); - svr.stop(); - svr.join(); +} + +TEST(OPC_UA_Server, variable_node_io) +{ + rm::Server svr(4820, "TestServer"); + uaCreateVariable(variable, 1); + auto node = svr.addVariableNode(variable); + svr.start(); + EXPECT_EQ(svr.read(node), 1); + EXPECT_TRUE(svr.write(node, 2)); + EXPECT_EQ(svr.read(node), 2); } static int data_source; @@ -69,19 +78,17 @@ static UA_StatusCode onWrite(UA_Server *, const UA_NodeId *, void *, const UA_No // 服务器添加数据源变量节点 TEST(OPC_UA_Server, add_data_source_variable_node) { - rm::Server svr(4821, "TestServer"); + rm::Server svr(4825, "TestServer"); uaCreateVariable(variable); auto node = svr.addDataSourceVariableNode(variable, onRead, onWrite); EXPECT_FALSE(UA_NodeId_isNull(&node)); svr.start(); - svr.stop(); - svr.join(); } // 服务器添加变量类型节点 TEST(OPC_UA_Server, add_variable_type_node) { - rm::Server svr(4825); + rm::Server svr(4830); rm::VariableType variable_type = "string_test"; variable_type.browse_name = "test_string"; variable_type.description = "this is test string"; @@ -89,14 +96,12 @@ TEST(OPC_UA_Server, add_variable_type_node) auto node = svr.addVariableTypeNode(variable_type); EXPECT_FALSE(UA_NodeId_isNull(&node)); svr.start(); - svr.stop(); - svr.join(); } // 服务器添加方法节点 TEST(OPC_UA_Server, add_method_node) { - rm::Server svr(4830); + rm::Server svr(4832); rm::Method method; method.browse_name = "test_method"; method.description = "this is test method"; @@ -107,8 +112,6 @@ TEST(OPC_UA_Server, add_method_node) }; svr.addMethodNode(method); svr.start(); - svr.stop(); - svr.join(); } // 服务器添加对象节点 @@ -127,8 +130,6 @@ TEST(OPC_UA_Server, add_object_node) auto id = svr.addObjectNode(object); EXPECT_FALSE(UA_NodeId_isNull(&id)); svr.start(); - svr.stop(); - svr.join(); } // 服务器添加包含方法节点的对象节点 @@ -156,8 +157,6 @@ TEST(OPC_UA_Server, add_object_node_with_method) auto id = svr.addObjectNode(object); EXPECT_FALSE(UA_NodeId_isNull(&id)); svr.start(); - svr.stop(); - svr.join(); } // 服务器添加对象类型节点 @@ -176,8 +175,6 @@ TEST(OPC_UA_Server, add_object_type_node) auto id = svr.addObjectTypeNode(object_type); EXPECT_FALSE(UA_NodeId_isNull(&id)); svr.start(); - svr.stop(); - svr.join(); } // 从对象类型节点派生对象节点,并添加到服务器 @@ -201,8 +198,6 @@ TEST(OPC_UA_Server, create_object_by_object_type) auto id = svr.addObjectNode(object); EXPECT_FALSE(UA_NodeId_isNull(&id)); svr.start(); - svr.stop(); - svr.join(); } // 服务器节点服务端路径搜索 @@ -222,8 +217,6 @@ TEST(OPC_UA_Server, find_node) auto target = rm::nodeObjectsFolder | svr.find("test_object"); EXPECT_TRUE(UA_NodeId_equal(&id, &target)); svr.start(); - svr.stop(); - svr.join(); } // 添加自定义事件类型节点 @@ -240,8 +233,6 @@ TEST(OPC_UA_Server, add_event_type_node) auto target = rm::nodeBaseEventType | svr.find("test_event_type"); EXPECT_TRUE(UA_NodeId_equal(&id, &target)); svr.start(); - svr.stop(); - svr.join(); } // 手动触发事件 @@ -265,8 +256,6 @@ TEST(OPC_UA_Server, trigger_event) // 触发事件 EXPECT_TRUE(svr.triggerEvent(UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER), event)); svr.start(); - svr.stop(); - svr.join(); } // 从函数指针配置服务器 @@ -276,8 +265,6 @@ TEST(OPC_UA_Server, function_ptr) svr.start(); auto id = rm::nodeObjectsFolder | svr.find("TestNumber"); EXPECT_FALSE(UA_NodeId_isNull(&id)); - svr.stop(); - svr.join(); } // 视图节点 @@ -300,9 +287,6 @@ TEST(OPC_UA_Server, view_node) auto view_id = svr.addViewNode(view); auto target_view_id = rm::nodeViewsFolder | svr.find("test_view"); EXPECT_TRUE(UA_NodeId_equal(&view_id, &target_view_id)); - - svr.stop(); - svr.join(); } } // namespace rm_test diff --git a/modules/opcua/test/test_opcua_serverview.cpp b/modules/opcua/test/test_opcua_serverview.cpp new file mode 100644 index 00000000..43fd71f4 --- /dev/null +++ b/modules/opcua/test/test_opcua_serverview.cpp @@ -0,0 +1,65 @@ +/** + * @file test_opcua_serverview.cpp + * @author zhaoxi (535394140@qq.com) + * @brief OPC UA 服务器视图单元测试 + * @version 1.0 + * @date 2024-07-06 + * + * @copyright Copyright 2024 (c), zhaoxi + * + */ + +#include + +#include "rmvl/opcua/client.hpp" +#include "rmvl/opcua/server.hpp" + +namespace rm::rm_test +{ + +using namespace std::chrono_literals; + +void setup(Server &svr) +{ + Variable num_var = 1; + num_var.browse_name = "num"; + num_var.display_name = "Number"; + num_var.description = "数"; + svr.addVariableNode(num_var); + + Method method; + method.browse_name = "plus"; + method.display_name = "Input + Number"; + method.description = "输入值加数"; + method.func = [](UA_Server *p_server, const UA_NodeId *, void *, const UA_NodeId *, void *, const UA_NodeId *, + void *, size_t, const UA_Variant *inputs, size_t, UA_Variant *) -> UA_StatusCode { + ServerView sv = p_server; + auto num_node = nodeObjectsFolder | sv.find("num"); + int num = sv.read(num_node).cast(); + Variable dst = *reinterpret_cast(inputs->data) + num; + sv.write(num_node, dst); + return UA_STATUSCODE_GOOD; + }; + method.iargs = {{"input", UA_TYPES_INT32, 1, "输入值"}}; + svr.addMethodNode(method); + + svr.start(); + std::this_thread::sleep_for(10ms); +} + +TEST(OPC_UA_ServerView, read_variable_in_method) +{ + Server svr(6000); + setup(svr); + + Client clt("opc.tcp://127.0.0.1:6000"); + ASSERT_TRUE(clt.ok()); + EXPECT_EQ(clt.read(nodeObjectsFolder | clt.find("num")), 1); + std::vector inputs = {2}; + std::vector outputs; + + EXPECT_TRUE(clt.call("plus", inputs, outputs)); + EXPECT_EQ(clt.read(nodeObjectsFolder | clt.find("num")), 3); +} + +} // namespace rm::rm_test