Так что я недавно построил дом и не хотел полагаться на «сомнительные» системы, такие как Google Home или Amazon, что угодно, поэтому я решил сам построить систему дверного звонка.
Аппаратное обеспечение
Я использую RPI4B + в качестве оборудования для дверного звонка и MQTT в качестве платформы связи между моим домашним сервером, оборудованием дверного звонка и моим приложением Android, которое состоит из трех компонентов.
- RPI4B + — мой уличный дверной звонок, включая веб-камеру RPI, простой микрофон и простой динамик
- Сервер Ubuntu — мой сервер MQTT
- Клиент 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
}
}
}