Compare commits
4 Commits
7a0bb14df4
...
v0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| a9c5139d55 | |||
| 7cc6056e7e | |||
| 7516eb4444 | |||
| ef2403b3cd |
32
.drone.yml
Normal file
32
.drone.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build-deb
|
||||||
|
image: cache.coadcorp.com/library/ubuntu:22.04
|
||||||
|
environment:
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
commands:
|
||||||
|
- apt-get update
|
||||||
|
- apt-get install -y --no-install-recommends build-essential qt5-qmake qtbase5-dev qtbase5-dev-tools libexif-dev qt5-image-formats-plugins libmosquitto-dev dpkg-dev fakeroot ca-certificates
|
||||||
|
- bash sbin/build_deb.sh "${DRONE_TAG:-${DRONE_COMMIT_SHA:0:8}}"
|
||||||
|
- ls -la dist
|
||||||
|
|
||||||
|
- name: gitea-release
|
||||||
|
image: cache.coadcorp.com/plugins/gitea-release
|
||||||
|
depends_on:
|
||||||
|
- build-deb
|
||||||
|
settings:
|
||||||
|
api_key:
|
||||||
|
from_secret: GITEA_TOKEN
|
||||||
|
base_url: https://git.coadcorp.com
|
||||||
|
files:
|
||||||
|
- dist/*.deb
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
title: ${DRONE_TAG}
|
||||||
|
note: Automated release for ${DRONE_TAG}
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
3
Makefile
3
Makefile
@@ -7,7 +7,7 @@ all: build
|
|||||||
|
|
||||||
.PHONY: install-deps-deb
|
.PHONY: install-deps-deb
|
||||||
install-deps-deb:
|
install-deps-deb:
|
||||||
apt install qt5-qmake libexif12 qt5-default libexif-dev qt5-image-formats-plugins
|
apt install qt5-qmake libexif12 qt5-default libexif-dev qt5-image-formats-plugins libmosquitto-dev
|
||||||
|
|
||||||
check-deps-deb:
|
check-deps-deb:
|
||||||
dpkg -l | grep qt5-qmake
|
dpkg -l | grep qt5-qmake
|
||||||
@@ -15,6 +15,7 @@ check-deps-deb:
|
|||||||
dpkg -l | grep libexif-dev
|
dpkg -l | grep libexif-dev
|
||||||
dpkg -l | grep qt5-default
|
dpkg -l | grep qt5-default
|
||||||
dpkg -l | grep qt5-image-formats-plugins
|
dpkg -l | grep qt5-image-formats-plugins
|
||||||
|
dpkg -l | grep libmosquitto-dev
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
|
|||||||
33
README.md
33
README.md
@@ -117,6 +117,7 @@ Supported keys and values in the JSON configuration are:
|
|||||||
* `opacity` : the same as the command line `-o` argument
|
* `opacity` : the same as the command line `-o` argument
|
||||||
* `blur` : the same as the command line `-b` argument
|
* `blur` : the same as the command line `-b` argument
|
||||||
* `debug` : set to true to enable verbose output from the program
|
* `debug` : set to true to enable verbose output from the program
|
||||||
|
* `mqtt` : MQTT playback control (see below)
|
||||||
* `immich` : connect to an Immich server instead of a local path (see below)
|
* `immich` : connect to an Immich server instead of a local path (see below)
|
||||||
* `scheduler` : this entry is an array of possible path values and associated settings. This key lets you manage display times/settings for a collection of paths. In the example above the top entry shows ONLY files from a Redit feed between 2 and 4pm, ONLY files from the `show_peak_times` folder from 8am to 10am and then 4pm to 7pm. At all other times it alternates displaying files in the `always_show_1` and `always_show_2` folder.
|
* `scheduler` : this entry is an array of possible path values and associated settings. This key lets you manage display times/settings for a collection of paths. In the example above the top entry shows ONLY files from a Redit feed between 2 and 4pm, ONLY files from the `show_peak_times` folder from 8am to 10am and then 4pm to 7pm. At all other times it alternates displaying files in the `always_show_1` and `always_show_2` folder.
|
||||||
* `exclusive` : When set to `true` only this entry will be used when it is in its valid time window.
|
* `exclusive` : When set to `true` only this entry will be used when it is in its valid time window.
|
||||||
@@ -124,9 +125,36 @@ Supported keys and values in the JSON configuration are:
|
|||||||
* `path` : the path to image files
|
* `path` : the path to image files
|
||||||
* `stretch` : as above
|
* `stretch` : as above
|
||||||
|
|
||||||
|
### MQTT control
|
||||||
|
|
||||||
|
Add an `mqtt` block to control playback remotely. Publish one of the commands below to the configured topic.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"mqtt": {
|
||||||
|
"host": "mqtt.local",
|
||||||
|
"port": 1883,
|
||||||
|
"topic": "slide/control",
|
||||||
|
"clientId": "slide-frame",
|
||||||
|
"username": "slide",
|
||||||
|
"password": "secret",
|
||||||
|
"keepAlive": 30,
|
||||||
|
"qos": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
* `play` / `resume` — resume slideshow
|
||||||
|
* `pause` — pause slideshow
|
||||||
|
* `next` / `next-image` — advance to next image
|
||||||
|
* `next-folder` — jump to next configured path (if multiple paths are configured)
|
||||||
|
* `restart` / `reset` — recreate the selector and restart playback
|
||||||
|
|
||||||
### Immich configuration (lightweight + low power)
|
### Immich configuration (lightweight + low power)
|
||||||
|
|
||||||
Immich uses an API key and a `/api` base path. This integration requests the asset search endpoint and downloads thumbnail images into a local cache before displaying them. That keeps bandwidth and power usage low while still letting `slide` do its normal scaling and transitions.
|
Immich uses an API key and a `/api` base path. This integration requests the asset search endpoint and downloads the configured image size into a local cache before displaying them. That keeps bandwidth and power usage low while still letting `slide` do its normal scaling and transitions.
|
||||||
|
|
||||||
Example (single source):
|
Example (single source):
|
||||||
```
|
```
|
||||||
@@ -171,7 +199,7 @@ Immich settings:
|
|||||||
* `order`: `"asc"` or `"desc"` ordering for asset search.
|
* `order`: `"asc"` or `"desc"` ordering for asset search.
|
||||||
* `pageSize`: assets fetched per page.
|
* `pageSize`: assets fetched per page.
|
||||||
* `maxAssets`: cap on total assets fetched (0 means no cap).
|
* `maxAssets`: cap on total assets fetched (0 means no cap).
|
||||||
* `cachePath`: local cache directory for downloaded thumbnails.
|
* `cachePath`: local cache directory for downloaded images.
|
||||||
* `cacheMaxMB`: maximum cache size in MB (0 disables cleanup).
|
* `cacheMaxMB`: maximum cache size in MB (0 disables cleanup).
|
||||||
* `includeArchived`: include archived assets in search results.
|
* `includeArchived`: include archived assets in search results.
|
||||||
When `immich` is set on an entry, `path` and `imageList` are ignored.
|
When `immich` is set on an entry, `path` and `imageList` are ignored.
|
||||||
@@ -205,6 +233,7 @@ See the `Configuration File` section for details of each setting.
|
|||||||
* qt5
|
* qt5
|
||||||
* qt5-image-formats-plugins
|
* qt5-image-formats-plugins
|
||||||
* libexif
|
* libexif
|
||||||
|
* libmosquitto-dev
|
||||||
|
|
||||||
Ubuntu/Raspbian:
|
Ubuntu/Raspbian:
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ set -euo pipefail
|
|||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
VERSION="${1:-${VERSION:-0.0.0}}"
|
VERSION="${1:-${VERSION:-0.0.0}}"
|
||||||
VERSION="${VERSION#v}"
|
VERSION="${VERSION#v}"
|
||||||
|
# Debian versions must start with a digit; fall back to 0.0.0+<sha/tag>.
|
||||||
|
if [[ ! "$VERSION" =~ ^[0-9] ]]; then
|
||||||
|
VERSION="0.0.0+${VERSION}"
|
||||||
|
fi
|
||||||
ARCH="${ARCH:-$(dpkg --print-architecture)}"
|
ARCH="${ARCH:-$(dpkg --print-architecture)}"
|
||||||
|
|
||||||
PACKAGE_NAME="slide"
|
PACKAGE_NAME="slide"
|
||||||
@@ -27,7 +31,7 @@ Section: graphics
|
|||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: ${ARCH}
|
Architecture: ${ARCH}
|
||||||
Maintainer: slide build
|
Maintainer: slide build
|
||||||
Depends: libqt5core5a, libqt5gui5, libqt5widgets5, libqt5network5, libexif12, qt5-image-formats-plugins
|
Depends: libqt5core5a, libqt5gui5, libqt5widgets5, libqt5network5, libexif12, qt5-image-formats-plugins, libmosquitto1
|
||||||
Description: Lightweight slideshow for photo frames
|
Description: Lightweight slideshow for photo frames
|
||||||
Simple, lightweight slideshow designed for low power devices.
|
Simple, lightweight slideshow designed for low power devices.
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -112,6 +112,39 @@ ImmichConfig ParseImmichConfigObject(QJsonObject immichJson) {
|
|||||||
return config;
|
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) {
|
Config loadConfiguration(const std::string &configFilePath, const Config ¤tConfig) {
|
||||||
if(configFilePath.empty())
|
if(configFilePath.empty())
|
||||||
{
|
{
|
||||||
@@ -327,6 +360,11 @@ AppConfig loadAppConfiguration(const AppConfig &commandLineConfig) {
|
|||||||
loadedConfig.overlay = overlayString;
|
loadedConfig.overlay = overlayString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(jsonDoc.contains("mqtt") && jsonDoc["mqtt"].isObject())
|
||||||
|
{
|
||||||
|
loadedConfig.mqtt = ParseMqttConfigObject(jsonDoc["mqtt"].toObject());
|
||||||
|
}
|
||||||
|
|
||||||
loadedConfig.paths = parsePathEntry(jsonDoc, baseRecursive, baseShuffle, baseSorted);
|
loadedConfig.paths = parsePathEntry(jsonDoc, baseRecursive, baseShuffle, baseSorted);
|
||||||
if(loadedConfig.paths.count() <= 0)
|
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
|
// configuration options that apply to an image/folder of images
|
||||||
struct Config {
|
struct Config {
|
||||||
public:
|
public:
|
||||||
@@ -106,6 +136,7 @@ struct AppConfig : public Config {
|
|||||||
std::string overlay = "";
|
std::string overlay = "";
|
||||||
QString overlayHexRGB = "#FFFFFF";
|
QString overlayHexRGB = "#FFFFFF";
|
||||||
QVector<PathEntry> paths;
|
QVector<PathEntry> paths;
|
||||||
|
MqttConfig mqtt;
|
||||||
|
|
||||||
bool debugMode = false;
|
bool debugMode = false;
|
||||||
|
|
||||||
|
|||||||
@@ -371,4 +371,4 @@ const ImageDetails ListImageSelector::getNextImage(const ImageDisplayOptions& ba
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
while(true);
|
while(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ ImageSwitcher::ImageSwitcher(MainWindow& w, unsigned int timeoutMsec, std::uniqu
|
|||||||
|
|
||||||
void ImageSwitcher::updateImage()
|
void ImageSwitcher::updateImage()
|
||||||
{
|
{
|
||||||
|
if (paused)
|
||||||
|
return;
|
||||||
if(reloadConfigIfNeeded)
|
if(reloadConfigIfNeeded)
|
||||||
{
|
{
|
||||||
reloadConfigIfNeeded(window, this);
|
reloadConfigIfNeeded(window, this);
|
||||||
@@ -60,10 +62,64 @@ void ImageSwitcher::setConfigFileReloader(std::function<void(MainWindow &w, Imag
|
|||||||
void ImageSwitcher::setRotationTime(unsigned int timeoutMsecIn)
|
void ImageSwitcher::setRotationTime(unsigned int timeoutMsecIn)
|
||||||
{
|
{
|
||||||
timeout = timeoutMsecIn;
|
timeout = timeoutMsecIn;
|
||||||
timer.start(timeout);
|
if (!paused)
|
||||||
|
timer.start(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImageSwitcher::setImageSelector(std::unique_ptr<ImageSelector>& selectorIn)
|
void ImageSwitcher::setImageSelector(std::unique_ptr<ImageSelector>& selectorIn)
|
||||||
{
|
{
|
||||||
selector = std::move(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 setConfigFileReloader(std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloadConfigIfNeededIn);
|
||||||
void setRotationTime(unsigned int timeoutMsec);
|
void setRotationTime(unsigned int timeoutMsec);
|
||||||
void setImageSelector(std::unique_ptr<ImageSelector>& selector);
|
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:
|
public slots:
|
||||||
void updateImage();
|
void updateImage();
|
||||||
@@ -30,6 +36,7 @@ private:
|
|||||||
const unsigned int timeoutNoContent = 5 * 1000; // 5 sec
|
const unsigned int timeoutNoContent = 5 * 1000; // 5 sec
|
||||||
QTimer timerNoContent;
|
QTimer timerNoContent;
|
||||||
std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloadConfigIfNeeded;
|
std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloadConfigIfNeeded;
|
||||||
|
bool paused = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // IMAGESWITCHER_H
|
#endif // IMAGESWITCHER_H
|
||||||
|
|||||||
19
src/main.cpp
19
src/main.cpp
@@ -3,6 +3,7 @@
|
|||||||
#include "imageswitcher.h"
|
#include "imageswitcher.h"
|
||||||
#include "pathtraverser.h"
|
#include "pathtraverser.h"
|
||||||
#include "immichpathtraverser.h"
|
#include "immichpathtraverser.h"
|
||||||
|
#include "mqttcontroller.h"
|
||||||
#include "overlay.h"
|
#include "overlay.h"
|
||||||
#include "appconfig.h"
|
#include "appconfig.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
@@ -275,6 +276,24 @@ int main(int argc, char *argv[])
|
|||||||
w.setImageSwitcher(&switcher);
|
w.setImageSwitcher(&switcher);
|
||||||
std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloader = [&appConfig](MainWindow &w, ImageSwitcher *switcher) { ReloadConfigIfNeeded(appConfig, w, switcher); };
|
std::function<void(MainWindow &w, ImageSwitcher *switcher)> reloader = [&appConfig](MainWindow &w, ImageSwitcher *switcher) { ReloadConfigIfNeeded(appConfig, w, switcher); };
|
||||||
switcher.setConfigFileReloader(reloader);
|
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();
|
switcher.start();
|
||||||
return a.exec();
|
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 \
|
pathtraverser.cpp \
|
||||||
immichpathtraverser.cpp \
|
immichpathtraverser.cpp \
|
||||||
immichclient.cpp \
|
immichclient.cpp \
|
||||||
|
mqttcontroller.cpp \
|
||||||
overlay.cpp \
|
overlay.cpp \
|
||||||
imageselector.cpp \
|
imageselector.cpp \
|
||||||
appconfig.cpp \
|
appconfig.cpp \
|
||||||
@@ -49,6 +50,7 @@ HEADERS += \
|
|||||||
pathtraverser.h \
|
pathtraverser.h \
|
||||||
immichpathtraverser.h \
|
immichpathtraverser.h \
|
||||||
immichclient.h \
|
immichclient.h \
|
||||||
|
mqttcontroller.h \
|
||||||
overlay.h \
|
overlay.h \
|
||||||
imageswitcher.h \
|
imageswitcher.h \
|
||||||
imagestructs.h \
|
imagestructs.h \
|
||||||
@@ -62,3 +64,4 @@ target.path = /usr/local/bin/
|
|||||||
INSTALLS += target
|
INSTALLS += target
|
||||||
|
|
||||||
unix|win32: LIBS += -lexif
|
unix|win32: LIBS += -lexif
|
||||||
|
unix|win32: LIBS += -lmosquitto
|
||||||
|
|||||||
Reference in New Issue
Block a user