basysKom AnwendungsEntwicklung

Qt OPC UA – Data Type Code Generation
TLDR;
The type system of OPC UA permits the creation of complex and nested data types. With the merge of the generic struct decoding and encoding feature, the Qt OPC UA module has greatly improved the comfort of handling such types. But for large projects with lots of custom data types, its QVariant based interface might still feel a bit too complicated.

What if we could have C++ data classes for custom types that feel and handle like QOpcUaQualifiedName or QOpcUaEuInformation in Qt OPC UA?

We’ve had the same thought and decided to implement a code generator for Qt OPC UA that takes OPC UA .bsd files as input and generates C++ data classes and an encoder/decoder class based on QOpcUaBinaryDataEncoding. The generator application is called qopcuaxmldatatypes2cpp and has been added to the Qt OPC UA repository’s dev branch as an official tool.

How to use qopcuaxmldatatypes2cpp

The qopcuaxmldatatypes2cpp executable is automatically built as part of the Qt OPC UA module.

In order to generate code for your custom model, you need its .bsd file and the .bsd files of any other nodesets your model depends on if you use any of the struct or enum types defined there.

To demonstrate code generation with a simple Example, we have modelled a structure with optional field and a union, both having a member of the SignedSoftwareCertificate type defined in the official OPC UA Opc.Ua.Types.bsd file:

<opc:TypeDictionary xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tns="http://yourorganisation.org/BlogGeneratorTestModel/" DefaultByteOrder="LittleEndian" xmlns:opc="http://opcfoundation.org/BinarySchema/" xmlns:ua="http://opcfoundation.org/UA/" TargetNamespace="http://yourorganisation.org/BlogGeneratorTestModel/">
    <opc:Import Namespace="http://opcfoundation.org/UA/"/>
    <opc:StructuredType BaseType="ua:ExtensionObject" Name="MyTestStructWithOptionalField">
        <opc:Field TypeName="opc:CharArray" Name="MandatoryMember"/>
        <opc:Field TypeName="ua:SignedSoftwareCertificate" Name="OptionalMember"/>
    </opc:StructuredType>
    <opc:StructuredType BaseType="ua:Union" Name="MyTestUnion">
        <opc:Field TypeName="opc:UInt32" Name="SwitchField"/>
        <opc:Field SwitchField="SwitchField" TypeName="ua:LocalizedText" SwitchValue="1" Name="LocalizedTextMember"/>
        <opc:Field SwitchField="SwitchField" TypeName="ua:SignedSoftwareCertificate" SwitchValue="2" Name="SignedCertMember"/>
    </opc:StructuredType>
    <opc:EnumeratedType LengthInBits="32" Name="MyTestEnum">
        <opc:EnumeratedValue Name="Unknown" Value="0"/>
        <opc:EnumeratedValue Name="Option1" Value="1"/>
        <opc:EnumeratedValue Name="Option2" Value="2"/>
    </opc:EnumeratedType>
</opc:TypeDictionary> 

To generate code for these two types, we invoke the qopcuaxmldatatypes2cpp executable while passing our custom .bsd file as -i and the dependency file Opc.Ua.Types.bsd as -d. The parameter -p determines the prefix for the names of generated files, classes and namespaces.

qopcuaxmldatatypes2cpp -i ~/path/to/bloggeneratortestmodel.bsd -d /path/to/ua-nodeset/Schema/Opc.Ua.Types.bsd -o myProject/generated -p BlogDemo 

The generated code

The following files are generated by qopcuaxmldatatypes2cpp:
Name Purpose
blogdemobinarydeencoder.cpp Implementation of constructors and the getter for the QOpcUaBinaryDataEncoding
blogdemobinarydeencoder.h Template based decoder and encoder methods for the generated types
blogdemoenumerations.h Enum definition for MyTestEnum
blogdemomyteststructwithoptionalfield.cpp Implementation for MyTestStructWithOptionalField
blogdemomyteststructwithoptionalfield.h Header for MyTestStructWithOptionalField
blogdemomytestunion.cpp Implementation for MyTestUnion
blogdemomytestunion.h Header for MyTestUnion
blogdemosignedsoftwarecertificate.cpp Implementation for SignedSoftwareCertificate
blogdemosignedsoftwarecertificate.h Header for SignedSoftwareCertificate

Enumerations

Our custom enum type MyTestEnum becomes an enum class in a namespace. The namespace has the Q_NAMESPACE macro, which allows us to add Q_ENUM_NS. This automatically creates the ability to pretty-print the enum value with qDebug().

namespace BlogDemo {
Q_NAMESPACE

enum class MyTestEnum {
    Unknown = 0,
    Option1 = 1,
    Option2 = 2
};
Q_ENUM_NS(MyTestEnum)
}  

Structure without optional fields

The SignedSoftwareCertificate type is generated as a C++ class with constructors, destructor, equality operator and getter and setter methods for all fields. The QDebug streaming operator allows quick output of the field values on the terminal.

class BlogDemoSignedSoftwareCertificateData;
class BlogDemoSignedSoftwareCertificate
{
public:
    BlogDemoSignedSoftwareCertificate();
    BlogDemoSignedSoftwareCertificate(const BlogDemoSignedSoftwareCertificate &);
    BlogDemoSignedSoftwareCertificate &operator=(const BlogDemoSignedSoftwareCertificate &rhs);
    bool operator==(const BlogDemoSignedSoftwareCertificate &rhs) const;
    inline bool operator!=(const BlogDemoSignedSoftwareCertificate &rhs) const
        { return !(*this == rhs); }
    operator QVariant() const;
    ~BlogDemoSignedSoftwareCertificate();

    QByteArray certificateData() const;
    void setCertificateData(const QByteArray &certificateData);

    QByteArray signature() const;
    void setSignature(const QByteArray &signature);

    friend QDebug operator<<(QDebug debug, const BlogDemoSignedSoftwareCertificate &v);

private:
    QSharedDataPointer<BlogDemoSignedSoftwareCertificateData> data;
}; 

Structure with optional field

The StructWithOptionalField type only differs from SignedSoftwareCertificate’s structure by the additional pair of getter and setter methods to indicate if OptionalMember is set.

class BlogDemoMyTestStructWithOptionalFieldData;
class BlogDemoMyTestStructWithOptionalField
{
public:
    BlogDemoMyTestStructWithOptionalField();
    BlogDemoMyTestStructWithOptionalField(const BlogDemoMyTestStructWithOptionalField &);
    BlogDemoMyTestStructWithOptionalField &operator=(const BlogDemoMyTestStructWithOptionalField &rhs);
    bool operator==(const BlogDemoMyTestStructWithOptionalField &rhs) const;
    inline bool operator!=(const BlogDemoMyTestStructWithOptionalField &rhs) const
        { return !(*this == rhs); }
    operator QVariant() const;
    ~BlogDemoMyTestStructWithOptionalField();

    bool optionalMemberSpecified() const;
    void setOptionalMemberSpecified(const bool &optionalMemberSpecified);

    QString mandatoryMember() const;
    void setMandatoryMember(const QString &mandatoryMember);

    BlogDemoSignedSoftwareCertificate optionalMember() const;
    void setOptionalMember(const BlogDemoSignedSoftwareCertificate &optionalMember);

    friend QDebug operator<<(QDebug debug, const BlogDemoMyTestStructWithOptionalField &v);

private:
    QSharedDataPointer<BlogDemoMyTestStructWithOptionalFieldData> data;
}; 

Union

The MyTestUnion type has getter and setter methods for all possible union values along with an enum value getter that indicates which member of the union is set. The last setter used determines which field is set.

class BlogDemoMyTestUnionData;
class BlogDemoMyTestUnion
{
public:
    enum class SwitchField {
        None = 0,
        LocalizedTextMember = 1,
        SignedCertMember = 2
    };

    BlogDemoMyTestUnion();
    BlogDemoMyTestUnion(const BlogDemoMyTestUnion &);
    BlogDemoMyTestUnion &operator=(const BlogDemoMyTestUnion &rhs);
    bool operator==(const BlogDemoMyTestUnion &rhs) const;
    inline bool operator!=(const BlogDemoMyTestUnion &rhs) const
        { return !(*this == rhs); }
    operator QVariant() const;
    ~BlogDemoMyTestUnion();

    SwitchField switchField() const;

    QOpcUaLocalizedText localizedTextMember() const;
    void setLocalizedTextMember(const QOpcUaLocalizedText &localizedTextMember);

    BlogDemoSignedSoftwareCertificate signedCertMember() const;
    void setSignedCertMember(const BlogDemoSignedSoftwareCertificate &signedCertMember);

    friend QDebug operator<<(QDebug debug, const BlogDemoMyTestUnion &v);

private:
    QSharedDataPointer<BlogDemoMyTestUnionData> data;
}; 

Using the generated code

The following example shows how the generated classes can be used to decode and encode custom struct values and how to write an encoded value to a connected server.

#include <QDebug>

#include "blogdemomytestunion.h"
#include "blogdemomyteststructwithoptionalfield.h"
#include "blogdemobinarydeencoder.h"

int main(int argc, char **argv)
{
    // Demo the union
    BlogDemoMyTestUnion myUnion;
    myUnion.setLocalizedTextMember(QOpcUaLocalizedText("en", "Hello"));

    QOpcUaExtensionObject unionObj;
    BlogDemoBinaryDeEncoder unionEnc(unionObj);
    auto success = unionEnc.encode<BlogDemoMyTestUnion>(myUnion);

    qDebug() << "Success:" << success << "data:" << unionObj.encodedBody();

    // Set the cursor to the beginning of the extension object
    // and decode the data we just encoded
    unionEnc.binaryDataEncoding().setOffset(0);
    const auto decodedUnion = unionEnc.decode<BlogDemoMyTestUnion>(success);

    qDebug() << "Success:" << success << "content:" <<
        static_cast<qint32>(decodedUnion.switchField()) <<
        "value:" << decodedUnion;

    // Demo the struct with optional field
    BlogDemoMyTestStructWithOptionalField myOptional;
    myOptional.setMandatoryMember(QStringLiteral("Test string"));
    myOptional.setOptionalMemberSpecified(true);
    BlogDemoSignedSoftwareCertificate innerStruct;
    innerStruct.setCertificateData({ "Cert" });
    innerStruct.setSignature({ "Signature" });
    myOptional.setOptionalMember(innerStruct);

    QOpcUaExtensionObject optionalObj;
    BlogDemoBinaryDeEncoder optionalEnc(optionalObj);
    success = optionalEnc.encode<BlogDemoMyTestStructWithOptionalField>(myOptional);

    qDebug() << "Success:" << success << "data:" << optionalObj.encodedBody();

    // Set the cursor to the beginning of the extension object
    // and decode the data we just encoded
    optionalEnc.binaryDataEncoding().setOffset(0);
    const auto decodedOptional = optionalEnc.decode<BlogDemoMyTestStructWithOptionalField>(success);

    qDebug() << "Success:" << success << "value:" << decodedOptional;

    // To use the encoded struct in a request to the server, there are two additional steps
    // Set the encoding to ByteString
    optionalObj.setEncoding(QOpcUaExtensionObject::Encoding::ByteString);
    // Set the encoding id to the type's default binary encoding id used by the server
    optionalObj.setEncodingTypeId(QStringLiteral("ns=2;i=1234"));
    // Write the extension object to a node's value attribute
    // node->writeValueAttribute(optionalObj, QOpcUa::Types::ExtensionObject);
} 

Conclusion

Using the code generator in a Qt OPC UA based project with custom data types provides an easy to use interface to interact with struct values. It is no longer necessary to write custom encoder and decoder code or to implement own data classes.

To use custom types in service calls to the server is as easy as creating objects of the generated data classes, setting field values and encoding them into a QOpcUaExtensionObject using the generated encoder and decoder class. The QVariant() operator in QOpcUaExtensionObject makes the encoded data compatible with the QVariant based API for writing node attributes and passing input parameters for OPC UA method calls.

Extension objects delivered by the server as a node’s value attribute, an event field, a method call result or a historical value can be decoded by instantiating the generated encoder and decoder class for the QOpcUaExtensionObject returned by the Qt OPC UA API and then calling the decode() template function for the corresponding type.

While the new code generator is the most comfortable solution for projects with custom data types with an existing XML description, the generic struct handler has its place for applications that communicate with servers with unknown data models or without a formal XML description.

Jannis Völker

Jannis Völker

Jannis Völker is a software engineer at basysKom GmbH in Darmstadt. After joining basysKom in 2017, he has been working in connectivity projects for embedded devices, Azure based cloud projects and has made contributions to Qt OPC UA and open62541. He has a background in embedded Linux, Qt and OPC UA and holds a master's degree in computer science from the University of Applied Sciences in Darmstadt.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Weitere Blogartikel

basysKom Newsletter

We collect only the data you enter in this form (no IP address or information that can be derived from it). The collected data is only used in order to send you our regular newsletters, from which you can unsubscribe at any point using the link at the bottom of each newsletter. We will retain this information until you ask us to delete it permanently. For more information about our privacy policy, read Privacy Policy