快捷CoAP多播发现

使用CoAP客户端和Qt Quick用户界面进行多播资源发现。

快捷CoAP多播发现示例演示了如何将QCoapClient注册为QML类型,并在Qt Quick应用程序中用于CoAP多播资源发现。

注意:Qt CoAP在其当前版本中不提供QML API。但是,您可以根据此示例使模块的C++类对QML可用。

运行示例

要从Qt Creator中运行示例,请打开欢迎模式,然后从示例中选择示例。有关更多信息,请访问构建和运行示例

设置CoAP服务器

要运行示例应用程序,您首先需要设置并至少启动一个支持多播资源发现的CoAP服务器。您有以下选择

  • 手动使用libcoapCalifornium或其他任何支持多播和资源发现功能的CoAP服务器实现来构建和运行CoAP服务器。
  • 使用Docker Hub上可用的预构建Docker映像,它基于Californium的多播服务器示例构建并启动CoAP服务器。
使用基于Docker的测试服务器

以下命令从Docker Hub拉取CoAP服务器的docker容器并启动它

docker run --name coap-multicast-server -d --rm --net=host tqtc/coap-multicast-test-server:californium-2.0.0

注意:您可以通过在上述命令中传递不同的--name来运行多个多播CoAP服务器(在同一主机上或其他主机上的网络中)。

在使用完毕后终止docker容器,首先通过执行docker ps命令获取容器的ID。输出将类似于以下内容

$ docker ps
CONTAINER ID   IMAGE
8b991fae7789   tqtc/coap-multicast-test-server:californium-2.0.0

之后,使用此ID来停止容器

docker stop <container_id>

将C++类公开给QML

在此示例中,您需要将QCoapResourceQCoapClient类,以及QtCoap命名空间,公开给QML。为了实现这一点,请创建自定义封装类并使用特殊的注册宏

创建一个QmlCoapResource类,作为QCoapResource的封装。使用Q_PROPERTY宏使得多个属性可以从QML访问。该类不需要直接从QML中实例化,因此请使用QML_ANONYMOUS宏来注册它。

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

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

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

之后,创建以 QCoapClient 类为基类的 QmlCoapMulticastClient 类。使用 Q_PROPERTY 宏来公开自定义属性,并创建几个 Q_INVOKABLE 方法。属性和可调用方法都可以从 QML 访问。与 QmlCoapResource 不同,您希望能够从 QML 创建此类,因此使用 QML_NAMED_ELEMENT 宏在 QML 中注册类。

class QmlCoapMulticastClient : public QCoapClient
{
    Q_OBJECT

    Q_PROPERTY(bool isDiscovering READ isDiscovering NOTIFY isDiscoveringChanged)

    QML_NAMED_ELEMENT(CoapMulticastClient)
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_INVOKABLE void stopDiscovery();

    bool isDiscovering() const;

Q_SIGNALS:
    void discovered(const QmlCoapResource &resource);
    void finished(int error);
    // The bool parameter is not provided, because the signal is only used by
    // the QML property system, and it does not use the passed value anyway.
    void isDiscoveringChanged();

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

private:
    QCoapResourceDiscoveryReply *m_reply = nullptr;
};

最后,注册 QtCoap 命名空间,以便您可以使用那里提供的枚举

namespace QCoapForeignNamespace
{
    Q_NAMESPACE
    QML_FOREIGN_NAMESPACE(QtCoap)
    QML_NAMED_ELEMENT(QtCoap)
}

调整构建文件

为了将自定义类型从 QML 访问,相应地更新构建系统文件。

CMake 构建过程

对于基于 CMake 的构建,将以下内容添加到 CMakeLists.txt

qt_add_qml_module(quickmulticastclient
    URI CoapClientModule
    VERSION 1.0
    SOURCES
        qmlcoapmulticastclient.cpp qmlcoapmulticastclient.h
    QML_FILES
        Main.qml
)
qmake 构建过程

对于 qmake 构建,按以下方式修改 quickmulticastclient.pro 文件

CONFIG += qmltypes
QML_IMPORT_NAME = CoapClientModule
QML_IMPORT_MAJOR_VERSION = 1
    ...
qml_resources.files = \
    qmldir \
    Main.qml

qml_resources.prefix = /qt/qml/CoapClientModule

RESOURCES += qml_resources

使用新的 QML 类型

现在,当 C++ 类已正确公开给 QML 时,您可以使用新类型

CoapMulticastClient {
    id: client
    onDiscovered: (resource) => { root.addResource(resource) }

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

QmlCoapMulticastClient::finished() 信号触发 onFinished 信号处理器,在 UI 中显示请求的状态。请注意,示例没有直接使用 QCoapClient 的信号,因为 error() 和 finished() 信号都接受一个 QCoapReply 作为参数(该参数未公开给 QML),而示例只需要错误代码。

QmlCoapMulticastClient 的构造函数将 QCoapClient 的信号传递给 QmlCoapMulticastClient::finished() 信号

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()));
                    reply->deleteLater();
                    if (m_reply == reply) {
                        m_reply = nullptr;
                        emit isDiscoveringChanged();
                    }
                } else {
                    qCWarning(lcCoapClient, "Something went wrong, received a null reply");
                }
            });

    connect(this, &QCoapClient::error, this,
            [this](QCoapReply *, QtCoap::Error err) {
                emit finished(static_cast<int>(err));
            });
}

当按下 Discover 按钮时,将基于选择的组播组调用重载的 discover() 方法

Button {
    id: discoverButton
    text: client.isDiscovering ? qsTr("Stop Discovery") : qsTr("Discover")
    Layout.preferredWidth: 100

    onClicked: {
        if (client.isDiscovering) {
            client.stopDiscovery()
        } else {
            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);
        }
    }
}

当选择自定义组播组或主机地址时调用此重载

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

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

当在 UI 中选择建议的组播组之一时调用此重载

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

QCoapResourceDiscoveryReply::discovered() 信号传递一个 QCoapResource 列表,这不是 QML 类型。为了在 QML 中使资源可用,将列表中的每个资源转发到 QmlCoapMulticastClient::discovered() 信号,该信号接受一个 QmlCoapResource

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

发现资源被添加到 UI 中列表视图的 resourceModel

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

在发现过程中,按下 Stop Discovery 按钮停止发现。内部通过中止当前请求来实现

void QmlCoapMulticastClient::stopDiscovery()
{
    if (m_reply)
        m_reply->abortRequest();
}

文件

© 2024 Qt 公司有限公司。此处包含的文档贡献是各自所有者的版权。本提供的文档是根据自由软件基金会发布的 GNU 自由文档许可证版本 1.3 术语许可的。Qt 和相应的商标是芬兰及其它国家/地区的 Qt 公司有限公司的 商标。所有其他商标均为各自所有者的财产。