feat: Implement WLED direct communication, config tool, and screen capture test setup.

This commit is contained in:
Tobias J. Endres 2025-08-15 23:31:28 +02:00
parent 79c5aa7f41
commit c50aed732e
11 changed files with 421 additions and 29 deletions

View File

@ -9,13 +9,21 @@ set(CMAKE_BUILD_TYPE Release)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Option to enable DebugClient
option(USE_DEBUG_CLIENT "Use DebugClient for terminal output instead of WLEDClient" OFF)
if(USE_DEBUG_CLIENT)
add_compile_definitions(DEBUG_MODE)
endif()
find_package(Qt6 COMPONENTS Core Gui WaylandClient Multimedia Widgets MultimediaWidgets Network OpenGL REQUIRED)
add_executable(Hyperion_Grabber_Wayland_QT
hyperiongrabber.cpp
wledclient.cpp
debugclient.cpp # Added debugclient.cpp
main.cpp
WaylandGrabber.cpp
wledconfigclient.cpp
)
target_link_libraries(Hyperion_Grabber_Wayland_QT
@ -25,10 +33,22 @@ target_link_libraries(Hyperion_Grabber_Wayland_QT
Qt6::Multimedia
Qt6::Widgets
Qt6::MultimediaWidgets
Qt6::Network
Qt6::OpenGL
${PIPEWIRE_LIBRARIES}
)
add_executable(wled_config_tool
main.cpp
wledconfigclient.cpp
)
target_link_libraries(wled_config_tool
Qt6::Core
Qt6::Network
)
target_compile_definitions(wled_config_tool PRIVATE WLED_CONFIG_TOOL_BUILD)
add_executable(hyperion-mock
hyperion-mock.cpp
)
@ -39,6 +59,28 @@ target_link_libraries(hyperion-mock
Qt6::Gui
)
add_executable(wled_test
wled_test.cpp
wledclient.cpp
)
target_link_libraries(wled_test
Qt6::Core
Qt6::Network
Qt6::Gui
)
add_executable(screen_capture_test
screen_capture_test.cpp
)
target_link_libraries(screen_capture_test
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::MultimediaWidgets
)
find_program(WAYLAND_SCANNER_EXECUTABLE NAMES wayland-scanner)
@ -65,4 +107,4 @@ add_custom_target(generate_wayland_protocols ALL
)
find_package(PkgConfig REQUIRED)
pkg_check_modules(PIPEWIRE REQUIRED libpipewire-0.3)
pkg_check_modules(PIPEWIRE REQUIRED libpipewire-0.3)

41
debugclient.cpp Normal file
View File

@ -0,0 +1,41 @@
#include "debugclient.h"
#include <QTextStream>
DebugClient::DebugClient(QObject *parent) :
QObject(parent)
{
qDebug() << "DebugClient initialized.";
}
DebugClient::~DebugClient()
{
qDebug() << "DebugClient destroyed.";
}
QString DebugClient::rgbToAnsiColor(int r, int g, int b)
{
// ANSI escape codes for 256-color terminal
// Using a simple mapping for now, can be improved for better visual representation
return QString("\033[48;2;%1;%2;%3m \033[0m").arg(r).arg(g).arg(b);
}
void DebugClient::sendImage(const QImage &image)
{
if (image.isNull()) {
qWarning() << "DebugClient: Cannot display null image.";
return;
}
QTextStream out(stdout);
out << "\033[H\033[2J"; // Clear screen
// Iterate through the image pixels and print ASCII representation
for (int y = 0; y < image.height(); ++y) {
for (int x = 0; x < image.width(); ++x) {
QRgb pixel = image.pixel(x, y);
out << rgbToAnsiColor(qRed(pixel), qGreen(pixel), qBlue(pixel));
}
out << Qt::endl;
}
out << Qt::endl;
}

23
debugclient.h Normal file
View File

@ -0,0 +1,23 @@
#ifndef DEBUGCLIENT_H
#define DEBUGCLIENT_H
#include <QObject>
#include <QImage>
#include <QDebug>
class DebugClient : public QObject
{
Q_OBJECT
public:
explicit DebugClient(QObject *parent = nullptr);
~DebugClient();
void sendImage(const QImage &image);
private:
// Helper to convert RGB to an ASCII character or colored block
QString rgbToAnsiColor(int r, int g, int b);
};
#endif // DEBUGCLIENT_H

View File

@ -35,7 +35,11 @@ HyperionGrabber::HyperionGrabber(QHash<QString, QString> opts)
}
}
_wledClient_p = new WledClient(addr, port, this);
#ifdef DEBUG_MODE
_client_p = new DebugClient(this);
#else
_client_p = new WledClient(addr, port, this);
#endif
_waylandGrabber_p = new WaylandGrabber(this);
connect(_waylandGrabber_p, &WaylandGrabber::frameReady, this, &HyperionGrabber::_processFrame);
@ -55,7 +59,7 @@ HyperionGrabber::~HyperionGrabber()
_timer_p->stop();
delete _timer_p;
}
delete _wledClient_p;
delete _client_p;
}
// private slots
@ -92,5 +96,5 @@ void HyperionGrabber::_processFrame(const QVideoFrame &frame)
scaledImage = scaledImage.convertToFormat(QImage::Format_RGB888);
}
_wledClient_p->sendImage(scaledImage);
_client_p->sendImage(scaledImage);
}

View File

@ -6,7 +6,12 @@
#include <QImage>
#include <QVideoFrame>
#include "wledclient.h" // Use WledClient instead of HyperionClient
#ifdef DEBUG_MODE
#include "debugclient.h"
#else
#include "wledclient.h"
#endif
#include "WaylandGrabber.h"
class HyperionGrabber : public QObject
@ -17,7 +22,11 @@ public:
~HyperionGrabber();
private:
WledClient *_wledClient_p; // Changed from HyperionClient
#ifdef DEBUG_MODE
DebugClient *_client_p; // Use DebugClient in debug mode
#else
WledClient *_client_p; // Use WledClient in normal mode
#endif
QTimer *_timer_p;
WaylandGrabber *_waylandGrabber_p;
@ -25,6 +34,9 @@ private:
unsigned short _scale_m = 8;
unsigned short _frameskip_m = 0;
long long _frameCounter_m = 0;
quint64 _lastImageChecksum_m = 0; // Added for image stability check
QImage _lastScaledImage_m; // Stores the previously sent image for comparison
int _changeThreshold_m = 100; // Threshold for image change detection
private slots:
void _processFrame(const QVideoFrame &frame);

170
main.cpp
View File

@ -1,21 +1,127 @@
#include <QApplication>
#include <QCommandLineParser>
#include <signal.h>
#include "hyperiongrabber.h"
#include <QFile>
#include <QTextStream>
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
#ifdef WLED_CONFIG_TOOL_BUILD
#include <QCoreApplication>
#include "wledconfigclient.h"
#else
#include <QApplication>
#include "hyperiongrabber.h"
#endif
#ifndef WLED_CONFIG_TOOL_BUILD
static HyperionGrabber *grab;
static QApplication *qapp;
#endif
static QFile *logFile = nullptr;
void customMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
QByteArray localMsg = msg.toLocal8Bit();
QString logMessage = QString("%1 %2 %3 %4 %5")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(context.category)
.arg(context.function)
.arg(context.line)
.arg(localMsg.constData());
switch (type) {
case QtDebugMsg:
logMessage = "Debug: " + logMessage;
break;
case QtInfoMsg:
logMessage = "Info: " + logMessage;
break;
case QtWarningMsg:
logMessage = "Warning: " + logMessage;
break;
case QtCriticalMsg:
logMessage = "Critical: " + logMessage;
break;
case QtFatalMsg:
logMessage = "Fatal: " + logMessage;
break;
}
if (logFile && logFile->isOpen()) {
QTextStream stream(logFile);
stream << logMessage << Qt::endl;
stream.flush();
}
if (type == QtCriticalMsg || type == QtFatalMsg || !logFile || !logFile->isOpen()) {
fprintf(stderr, "%s\n", logMessage.toLocal8Bit().constData());
}
}
#ifndef WLED_CONFIG_TOOL_BUILD
static void quit(int)
{
if (grab != nullptr) {
delete grab;
}
if (logFile) {
logFile->close();
delete logFile;
logFile = nullptr;
}
qapp->exit();
}
#endif
#ifdef WLED_CONFIG_TOOL_BUILD
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QCommandLineParser parser;
parser.setApplicationDescription("WLED Configuration Tool");
parser.addHelpOption();
parser.addOption(QCommandLineOption({"a", "address"}, "IP address of the WLED device.", "address"));
parser.process(app);
if (!parser.isSet("address")) {
qWarning() << "Error: WLED IP address not provided. Use -a or --address.";
parser.showHelp(1);
}
QString wledIp = parser.value("address");
WledConfigClient client(wledIp);
QObject::connect(&client, &WledConfigClient::infoReceived, [&](const QJsonObject &info) {
QJsonDocument doc(info);
qDebug() << "WLED Configuration Info:\n" << doc.toJson(QJsonDocument::Indented);
app.quit();
});
QObject::connect(&client, &WledConfigClient::error, [&](const QString &message) {
qWarning() << "Error retrieving WLED info:" << message;
app.quit();
});
client.getInfo();
return app.exec();
}
#else
int main(int argc, char *argv[])
{
logFile = new QFile("output.log");
if (!logFile->open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) {
fprintf(stderr, "Warning: Could not open log file output.log for writing.\n");
delete logFile;
logFile = nullptr;
}
qInstallMessageHandler(customMessageOutput);
qapp = new QApplication(argc, argv);
signal(SIGINT, quit);
signal(SIGTERM, quit);
@ -26,32 +132,49 @@ int main(int argc, char *argv[])
parser.setApplicationDescription("Hyperion grabber for Wayland using Qt");
parser.addHelpOption();
parser.addVersionOption();
parser.addOption({{"a", "address"}, "Address to the hyperion json server. (ex. 127.0.0.1)", "address"});
parser.addOption({{"p", "port"}, "Port for the hyperion json server. (ex. 19444)", "port"});
parser.addOption({{"c", "priority"}, "Priority to send to Hyperion, lower number means higher priority, defaults to 100. Range 0-255", "number"});
parser.addOption({{"s", "scale"}, "Divisor used to scale your screen resolution (ex. 8 ; if your screen is 1920x1080, the result image sent to hyperion is 240x135", "scale"});
parser.addOption({{"f", "frameskip"}, "Number of frames to skip between captures (ex. 1 ; 0 means no frames are skipped)", "frameskip"});
parser.addOption({{"i", "inactive"}, "How many seconds after the screen is inactive to turn off the LED's. Set to 0 to disable.", "seonds"});
parser.addOption({{"r", "redadjust"}, "Adjustment of the LED's red color (requires 3 space seperated values between 0 and 255) (ex. \"255,10,0\")", "value"});
parser.addOption({{"g", "greenadjust"}, "Adjustment of the LED's green color (requires 3 space seperated values between 0 and 255) (ex. \"75,210,0\")", "value"});
parser.addOption({{"b", "blueadjust"}, "Adjustment of the LED's blue color (requires 3 space seperated values between 0 and 255) (ex. \"0,10,160\")", "value"});
parser.addOption({{"t", "temperature"}, "Adjustment of the LED's color temperature (requires 3 space seperated values between 0 and 255) (ex. \"255,255,250\")", "value"});
parser.addOption({{"d", "threshold"}, "Set the threshold of the LED's (requires 3 space seperated values between 0.0 and 1.0) (ex. \"0.0025,0.005,0.01\")", "value"});
parser.addOption({{"l", "transform"}, "Adjusts the luminance / saturation of the LED's values are in this order: luminanceGain, luminanceMin, saturationL (requires 3 space seperated values between 0.0 and 1.0) (ex. \"1.0,0.01,1.0\")", "value"});
parser.addOption(QCommandLineOption({"a", "address"}, "Address to the hyperion json server. (ex. 127.0.0.1)", "address"));
parser.addOption(QCommandLineOption({"p", "port"}, "Port for the hyperion json server. (ex. 19444)", "port"));
parser.addOption(QCommandLineOption({"s", "scale"}, "Divisor used to scale your screen resolution (ex. 8 ; if your screen is 1920x1080, the result image sent to hyperion is 240x135", "scale"));
parser.addOption(QCommandLineOption({"f", "frameskip"}, "Number of frames to skip between captures (ex. 1 ; 0 means no frames are skipped)", "frameskip"));
parser.addOption(QCommandLineOption({"i", "inactive"}, "How many seconds after the screen is inactive to turn off the LED's. Set to 0 to disable.", "seonds"));
parser.addOption(QCommandLineOption({"r", "redadjust"}, "Adjustment of the LED's red color (requires 3 space seperated values between 0 and 255) (ex. \"255,10,0\")", "value"));
parser.addOption(QCommandLineOption({"g", "greenadjust"}, "Adjustment of the LED's green color (requires 3 space seperated values between 0 and 255) (ex. \"75,210,0\")", "value"));
parser.addOption(QCommandLineOption({"b", "blueadjust"}, "Adjustment of the LED's blue color (requires 3 space seperated values between 0 and 255) (ex. \"0,10,160\")", "value"));
parser.addOption(QCommandLineOption({"t", "temperature"}, "Adjustment of the LED's color temperature (requires 3 space seperated values between 0 and 255) (ex. \"255,255,250\")", "value"));
parser.addOption(QCommandLineOption({"d", "threshold"}, "Set the threshold of the LED's (requires 3 space seperated values between 0.0 and 1.0) (ex. \"0.0025,0.005,0.01\")", "value"));
parser.addOption(QCommandLineOption({"l", "transform"}, "Adjusts the luminance / saturation of the LED's values are in this order: luminanceGain, luminanceMin, saturationL (requires 3 space seperated values between 0.0 and 1.0) (ex. \"1.0,0.01,1.0\")", "value"));
parser.addOption(QCommandLineOption("flash", "Flash a range of LEDs with a specific color. Usage: --flash <startIndex> <count> <R> <G> <B>", "startIndex count R G B"));
parser.process(*qapp);
if (!parser.isSet("address")) {
qWarning() << "Hyperion address missing.";
parser.showHelp();
return 1;
}
#ifndef DEBUG_MODE
if (parser.isSet("flash")) {
QStringList flashArgs = parser.value("flash").split(" ", Qt::SkipEmptyParts);
if (flashArgs.size() == 5) {
int startIndex = flashArgs.at(0).toInt();
int count = flashArgs.at(1).toInt();
int r = flashArgs.at(2).toInt();
int g = flashArgs.at(3).toInt();
int b = flashArgs.at(4).toInt();
if (!parser.isSet("port")) {
qWarning() << "Hyperion port missing.";
parser.showHelp();
return 1;
// Create a temporary HyperionGrabber to access WledClient
// This is a temporary solution for testing the flash function
// In a real application, you'd want a more robust way to control WledClient
QHash<QString, QString> tempOpts;
// Pass the address and port from command line to tempGrabber
if (parser.isSet("address")) tempOpts.insert("address", parser.value("address"));
if (parser.isSet("port")) tempOpts.insert("port", parser.value("port"));
HyperionGrabber tempGrabber(tempOpts);
tempGrabber._client_p->flashLeds(startIndex, count, QColor(r, g, b));
qDebug() << "Flashing LEDs: startIndex=" << startIndex << ", count=" << count << ", color=" << r << "," << g << "," << b;
return 0; // Exit after flashing
} else {
qWarning() << "Invalid arguments for --flash. Usage: --flash <startIndex> <count> <R> <G> <B>";
return 1;
}
}
#endif
QHash<QString, QString> opts;
for (const auto &optName : parser.optionNames()) {
@ -63,3 +186,4 @@ int main(int argc, char *argv[])
grab = new HyperionGrabber(opts);
return qapp->exec();
}
#endif

View File

@ -3,6 +3,10 @@
#include <QScreenCapture>
#include <QTimer>
#include <QDebug>
#include <QVideoSink>
#include <QVideoFrame>
#include <QImage>
#include <QMediaCaptureSession> // Added
int main(int argc, char *argv[])
{
@ -14,6 +18,11 @@ int main(int argc, char *argv[])
window.show();
QScreenCapture screenCapture;
QVideoSink videoSink;
QMediaCaptureSession captureSession; // Added
captureSession.setScreenCapture(&screenCapture); // Modified
captureSession.setVideoSink(&videoSink); // Modified
QObject::connect(&screenCapture, &QScreenCapture::activeChanged, [&](bool active) {
qDebug() << "Screen capture active:" << active;
@ -23,6 +32,21 @@ int main(int argc, char *argv[])
qWarning() << "Screen capture error:" << error;
});
// Connect to videoFrameChanged to save the frame
QObject::connect(&videoSink, &QVideoSink::videoFrameChanged, [&](const QVideoFrame &frame) {
if (frame.isValid()) {
qDebug() << "Frame received. Saving to captured_frame.png...";
QImage image = frame.toImage();
if (!image.isNull()) {
image.save("captured_frame.png");
qDebug() << "Frame saved. Exiting.";
QApplication::quit(); // Quit after saving the first frame
} else {
qWarning() << "Failed to convert QVideoFrame to QImage.";
}
}
});
// Start screen capture after a short delay to ensure window is visible
QTimer::singleShot(1000, [&]() {
qDebug() << "Attempting to start screen capture...";

View File

@ -1,6 +1,8 @@
#include "wledclient.h"
#include <QDebug>
#include <QColor>
#include <QByteArray>
#include <QtGlobal>
// DDP Port for WLED
const ushort WLED_DDP_PORT = 4048;
@ -73,4 +75,51 @@ void WledClient::sendImage(const QImage &image)
qWarning() << "WledClient: Sent fewer bytes than expected. Expected:" << datagram.size() << "Sent:" << bytesSent;
}
}
}
void WledClient::flashLeds(int startIndex, int count, QColor color)
{
if (count <= 0) {
qWarning() << "WledClient: Count must be positive for flashLeds.";
return;
}
// Max UDP payload size (approx 508 bytes for safe transmission)
// 4 bytes for DDP header, so 504 bytes for LED data
const int MAX_LED_DATA_PER_PACKET = 504; // 504 bytes / 3 bytes per LED = 168 LEDs
int ledsPerPacket = MAX_LED_DATA_PER_PACKET / 3; // 3 bytes per LED (RGB)
for (int i = 0; i < count; i += ledsPerPacket) {
QByteArray datagram;
datagram.reserve(4 + (ledsPerPacket * 3)); // Header + max LED data
// Byte 0: Protocol type (DNRGB)
datagram.append(DDP_PROTOCOL_DNRGB);
// Byte 1: Timeout (1 second)
datagram.append(1);
// Bytes 2 & 3: Starting LED index (low byte, then high byte)
quint16 currentStartIndex = startIndex + i;
datagram.append(currentStartIndex & 0xFF); // Low byte
datagram.append((currentStartIndex >> 8) & 0xFF); // High byte
int currentLedsInPacket = qMin(ledsPerPacket, count - i);
for (int j = 0; j < currentLedsInPacket; ++j) {
datagram.append(color.red());
datagram.append(color.green());
datagram.append(color.blue());
}
qDebug() << "WledClient: Sending flashLeds datagram (hex):" << datagram.toHex();
qDebug() << "WledClient: Datagram size:" << datagram.size();
qint64 bytesSent = _udpSocket->writeDatagram(datagram, _wledHost, _wledPort);
if (bytesSent == -1) {
qWarning() << "WledClient: Failed to send datagram for flashLeds:" << _udpSocket->errorString();
emit error(_udpSocket->errorString());
} else if (bytesSent != datagram.size()) {
qWarning() << "WledClient: Sent fewer bytes than expected for flashLeds. Expected:" << datagram.size() << "Sent:" << bytesSent;
}
}
}

View File

@ -15,6 +15,7 @@ public:
~WledClient();
void sendImage(const QImage &image);
void flashLeds(int startIndex, int count, QColor color);
private:
QUdpSocket *_udpSocket;

39
wledconfigclient.cpp Normal file
View File

@ -0,0 +1,39 @@
#include "wledconfigclient.h"
#include <QDebug>
WledConfigClient::WledConfigClient(const QString &host, QObject *parent) :
QObject(parent),
_wledHost(host)
{
_networkAccessManager = new QNetworkAccessManager(this);
connect(_networkAccessManager, &QNetworkAccessManager::finished, this, &WledConfigClient::onInfoReplyFinished);
}
WledConfigClient::~WledConfigClient()
{
qDebug() << "WledConfigClient destroyed.";
}
void WledConfigClient::getInfo()
{
QUrl url(QString("http://%1/json/info").arg(_wledHost));
QNetworkRequest request(url);
_networkAccessManager->get(request);
qDebug() << "WledConfigClient: Requesting info from" << url.toString();
}
void WledConfigClient::onInfoReplyFinished(QNetworkReply *reply)
{
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData);
if (jsonDoc.isObject()) {
emit infoReceived(jsonDoc.object());
} else {
emit error("Invalid JSON response from WLED /json/info");
}
} else {
emit error(QString("Network error: %1").arg(reply->errorString()));
}
reply->deleteLater();
}

33
wledconfigclient.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef WLEDCONFIGCLIENT_H
#define WLEDCONFIGCLIENT_H
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
class WledConfigClient : public QObject
{
Q_OBJECT
public:
explicit WledConfigClient(const QString &host, QObject *parent = nullptr);
~WledConfigClient();
void getInfo();
signals:
void infoReceived(const QJsonObject &info);
void error(const QString &message);
private slots:
void onInfoReplyFinished(QNetworkReply *reply);
private:
QNetworkAccessManager *_networkAccessManager;
QString _wledHost;
};
#endif // WLEDCONFIGCLIENT_H