#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "GamesPanel.h" #include "LogPanel.h" #include "LightsPanel.h" #include "PanelsPanel.h" #include "PowerPanel.h" #include "SettingsTree.h" #include "VersionsPanel.h" #include "WebSocketController.h" #ifdef __EMSCRIPTEN__ #include #endif // --------------------------------------------------------------------------- // OS clipboard bridge (works from button clicks in WASM via navigator.clipboard) // --------------------------------------------------------------------------- static void copyToClipboard(const QString &text) { #ifdef __EMSCRIPTEN__ QByteArray utf8 = text.toUtf8(); EM_ASM({ var txt = UTF8ToString($0, $1); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(txt).catch(function() { var ta = document.createElement('textarea'); ta.value = txt; ta.style.position='fixed'; ta.style.opacity='0'; document.body.appendChild(ta); ta.focus(); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); }); } else { var ta = document.createElement('textarea'); ta.value = txt; ta.style.position='fixed'; ta.style.opacity='0'; document.body.appendChild(ta); ta.focus(); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } }, utf8.constData(), utf8.size()); #else QGuiApplication::clipboard()->setText(text); #endif } // --------------------------------------------------------------------------- // ICON number → IP address // --------------------------------------------------------------------------- static const auto iconToIp = [](int icon) -> QString { static const QHash overrides = { {280, QStringLiteral("10.10.1.30")}, }; if (overrides.contains(icon)) return overrides[icon]; const int subnet = (icon - 1) / 254; const int host = ((icon - 1) % 254) + 1; return QString("10.10.%1.%2").arg(subnet).arg(host); }; // --------------------------------------------------------------------------- static QWidget *makeGamesTab(WebSocketController *ctrl, QWidget *parent) { auto *panel = new GamesPanel(parent); ctrl->setGamesPanel(panel); QObject::connect(panel, &GamesPanel::commandRequested, ctrl, &WebSocketController::sendCommand); return panel; } static QWidget *makeVersionsTab(WebSocketController *ctrl, QWidget *parent) { auto *panel = new VersionsPanel(parent); ctrl->setVersionsPanel(panel); return panel; } static QWidget *makeSettingsTab(WebSocketController *ctrl, QWidget *parent) { auto *page = new QWidget(parent); auto *layout = new QVBoxLayout(page); layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(4); // --- Sliders row (top) --- auto *slidersRow = new QHBoxLayout(); auto *brightnessLabel = new QLabel("Brightness:", page); auto *brightnessSlider = new QSlider(Qt::Horizontal, page); brightnessSlider->setMinimum(1); brightnessSlider->setMaximum(12); brightnessSlider->setValue(1); brightnessSlider->setEnabled(false); auto *brightnessVal = new QLabel("–", page); brightnessVal->setFixedWidth(28); auto *volumeLabel = new QLabel("Volume:", page); auto *volumeSlider = new QSlider(Qt::Horizontal, page); volumeSlider->setMinimum(1); volumeSlider->setMaximum(6); volumeSlider->setValue(1); volumeSlider->setEnabled(false); auto *volumeVal = new QLabel("–", page); volumeVal->setFixedWidth(28); slidersRow->addWidget(brightnessLabel); slidersRow->addWidget(brightnessSlider, 1); slidersRow->addWidget(brightnessVal); slidersRow->addSpacing(16); slidersRow->addWidget(volumeLabel); slidersRow->addWidget(volumeSlider, 1); slidersRow->addWidget(volumeVal); layout->addLayout(slidersRow); // --- Tree --- auto *tree = new SettingsTree(page); auto *ph = new QTreeWidgetItem(tree); ph->setText(0, "Connect to load settings..."); layout->addWidget(tree, 1); ctrl->setSettingsTree(tree); // --- Toolbar (below tree) --- auto *toolbar = new QHBoxLayout(); auto *nameEdit = new QLineEdit(page); nameEdit->setPlaceholderText("Config name..."); nameEdit->setMaximumWidth(160); auto *saveBtn = new QPushButton("Save to Device", page); auto *loadBtn = new QPushButton("Restore from Device", page); auto *resetBtn = new QPushButton("Restore Defaults", page); resetBtn->setStyleSheet( "QPushButton { color:white; background:#c62828; border-radius:4px; padding:3px 8px; }" "QPushButton:hover { background:#b71c1c; }" "QPushButton:disabled { background:#888; color:#ccc; }"); auto *autoPowerOff = new QCheckBox("Auto Power Off", page); toolbar->addWidget(nameEdit); toolbar->addWidget(saveBtn); toolbar->addWidget(loadBtn); toolbar->addWidget(resetBtn); toolbar->addStretch(1); toolbar->addWidget(autoPowerOff); layout->addLayout(toolbar); // --- Reset confirm row --- auto *confirmRow = new QHBoxLayout(); auto *confirmLabel = new QLabel("Click again to confirm reset:", page); confirmLabel->setStyleSheet("color:#c62828;"); confirmLabel->setVisible(false); auto *confirmYesBtn = new QPushButton("Yes, Reset", page); confirmYesBtn->setStyleSheet("QPushButton{color:white;background:#c62828;border-radius:4px;padding:2px 8px;}"); confirmYesBtn->setVisible(false); auto *confirmNoBtn = new QPushButton("Cancel", page); confirmNoBtn->setVisible(false); confirmRow->addWidget(confirmLabel); confirmRow->addWidget(confirmYesBtn); confirmRow->addWidget(confirmNoBtn); confirmRow->addStretch(1); layout->addLayout(confirmRow); // Save auto doSave = [ctrl, nameEdit]() { QString name = nameEdit->text().trimmed(); name.remove(QRegularExpression("[^A-Za-z0-9]")); if (name.isEmpty()) return; ctrl->sendCommand(QString("GBL SaveToFile %1").arg(name)); nameEdit->clear(); }; QObject::connect(saveBtn, &QPushButton::clicked, page, doSave); QObject::connect(nameEdit, &QLineEdit::returnPressed, page, doSave); QObject::connect(loadBtn, &QPushButton::clicked, page, [ctrl, nameEdit]() { QString name = nameEdit->text().trimmed(); name.remove(QRegularExpression("[^A-Za-z0-9]")); if (name.isEmpty()) return; ctrl->sendCommand(QString("GBL LoadFromFile %1").arg(name)); nameEdit->clear(); }); QObject::connect(resetBtn, &QPushButton::clicked, page, [confirmLabel, confirmYesBtn, confirmNoBtn]() { confirmLabel->setVisible(true); confirmYesBtn->setVisible(true); confirmNoBtn->setVisible(true); }); QObject::connect(confirmYesBtn, &QPushButton::clicked, page, [ctrl, confirmLabel, confirmYesBtn, confirmNoBtn]() { ctrl->sendCommand(QStringLiteral("GBL ResetAllToDefaults")); confirmLabel->setVisible(false); confirmYesBtn->setVisible(false); confirmNoBtn->setVisible(false); }); QObject::connect(confirmNoBtn, &QPushButton::clicked, page, [confirmLabel, confirmYesBtn, confirmNoBtn]() { confirmLabel->setVisible(false); confirmYesBtn->setVisible(false); confirmNoBtn->setVisible(false); }); QObject::connect(autoPowerOff, &QCheckBox::toggled, page, [ctrl](bool checked) { ctrl->sendCommand(checked ? QStringLiteral("POW EnableAutoOff") : QStringLiteral("POW KeepOn")); }); // Slider: query ranges on connect, disable on disconnect QObject::connect(ctrl->socket(), &QWebSocket::connected, page, [ctrl]() { ctrl->sendCommand(QStringLiteral("GBL brightnessMin")); ctrl->sendCommand(QStringLiteral("GBL brightnessMax")); ctrl->sendCommand(QStringLiteral("GBL brightness")); ctrl->sendCommand(QStringLiteral("GBL sound/volumeMin")); ctrl->sendCommand(QStringLiteral("GBL sound/volumeMax")); ctrl->sendCommand(QStringLiteral("GBL sound/volume")); }); QObject::connect(ctrl->socket(), &QWebSocket::disconnected, page, [brightnessSlider, brightnessVal, volumeSlider, volumeVal]() { brightnessSlider->setEnabled(false); brightnessVal->setText("–"); volumeSlider->setEnabled(false); volumeVal->setText("–"); }); QObject::connect(ctrl->socket(), &QWebSocket::textMessageReceived, page, [brightnessSlider, brightnessVal, volumeSlider, volumeVal](const QString &msg) { auto parseGbl = [&](const QString &key) -> std::pair { const QString prefix = QString("GBL %1=").arg(key); if (!msg.startsWith(prefix)) return {false,0}; bool ok=false; int v=msg.mid(prefix.length()).toInt(&ok); return {ok,v}; }; if (auto [ok,v]=parseGbl("brightness"); ok) { brightnessSlider->setValue(v); brightnessVal->setText(QString::number(v)); brightnessSlider->setEnabled(true); } else if (auto [ok,v]=parseGbl("brightnessMin"); ok) { brightnessSlider->setMinimum(v); } else if (auto [ok,v]=parseGbl("brightnessMax"); ok) { brightnessSlider->setMaximum(v); } else if (auto [ok,v]=parseGbl("sound/volume"); ok) { volumeSlider->setValue(v); volumeVal->setText(QString::number(v)); volumeSlider->setEnabled(true); } else if (auto [ok,v]=parseGbl("sound/volumeMin"); ok) { volumeSlider->setMinimum(v); } else if (auto [ok,v]=parseGbl("sound/volumeMax"); ok) { volumeSlider->setMaximum(v); } }); QObject::connect(brightnessSlider, &QSlider::valueChanged, page, [brightnessVal](int v){ brightnessVal->setText(QString::number(v)); }); QObject::connect(brightnessSlider, &QSlider::sliderReleased,page, [ctrl,brightnessSlider](){ ctrl->sendCommand(QString("GBL brightness=%1").arg(brightnessSlider->value())); }); QObject::connect(volumeSlider, &QSlider::valueChanged, page, [volumeVal](int v){ volumeVal->setText(QString::number(v)); }); QObject::connect(volumeSlider, &QSlider::sliderReleased,page, [ctrl,volumeSlider](){ ctrl->sendCommand(QString("GBL sound/volume=%1").arg(volumeSlider->value())); }); return page; } static QWidget *makeManualTab(WebSocketController *ctrl, QWidget *parent) { auto *page = new QWidget(parent); auto *layout = new QVBoxLayout(page); layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(4); const QStringList placeholders = {"Command 1...","Command 2...","Command 3...","Command 4..."}; auto *inputGrid = new QGridLayout(); inputGrid->setSpacing(4); for (int i = 0; i < 4; ++i) { auto *edit = new QLineEdit(page); edit->setPlaceholderText(placeholders[i]); edit->setFont(QFont("Courier", 10)); inputGrid->addWidget(edit, i / 2, i % 2); QObject::connect(edit, &QLineEdit::returnPressed, page, [edit, ctrl]() { const QString cmd = edit->text().trimmed(); if (!cmd.isEmpty()) { ctrl->sendCommand(cmd); edit->clear(); } }); } layout->addLayout(inputGrid); auto *log = new QTextEdit(page); log->setReadOnly(true); log->setFont(QFont("Courier", 9)); auto *logBtnRow = new QHBoxLayout(); auto *copyLogBtn = new QPushButton("Copy Log", page); auto *clearBtn = new QPushButton("Clear Log", page); copyLogBtn->setMaximumWidth(110); clearBtn->setMaximumWidth(100); logBtnRow->addWidget(copyLogBtn); logBtnRow->addWidget(clearBtn); logBtnRow->addStretch(1); layout->addWidget(log, 1); layout->addLayout(logBtnRow); ctrl->addLogView(log); QObject::connect(clearBtn, &QPushButton::clicked, log, &QTextEdit::clear); QObject::connect(copyLogBtn, &QPushButton::clicked, page, [log](){ copyToClipboard(log->toPlainText()); }); return page; } static QWidget *makePowerTab(WebSocketController *ctrl, QWidget *parent) { auto *panel = new PowerPanel(parent); ctrl->setPowerPanel(panel); return panel; } static QWidget *makeLogsTab(WebSocketController *ctrl, QWidget *parent) { auto *panel = new LogPanel(ctrl, parent); ctrl->setLogPanel(panel); return panel; } static QWidget *makeLightsTab(WebSocketController *ctrl, QWidget *parent) { auto *panel = new LightsPanel(parent); ctrl->setLightsPanel(panel); QObject::connect(panel, &LightsPanel::commandRequested, ctrl, &WebSocketController::sendCommand); return panel; } static QWidget *makePanelsTab(WebSocketController *ctrl, QWidget *parent) { auto *panel = new PanelsPanel(parent); ctrl->setPanelsPanel(panel); return panel; } int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget window; window.setWindowTitle("ESA WebSocket Client"); auto *mainLayout = new QVBoxLayout(&window); mainLayout->setContentsMargins(6, 6, 6, 6); mainLayout->setSpacing(4); // --- Hidden urlEdit used by WebSocketController --- auto *urlEdit = new QLineEdit(&window); urlEdit->setVisible(false); auto *statusLabel = new QLabel("Disconnected", &window); // --- Network connection row --- auto *headerRow = new QHBoxLayout(); headerRow->addWidget(new QLabel("Network connection:", &window)); auto *radioFixed = new QRadioButton("10.110.110.10", &window); auto *radioIcon = new QRadioButton(&window); auto *iconSpin = new QSpinBox(&window); iconSpin->setRange(1, 9999); iconSpin->setValue(280); iconSpin->setFixedWidth(68); iconSpin->setAlignment(Qt::AlignRight); auto *iconIpLabel = new QLabel(iconToIp(280), &window); iconIpLabel->setStyleSheet("color:#555; font-style:italic;"); auto *radioOther = new QRadioButton("other:", &window); auto *otherEdit = new QLineEdit(&window); otherEdit->setText("ftsDevkit4.local"); otherEdit->setFixedWidth(160); otherEdit->setEnabled(false); auto *btnGroup = new QButtonGroup(&window); btnGroup->addButton(radioFixed, 0); btnGroup->addButton(radioIcon, 1); btnGroup->addButton(radioOther, 2); radioIcon->setChecked(true); headerRow->addWidget(radioFixed); headerRow->addWidget(radioIcon); headerRow->addWidget(iconSpin); headerRow->addWidget(iconIpLabel); headerRow->addSpacing(8); headerRow->addWidget(radioOther); headerRow->addWidget(otherEdit); headerRow->addStretch(1); auto *connectBtn = new QPushButton("Connect", &window); auto *disconnectBtn = new QPushButton("Disconnect", &window); auto *shutdownBtn = new QPushButton("Shutdown", &window); disconnectBtn->setEnabled(false); shutdownBtn->setEnabled(false); shutdownBtn->setStyleSheet( "QPushButton{color:white;background:#c62828;border-radius:4px;padding:3px 10px;}" "QPushButton:hover{background:#b71c1c;}" "QPushButton:disabled{background:#888;color:#ccc;}"); headerRow->addWidget(connectBtn); headerRow->addWidget(disconnectBtn); headerRow->addWidget(shutdownBtn); headerRow->addWidget(statusLabel); mainLayout->addLayout(headerRow); auto updateUrl = [=]() { const int port = 5424; QString host; if (radioFixed->isChecked()) host = QStringLiteral("10.110.110.10"); else if (radioIcon->isChecked()) host = iconToIp(iconSpin->value()); else { host = otherEdit->text().trimmed(); if (host.isEmpty()) host = "127.0.0.1"; } urlEdit->setText(QString("ws://%1:%2/").arg(host).arg(port)); }; updateUrl(); QObject::connect(iconSpin, QOverload::of(&QSpinBox::valueChanged), &window, [=](int v){ iconIpLabel->setText(iconToIp(v)); updateUrl(); }); QObject::connect(btnGroup, QOverload::of(&QButtonGroup::idClicked), &window, [=](int){ otherEdit->setEnabled(radioOther->isChecked()); updateUrl(); }); QObject::connect(otherEdit, &QLineEdit::textChanged, &window, [=](){ if (radioOther->isChecked()) updateUrl(); }); // --- Controller --- auto *ctrl = new WebSocketController(urlEdit, statusLabel, &window); // --- Tabs --- auto *tabs = new QTabWidget(&window); tabs->addTab(makeGamesTab (ctrl, &window), "Games"); tabs->addTab(makeVersionsTab(ctrl, &window), "Versions"); tabs->addTab(makeManualTab (ctrl, &window), "Manual Websocket"); tabs->addTab(makeSettingsTab(ctrl, &window), "Settings"); tabs->addTab(makePowerTab (ctrl, &window), "Power"); tabs->addTab(makeLogsTab (ctrl, &window), "Logs"); tabs->addTab(makePanelsTab (ctrl, &window), "Panels Impacts"); tabs->addTab(makeLightsTab (ctrl, &window), "Panel Lights"); mainLayout->addWidget(tabs, 1); // --- Shutdown inline confirm --- auto *shutdownConfirmLabel = new QLabel("Confirm shutdown:", &window); auto *shutdownConfirmYes = new QPushButton("Yes, Shutdown", &window); auto *shutdownConfirmNo = new QPushButton("Cancel", &window); shutdownConfirmLabel->setStyleSheet("color:#c62828; font-weight:bold;"); shutdownConfirmYes->setStyleSheet("QPushButton{color:white;background:#c62828;border-radius:4px;padding:2px 8px;}"); shutdownConfirmLabel->setVisible(false); shutdownConfirmYes->setVisible(false); shutdownConfirmNo->setVisible(false); headerRow->addWidget(shutdownConfirmLabel); headerRow->addWidget(shutdownConfirmYes); headerRow->addWidget(shutdownConfirmNo); QObject::connect(shutdownBtn, &QPushButton::clicked, &window, [shutdownConfirmLabel, shutdownConfirmYes, shutdownConfirmNo](){ shutdownConfirmLabel->setVisible(true); shutdownConfirmYes->setVisible(true); shutdownConfirmNo->setVisible(true); }); QObject::connect(shutdownConfirmYes, &QPushButton::clicked, &window, [ctrl, shutdownConfirmLabel, shutdownConfirmYes, shutdownConfirmNo](){ ctrl->sendCommand(QStringLiteral("POW ShutDown")); shutdownConfirmLabel->setVisible(false); shutdownConfirmYes->setVisible(false); shutdownConfirmNo->setVisible(false); }); QObject::connect(shutdownConfirmNo, &QPushButton::clicked, &window, [shutdownConfirmLabel, shutdownConfirmYes, shutdownConfirmNo](){ shutdownConfirmLabel->setVisible(false); shutdownConfirmYes->setVisible(false); shutdownConfirmNo->setVisible(false); }); QObject::connect(connectBtn, &QPushButton::clicked, ctrl, &WebSocketController::startConnection); QObject::connect(disconnectBtn, &QPushButton::clicked, ctrl, &WebSocketController::closeConnection); QObject::connect(ctrl->socket(), &QWebSocket::connected, ctrl, &WebSocketController::onConnected); QObject::connect(ctrl->socket(), &QWebSocket::disconnected, ctrl, &WebSocketController::onDisconnected); QObject::connect(ctrl->socket(), &QWebSocket::textMessageReceived, ctrl, &WebSocketController::onTextMessageReceived); QObject::connect(ctrl->socket(), &QWebSocket::errorOccurred, ctrl, &WebSocketController::onErrorOccurred); QObject::connect(ctrl->socket(), &QWebSocket::connected, &window, [connectBtn, disconnectBtn, shutdownBtn](){ connectBtn->setEnabled(false); disconnectBtn->setEnabled(true); shutdownBtn->setEnabled(true); }); QObject::connect(ctrl->socket(), &QWebSocket::disconnected, &window, [connectBtn, disconnectBtn, shutdownBtn](){ connectBtn->setEnabled(true); disconnectBtn->setEnabled(false); shutdownBtn->setEnabled(false); }); window.resize(1050, 620); window.show(); return app.exec(); }