From c50aed732e1a133ebfb4acf5d84b607f13f3c0e9 Mon Sep 17 00:00:00 2001 From: "Tobias J. Endres" Date: Fri, 15 Aug 2025 23:31:28 +0200 Subject: [PATCH] feat: Implement WLED direct communication, config tool, and screen capture test setup. --- CMakeLists.txt | 44 ++++++++++- debugclient.cpp | 41 ++++++++++ debugclient.h | 23 ++++++ hyperiongrabber.cpp | 10 ++- hyperiongrabber.h | 16 +++- main.cpp | 170 ++++++++++++++++++++++++++++++++++------ screen_capture_test.cpp | 24 ++++++ wledclient.cpp | 49 ++++++++++++ wledclient.h | 1 + wledconfigclient.cpp | 39 +++++++++ wledconfigclient.h | 33 ++++++++ 11 files changed, 421 insertions(+), 29 deletions(-) create mode 100644 debugclient.cpp create mode 100644 debugclient.h create mode 100644 wledconfigclient.cpp create mode 100644 wledconfigclient.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e658d0..3ed30ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/debugclient.cpp b/debugclient.cpp new file mode 100644 index 0000000..7f688a9 --- /dev/null +++ b/debugclient.cpp @@ -0,0 +1,41 @@ +#include "debugclient.h" +#include + +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; +} diff --git a/debugclient.h b/debugclient.h new file mode 100644 index 0000000..64d6419 --- /dev/null +++ b/debugclient.h @@ -0,0 +1,23 @@ +#ifndef DEBUGCLIENT_H +#define DEBUGCLIENT_H + +#include +#include +#include + +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 diff --git a/hyperiongrabber.cpp b/hyperiongrabber.cpp index 28d3be5..602040f 100644 --- a/hyperiongrabber.cpp +++ b/hyperiongrabber.cpp @@ -35,7 +35,11 @@ HyperionGrabber::HyperionGrabber(QHash 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); } diff --git a/hyperiongrabber.h b/hyperiongrabber.h index bd322c5..6dd2e78 100644 --- a/hyperiongrabber.h +++ b/hyperiongrabber.h @@ -6,7 +6,12 @@ #include #include -#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); diff --git a/main.cpp b/main.cpp index 93810b1..4c872f9 100644 --- a/main.cpp +++ b/main.cpp @@ -1,21 +1,127 @@ -#include #include #include -#include "hyperiongrabber.h" +#include +#include +#include +#include +#include +#ifdef WLED_CONFIG_TOOL_BUILD +#include +#include "wledconfigclient.h" +#else +#include +#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")); 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 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 "; + return 1; + } } +#endif QHash 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 diff --git a/screen_capture_test.cpp b/screen_capture_test.cpp index 2ed0fa3..377dad2 100644 --- a/screen_capture_test.cpp +++ b/screen_capture_test.cpp @@ -3,6 +3,10 @@ #include #include #include +#include +#include +#include +#include // 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..."; diff --git a/wledclient.cpp b/wledclient.cpp index ce67376..904f753 100644 --- a/wledclient.cpp +++ b/wledclient.cpp @@ -1,6 +1,8 @@ #include "wledclient.h" #include #include +#include +#include // 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; + } + } } \ No newline at end of file diff --git a/wledclient.h b/wledclient.h index b86c64d..64dc8a1 100644 --- a/wledclient.h +++ b/wledclient.h @@ -15,6 +15,7 @@ public: ~WledClient(); void sendImage(const QImage &image); + void flashLeds(int startIndex, int count, QColor color); private: QUdpSocket *_udpSocket; diff --git a/wledconfigclient.cpp b/wledconfigclient.cpp new file mode 100644 index 0000000..933ee26 --- /dev/null +++ b/wledconfigclient.cpp @@ -0,0 +1,39 @@ +#include "wledconfigclient.h" +#include + +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(); +} \ No newline at end of file diff --git a/wledconfigclient.h b/wledconfigclient.h new file mode 100644 index 0000000..3381758 --- /dev/null +++ b/wledconfigclient.h @@ -0,0 +1,33 @@ +#ifndef WLEDCONFIGCLIENT_H +#define WLEDCONFIGCLIENT_H + +#include +#include +#include +#include +#include +#include + +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 \ No newline at end of file