add mqtt control
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing

This commit is contained in:
2026-01-31 16:50:34 +11:00
parent 7cc6056e7e
commit a9c5139d55
13 changed files with 385 additions and 7 deletions

View File

@@ -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 &currentConfig) {
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)
{

View File

@@ -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;

View File

@@ -371,4 +371,4 @@ const ImageDetails ListImageSelector::getNextImage(const ImageDisplayOptions& ba
}
}
while(true);
}
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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
View 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
View 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

View File

@@ -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