Files
esa-remote-lite/main.cpp

437 lines
20 KiB
C++

#include <QApplication>
#include <QButtonGroup>
#include <QCheckBox>
#include <QClipboard>
#include <QFont>
#include <QGridLayout>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QRadioButton>
#include <QRegularExpression>
#include <QSlider>
#include <QSpinBox>
#include <QTabWidget>
#include <QTextEdit>
#include <QTreeWidgetItem>
#include <QVBoxLayout>
#include <QWidget>
#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 <emscripten.h>
#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<int,QString> 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<bool,int> {
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<int>::of(&QSpinBox::valueChanged), &window,
[=](int v){ iconIpLabel->setText(iconToIp(v)); updateUrl(); });
QObject::connect(btnGroup, QOverload<int>::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();
}