Простое приложение дверного звонка Qt + MQTT

Так что я недавно построил дом и не хотел полагаться на «сомнительные» системы, такие как Google Home или Amazon, что угодно, поэтому я решил сам построить систему дверного звонка.

Аппаратное обеспечение

Я использую RPI4B + в качестве оборудования для дверного звонка и MQTT в качестве платформы связи между моим домашним сервером, оборудованием дверного звонка и моим приложением Android, которое состоит из трех компонентов.

  1. RPI4B + — мой уличный дверной звонок, включая веб-камеру RPI, простой микрофон и простой динамик
  2. Сервер Ubuntu — мой сервер MQTT
  3. Клиент Android — мое приложение дверного звонка (которое будет рассмотрено здесь)

Программное обеспечение — Клиент Android

Для разработки я использую Qt 5.15 с QtMQTT. Для передачи видео с веб-камеры я использую простой сервер RTSP.

У меня серьезные архитектурные проблемы в моем коде, которые вы скоро увидите.

Эта проблема

Мой клиент MQTT построен на QMqttClient класс, который я использую как в QML, так и в C ++. При срабатывании открытая дверь Сообщение MQTT, я хочу использовать тот же клиент, который я использую, объявленный в моем файле QML, но я просто не чувствую, что то, что я там делаю, является правильным, потому что я просто не могу использовать MQTT-клиент, объявленный QML, в мой код C ++ разумно.

Пожалуйста, дайте мне несколько отзывов об архитектуре моего кода и подскажите лучший способ получить здесь чистую архитектуру, заранее большое спасибо.

Код

DoorOpener.h

#ifndef DOOROPENER_H
#define DOOROPENER_H

#include <QObject>
#include <QByteArray>
#include <QMqttClient>

class DoorOpener : public QObject
{
    Q_OBJECT
public:
    DoorOpener(QObject *parent = nullptr);
    ~DoorOpener() = default;

public slots:
    bool open();

private:
    QMqttClient* _client;
    std::string _access_token {};

    void initializeWithMqtt();
};

#endif // DOOROPENER_H

DoorOpener.cpp

#include "DoorOpener.h"
#include "log.h"
#include "variables.h"

#include <QFile>
#include <QDebug>

DoorOpener::DoorOpener(QObject* parent)
{   
    Q_UNUSED(parent);

    initializeWithMqtt();
}

void DoorOpener::initializeWithMqtt()
{
    _client = new QMqttClient();
    _client->setHostname(hdvars::MQTT_HOSTNAME.c_str());
    _client->setPort(hdvars::MQTT_PORT);

    LOG_DBG("Initializing DoorOpener:");
    LOG_DBG("t - Host: " + _client->hostname().toStdString());
    LOG_DBG("t - Port: " + std::to_string(_client->port()));

    // TODO this is bad architecture. it should be safe to know when the connection is established. (see main.qml)
    if (_client->state() != QMqttClient::Connected) {
        _client->connectToHost();
    }

    connect(_client, &QMqttClient::stateChanged, [](QMqttClient::ClientState state) {
        LOG_INF("MQTT client state is now " + [&]() -> std::string {
            switch (state) {
            case QMqttClient::Disconnected:
                return ""Disconnected"";
            case QMqttClient::Connecting:
                return ""Connecting"";
            case QMqttClient::Connected:
                return ""Connected"";
            default:
                return "";
            }
        }());
    });

    if (QFile atFile { "access_token" }; atFile.open(QIODevice::ReadOnly)) {
        LOG_DBG("Reading access token.");
        QString token { atFile.readAll() };

        // we only care about the first line of the "access_token" file
        _access_token = token.split("n").first().toStdString();

        if (_access_token.empty()) {
            LOG_ERR("Unable to read access token.");
        }
        else {
            LOG_INF("Access token read.");
        }
    }
}

bool DoorOpener::open()
{
    if (_client->state() != QMqttClient::Connected) {
        LOG_ERR("Connect to MQTT client before calling open() function.");
        return false;
    }

    LOG_DBG("Trying to send MQTT message to server with topic " + hdvars::MQTT_TOPIC + ".");

    if (_access_token.empty()) {
        LOG_WRN("No access token provided.");
    }

    qint32 retval { _client->publish(QMqttTopicName(hdvars::MQTT_TOPIC.c_str()), QByteArray(_access_token.c_str())) };

    LOG_DBG("publish() returned " + std::to_string(retval) + ".");

    return 0 == retval;
}

QmlMqttClient.h

#ifndef QMLMQTTCLIENT_H
#define QMLMQTTCLIENT_H

#include <QtCore/QMap>
#include <QtMqtt/QMqttClient>
#include <QtMqtt/QMqttSubscription>

class QmlMqttClient;

class QmlMqttSubscription : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QMqttTopicFilter topic MEMBER m_topic NOTIFY topicChanged)
public:
    QmlMqttSubscription(QMqttSubscription *s, QmlMqttClient *c);
    ~QmlMqttSubscription();

Q_SIGNALS:
    void topicChanged(QString);
    void messageReceived(const QString &msg);

public slots:
    void handleMessage(const QMqttMessage &qmsg);

private:
    Q_DISABLE_COPY(QmlMqttSubscription)
    QMqttSubscription *sub;
    QmlMqttClient *client;
    QMqttTopicFilter m_topic;
};

class QmlMqttClient : public QMqttClient
{
    Q_OBJECT
public:
    QmlMqttClient(QObject *parent = nullptr);

    Q_INVOKABLE QmlMqttSubscription* subscribe();
private:
    Q_DISABLE_COPY(QmlMqttClient)
};

#endif // QMLMQTTCLIENT_H

QmlMqttClient.cpp

#include "QmlMqttClient.h"
#include "log.h"
#include "variables.h"

QmlMqttClient::QmlMqttClient(QObject *parent)
    : QMqttClient(parent)
{
    setHostname(hdvars::MQTT_HOSTNAME.c_str());
    setPort(hdvars::MQTT_PORT);
}

QmlMqttSubscription* QmlMqttClient::subscribe()
{
    LOG_INF("Initializing subscription with topic " + hdvars::MQTT_TOPIC + ".");

    QMqttSubscription* sub { QMqttClient::subscribe(QMqttTopicFilter(hdvars::MQTT_TOPIC.c_str()), 0) };
    QmlMqttSubscription* result { new QmlMqttSubscription(sub, this) };

    return result;
}

QmlMqttSubscription::QmlMqttSubscription(QMqttSubscription *s, QmlMqttClient *c)
    : sub(s)
    , client(c)
{
    connect(sub, &QMqttSubscription::messageReceived, this, &QmlMqttSubscription::handleMessage);
    m_topic = sub->topic();
}

QmlMqttSubscription::~QmlMqttSubscription()
{
}

void QmlMqttSubscription::handleMessage(const QMqttMessage &qmsg)
{
    emit messageReceived(qmsg.payload());
}

переменные.h

#ifndef VARIABLES_H
#define VARIABLES_H

#include <string>

namespace hdvars
{
static const std::string MQTT_HOSTNAME { "localhost" };
static const std::string MQTT_TOPIC { "my_topic/heimdall" };

static const std::uint32_t MQTT_PORT { 1883 };
}

#endif // VARIABLES_H

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>

#include "log.h"
#include "QmlMqttClient.h"
#include "DoorOpener.h"

int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif

    // TODO put all these log lines in a static function in log.h: initialize(std::string filename)
    // Logging configuration
    std::string const& logfile { "heimdall-client.log" };

    if (!freopen(logfile.c_str(), "a", stderr)) {
        // we can still write to stderr.
        LOG_ERR("Unable to create log file! Continuing without log.");
    }

    LOG_INF("------------------n");
    LOG_INF("Hello from heimdall client.");

    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl) {
            LOG_FTL("Unable to load " + url.toString().toStdString() + ". Exiting.");
            QCoreApplication::exit(-1);
        }
    }, Qt::QueuedConnection);

    qmlRegisterType<QmlMqttClient>("MqttClient", 1, 0, "MqttClient");
    qmlRegisterUncreatableType<QmlMqttSubscription>("MqttClient", 1, 0, "MqttSubscription", QLatin1String("Subscriptions are read-only"));

    qmlRegisterType<DoorOpener>("DoorOpener", 1, 0, "DoorOpener");

    engine.load(url);

    return app.exec();
}

main.qml

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.1
import QtMultimedia 5.12
import MqttClient 1.0
import DoorOpener 1.0

Window {
    width: 360
    height: 640
    visible: true
    title: qsTr("heimdall")

    // background image
    Image {
        source: "qrc:/bg.jpg"
        anchors.fill: parent
        fillMode: Image.Pad
        opacity: .25
    }

    // mqtt client
    MqttClient {
        id: client

        Component.onCompleted: {
            // TODO this is bad architecture. it should be safe to know when the connection is established. (see DoorOpener.cpp)
            connectToHost()
        }

        onConnected: {
            subscribe()
        }

        onDisconnected: {
            video.stop()
        }

        onStateChanged: {
            stateIndicator.color = getState()
            stateIndicatorText.text = color == "green" ? "✔" : "𐄂"
        }

        onMessageReceived: {
            video.play()
        }

        Component.onDestruction: {
            disconnectFromHost()
        }

        function getState() {
            switch (state) {
            case MqttClient.Disconnected:
                return "red"
            case MqttClient.Connecting:
                return "yellow"
            case MqttClient.Connected:
                return "green"
            default:
                return "gray"
            }
        }
    }

    DoorOpener {
        id: doorOpener
    }

    // caption
    Label {
        id: caption
        // text: "Heimdall - Welcome Home"
        text: "Here's Heimdall"

        font {
            family: "Open Sans MS"
            bold: true
            capitalization: Font.SmallCaps
            pointSize: 14
        }

        anchors {
            top: parent.top
            topMargin: 10
            horizontalCenter: parent.horizontalCenter
        }
    }

    // webcam stream
    Video {
        id: video
        objectName: "vplayer"
        width: parent.width - 20
        height: 200
        autoPlay: client.connected
        source: "rtsp://localhost:8554/mystream" // TODO

        onPlaying: vplaceholder.visible = false

        Image {
            id: vplaceholder
            width: parent.width
            height: parent.height
            source: "qrc:/test_picture.png"
        }

        anchors {
            top: caption.bottom
            topMargin: 10
            horizontalCenter: parent.horizontalCenter
        }

        onErrorChanged: {
            console.log("error: " + video.errorString)
        }

        MouseArea {
            anchors.fill: parent
            onClicked: {
                video.muted = !video.muted
            }
        }

        focus: true

        Image {
            id: muteIndicator
            source: "mute_white.png"

            width: 64
            height: width

            visible: video.muted

            anchors.centerIn: parent
        }
    } // Video

    // Buttons
    Item {
        width: parent.width
        height: 50

        anchors {
            top: video.bottom
            topMargin: 10
            centerIn: parent
        }

        Button {
            id: btnTalk
            objectName: "btnTalk"

            text: "Sprechen"

            anchors.left: parent.left
            anchors.leftMargin: 10

            onClicked: {
                bgrTalk.start()
            }

            Rectangle {
                id: bgrect_talk
                width: parent.width
                height: parent.height
                color: "white"
            }

            SequentialAnimation {
                id: bgrTalk

                PropertyAnimation {
                    target: bgrect_talk
                    property: "color"
                    to: "darkseagreen"

                    duration: 500
                }
                PropertyAnimation {
                    target: bgrect_talk
                    property: "color"
                    to: "white"

                    duration: 500
                    easing.type: Easing.InCirc
                }
            }
        }

        Button {
            id: btnOpen
            objectName: "btnOpen"

            text: "Öffnen"

            property var bgColor: "darkred"

            anchors.right: parent.right
            anchors.rightMargin: 10

            onClicked: {
                var opened = doorOpener.open()

                if (opened) {
                    bgColor = "forestgreen"
                }

                bgrOpened.start()
            }

            Rectangle {
                id: bgrect_open
                width: parent.width
                height: parent.height
                color: "white"
            }

            SequentialAnimation {
                id: bgrOpened

                PropertyAnimation {
                    target: bgrect_open
                    property: "color"
                    to: btnOpen.bgColor

                    duration: 500
                }
                PropertyAnimation {
                    target: bgrect_open
                    property: "color"
                    to: "white"

                    duration: 500
                    easing.type: Easing.InCirc
                }
            }
        }
    }

    Item {
        anchors {
            left: parent.left
            leftMargin: 10
            bottom: parent.bottom
            bottomMargin: 10
        }

        width: parent.width - 20
        height: 25

        Rectangle {
            id: stateIndicator
            width: 75
            height: 20

            color: "gray"

            Text {
                id: stateIndicatorText

                text: "?"
                color: "white"

                anchors.centerIn: parent
            }
        }

        Button {
            anchors.right: parent.right

            text: "Reconnect"

            onPressed: {
                client.disconnectFromHost()
                vplaceholder.visible = true
                client.connectToHost()
            }

            width: 75
            height: 20
        }
    }
}

0

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *