open62541pp

repository·master·Indexed 41 days ago

https://github.com/open62541pp/open62541pp

A high-level C++17 wrapper for the open62541 OPC UA library. It provides a modern API for building industrial-grade OPC UA servers and clients, featuring an asynchronous model similar to Boost.Asio, safe wrappers for UA_* types, and an extensible type conversion system. It offers two complementary APIs: the object-oriented Node API for concise sequential code and the services namespace for batching, non-throwing Result-based error handling, and asynchronous operations.

Tokens
13.5K
Snippets
36
Records
46
Agent score
51%

What's inside open62541pp

  1. Overview of open62541pp

    master

    open62541pp is a high-level C++17 wrapper for the open62541 OPC UA library. It provides an easy-to-use API for building OPC UA servers and clients, featuring classes similar to the Python asyncua API.

    Key features include:

    • High-level classes: opcua::Server, opcua::Client, and opcua::Node.
    • Safe wrappers for UA_* types to prevent memory leaks.
    • An asynchronous model similar to Boost.Asio.
    • An extensible type conversion system for seamless integration with C++ types.
    • Access to native open62541 objects via the .handle() method.
    • Cross-platform support (Windows, Linux, macOS).
    • Compatibility with all stable open62541 versions ≥ v1.0.
  2. How type conversion works in open62541pp

    master

    open62541pp uses the opcua::TypeConverter struct to automatically convert between C++ types and native OPC UA UA_* types.

    Automatic Conversions

    Many common types are handled automatically. For example, opcua::Variant::assign can accept primitive types directly, but complex types like std::string require explicit conversion or passing the object directly rather than a pointer to avoid compilation errors.

    Extending Type Conversion

    You can add support for custom types by providing template specializations for opcua::TypeConverter. You must implement fromNative and toNative methods.

    namespace opcua {
    
    template <>
    struct TypeConverter<std::byte> {
        using NativeType = UA_Byte;
        [[nodiscard]] static std::byte fromNative(const UA_Byte& src) { /* implementation */ }
        [[nodiscard]] static UA_Byte toNative(const std::byte& src) { /* implementation */ }
    };
    
    }
    namespace opcua {
    
    template <>
    struct TypeConverter<std::byte> {
        using NativeType = UA_Byte;
        [[nodiscard]] static std::byte fromNative(const UA_Byte& src) { /* ... */ }
        [[nodiscard]] static UA_Byte toNative(const std::byte& src) { /* ... */ }
    };
    
    }
  3. How wrapper types work in open62541pp

    master

    Wrapper types in open62541pp provide a modern C++ interface to native open62541 objects. They are designed to maintain full compatibility with native types while adding features like RAII (Resource Acquisition Is Initialization) and pointer interconvertibility. All wrapper classes are based on the opcua::Wrapper base class.

    opcua::DataValue wrapper; // Example of a wrapper type
  4. How exceptions propagate from user callbacks

    master

    Because callbacks are executed within the underlying open62541 C event loop (which does not support C++ exceptions), any exception thrown inside a user-defined callback is caught by opcua::detail::ExceptionCatcher.

    These exceptions are then re-thrown and propagated to the opcua::Client::run() or opcua::Server::run() calls. To handle errors occurring within callbacks, you must wrap your .run() call in a try-catch block.

    opcua::Client client;
    
    // define callback that throws an exception
    client.onSessionActivated([] { throw std::runtime_error("User exception!"); });
    
    client.connect("opc.tcp://localhost:4840");
    try {
        client.run();  // exceptions in callbacks are propagated to the run call
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
  5. How to combine TypeConverter and DataTypeBuilder

    master

    TypeConverter and DataTypeBuilder are orthogonal and can be used together.

    1. Layered Mapping: You can use a TypeConverter to map a C++ type (like std::filesystem::path) to a built-in OPC UA type (like UA_String), and then use DataTypeBuilder to define a structure that contains that field.

    2. Automatic Variant Construction: You can add an opcua::TypeRegistry specialization for your custom type. This allows template-deduced opcua::Variant construction to work without explicitly repeating the DataType argument every time.

    namespace opcua {
    
    template <>
    struct TypeRegistry<Point> {
        static const UA_DataType& getDataType() noexcept {
            return *getPointDataType().handle();
        }
    };
    
    } // namespace opcua
  6. Choosing between the Node API and the services namespace

    master

    Open62541pp provides two complementary APIs for interacting with the OPC UA address space. Choosing the right one depends on your error handling preference, whether you need batching, and your asynchronous requirements.

    The Node API

    opcua::Node is an object-oriented handle to a single node identified by an opcua::NodeId. It is designed for concise, sequential code.

    • Error Model: Throws opcua::BadStatus exceptions on failure.
    • Best for: Writing synchronous scripts, tools, or quick integrations where you are operating on individual nodes one at a time.

    The services namespace

    Functions in opcua::services mirror the OPC UA service set directly. They are free functions that accept either an opcua::Server or opcua::Client.

    • Error Model: Returns opcua::Result<T> and never throws.
    • Async Support: Full support via callbacks, futures, deferred execution, or custom completion tokens (asynchronous variants are suffixed with Async).
    • Best for: Composing error handling without exceptions, performing batched requests (multiple nodes in one round-trip), and using the full asynchronous model.
    // Node API example
    opcua::Node node{client, opcua::NodeId{1, 1000}};
    node.writeValue(opcua::Variant{42});
    auto value = node.readValue().to<int>();
    
    // services namespace example
    opcua::Result<opcua::Variant> result = opcua::services::readValue(client, id);
    if (result) {
        int v = result->to<int>();
    }
  7. How the asynchronous model and completion tokens work

    master

    Open62541pp uses an asynchronous model adapted from Boost Asio. Asynchronous operations accept a completion token as their final argument. This token determines how the caller receives the result of the operation.

    Commonly used tokens include:

    • Callback functions: The operation executes a provided function object upon completion.
    • opcua::useFuture: Returns a std::future<T> to allow synchronous-style waiting via .get().
    • opcua::useDeferred: Returns a callable object that allows you to delay the start of the operation and specify a completion handler later.
    • opcua::useDetached: Starts the operation but provides no way to track its result or completion.
    • Custom tokens: Users can define their own behavior by specializing the opcua::AsyncResult trait.

    Important: Callbacks are executed within the client's or server's event loop. Do not block the event loop inside a callback (e.g., by waiting for other asynchronous results), as this will halt the execution of the entire loop.

    // Example of the general pattern for an async operation
    template <typename CompletionToken>
    auto opcua::services::readAsync(
        opcua::Client& connection,
        const opcua::ReadRequest& request,
        CompletionToken&& token
    );
  8. Define new composite OPC UA types using DataTypeBuilder

    master

    Use opcua::DataTypeBuilder to define brand-new composite types (structures, unions, or enumerations) that have their own OPC UA type identity and binary encoding. This is required when the server exposes a structured type that the client must deserialize.

    Struct Layout Requirements

    C++ structs used with DataTypeBuilder must have a C-compatible memory layout:

    • No virtual functions or inheritance.
    • No standard library containers (e.g., std::vector, std::string). For dynamic arrays, use the open62541 convention: a size_t count field immediately followed by a T* pointer field.
    • Nullable pointer fields are supported; a nullptr indicates the field is absent.

    Registering Types

    Describe the binary layout using the builder. The resulting opcua::DataType object must be kept alive for the lifetime of the server or client.

    Note on NodeIds: The two NodeId arguments in create* methods are the type's node ID and its binary encoding node ID. These must match the IDs used in the server's address space.

    struct Point { float x, y, z; };
    
    const opcua::DataType& getPointDataType() {
        static const opcua::DataType dt = 
            opcua::DataTypeBuilder<Point>::createStructure("Point", {1, 4242}, {1, 1})
                .addField<&Point::x>("x")
                .addField<&Point::y>("y")
                .addField<&Point::z>("z")
                .build();
        return dt;
    }
  9. Store and read custom DataType values

    master

    When working with custom types in opcua::Variant, follow these patterns:

    Writing: Always pass the opcua::DataType when constructing the opcua::Variant to ensure correct encoding.

    Reading: Inspect the type using .isType(getYourDataType()) before casting the data.

    Arrays: Arrays of custom types are stored as arrays of opcua::ExtensionObject. While scalars are unwrapped transparently, arrays require manual iteration over the ExtensionObject elements.

    // Writing
    const Point p{1.0f, 2.0f, 3.0f};
    opcua::Variant var{p, getPointDataType()};
    node.writeValue(var);
    
    // Reading
    opcua::Variant var = node.readValue();
    if (var.isType(getPointDataType())) {
        const auto* p = static_cast<const Point*>(var.data());
    }
  10. Build and install open62541pp from source

    master

    Follow these steps to clone, build, test, and install the library using CMake. It is recommended to use --recursive when cloning to ensure submodules are included.

    # clone repository
    git clone --recursive https://github.com/open62541pp/open62541pp.git
    cd open62541pp
    
    # build
    mkdir build
    cd build
    cmake -DCMAKE_BUILD_TYPE=Release -DUAPP_BUILD_EXAMPLES=ON -DUAPP_BUILD_TESTS=ON ..
    cmake --build .                   # single-configuration generator like Make or Ninja
    cmake --build . --config Release  # multi-configuration generator like Visual Studio, Xcode
    
    # run tests
    ctest --output-on-failure
    
    # install to system
    cmake --install .
  11. Register custom DataTypes with a Server or Client

    master

    Before starting a server or connecting a client, you must register all custom type descriptions via the configuration object to ensure the stack understands the custom binary layouts.

    // For a Server
    opcua::Server server;
    server.config().addCustomDataTypes({getPointDataType(), getColorDataType()});
    
    // For a Client
    opcua::Client client;
    client.config().addCustomDataTypes({getPointDataType(), getColorDataType()});
    client.connect("opc.tcp://localhost:4840");