add mqtt control
This commit is contained in:
@@ -112,6 +112,39 @@ ImmichConfig ParseImmichConfigObject(QJsonObject immichJson) {
|
||||
return config;
|
||||
}
|
||||
|
||||
MqttConfig ParseMqttConfigObject(QJsonObject mqttJson) {
|
||||
MqttConfig config;
|
||||
|
||||
std::string host = ParseJSONString(mqttJson, "host");
|
||||
if(!host.empty())
|
||||
config.host = host;
|
||||
|
||||
std::string topic = ParseJSONString(mqttJson, "topic");
|
||||
if(!topic.empty())
|
||||
config.topic = topic;
|
||||
|
||||
std::string clientId = ParseJSONString(mqttJson, "clientId");
|
||||
if(!clientId.empty())
|
||||
config.clientId = clientId;
|
||||
|
||||
std::string username = ParseJSONString(mqttJson, "username");
|
||||
if(!username.empty())
|
||||
config.username = username;
|
||||
|
||||
std::string password = ParseJSONString(mqttJson, "password");
|
||||
if(!password.empty())
|
||||
config.password = password;
|
||||
|
||||
SetJSONInt(config.port, mqttJson, "port");
|
||||
SetJSONInt(config.keepAlive, mqttJson, "keepAlive");
|
||||
SetJSONInt(config.qos, mqttJson, "qos");
|
||||
|
||||
if(!config.host.empty() && !config.topic.empty())
|
||||
config.enabled = true;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
Config loadConfiguration(const std::string &configFilePath, const Config ¤tConfig) {
|
||||
if(configFilePath.empty())
|
||||
{
|
||||
@@ -327,6 +360,11 @@ AppConfig loadAppConfiguration(const AppConfig &commandLineConfig) {
|
||||
loadedConfig.overlay = overlayString;
|
||||
}
|
||||
|
||||
if(jsonDoc.contains("mqtt") && jsonDoc["mqtt"].isObject())
|
||||
{
|
||||
loadedConfig.mqtt = ParseMqttConfigObject(jsonDoc["mqtt"].toObject());
|
||||
}
|
||||
|
||||
loadedConfig.paths = parsePathEntry(jsonDoc, baseRecursive, baseShuffle, baseSorted);
|
||||
if(loadedConfig.paths.count() <= 0)
|
||||
{
|
||||
|
||||
@@ -49,6 +49,36 @@ struct ImmichConfig {
|
||||
}
|
||||
};
|
||||
|
||||
struct MqttConfig {
|
||||
bool enabled = false;
|
||||
std::string host = "localhost";
|
||||
int port = 1883;
|
||||
std::string topic = "slide/control";
|
||||
std::string clientId = "slide";
|
||||
std::string username = "";
|
||||
std::string password = "";
|
||||
int keepAlive = 30;
|
||||
int qos = 0;
|
||||
|
||||
bool operator==(const MqttConfig &b) const
|
||||
{
|
||||
return enabled == b.enabled &&
|
||||
host == b.host &&
|
||||
port == b.port &&
|
||||
topic == b.topic &&
|
||||
clientId == b.clientId &&
|
||||
username == b.username &&
|
||||
password == b.password &&
|
||||
keepAlive == b.keepAlive &&
|
||||
qos == b.qos;
|
||||
}
|
||||
|
||||
bool operator!=(const MqttConfig &b) const
|
||||
{
|
||||
return !operator==(b);
|
||||
}
|
||||
};
|
||||
|
||||
// configuration options that apply to an image/folder of images
|
||||
struct Config {
|
||||
public:
|
||||
@@ -106,6 +136,7 @@ struct AppConfig : public Config {
|
||||
std::string overlay = "";
|
||||
QString overlayHexRGB = "#FFFFFF";
|
||||
QVector<PathEntry> paths;
|
||||
MqttConfig mqtt;
|
||||
|
||||
bool debugMode = false;
|
||||
|
||||
|
||||
@@ -371,4 +371,4 @@ const ImageDetails ListImageSelector::getNextImage(const ImageDisplayOptions& ba
|
||||
}
|
||||
}
|
||||
while(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ ImageSwitcher::ImageSwitcher(MainWindow& w, unsigned int timeoutMsec, std::uniqu
|
||||
|
||||
void ImageSwitcher::updateImage()
|
||||
{
|
||||
if (paused)
|
||||
return;
|
||||
if(reloadConfigIfNeeded)
|
||||
{
|
||||
reloadConfigIfNeeded(window, this);
|
||||
@@ -60,10 +62,64 @@ void ImageSwitcher::setConfigFileReloader(std::function<void(MainWindow &w, Imag
|
||||
void ImageSwitcher::setRotationTime(unsigned int timeoutMsecIn)
|
||||
{
|
||||
timeout = timeoutMsecIn;
|
||||
timer.start(timeout);
|
||||
if (!paused)
|
||||
timer.start(timeout);
|
||||
}
|
||||
|
||||
void ImageSwitcher::setImageSelector(std::unique_ptr<ImageSelector>& selectorIn)
|
||||
{
|
||||
selector = std::move(selectorIn);
|
||||
}
|
||||
|
||||
void ImageSwitcher::pause()
|
||||
{
|
||||
paused = true;
|
||||
timer.stop();
|
||||
timerNoContent.stop();
|
||||
}
|
||||
|
||||
void ImageSwitcher::resume()
|
||||
{
|
||||
if (!paused)
|
||||
return;
|
||||
paused = false;
|
||||
timer.start(timeout);
|
||||
scheduleImageUpdate();
|
||||
}
|
||||
|
||||
void ImageSwitcher::stepOnce()
|
||||
{
|
||||
bool wasPaused = paused;
|
||||
if (wasPaused)
|
||||
paused = false;
|
||||
updateImage();
|
||||
if (wasPaused)
|
||||
{
|
||||
paused = true;
|
||||
timer.stop();
|
||||
timerNoContent.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void ImageSwitcher::restart(std::unique_ptr<ImageSelector>& selectorIn)
|
||||
{
|
||||
paused = false;
|
||||
timerNoContent.stop();
|
||||
setImageSelector(selectorIn);
|
||||
timer.start(timeout);
|
||||
scheduleImageUpdate();
|
||||
}
|
||||
|
||||
bool ImageSwitcher::skipToNextFolder()
|
||||
{
|
||||
auto *listSelector = dynamic_cast<ListImageSelector*>(selector.get());
|
||||
if (!listSelector)
|
||||
return false;
|
||||
stepOnce();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImageSwitcher::isPaused() const
|
||||
{
|
||||
return paused;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@ public:
|
||||
void setConfigFileReloader(std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloadConfigIfNeededIn);
|
||||
void setRotationTime(unsigned int timeoutMsec);
|
||||
void setImageSelector(std::unique_ptr<ImageSelector>& selector);
|
||||
void pause();
|
||||
void resume();
|
||||
void stepOnce();
|
||||
void restart(std::unique_ptr<ImageSelector>& selector);
|
||||
bool skipToNextFolder();
|
||||
bool isPaused() const;
|
||||
|
||||
public slots:
|
||||
void updateImage();
|
||||
@@ -30,6 +36,7 @@ private:
|
||||
const unsigned int timeoutNoContent = 5 * 1000; // 5 sec
|
||||
QTimer timerNoContent;
|
||||
std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloadConfigIfNeeded;
|
||||
bool paused = false;
|
||||
};
|
||||
|
||||
#endif // IMAGESWITCHER_H
|
||||
|
||||
19
src/main.cpp
19
src/main.cpp
@@ -3,6 +3,7 @@
|
||||
#include "imageswitcher.h"
|
||||
#include "pathtraverser.h"
|
||||
#include "immichpathtraverser.h"
|
||||
#include "mqttcontroller.h"
|
||||
#include "overlay.h"
|
||||
#include "appconfig.h"
|
||||
#include "logger.h"
|
||||
@@ -275,6 +276,24 @@ int main(int argc, char *argv[])
|
||||
w.setImageSwitcher(&switcher);
|
||||
std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloader = [&appConfig](MainWindow &w, ImageSwitcher *switcher) { ReloadConfigIfNeeded(appConfig, w, switcher); };
|
||||
switcher.setConfigFileReloader(reloader);
|
||||
|
||||
std::unique_ptr<MqttController> mqttController;
|
||||
if (appConfig.mqtt.enabled)
|
||||
{
|
||||
mqttController = std::unique_ptr<MqttController>(new MqttController(appConfig.mqtt, &a));
|
||||
QObject::connect(mqttController.get(), &MqttController::play, [&switcher]() { switcher.resume(); });
|
||||
QObject::connect(mqttController.get(), &MqttController::pause, [&switcher]() { switcher.pause(); });
|
||||
QObject::connect(mqttController.get(), &MqttController::nextImage, [&switcher]() { switcher.stepOnce(); });
|
||||
QObject::connect(mqttController.get(), &MqttController::nextFolder, [&switcher]() {
|
||||
if (!switcher.skipToNextFolder())
|
||||
switcher.stepOnce();
|
||||
});
|
||||
QObject::connect(mqttController.get(), &MqttController::restart, [&appConfig, &switcher]() {
|
||||
std::unique_ptr<ImageSelector> newSelector = GetSelectorForApp(appConfig);
|
||||
switcher.restart(newSelector);
|
||||
});
|
||||
mqttController->start();
|
||||
}
|
||||
switcher.start();
|
||||
return a.exec();
|
||||
}
|
||||
|
||||
152
src/mqttcontroller.cpp
Normal file
152
src/mqttcontroller.cpp
Normal file
@@ -0,0 +1,152 @@
|
||||
#include "mqttcontroller.h"
|
||||
#include "logger.h"
|
||||
|
||||
#include <QMetaObject>
|
||||
|
||||
#include <mosquitto.h>
|
||||
|
||||
namespace {
|
||||
bool g_mqttInitialized = false;
|
||||
}
|
||||
|
||||
MqttController::MqttController(const MqttConfig &configIn, QObject *parent)
|
||||
: QObject(parent),
|
||||
config(configIn)
|
||||
{
|
||||
if (!g_mqttInitialized)
|
||||
{
|
||||
mosquitto_lib_init();
|
||||
g_mqttInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
MqttController::~MqttController()
|
||||
{
|
||||
if (client)
|
||||
{
|
||||
mosquitto_disconnect(client);
|
||||
mosquitto_loop_stop(client, true);
|
||||
mosquitto_destroy(client);
|
||||
client = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void MqttController::start()
|
||||
{
|
||||
if (!config.enabled)
|
||||
{
|
||||
Log("MQTT disabled or missing host/topic.");
|
||||
return;
|
||||
}
|
||||
|
||||
QString clientId = QString::fromStdString(config.clientId);
|
||||
if (clientId.isEmpty())
|
||||
clientId = "slide";
|
||||
|
||||
client = mosquitto_new(clientId.toUtf8().constData(), true, this);
|
||||
if (!client)
|
||||
{
|
||||
Log("MQTT: failed to create client.");
|
||||
return;
|
||||
}
|
||||
|
||||
mosquitto_connect_callback_set(client, &MqttController::HandleConnect);
|
||||
mosquitto_message_callback_set(client, &MqttController::HandleMessage);
|
||||
|
||||
if (!config.username.empty())
|
||||
{
|
||||
mosquitto_username_pw_set(client, config.username.c_str(),
|
||||
config.password.empty() ? nullptr : config.password.c_str());
|
||||
}
|
||||
|
||||
mosquitto_reconnect_delay_set(client, 2, 30, true);
|
||||
|
||||
int rc = mosquitto_connect_async(client, config.host.c_str(), config.port, config.keepAlive);
|
||||
if (rc != MOSQ_ERR_SUCCESS)
|
||||
{
|
||||
Log("MQTT connect failed: ", mosquitto_strerror(rc));
|
||||
return;
|
||||
}
|
||||
|
||||
rc = mosquitto_loop_start(client);
|
||||
if (rc != MOSQ_ERR_SUCCESS)
|
||||
{
|
||||
Log("MQTT loop start failed: ", mosquitto_strerror(rc));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void MqttController::HandleConnect(struct mosquitto *mosq, void *userdata, int rc)
|
||||
{
|
||||
auto *self = static_cast<MqttController *>(userdata);
|
||||
if (!self || !mosq)
|
||||
return;
|
||||
if (rc != 0)
|
||||
{
|
||||
Log("MQTT connect error: ", mosquitto_strerror(rc));
|
||||
return;
|
||||
}
|
||||
self->connected = true;
|
||||
self->subscribe();
|
||||
}
|
||||
|
||||
void MqttController::subscribe()
|
||||
{
|
||||
if (!client || !connected)
|
||||
return;
|
||||
int rc = mosquitto_subscribe(client, nullptr, config.topic.c_str(), config.qos);
|
||||
if (rc != MOSQ_ERR_SUCCESS)
|
||||
{
|
||||
Log("MQTT subscribe failed: ", mosquitto_strerror(rc));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log("MQTT subscribed to ", config.topic);
|
||||
}
|
||||
}
|
||||
|
||||
void MqttController::HandleMessage(struct mosquitto *mosq, void *userdata, const struct mosquitto_message *message)
|
||||
{
|
||||
Q_UNUSED(mosq);
|
||||
auto *self = static_cast<MqttController *>(userdata);
|
||||
if (!self || !message || !message->payload)
|
||||
return;
|
||||
|
||||
QString payload = QString::fromUtf8(static_cast<const char *>(message->payload), message->payloadlen);
|
||||
QMetaObject::invokeMethod(self, "handleCommand", Qt::QueuedConnection, Q_ARG(QString, payload));
|
||||
}
|
||||
|
||||
void MqttController::handleCommand(const QString &payload)
|
||||
{
|
||||
QString cmd = payload.trimmed().toLower();
|
||||
if (cmd.isEmpty())
|
||||
return;
|
||||
|
||||
if (cmd == "play" || cmd == "resume")
|
||||
{
|
||||
emit play();
|
||||
return;
|
||||
}
|
||||
if (cmd == "pause")
|
||||
{
|
||||
emit pause();
|
||||
return;
|
||||
}
|
||||
if (cmd == "next" || cmd == "skip" || cmd == "next-image")
|
||||
{
|
||||
emit nextImage();
|
||||
return;
|
||||
}
|
||||
if (cmd == "next-folder" || cmd == "folder-next" || cmd == "skip-folder")
|
||||
{
|
||||
emit nextFolder();
|
||||
return;
|
||||
}
|
||||
if (cmd == "restart" || cmd == "reset")
|
||||
{
|
||||
emit restart();
|
||||
return;
|
||||
}
|
||||
|
||||
Log("MQTT unknown command: ", cmd.toStdString());
|
||||
}
|
||||
42
src/mqttcontroller.h
Normal file
42
src/mqttcontroller.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#ifndef MQTTCONTROLLER_H
|
||||
#define MQTTCONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "appconfig.h"
|
||||
|
||||
struct mosquitto;
|
||||
struct mosquitto_message;
|
||||
|
||||
class MqttController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MqttController(const MqttConfig &config, QObject *parent = nullptr);
|
||||
~MqttController();
|
||||
|
||||
void start();
|
||||
|
||||
signals:
|
||||
void play();
|
||||
void pause();
|
||||
void nextImage();
|
||||
void nextFolder();
|
||||
void restart();
|
||||
|
||||
private slots:
|
||||
void handleCommand(const QString &payload);
|
||||
|
||||
private:
|
||||
static void HandleConnect(struct mosquitto *mosq, void *userdata, int rc);
|
||||
static void HandleMessage(struct mosquitto *mosq, void *userdata, const struct mosquitto_message *message);
|
||||
|
||||
void subscribe();
|
||||
|
||||
MqttConfig config;
|
||||
struct mosquitto *client = nullptr;
|
||||
bool connected = false;
|
||||
};
|
||||
|
||||
#endif // MQTTCONTROLLER_H
|
||||
@@ -37,6 +37,7 @@ SOURCES += \
|
||||
pathtraverser.cpp \
|
||||
immichpathtraverser.cpp \
|
||||
immichclient.cpp \
|
||||
mqttcontroller.cpp \
|
||||
overlay.cpp \
|
||||
imageselector.cpp \
|
||||
appconfig.cpp \
|
||||
@@ -49,6 +50,7 @@ HEADERS += \
|
||||
pathtraverser.h \
|
||||
immichpathtraverser.h \
|
||||
immichclient.h \
|
||||
mqttcontroller.h \
|
||||
overlay.h \
|
||||
imageswitcher.h \
|
||||
imagestructs.h \
|
||||
@@ -62,3 +64,4 @@ target.path = /usr/local/bin/
|
||||
INSTALLS += target
|
||||
|
||||
unix|win32: LIBS += -lexif
|
||||
unix|win32: LIBS += -lmosquitto
|
||||
|
||||
Reference in New Issue
Block a user