diff --git a/doc/tutorials/modules/tools/opcua.md b/doc/tutorials/modules/tools/opcua.md index 44d2769..b7030f5 100644 --- a/doc/tutorials/modules/tools/opcua.md +++ b/doc/tutorials/modules/tools/opcua.md @@ -48,7 +48,7 @@ OPC UA 的设计目标是建立一种通用的、独立于厂商和平台的通 3. 引用类型节点 **ReferenceType** :引用类型描述了引用的语义,而引用用于定义引用两端的节点之间的关系。最常用的引用类型如 Organizes(在 RMVL 中表示为 `rm::nodeOrganizes`),表示节点之间的层级关系,如同文件夹与文件夹内的文件,数据层级复杂的设备,需要通过多种引用类型对设备信息节点之间的关系进行描述。 -4. 数据类型节点 **DataType** :数据类型节点描述了变量节点中变量的数据类型。在 OPC UA 信息模型在命名空间 `0` 中定义了多种内置的数据类型,包括整型、浮点型、 字符串等多个类型,能对变量的数据进行准确的描述。也可以自定义数据类型,比如描述二维坐标的 `2DPoint` 等类型,获得更符合数据本身的描述。@note 注意:此类节点并不能提供具体的数据构成,只是提供了数据类型的一个描述,因此 RMVL 中的 @ref opcua 不提供该功能。若计划提供数据的构成,比如包含的数据长度等信息,请使用变量类型节点 rm::VariableType 。 +4. 数据类型节点 rm::DataType :数据类型节点描述了变量节点中变量的数据类型。在 OPC UA 信息模型在命名空间 `0` 中定义了多种内置的数据类型,包括整型、浮点型、 字符串等多个类型,能对变量的数据进行准确的描述。也可以自定义数据类型,比如描述二维坐标的 `2DPoint` 等类型,获得更符合数据本身的描述。@note 注意:此类节点并不能提供具体的数据构成,只是提供了数据类型的一个描述,因此 RMVL 中的 @ref opcua 仅提供内置数据类型。若计划提供数据的构成,比如包含的数据长度等信息,请使用变量类型节点 rm::VariableType 。 5. 变量类型节点 rm::VariableType :该节点提供了对变量节点的定义,是设备中各种数据的抽象。常用引用中的 HasTypeDefinition 引用节点连接数据类型节点,对数据类型进行描述(在 RMVL 中表示为 `rm::nodeHasTypeDefinition`)。用 HasProperty 引用节点对数据的语义进行描述(在 RMVL 中表示为 `rm::nodeHasProperty`)。也可以使用自定义的数据类型节点对变量的数据进行描述,具有灵活性。 @@ -98,7 +98,7 @@ int main() int main() { // 创建 OPC UA 客户端,连接到 127.0.0.1:4840 - rm::Client clt("opc.tcp://127.0.0.1:4840"); + rm::Client cli("opc.tcp://127.0.0.1:4840"); /* other code */ } @@ -141,12 +141,12 @@ int main() int main() { - rm::Client clt("opc.tcp://127.0.0.1:4840"); + rm::Client cli("opc.tcp://127.0.0.1:4840"); // 使用管道运算符 "|" 进行路径搜索,寻找待读取的变量 - auto node = rm::nodeObjectsFolder | clt.find("number"); + auto node = rm::nodeObjectsFolder | cli.find("number"); // 读取变量 - rm::Variable target = clt.read(node); + rm::Variable target = cli.read(node); // 判断是否为空 if (target.empty()) { @@ -210,14 +210,14 @@ int main() int main() { - rm::Client clt("opc.tcp://127.0.0.1:4840"); + rm::Client cli("opc.tcp://127.0.0.1:4840"); // 设置输入参数,1 和 2 是 Int32 类型的,因此可以直接隐式构造 std::vector input = {1, 2}; // 设置输出参数,用来存储结果 std::vector output; // 调用方法,判断调用是否成功 - if (!clt.call("add", input, output)) + if (!cli.call("add", input, output)) { ERROR_("Failed to call the method"); return 0; @@ -287,17 +287,17 @@ int main() int main() { - rm::Client clt("opc.tcp://127.0.0.1:4840"); + rm::Client cli("opc.tcp://127.0.0.1:4840"); // 路径搜索寻找 C2 - auto node_c2 = rm::nodeObjectsFolder | clt.find("A") | clt.find("B1") | clt.find("C2"); + auto node_c2 = rm::nodeObjectsFolder | cli.find("A") | cli.find("B1") | cli.find("C2"); rm::Variable c2; - clt.read(node_c2, c2); + cli.read(node_c2, c2); std::cout << rm::Variable::cast(c2) << std::endl; // 路径搜索寻找 C3 - auto node_c3 = rm::nodeObjectsFolder | clt.find("A") | clt.find("B2") | clt.find("C3"); + auto node_c3 = rm::nodeObjectsFolder | cli.find("A") | cli.find("B2") | cli.find("C3"); rm::Variable c3; - clt.read(node_c3, c3); + cli.read(node_c3, c3); std::cout << rm::Variable::cast(c3) << std::endl; } ``` @@ -371,13 +371,13 @@ using namespace std::chrono_literals; int main() { - rm::Client clt("opc.tcp://127.0.0.1:4840"); - auto node = rm::nodeObjectsFolder | clt.find("number"); + rm::Client cli("opc.tcp://127.0.0.1:4840"); + auto node = rm::nodeObjectsFolder | cli.find("number"); for (int i = 0; i < 100; ++i) { std::this_thread::sleep_for(1s); // 写入数据,i + 200 隐式构造成了 rm::Variable - bool success = clt.write(node, i + 200); + bool success = cli.write(node, i + 200); if (!success) ERROR_("Failed to write data to the variable."); } @@ -398,8 +398,8 @@ void onChange(UA_Client *, UA_UInt32, void *, UA_UInt32, void *, UA_DataValue *v int main() { - rm::Client clt("opc.tcp://127.0.0.1:4840"); - auto node = rm::nodeObjectsFolder | clt.find("number"); + rm::Client cli("opc.tcp://127.0.0.1:4840"); + auto node = rm::nodeObjectsFolder | cli.find("number"); // 监视变量,这里的 onChange 同样可以写成无捕获列表的 lambda 表达式,因为存在隐式转换 client.monitor(node, onChange, 5); // 线程阻塞 diff --git a/modules/opcua/include/rmvl/opcua/method.hpp b/modules/opcua/include/rmvl/opcua/method.hpp index ed796b5..428a8c1 100644 --- a/modules/opcua/include/rmvl/opcua/method.hpp +++ b/modules/opcua/include/rmvl/opcua/method.hpp @@ -31,7 +31,7 @@ namespace rm struct Argument final { std::string name; //!< 参数名称 - UA_TypeFlag data_type{}; //!< 参数数据类型 @note 形如 `UA_TYPES_` 的类型标志位 + DataType data_type{}; //!< 参数数据类型 @note 形如 `UA_TYPES_` 的类型标志位 uint32_t dims{1U}; //!< 参数维数,单数据则是 `1`,数组则是数组长度 @warning 不能为 `0` std::string description{}; //!< 参数描述 }; diff --git a/modules/opcua/include/rmvl/opcua/subscriber.hpp b/modules/opcua/include/rmvl/opcua/subscriber.hpp index 7191f05..dd44911 100644 --- a/modules/opcua/include/rmvl/opcua/subscriber.hpp +++ b/modules/opcua/include/rmvl/opcua/subscriber.hpp @@ -32,9 +32,9 @@ struct FieldMetaData final /** * @brief 字段类型 - * @see UA_TypeFlag + * @see DataType */ - UA_TypeFlag type; + DataType type; //! 字段 ValueRank int value_rank{}; @@ -45,11 +45,11 @@ struct FieldMetaData final * @brief 创建字段元数据 * * @param[in] name_ 字段名称 - * @param[in] type_ 字段类型,可参考 @ref UA_TypeFlag + * @param[in] type_ 字段类型,可参考 @ref DataType * @param[in] value_rank_ 字段 ValueRank * @param[in] ns_ 命名空间索引,默认为 `1` */ - FieldMetaData(const std::string &name_, UA_TypeFlag type_, int value_rank_, uint16_t ns_ = 1U) : ns(ns_), name(name_), type(type_), value_rank(value_rank_) {} + FieldMetaData(const std::string &name_, DataType type_, int value_rank_, uint16_t ns_ = 1U) : ns(ns_), name(name_), type(type_), value_rank(value_rank_) {} /** * @brief 从变量创建字段元数据 diff --git a/modules/opcua/include/rmvl/opcua/utilities.hpp b/modules/opcua/include/rmvl/opcua/utilities.hpp index 524177e..6592ef9 100644 --- a/modules/opcua/include/rmvl/opcua/utilities.hpp +++ b/modules/opcua/include/rmvl/opcua/utilities.hpp @@ -12,8 +12,8 @@ #pragma once #include -#include #include +#include #include #include @@ -66,13 +66,12 @@ struct NodeId final inline UA_NodeId *operator&() { return &nid; } inline const UA_NodeId *operator&() const { return &nid; } - //! 到 `UA_NodeId` 的转换 constexpr operator UA_NodeId() const { return nid; } /** * @brief 判断节点 ID 是否为空 - * + * * @return 是否为空 */ inline bool empty() const { return UA_NodeId_isNull(&nid); } @@ -81,6 +80,34 @@ struct NodeId final inline void clear() { UA_NodeId_clear(&nid); } }; +//! OPC UA 数据类型 +class DataType +{ + //! 形如 `UA_TYPES_` 的类型标志位 + static const std::unordered_map _map; + //! 数据类型 ID + UA_UInt32 id{}; + +public: + DataType() = default; + + /** + * @brief 从 `UA_TYPES_` 枚举值构造数据类型 + * + * @param[in] id `UA_TYPES_` 枚举值 + */ + constexpr DataType(UA_UInt32 id) : id(id) {} + + /** + * @brief 从 `std::type_info` 构造数据类型 + * + * @param[in] tp `std::type_info` 类型,可用 `typeid()` 获取 + */ + DataType(const std::type_info &tp) : id(_map.at(std::type_index(tp))) {} + + operator UA_UInt32() const { return id; } +}; + /** * @brief 用户信息 * @brief @@ -94,24 +121,6 @@ struct UserConfig final std::string passwd; //!< 密码 }; -//! 类型标志位,可通过 `typeflag.at(xxx)` 进行获取 -using UA_TypeFlag = UA_UInt32; - -//! 获取形如 `UA_TYPES_` 的类型标志位 -inline const std::unordered_map typeflag = - {{std::type_index(typeid(bool)), UA_TYPES_BOOLEAN}, - {std::type_index(typeid(int8_t)), UA_TYPES_SBYTE}, - {std::type_index(typeid(uint8_t)), UA_TYPES_BYTE}, - {std::type_index(typeid(UA_Int16)), UA_TYPES_INT16}, - {std::type_index(typeid(UA_UInt16)), UA_TYPES_UINT16}, - {std::type_index(typeid(UA_Int32)), UA_TYPES_INT32}, - {std::type_index(typeid(UA_UInt32)), UA_TYPES_UINT32}, - {std::type_index(typeid(UA_Int64)), UA_TYPES_INT64}, - {std::type_index(typeid(UA_UInt64)), UA_TYPES_UINT64}, - {std::type_index(typeid(UA_Float)), UA_TYPES_FLOAT}, - {std::type_index(typeid(UA_Double)), UA_TYPES_DOUBLE}, - {std::type_index(typeid(const char *)), UA_TYPES_STRING}}; - //! 传输协议 enum class TransportID : uint8_t { @@ -168,7 +177,7 @@ using FindNodeInServer = ::std::tuple using FindNodeInClient = ::std::tuple; /** - * @brief `UA_TypeFlag` 到对应 `NS0` 下的类型名的映射 + * @brief `DataType` 到对应 `NS0` 下的类型名的映射 * @brief * - 例如:`typeflag_ns0[UA_TYPES_INT16]` 为 `UA_NS0ID_INT16` */ diff --git a/modules/opcua/include/rmvl/opcua/variable.hpp b/modules/opcua/include/rmvl/opcua/variable.hpp index 5bec1b9..fc44f22 100644 --- a/modules/opcua/include/rmvl/opcua/variable.hpp +++ b/modules/opcua/include/rmvl/opcua/variable.hpp @@ -63,7 +63,7 @@ class VariableType final //! 默认数据 std::any _value; //! 数据类型 - UA_TypeFlag _data_type{}; + DataType _data_type{}; //! 数据大小 UA_UInt32 _size{}; @@ -74,7 +74,7 @@ class VariableType final * @param[in] str 字面量字符串 */ template - VariableType(const char (&str)[N]) : _value(str), _data_type(typeflag.at(typeid(const char *))), _size(1) {} + VariableType(const char (&str)[N]) : _value(str), _data_type(DataType(typeid(const char *))), _size(1) {} /** * @brief 单值构造,设置默认值 @@ -83,7 +83,7 @@ class VariableType final * @param[in] val 标量、数量值 */ template || std::is_same_v>> - VariableType(Tp &&val) : _value(val), _data_type(typeflag.at(typeid(Tp))), _size(1) {} + VariableType(Tp &&val) : _value(val), _data_type(DataType(typeid(Tp))), _size(1) {} /** * @brief 列表构造,设置默认值 @@ -92,7 +92,7 @@ class VariableType final * @param[in] arr 列表、数组 */ template && !std::is_same_v>> - VariableType(const std::vector &arr) : _value(arr), _data_type(typeflag.at(typeid(Tp))), _size(arr.size()) {} + VariableType(const std::vector &arr) : _value(arr), _data_type(DataType(typeid(Tp))), _size(arr.size()) {} /** * @brief 将变量类型节点转化为指定类型的数据 @@ -108,7 +108,7 @@ class VariableType final inline const auto &data() const { return _value; } //! 获取数据类型 - inline UA_TypeFlag getDataType() const { return _data_type; } + inline DataType getDataType() const { return _data_type; } //! 获取大小 @note 未初始化则返回 `0` inline UA_UInt32 size() const { return _size; } @@ -155,7 +155,7 @@ class Variable final //! 数据 std::any _value; //! 数据类型 - UA_TypeFlag _data_type{}; + DataType _data_type{}; //! 数据大小 UA_UInt32 _size{}; @@ -168,7 +168,7 @@ class Variable final * @param[in] str 字面量字符串 */ template - Variable(const char (&str)[N]) : access_level(3U), _value(str), _data_type(typeflag.at(typeid(const char *))), _size(1) {} + Variable(const char (&str)[N]) : access_level(3U), _value(str), _data_type(DataType(typeid(const char *))), _size(1) {} /** * @brief 单值构造 @@ -177,7 +177,7 @@ class Variable final * @param[in] val 标量、数量值 */ template || std::is_same_v>> - Variable(const Tp &val) : access_level(3U), _value(val), _data_type(typeflag.at(typeid(Tp))), _size(1) {} + Variable(const Tp &val) : access_level(3U), _value(val), _data_type(DataType(typeid(Tp))), _size(1) {} /** * @brief 列表构造 @@ -186,7 +186,7 @@ class Variable final * @param[in] arr 列表、数组 */ template && !std::is_same_v>> - Variable(const std::vector &arr) : access_level(3U), _value(arr), _data_type(typeflag.at(typeid(Tp))), _size(arr.size()) {} + Variable(const std::vector &arr) : access_level(3U), _value(arr), _data_type(DataType(typeid(Tp))), _size(arr.size()) {} /** * @brief 从变量类型构造新的变量节点 @@ -247,7 +247,7 @@ class Variable final inline const auto &data() const { return _value; } //! 获取形如 `UA_TYPES_` 的数据类型 - inline UA_TypeFlag getDataType() const { return _data_type; } + inline DataType getDataType() const { return _data_type; } //! 获取大小 @note 未初始化则返回 `0` inline UA_UInt32 size() const { return _size; } diff --git a/modules/opcua/src/helper.cpp b/modules/opcua/src/helper.cpp index 0271646..d7b460b 100644 --- a/modules/opcua/src/helper.cpp +++ b/modules/opcua/src/helper.cpp @@ -20,6 +20,20 @@ namespace rm { +const std::unordered_map DataType::_map = + {{std::type_index(typeid(bool)), UA_TYPES_BOOLEAN}, + {std::type_index(typeid(int8_t)), UA_TYPES_SBYTE}, + {std::type_index(typeid(uint8_t)), UA_TYPES_BYTE}, + {std::type_index(typeid(UA_Int16)), UA_TYPES_INT16}, + {std::type_index(typeid(UA_UInt16)), UA_TYPES_UINT16}, + {std::type_index(typeid(UA_Int32)), UA_TYPES_INT32}, + {std::type_index(typeid(UA_UInt32)), UA_TYPES_UINT32}, + {std::type_index(typeid(UA_Int64)), UA_TYPES_INT64}, + {std::type_index(typeid(UA_UInt64)), UA_TYPES_UINT64}, + {std::type_index(typeid(UA_Float)), UA_TYPES_FLOAT}, + {std::type_index(typeid(UA_Double)), UA_TYPES_DOUBLE}, + {std::type_index(typeid(const char *)), UA_TYPES_STRING}}; + NodeId operator|(NodeId origin, FindNodeInServer &&fnis) { if (origin.empty()) @@ -278,7 +292,7 @@ UA_Variant cvtVariable(const Variable &val) Variable cvtVariable(const UA_Variant &p_val) { UA_UInt32 dims = (p_val.arrayLength == 0 ? 1 : p_val.arrayLength); - UA_TypeFlag type_flag = p_val.type->typeKind; + DataType type_flag = p_val.type->typeKind; void *data = p_val.data; if (dims == 1) { diff --git a/modules/opcua/test/test_opcua_client.cpp b/modules/opcua/test/test_opcua_client.cpp index f75d354..7af65e0 100644 --- a/modules/opcua/test/test_opcua_client.cpp +++ b/modules/opcua/test/test_opcua_client.cpp @@ -21,7 +21,20 @@ namespace rm_test using namespace std::chrono_literals; -void setSvr(rm::Server &srv) +TEST(OPC_UA_ClientTest, connect) +{ + rm::UserConfig config; + rm::Server srv(4999, "Test Server", {{"admin", "admin"}}); + srv.start(); + std::this_thread::sleep_for(10ms); + + rm::Client cli1("opc.tcp://127.0.0.1:4999", {"admin", "admin"}); + EXPECT_TRUE(cli1.ok()); + rm::Client cli2("opc.tcp://127.0.0.1:4999", {"admin", "123456"}); + EXPECT_FALSE(cli2.ok()); +}; + +void configServer(rm::Server &srv) { // 添加单变量节点 rm::Variable single_value = 42; @@ -59,11 +72,11 @@ void setSvr(rm::Server &srv) TEST(OPC_UA_ClientTest, read_variable) { rm::Server srv(5000); - setSvr(srv); - rm::Client client("opc.tcp://127.0.0.1:5000"); + configServer(srv); + rm::Client cli("opc.tcp://127.0.0.1:5000"); // 读取测试服务器上的变量值 - auto id = rm::nodeObjectsFolder | client.find("array"); - rm::Variable variable = client.read(id); + auto id = rm::nodeObjectsFolder | cli.find("array"); + rm::Variable variable = cli.read(id); EXPECT_FALSE(variable.empty()); auto vec = rm::Variable::cast>(variable); for (size_t i = 0; i < vec.size(); ++i) @@ -76,12 +89,12 @@ TEST(OPC_UA_ClientTest, read_variable) TEST(OPC_UA_ClientTest, variable_IO) { rm::Server srv(5001); - setSvr(srv); - rm::Client client("opc.tcp://127.0.0.1:5001"); + configServer(srv); + rm::Client cli("opc.tcp://127.0.0.1:5001"); // 读取测试服务器上的变量值 - auto id = rm::nodeObjectsFolder | client.find("single"); - EXPECT_TRUE(client.write(id, 99)); - rm::Variable variable = client.read(id); + auto id = rm::nodeObjectsFolder | cli.find("single"); + EXPECT_TRUE(cli.write(id, 99)); + rm::Variable variable = cli.read(id); EXPECT_FALSE(variable.empty()); int single_value = rm::Variable::cast(variable); EXPECT_EQ(single_value, 99); @@ -93,12 +106,12 @@ TEST(OPC_UA_ClientTest, variable_IO) TEST(OPC_UA_ClientTest, call) { rm::Server srv(5002); - setSvr(srv); - rm::Client client("opc.tcp://127.0.0.1:5002"); + configServer(srv); + rm::Client cli("opc.tcp://127.0.0.1:5002"); // 调用测试服务器上的方法 std::vector input = {1, 2}; std::vector output; - EXPECT_TRUE(client.call("add", input, output)); + EXPECT_TRUE(cli.call("add", input, output)); EXPECT_EQ(rm::Variable::cast(output[0]), 3); srv.stop(); srv.join(); @@ -117,22 +130,22 @@ void onChange(UA_Client *, UA_UInt32, void *, UA_UInt32, void *, UA_DataValue *v TEST(OPC_UA_ClientTest, variable_monitor) { rm::Server srv(5003); - setSvr(srv); - rm::Client client("opc.tcp://127.0.0.1:5003"); + configServer(srv); + rm::Client cli("opc.tcp://127.0.0.1:5003"); // 订阅测试服务器上的变量 - auto node_id = rm::nodeObjectsFolder | client.find("single"); - EXPECT_TRUE(client.monitor(node_id, onChange, 5)); + auto node_id = rm::nodeObjectsFolder | cli.find("single"); + EXPECT_TRUE(cli.monitor(node_id, onChange, 5)); // 数据更新 - client.write(node_id, 66); + cli.write(node_id, 66); std::this_thread::sleep_for(10ms); - client.spinOnce(); + cli.spinOnce(); EXPECT_EQ(type_name, "Int32"); EXPECT_EQ(receive_data, 66); // 移除监视后的数据按预期不会更新 - client.remove(node_id); - client.write(node_id, 123); + cli.remove(node_id); + cli.write(node_id, 123); std::this_thread::sleep_for(10ms); - client.spinOnce(); + cli.spinOnce(); EXPECT_EQ(receive_data, 66); srv.stop(); srv.join(); @@ -158,22 +171,22 @@ void onEvent(UA_Client *, UA_UInt32, void *, UA_UInt32, void *, size_t size, UA_ TEST(OPC_UA_ClientTest, event_monitor) { rm::Server srv(5004); - setSvr(srv); + configServer(srv); rm::EventType etype; etype.browse_name = "TestEventType"; etype.display_name = "测试事件类型"; etype.description = "测试事件类型"; etype.add("aaa", 3); srv.addEventTypeNode(etype); - rm::Client client("opc.tcp://127.0.0.1:5004"); - client.monitor(rm::nodeServer, {"SourceName", "aaa"}, onEvent); + rm::Client cli("opc.tcp://127.0.0.1:5004"); + cli.monitor(rm::nodeServer, {"SourceName", "aaa"}, onEvent); // 触发事件 rm::Event event(etype); event.source_name = "GtestServer"; event.message = "this is test event"; event["aaa"] = 66; srv.triggerEvent(rm::nodeServer, event); - client.spinOnce(); + cli.spinOnce(); std::this_thread::sleep_for(10ms); EXPECT_EQ(source_name, "GtestServer"); EXPECT_EQ(aaa, 66);