Mar 31

Playing with QML and DBus (Part 2)

second part of the QML/DBus integration tutorial

In part 1 I showed how to modify the recipes.qml example in order to show contacts instead of recipes, and a basic skeleton to provide a ListModel with contact data from QT/C++ space.

In part 2, I am going to show how to retrieve this contact data from a DBus-backed application, Wader in this case, and the required steps to make it work.

Wader, ModemManager and Betavine Connection Manager

Wader is a 3G daemon accessible via DBus, written in Python and released under the GPLv2. Wader runs on Linux and OSX and supports more than fifty heterogeneous devices. Wader also happens to implement the ModemManager API.

ModemManager is a joint effort between the NetworkManager and Wader projects to produce a modern 3G DBus API that can be reused by other applications of the Unix Desktop. You can have a look at the ModemManager specification to get a glimpse of it.

Betavine Connection Manager is the third generation of Vodafone's open source mobile connect project, hosted at Betavine's site. BCM 3.0 is built upon Wader core, with a brand new user interface and completely integrated in the Linux desktop.

ModemManager internals

ModemManager is a heavy user of DBus interfaces, we are going to interact with just four of the ModemManager specification:

First we need to get a list with the object paths of the devices registered in the system. To do so we will call EnumerateDevices, this will return an array of object paths. We will use the first element in this array for the rest of the operations.

Once we have the object path, we need to setup the device with the Enable method. This method accepts a boolean indicating whether to enable or disable the device. This method may be interrupted if PIN authentication is enabled, for simplicity we are not going to handle possible SimPinRequired errors. So make sure that authentication is disabled, or else send the PIN with minicom beforehand.

With the device enabled, we are just two calls away from finishing! We are going to get the list of contacts and messages, and we will correlate them before populating our classes. We tried to provide a set of common operations in ModemManager, and the Contacts and Sms APIs are the best examples. Both provide a List method that will return the contacts or messages list.

How to wrap a DBus daemon with QT

Wrapping a DBus daemon with QT could not be any easier thanks to qdbusxml2cpp. This tool will parse the ModemManager interfaces that we are going to use and will generate the necessary boilerplate to make calls to remote objects. First things first, create the following files in ~/devel/git/contacts:

org.freedesktop.ModemManager.xml

<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
        "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/org/freedesktop/ModemManager">
    <interface name="org.freedesktop.ModemManager">
        <method name="EnumerateDevices">
            <annotation name="com.trolltech.QtDBus.QtTypeName.Out0" value="QList<QDBusObjectPath>"/>
            <arg name="devicesList" type="ao" direction="out" />
        </method>
    </interface>
</node>

org.freedesktop.ModemManager.Modem.xml

<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
        "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/">
    <interface name="org.freedesktop.ModemManager.Modem">
        <method name="Enable">
            <arg name="enable" type="b" direction="in" />
        </method>
    </interface>
</node>

org.freedesktop.ModemManager.Modem.Gsm.Contacts.xml

<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
        "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/">
    <interface name="org.freedesktop.ModemManager.Modem.Gsm.Contacts">
        <method name="List">
            <annotation name="com.trolltech.QtDBus.QtTypeName.Out0" value="QList<ContactStruct>"/>
            <arg name="contactList" type="a(uss)" direction="out" />
        </method>
    </interface>
</node>

org.freedesktop.ModemManager.Modem.Gsm.Sms.xml

<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
        "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/">
    <interface name="org.freedesktop.ModemManager.Modem.Gsm.Sms">
        <method name="List">
            <annotation name="com.trolltech.QtDBus.QtTypeName.Out0" value="QList<QVariantMap>"/>
            <arg name="smsList" type="a{sv}" direction="out" />
        </method>
    </interface>
</node>

This files follow the DBus introspection format and describe the methods available in each interface (only the ones we are going to use actually) and their arguments. If the type is a generic one, we can just use the standard DBus syntax -i.e. 'b' for bool. If on the other hand the response has a custom type, we need to provide an annotation for it with its type plus some boilerplate code so that the QT type system is able to transform in runtime a DBus response to a QT struct/class. We will revisit this point later on.

Now we are going to create a script that will generate the necessary code to access the ModemManager service. For each of the four interfaces that we are going to interact with, we need to create a proxy to it. The code bellow generates the static code necessary to access them.

generate-interfaces.sh

#!/bin/sh

set -e

qdbusxml2cpp -N -c ModemManagerProxy \
             -p modemmanager_generic.h:modemmanager_generic.cpp \
             org.freedesktop.ModemManager.xml \
             org.freedesktop.ModemManager

qdbusxml2cpp -N -c ModemManagerModemProxy \
             -p modemmanager_modem.h:modemmanager_modem.cpp \
             org.freedesktop.ModemManager.Modem.xml \
             org.freedesktop.ModemManager.Modem

qdbusxml2cpp -N -c ModemManagerContactsProxy \
             -p modemmanager_contacts.h:modemmanager_contacts.cpp \
             org.freedesktop.ModemManager.Modem.Gsm.Contacts.xml \
             org.freedesktop.ModemManager.Modem.Gsm.Contacts

qdbusxml2cpp -N -c ModemManagerSmsProxy \
             -p modemmanager_sms.h:modemmanager_sms.cpp \
             org.freedesktop.ModemManager.Modem.Gsm.Sms.xml \
             org.freedesktop.ModemManager.Modem.Gsm.Sms

This commands generate a class with the -c switch (first argument), contained in a .cpp and .h files (second argument), by reading the given interface description (third argument) and finally the name of the accessed interface as fourth argument. Note the initial switch -N that produces classes with no namespaces, we had to use it as it would not work otherwise. I think it has to do with the different "depths" of the interfaces used here. Check out qdbusxml2cpp documentation for more details.

Add this new files to contacts.pro:

TEMPLATE = app
TARGET = contacts
DEPENDPATH += .
INCLUDEPATH += .
CONFIG += qdbus
QT += declarative

# Input
SOURCES += contact.cpp modemmanager_generic.cpp \
           modemmanager_modem.cpp modemmanager_contacts.cpp \
           modemmanager_sms.cpp main.cpp
HEADERS += contact.h modemmanager_generic.h modemmanager_modem.h \
           modemmanager_contacts.h modemmanager_sms.h
RESOURCES += contacts.qrc
sources.files = $$SOURCES $$HEADERS $$RESOURCES contacts.pro contacts.qml
sources.path = .
target.path = .

INSTALLS += sources target

Now add the modemmanager_* headers to main.cpp, generate the interfaces and make sure it compiles after this last changes:

chmod +x generate-interfaces.sh
./generate-interfaces.sh
/usr/local/Trolltech/Qt-4.7.0/bin/qmake contacts.pro
make

We are about to access a DBus daemon written in Python, from a small C++ binary that will in turn show the results in a QML application. Interesting mix isn't it?

We need to obtain the object path of the device first, to do so add this in your main.cpp (right after the QApplication declaration):

ModemManagerProxy *proxy = new ModemManagerProxy("org.freedesktop.ModemManager",
                                                 "/org/freedesktop/ModemManager",
                                                 QDBusConnection::systemBus(),
                                                 static_cast<QObject*>(&app));

QDBusObjectPath opath;
QDBusPendingReply<QList<QDBusObjectPath> > reply = proxy->EnumerateDevices();
reply.waitForFinished();
if (reply.isError()) {
    qDebug() << reply.error();
    app.exit(1);
} else {
    opath = reply.value().first();
    qDebug() << opath.path();
}

ModemManagerModemProxy *modem_proxy = new ModemManagerModemProxy(
                                                 "org.freedesktop.ModemManager",
                                                 opath.path(),
                                                 QDBusConnection::systemBus(),
                                                 proxy);
QDBusPendingReply<> modem_reply = modem_proxy->Enable(true);
modem_reply.waitForFinished();
if (modem_reply.isError()) {
    qDebug() << "Error enabling device: " << modem_reply.error();
    app.exit(1);
}

This code instantiates a new object, proxy which is a pointer to the ModemManagerProxy class we've just created in generate-interfaces.sh. The constructor receives the service and object path of the object as first and second arguments, and the system bus is used. Ignore the fourth argument for now.

We call EnumerateDevices, check that the reply has no errors, and we store the first value of the response in the opath variable. This object path identifies our device in ModemManager and thus will be used in the rest of the tutorial.

The next step is to enable the device with Enable, ModemManagerModemProxy is instantiated and receives the object path of the device this time. Enable does not return anything, so we only check for errors in the reply object.

At this point we have an enabled device ready to receive operations, guess what's next? Yep, retrieving the list of contacts and messages, correlate them and populate the data model with it. Add the following code before the main function:

QList<QObject*> GetContacts(QDBusObjectPath opath)
{
    ModemManagerContactsProxy *cts_proxy = new ModemManagerContactsProxy(
                                                 "org.freedesktop.ModemManager",
                                                 opath.path(),
                                                 QDBusConnection::systemBus(),
                                                 0);
    ModemManagerSmsProxy *sms_proxy = new ModemManagerSmsProxy(
                                                 "org.freedesktop.ModemManager",
                                                 opath.path(),
                                                 QDBusConnection::systemBus(),
                                                 0);

    QList<QObject*> ret;

    QList<ContactStruct> contactList;
    QDBusPendingReply<QList<ContactStruct> > cts_reply = cts_proxy->List();
    cts_reply.waitForFinished();
    if (cts_reply.isError()) {
        qDebug() << cts_reply.error();
        return ret;
    } else {
        contactList = cts_reply.value();
    }

    QList<QVariantMap> messagesList;
    QDBusPendingReply<QList<QVariantMap> > sms_reply = sms_proxy->List();
    sms_reply.waitForFinished();
    if (sms_reply.isError()) {
        qDebug() << sms_reply.error();
        return ret;
    } else {
        messagesList = sms_reply.value();
    }

    const QString picture = QString("qrc:/content/pics/person.png");

    foreach(ContactStruct contact, contactList) {
        QList<QString> smsList;
        foreach(QVariantMap sms, messagesList) {
            if (sms.value("number").toString().endsWith(contact.number)) {
                smsList.append(sms.value("text").toString());
            }
        }
        QString messages = "<html><ul>";
        if (!smsList.size()) {
            messages.append("<li>No messages!</li>");
        } else {
            foreach(QString sms, smsList) {
                messages.append(QString("<li>%1</li>").arg(sms));
            }
        }
        messages.append("</ul></html>");
        ret.append(new Contact(contact.name, contact.number, messages, picture));
    }

    delete cts_proxy;
    delete sms_proxy;

    return ret;
}

This method accepts the object path of a device, obtains the list of contacts and messages, correlates them and returns a QList<QObject *> object with the data. If you try to compile it as it is, it will fail by several reasons: First ContactStruct (Gsm.Contacts.List return value) is undefined, second we need to register it with the QT type system, and third we need to provide marshalling functions to serialize/deserialize ContactStruct from DBus.

Create the following file dbustypes.h and import it in main.cpp before the modemmanager_* ones:

#include <QtDBus>

struct ContactStruct {
     uint index;
     QString name;
     QString number;
};

Q_DECLARE_METATYPE(ContactStruct);
Q_DECLARE_METATYPE(QList<ContactStruct>);
Q_DECLARE_METATYPE(QList<QVariantMap>);

// Marshall the ContactStruct data into a D-BUS argument
QDBusArgument &operator<<(QDBusArgument &argument, const ContactStruct &mystruct)
{
     argument.beginStructure();
     argument << mystruct.index << mystruct.name << mystruct.number;
     argument.endStructure();
     return argument;
}

// Retrieve the ContactStruct data from the D-BUS argument
const QDBusArgument &operator>>(const QDBusArgument &argument, ContactStruct &mystruct)
{
     argument.beginStructure();
     argument >> mystruct.index >> mystruct.name >> mystruct.number;
     argument.endStructure();
     return argument;
}

Now, before ModemManagerProxy's instantiation, add this lines in main.cpp:

qDBusRegisterMetaType<ContactStruct>();
qDBusRegisterMetaType<QList<ContactStruct> >();
qDBusRegisterMetaType<QList<QVariantMap> >();

This will register the compound types that we are going to use in the DBus calls, without this lines the project will not compile.

At this point we are ready to hook GetContacts in main.cpp. Add after the modem_reply block the following lines:

QList<QObject*> dataList;
foreach(QObject* contact, GetContacts(opath)) {
    dataList.append(contact);
}

This will populate our data model with the DBus data, and we are ready to go:

make clean
rm -rf modemmanager_*
./generate-interfaces.sh
/usr/local/Trolltech/Qt-4.7.0/bin/qmake contacts.pro
make
./contacts

Here you can see the end result of part 2, best viewed with Chrome/Firefox 3.6:

This is the current source of the tutorial. Next (and last) part of the tutorial will look at retrieving this data at runtime from QML.