KDEAmbi/wled_config_tool.cpp
Tobias J. Endres 24a94a4ab3 feat(wled-config-tool): Fix segfault, flickering, and add ASCII layout visualization
- Resolved persistent segmentation faults by correcting memory management and simplifying interactive input logic.
- Eliminated LED flickering by implementing a longer timeout in DNRGB packets and removing unnecessary refresh timers.
- Added an ASCII visualization of the LED layout to the terminal for better user feedback.
- Updated lessons_learned.md with detailed explanations of the problems and their solutions.
2025-08-16 02:04:09 +02:00

324 lines
11 KiB
C++

#include <QCoreApplication>
#include <QCommandLineParser>
#include <QDebug>
#include <QJsonDocument>
#include <QFile>
#include <iostream>
#include <cstdlib>
#include <QTimer>
#include <QTime>
#include <QSocketNotifier>
#include "wledconfigclient.h"
#include "wledclient.h"
// Forward declarations
void runDemoMode(const QString &address, quint16 port, int totalLeds);
void runFlashMode(const QString &address, quint16 port, int totalLeds, const QString &flashParams);
void delay(int milliseconds);
void printAsciiLayout(int ledsBottom, int ledsRight, int ledsTop, int ledsLeft, int offset, int totalLeds);
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QCoreApplication::setApplicationName("wled_config_tool");
QCoreApplication::setApplicationVersion("1.0");
QCommandLineParser parser;
parser.setApplicationDescription("WLED Configuration Tool");
parser.addHelpOption();
parser.addVersionOption();
QCommandLineOption addressOption(QStringList() << "a" << "address",
"WLED device IP address or hostname.",
"address");
parser.addOption(addressOption);
QCommandLineOption portOption(QStringList() << "p" << "port",
"WLED device UDP port.",
"port", "21324");
parser.addOption(portOption);
QCommandLineOption saveOption(QStringList() << "s" << "save",
"Save configuration to a file.",
"filepath");
parser.addOption(saveOption);
QCommandLineOption configureLayoutOption(QStringList() << "l" << "configure-layout",
"Start interactive LED layout configuration.");
parser.addOption(configureLayoutOption);
QCommandLineOption demoOption(QStringList() << "d" << "demo",
"Run a demo sequence on the LEDs.");
parser.addOption(demoOption);
QCommandLineOption flashOption(QStringList() << "f" << "flash",
"Flash a range of LEDs. Format: startIndex,count,color (e.g., 10,20,#FF0000).",
"params");
parser.addOption(flashOption);
parser.process(app);
if (!parser.isSet(addressOption)) {
qCritical() << "Error: WLED address not specified. Use -a or --address.";
parser.showHelp(1);
}
QString wledAddress = parser.value(addressOption);
quint16 wledPort = parser.value(portOption).toUShort();
if (parser.isSet(configureLayoutOption)) {
auto configClient = new WledConfigClient(wledAddress, &app);
QObject::connect(configClient, &WledConfigClient::infoReceived, [&](const QJsonObject &info) {
int totalLeds = info["leds"].toObject()["count"].toInt();
qDebug() << "Your WLED device reports" << totalLeds << "LEDs.";
QTextStream cout(stdout);
QTextStream cin(stdin);
cout << "Starting interactive LED layout configuration.\n";
cout << "Please enter the number of LEDs for each section.\n";
int ledsBottom, ledsRight, ledsTop, ledsLeft, offset;
cout << "LEDs on the bottom: ";
cout.flush();
cin >> ledsBottom;
cout << "LEDs on the right side: ";
cout.flush();
cin >> ledsRight;
cout << "LEDs on the top: ";
cout.flush();
cin >> ledsTop;
cout << "LEDs on the left side: ";
cout.flush();
cin >> ledsLeft;
cout << "Enter the starting offset (0 if the strip starts at the first LED): ";
cout.flush();
cin >> offset;
int configuredLeds = ledsBottom + ledsRight + ledsTop + ledsLeft;
cout << "Total LEDs configured: " << configuredLeds << "\n";
cout.flush();
if (configuredLeds > totalLeds) {
qWarning() << "Warning: The number of configured LEDs (" << configuredLeds
<< ") is greater than the number of LEDs reported by the device (" << totalLeds << ").";
}
auto wledClient = new WledClient(wledAddress, wledPort, &app);
auto colors = new QVector<QColor>(totalLeds, Qt::black);
int currentIndex = offset;
// Bottom
for (int i = 0; i < ledsBottom; ++i) { (*colors)[(currentIndex + i) % totalLeds] = Qt::red; }
currentIndex += ledsBottom;
// Right
for (int i = 0; i < ledsRight; ++i) { (*colors)[(currentIndex + i) % totalLeds] = Qt::green; }
currentIndex += ledsRight;
// Top
for (int i = 0; i < ledsTop; ++i) { (*colors)[(currentIndex + i) % totalLeds] = Qt::blue; }
currentIndex += ledsTop;
// Left
for (int i = 0; i < ledsLeft; ++i) { (*colors)[(currentIndex + i) % totalLeds] = Qt::yellow; }
wledClient->setLedsColor(*colors, 60); // Set timeout to 60 seconds
cout << "LEDs have been lit up according to your configuration.\n";
cout.flush();
printAsciiLayout(ledsBottom, ledsRight, ledsTop, ledsLeft, offset, totalLeds);
cout << "Press Enter to turn off LEDs and exit.\n";
cout.flush();
auto notifier = new QSocketNotifier(fileno(stdin), QSocketNotifier::Read, &app);
QObject::connect(notifier, &QSocketNotifier::activated, [=, &app]() {
colors->fill(Qt::black);
wledClient->setLedsColor(*colors);
app.quit();
});
});
QObject::connect(configClient, &WledConfigClient::error, [&](const QString &message) {
qCritical() << "Error:" << message;
app.quit();
});
configClient->getInfo();
return app.exec();
}
WledConfigClient client(wledAddress);
QObject::connect(&client, &WledConfigClient::infoReceived, [&](const QJsonObject &info) {
if (parser.isSet(demoOption) || parser.isSet(flashOption)) {
int totalLeds = info["leds"].toObject()["count"].toInt();
if (parser.isSet(demoOption)) {
runDemoMode(wledAddress, wledPort, totalLeds);
app.quit();
} else if (parser.isSet(flashOption)) {
runFlashMode(wledAddress, wledPort, totalLeds, parser.value(flashOption));
app.quit();
}
} else {
QJsonDocument doc(info);
QFile stdoutFile;
stdoutFile.open(stdout, QFile::WriteOnly);
QTextStream ts(&stdoutFile);
ts << "WLED Configuration Info:\n" << doc.toJson(QJsonDocument::Indented) << Qt::endl;
stdoutFile.close();
QString saveFilePath = parser.value(saveOption);
if (!saveFilePath.isEmpty()) {
QFile file(saveFilePath);
if (file.open(QIODevice::WriteOnly)) {
file.write(doc.toJson(QJsonDocument::Indented));
file.close();
qDebug() << "Configuration saved to" << saveFilePath;
} else {
qWarning() << "Failed to save configuration to" << saveFilePath << ":" << file.errorString();
}
}
app.quit();
}
});
QObject::connect(&client, &WledConfigClient::error, [&](const QString &message) {
qCritical() << "Error:" << message;
app.quit();
});
client.getInfo();
return app.exec();
}
// ANSI Color Codes
const QString ANSI_RESET = "\033[0m";
const QString ANSI_RED_BG = "\033[41m";
const QString ANSI_GREEN_BG = "\033[42m";
const QString ANSI_BLUE_BG = "\033[44m";
const QString ANSI_YELLOW_BG = "\033[43m";
void printAsciiLayout(int ledsBottom, int ledsRight, int ledsTop, int ledsLeft, int offset, int totalLeds) {
QTextStream cout(stdout);
int maxHorizontal = qMax(ledsTop, ledsBottom);
int maxVertical = qMax(ledsLeft, ledsRight);
// Top Row
cout << " "; // Indent for left side
for (int i = 0; i < ledsTop; ++i) {
cout << ANSI_BLUE_BG << " " << ANSI_RESET; // Two spaces for a square
}
cout << "\n";
// Middle Rows (Left and Right)
for (int i = 0; i < maxVertical; ++i) {
if (i < ledsLeft) {
cout << ANSI_YELLOW_BG << " " << ANSI_RESET;
} else {
cout << " "; // Empty space if no left LED
}
// Calculate spaces between left and right
int horizontalSpace = maxHorizontal * 2; // Each LED is two chars
cout << QString(horizontalSpace, ' ');
if (i < ledsRight) {
cout << ANSI_GREEN_BG << " " << ANSI_RESET;
}
cout << "\n";
}
// Bottom Row
cout << " "; // Indent for left side
for (int i = 0; i < ledsBottom; ++i) {
cout << ANSI_RED_BG << " " << ANSI_RESET;
}
cout << "\n";
cout << "\nLayout details:\n";
cout << " Bottom LEDs (Red): " << ledsBottom << "\n";
cout << " Right LEDs (Green): " << ledsRight << "\n";
cout << " Top LEDs (Blue): " << ledsTop << "\n";
cout << " Left LEDs (Yellow): " << ledsLeft << "\n";
cout << " Offset: " << offset << "\n";
cout << " Total LEDs: " << totalLeds << "\n";
cout.flush();
}
void runDemoMode(const QString &address, quint16 port, int totalLeds)
{
qDebug() << "Running demo mode with" << totalLeds << "LEDs on" << address << ":" << port;
qDebug() << "Press Ctrl+C to exit.";
WledClient client(address, port);
QVector<QColor> colors(totalLeds, Qt::black);
// Seed the random number generator
QTime time = QTime::currentTime();
srand(time.msec());
while (true) {
// Turn on LEDs one by one with random colors
for (int i = 0; i < totalLeds; ++i) {
colors[i] = QColor::fromRgb(rand() % 256, rand() % 256, rand() % 256);
client.setLedsColor(colors);
delay(50);
}
// Turn off all LEDs
colors.fill(Qt::black);
client.setLedsColor(colors);
delay(1000);
}
}
void runFlashMode(const QString &address, quint16 port, int totalLeds, const QString &flashParams)
{
qDebug() << "Running flash mode with params:" << flashParams;
QStringList params = flashParams.split(',');
if (params.size() != 3) {
qCritical() << "Error: Invalid flash parameters. Expected format: startIndex,count,color";
return;
}
bool ok1, ok2;
int startIndex = params[0].toInt(&ok1);
int count = params[1].toInt(&ok2);
QColor color(params[2]);
if (!ok1 || !ok2 || !color.isValid()) {
qCritical() << "Error: Invalid flash parameters. Could not parse values.";
return;
}
if (startIndex < 0 || count <= 0 || startIndex + count > totalLeds) {
qCritical() << "Error: Invalid range. Start index must be non-negative, count must be positive, and the range must be within the total number of LEDs (" << totalLeds << ").";
return;
}
WledClient client(address, port);
QVector<QColor> colors(totalLeds, Qt::black);
for (int i = 0; i < count; ++i) {
colors[startIndex + i] = color;
}
client.setLedsColor(colors);
qDebug() << "Flashed" << count << "LEDs starting from index" << startIndex << "with color" << color.name();
}
void delay(int milliseconds)
{
QEventLoop loop;
QTimer::singleShot(milliseconds, &loop, &QEventLoop::quit);
loop.exec();
}