Quick CoAP Multicast Discovery Example

Using the CoAP client for a multicast resource discovery with a Qt Quick user interface.

The Quick CoAP Multicast Discovery Example demonstrates how to register QCoapClient as a QML type and use it in a Qt Quick application for CoAP multicast resource discovery.

Note: Qt CoAP does not provide a QML API in its current version. However, you can make the C++ classes of the module available to QML as shown in this example.

Running the Example

To run the example application, you first need to set up and start at least one CoAP server supporting multicast resource discovery. You have the following options:

  • Manually build and run CoAP servers using libcoap, Californium, or any other CoAP server implementation, which supports multicast and resource discovery features.
  • Use the ready Docker image available at Docker Hub, which builds and starts CoAP server based on Californium's multicast server example.
Using the Docker-based Test Server

The following command pulls the docker container for the CoAP server from the Docker Hub and starts it:

docker run --name coap-multicast-server -d --rm --net=host sokurazy/coap-multicast-test-server:californium.2.0.x

Note: You can run more than one multicast CoAP servers (on the same host or other hosts in the network) by passing a different --name to the command above.

Creating a Client and Using It with QML

We create the QmlCoapMulticastClient class with the QCoapClient class as a base class:

class QmlCoapMulticastClient : public QCoapClient
{
    Q_OBJECT

public:
    QmlCoapMulticastClient(QObject *parent = nullptr);

    Q_INVOKABLE void discover(const QString &host, int port, const QString &discoveryPath);
    Q_INVOKABLE void discover(QtCoap::MulticastGroup group, int port, const QString &discoveryPath);

Q_SIGNALS:
    void discovered(const QmlCoapResource &resource);
    void finished(int error);

public slots:
    void onDiscovered(QCoapResourceDiscoveryReply *reply, const QVector<QCoapResource> &resources);
};

In the main.cpp file, we register the QmlCoapMulticastClient class as a QML type:

    qmlRegisterType<QmlCoapMulticastClient>("CoapMulticastClient", 1, 0, "CoapMulticastClient");

We also register the QtCoap namespace, to be able to use it in QML code:

    qmlRegisterUncreatableMetaObject(QtCoap::staticMetaObject, "qtcoap.example.namespace", 1, 0,
                                     "QtCoap", "Access to enums is read-only");

Now in the QML code, we can import and use these types:

    ...
import CoapMulticastClient 1.0
import qtcoap.example.namespace 1.0
    ...
    CoapMulticastClient {
        id: client
        onDiscovered: addResource(resource)

        onFinished: {
            statusLabel.text = (error === QtCoap.Error.Ok)
                    ? qsTr("Finished resource discovery.")
                    : qsTr("Resource discovery failed with error code: %1").arg(error)
        }

        onError:
            statusLabel.text = qsTr("Resource discovery failed with error code: %1").arg(error)
    }
    ...

The QCoapClient::error() signal triggers the onError signal handler of CoapMulticastClient, and the QmlCoapMulticastClient::finished() signal triggers the onFinished signal handler, to show the request's status in the UI. Note that we are not using the QCoapClient::finished() signal directly, because it takes a QCoapReply as a parameter (which is not a QML type), and we are interested only in the error code.

In the QmlCoapMulticastClient's constructor, we arrange for the QCoapClient::finished() signal to be forwarded to the QmlCoapMulticastClient::finished() signal:

QmlCoapMulticastClient::QmlCoapMulticastClient(QObject *parent)
    : QCoapClient(QtCoap::SecurityMode::NoSecurity, parent)
{
    connect(this, &QCoapClient::finished, this,
            [this](QCoapReply *reply) {
                    if (reply)
                        emit finished(static_cast<int>(reply->errorReceived()));
                    else
                        qCWarning(lcCoapClient, "Something went wrong, received a null reply");
            });
}

When the Discover button is pressed, we invoke one of the overloaded discover() methods, based on the selected multicast group:

    ...
        Button {
            id: discoverButton
            text: qsTr("Discover")
            Layout.columnSpan: 2

            onClicked: {
                var currentGroup = groupComboBox.model.get(groupComboBox.currentIndex).value;

                var path = "";
                if (currentGroup !== - 1) {
                    client.discover(currentGroup, parseInt(portField.text),
                                    discoveryPathField.text);
                    path = groupComboBox.currentText;
                } else {
                    client.discover(customGroupField.text, parseInt(portField.text),
                                    discoveryPathField.text);
                    path = customGroupField.text + discoveryPathField.text;
                }
                statusLabel.text = qsTr("Discovering resources at %1...").arg(path);
            }
        }
    ...

This overload is called when a custom multicast group or a host address is selected:

void QmlCoapMulticastClient::discover(const QString &host, int port, const QString &discoveryPath)
{
    QUrl url;
    url.setHost(host);
    url.setPort(port);

    QCoapResourceDiscoveryReply *discoverReply = QCoapClient::discover(url, discoveryPath);
    if (discoverReply) {
        connect(discoverReply, &QCoapResourceDiscoveryReply::discovered,
                this, &QmlCoapMulticastClient::onDiscovered);
    } else {
        qCWarning(lcCoapClient, "Discovery request failed.");
    }
}

And this overload is called when one of the suggested multicast groups is selected in the UI:

void QmlCoapMulticastClient::discover(QtCoap::MulticastGroup group, int port,
                                      const QString &discoveryPath)
{
    QCoapResourceDiscoveryReply *discoverReply = QCoapClient::discover(group, port, discoveryPath);
    if (discoverReply) {
        connect(discoverReply, &QCoapResourceDiscoveryReply::discovered,
                this, &QmlCoapMulticastClient::onDiscovered);
    } else {
        qCWarning(lcCoapClient, "Discovery request failed.");
    }
}

The QCoapClient::discovered() signal delivers a list of QCoapResources, which is not a QML type. To make the resources available in QML, we forward each resource in the list to the QmlCoapMulticastClient::discovered() signal, which takes a QmlCoapResource instead:

void QmlCoapMulticastClient::onDiscovered(QCoapResourceDiscoveryReply *reply,
                                          const QVector<QCoapResource> &resources)
{
    Q_UNUSED(reply)
    for (auto resource : resources)
        emit discovered(resource);
}

QmlCoapResource is a wrapper around QCoapResource, to make some of its properties available in QML:

class QmlCoapResource : public QCoapResource
{
    Q_GADGET
    Q_PROPERTY(QString title READ title)
    Q_PROPERTY(QString host READ hostStr)
    Q_PROPERTY(QString path READ path)

public:
    QmlCoapResource() : QCoapResource() {}
    QmlCoapResource(const QCoapResource &resource)
        : QCoapResource(resource) {}

    QString hostStr() const { return host().toString(); }
};

The discovered resources are added to the resourceModel of the list view in the UI:

    ...
    function addResource(resource) {
        resourceModel.insert(0, {"host" : resource.host,
                                 "path" : resource.path,
                                 "title" : resource.title})
    }
    ...

Files:

© 2024 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.