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
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.