Previously we modified the example application published on Qt Company’s blog to build on Ubuntu 16.04 LTS. Next, we will show what I believe to be an improved implementation using RTI Connext Modern C++. Although this example features RTI’s commercial implementation of DDS, both the API and on-the-wire representation of data are specified by OMG standards. The former as DDS (Data Distribution Service), the latter RTPS (Real Time Publish Subscribe).
As an open standard, there are a number of implementations to pick and choose from. OpenDDS, available on GitHub, being the easiest to get ahold of. Several commercial implementations exist by vendors such as ADLINK / PrismTech, TwinOaks Computing, and RTI. Some even offer a “community edition” for open source projects. Only small changes to the qmake project file should be neccessary to port to another DDS implementations. As such, I would encourage anyone familiar with (and having access to) these other implementations to fork the GitHub project and test this assumption.
The API is defined for popular languages such as Java, C, C++, C#, as well as less popular ones such as Ada. The example as written by the folks at Qt Company demonstrates what RTI calls the “Traditional C++ API”. It’s very much “C-with-classes”, throwing around pointers, getting return codes, manually allocating memory. In this blog post we will migrate the examples to the C++ PSM (Platform Specific Module), the characteristics of which are similar to Qt C++. Entities such as participants, publishers, subscribers are now all reference types, while the generated classes from IDL are value types.
To demonstrate how this all impacts application code, I’ve further modified the GitHub fork of the IoT blog example. Changes pertinent to this post can be found at the following GitHub release. The first thing you’re likely to notice, no more mucking about with pointers and memory allocation! DDS entities can be created as shown here, and passed around by reference. We can make use of retain / release, similar to Objective C / Cocoa on iOS, to keep entities around even if they fall out of scope.
const DDS_DomainId_t domainId = 0;
dds::domain::DomainParticipant participant(domainId);
if (dds::core::null == participant) {
qDebug() << "Could not create participant.";
return -1;
}
dds::pub::Publisher publisher(participant);
if (dds::core::null == publisher) {
qDebug() << "Could not create publisher.";
return -2;
}
std::string topic_name = "Sensor Information";
dds::topic::Topic<sensor_information> topic(participant, topic_name);
if (dds::core::null == topic) {
qDebug() << "Could not create topic.";
return -3;
}
dds::pub::DataWriter<sensor_information> writer(publisher, topic);
if (dds::core::null == writer) {
qDebug() << "Could not create writer.";
return -4;
}
Generated types on the other hand, are now treated as value types. Member variables are modified using accessor methods.
// Create real data
sensor_information sensorInformation;
sensorInformation.ID("Foobidoo");
sensorInformation.ambientTemperature(12.);
// As sensors have a unique ID, register each sensor to reuse data
dds::core::InstanceHandle sensorHandle = writer.register_instance(sensorInformation);
// Send data twice within a one second interval
writer.write(sensorInformation, sensorHandle);
I cheated a tiny bit here, making use of an RTI specific function. When porting to other implementations of DDS, I’m sure a std::this_thread::sleep_for()
will suffice.
dds::core::Duration send_period{1,0};
rti::util::sleep(send_period);
Another nice quality of life improvement, generated types implement the stream operator, such that you can cout the contents of a sample. In the case of the blog example, this simplified the qDebug() portion.
QDebug operator<<(QDebug d, const sensor_information &sensor)
{
std::ostringstream stream;
stream << sensor;
d << "Sensor( " << QString::fromStdString(stream.str()) << ")";
return d;
}
When it came to modifying the QML example, I skipped implementing error checking for the sake of showing a greatly simplified initialization. A 50 line init method is replaced by 6 additional lines in the constructor. In production code, it would be desirable to test created entities against dds::core::null
to see if they were succesfully constructed. Though not demonstrated here, the Modern C++ API will now also throw exceptions for certain illegal operations. I leave it as an exercise to the reader to investigate the relevant documentation.
SensorInformation::SensorInformation(QObject *parent) :
QObject(parent),
m_participant(domainId),
m_publisher(m_participant),
m_topic(m_participant, "Sensor Information"),
m_writer(m_publisher, m_topic)
{
// Create real data
m_id = QUuid::createUuid().toString().mid(1, 8);
m_info.ID(m_id.toStdString());
}
I think you’ll agree the migrated application code is not only an easier read, but far less risky to implement. If you have time, pull down the code and give it a go. It works on my machine, as the tired refrain goes. Having demonstrated the RTI Modern C++ API, aka DDS PSM C++, I’ve got one more post and code change I’d like to make before leaving well enough alone.