Files
esa-remote-lite/main.cpp
2026-04-01 19:44:03 +01:00

485 lines
19 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <QApplication>
#include <QButtonGroup>
#include <QCheckBox>
#include <QHBoxLayout>
#include <QHash>
#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 "PowerPanel.h"
#include "SettingsTree.h"
#include "VersionsPanel.h"
#include "WebSocketController.h"
static QWidget *makeGamesTab(WebSocketController *ctrl, QWidget *parent)
{
auto *panel = new GamesPanel(parent);
ctrl->setGamesPanel(panel);
// Route GAM add commands back through the controller
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: brightness and volume side by side (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 (takes all spare vertical space) ---
auto *tree = new SettingsTree(page);
auto *ph = new QTreeWidgetItem(tree);
ph->setText(0, "Connect to load settings...");
layout->addWidget(tree, 1);
ctrl->setSettingsTree(tree);
// --- Single compact toolbar row: name input + 3 GBL buttons + checkbox (below tree) ---
auto *toolbar = new QHBoxLayout();
auto *nameEdit = new QLineEdit(page);
nameEdit->setPlaceholderText("Config name...");
nameEdit->setMaximumWidth(160);
auto *saveBtn = new QPushButton("Save", page);
auto *loadBtn = new QPushButton("Restore From File", page);
auto *resetBtn = new QPushButton("Restore Defaults", page);
resetBtn->setStyleSheet(
"QPushButton { color: white; background-color: #c62828; border-radius: 4px; padding: 3px 8px; }"
"QPushButton:hover { background-color: #b71c1c; }"
"QPushButton:disabled { background-color: #888888; color: #cccccc; }");
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 to defaults (inline confirm row — no QMessageBox::exec on WASM) ---
auto *confirmRow = new QHBoxLayout();
auto *confirmLabel = new QLabel(page);
confirmLabel->setText("Click again to confirm reset:");
confirmLabel->setStyleSheet("color: #c62828;");
confirmLabel->setVisible(false);
auto *confirmYesBtn = new QPushButton("Yes, Reset", page);
confirmYesBtn->setStyleSheet(
"QPushButton { color: white; background-color: #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);
// ----------------------------------------------------------------
// Connections
// ----------------------------------------------------------------
// Save config
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);
// Restore from file
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();
});
// Reset confirm
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);
});
// Auto power off
QObject::connect(autoPowerOff, &QCheckBox::toggled, page, [ctrl](bool checked) {
ctrl->sendCommand(checked ? QStringLiteral("POW EnableAutoOff")
: QStringLiteral("POW KeepOn"));
});
// Query brightness/volume ranges+values 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(QStringLiteral(""));
volumeSlider->setEnabled(false);
volumeVal->setText(QStringLiteral(""));
});
// Parse incoming GBL brightness / sound/volume responses
QObject::connect(ctrl->socket(), &QWebSocket::textMessageReceived, page,
[brightnessSlider, brightnessVal, volumeSlider, volumeVal](const QString &msg) {
// helper: parse "GBL <key>=<int>" and return {ok, value}
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);
}
});
// Live label update while dragging; send command only on release
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);
auto *row = new QHBoxLayout();
auto *cmdEdit = new QLineEdit(page);
cmdEdit->setPlaceholderText("Enter command to send...");
auto *sendBtn = new QPushButton("Send", page);
auto *clearBtn = new QPushButton("Clear", page);
row->addWidget(new QLabel("Command:", page));
row->addWidget(cmdEdit, 1);
row->addWidget(sendBtn);
row->addWidget(clearBtn);
auto *log = new QTextEdit(page);
log->setReadOnly(true);
layout->addLayout(row);
layout->addWidget(log, 1);
ctrl->addLogView(log);
auto send = [cmdEdit, ctrl]() {
const QString cmd = cmdEdit->text().trimmed();
if (!cmd.isEmpty()) { ctrl->sendCommand(cmd); cmdEdit->clear(); }
};
QObject::connect(sendBtn, &QPushButton::clicked, page, send);
QObject::connect(cmdEdit, &QLineEdit::returnPressed, page, send);
QObject::connect(clearBtn, &QPushButton::clicked, log, &QTextEdit::clear);
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 *makePanelsTab(WebSocketController *ctrl, QWidget *parent)
{
auto *page = new QWidget(parent);
auto *layout = new QVBoxLayout(page);
auto *log = new QTextEdit(page);
log->setReadOnly(true);
layout->addWidget(log, 1);
ctrl->addLogView(log);
return page;
}
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);
// --- ICON → IP helper (formula + hardcoded overrides) ---
// Returns the IP string for a given ICON number.
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);
};
// --- Hidden urlEdit still used by WebSocketController::startConnection() ---
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));
// Option 1 fixed IP
auto *radioFixed = new QRadioButton("10.110.110.10", &window);
// Option 2 ICON number → derived IP
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;");
// Option 3 free text (hostname or raw IP)
auto *radioOther = new QRadioButton("other:", &window);
auto *otherEdit = new QLineEdit(&window);
otherEdit->setText("ftsDevkit4.local");
otherEdit->setFixedWidth(160);
otherEdit->setEnabled(false); // only active when radioOther selected
auto *btnGroup = new QButtonGroup(&window);
btnGroup->addButton(radioFixed, 0);
btnGroup->addButton(radioIcon, 1);
btnGroup->addButton(radioOther, 2);
radioIcon->setChecked(true); // default selection
headerRow->addWidget(radioFixed);
headerRow->addWidget(radioIcon);
headerRow->addWidget(iconSpin);
headerRow->addWidget(iconIpLabel);
headerRow->addSpacing(8);
headerRow->addWidget(radioOther);
headerRow->addWidget(otherEdit);
headerRow->addStretch(1);
// Buttons
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-color: #c62828;"
" border-radius: 4px; padding: 3px 10px; }"
"QPushButton:hover { background-color: #b71c1c; }"
"QPushButton:disabled { background-color: #888888; color: #cccccc; }");
headerRow->addWidget(connectBtn);
headerRow->addWidget(disconnectBtn);
headerRow->addWidget(shutdownBtn);
headerRow->addWidget(statusLabel);
mainLayout->addLayout(headerRow);
// --- Build URL from current selection into the hidden urlEdit ---
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 = QStringLiteral("127.0.0.1");
}
urlEdit->setText(QString("ws://%1:%2/").arg(host).arg(port));
};
updateUrl(); // set initial value
// Keep iconIpLabel in sync and rebuild URL whenever ICON changes
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");
tabs->addTab(makeSettingsTab(ctrl, &window), "Settings");
tabs->addTab(makePowerTab (ctrl, &window), "Power");
tabs->addTab(makeLogsTab (ctrl, &window), "Logs");
tabs->addTab(makePanelsTab (ctrl, &window), "Panels");
mainLayout->addWidget(tabs, 1);
// --- Shutdown: inline confirm (no QMessageBox::exec on WASM) ---
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-color: #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);
});
// --- Wire up buttons ---
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();
}