Fixed version control issue and reuploaded

This commit is contained in:
Jon ESA
2026-04-01 19:41:25 +01:00
parent f6c6d4e9dc
commit a21b5415fb
2 changed files with 339 additions and 106 deletions

View File

@@ -1,21 +1,14 @@
cmake_minimum_required(VERSION 3.16)
project(wsapp VERSION 1.0 LANGUAGES CXX)
cmake_minimum_required(VERSION 3.21)
project(WasmWebSocketClient LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Core Widgets WebSockets)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets WebSockets)
add_executable(wsapp
main.cpp
GamesPanel.cpp GamesPanel.h
SettingsTree.cpp SettingsTree.h
VersionsPanel.cpp VersionsPanel.h
WebSocketController.cpp WebSocketController.h
)
qt_standard_project_setup()
target_link_libraries(wsapp PRIVATE
Qt6::Core
Qt6::Widgets
Qt6::WebSockets
)
qt_add_executable(WasmWebSocketClient main.cpp)
target_link_libraries(WasmWebSocketClient
PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::WebSockets)

420
main.cpp
View File

@@ -1,92 +1,328 @@
#include <QApplication>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QTabWidget>
#include <QTextEdit>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QVBoxLayout>
#include <QWidget>
#include <QMap>
#include <QStringList>
#include "GamesPanel.h"
#include "SettingsTree.h"
#include "VersionsPanel.h"
#include "WebSocketController.h"
#include <QAbstractSocket>
#include <QUrl>
#include <QWebSocket>
// Must match tabs->addTab order below
static constexpr int TAB_GAMES = 0;
static constexpr int TAB_VERSIONS = 1;
static constexpr int TAB_MANUAL = 2;
static constexpr int TAB_SETTINGS = 3;
static constexpr int TAB_POWER = 4;
static constexpr int TAB_PANELS = 5;
static QWidget *makeGamesTab(WebSocketController *ctrl, QWidget *parent)
// ----------------------------------------------------------------
// Settings tree
// ----------------------------------------------------------------
class SettingsTree : public QTreeWidget
{
auto *panel = new GamesPanel(parent);
ctrl->setGamesPanel(panel);
return panel;
}
Q_OBJECT
public:
// Emitted when user edits a value cell
// key = full slash path, value = new string
Q_SIGNAL void valueEdited(const QString &key, const QString &value);
static QWidget *makeVersionsTab(WebSocketController *ctrl, QWidget *parent)
explicit SettingsTree(QWidget *parent = nullptr)
: QTreeWidget(parent)
{
setColumnCount(2);
setHeaderLabels({"Key", "Value"});
header()->setSectionResizeMode(0, QHeaderView::Interactive);
header()->setSectionResizeMode(1, QHeaderView::Stretch);
header()->setStretchLastSection(true);
setAlternatingRowColors(true);
setUniformRowHeights(true);
setAnimated(true);
setIndentation(16);
setColumnWidth(0, 240);
connect(this, &QTreeWidget::itemChanged,
this, &SettingsTree::onItemChanged);
}
void loadKeys(const QStringList &keys)
{
m_loading = true;
clear();
m_itemMap.clear();
for (const QString &key : keys) {
const QStringList parts = key.split('/');
QTreeWidgetItem *parent = invisibleRootItem();
QString accumulated;
for (int i = 0; i < parts.size(); ++i) {
if (!accumulated.isEmpty())
accumulated += '/';
accumulated += parts[i];
if (!m_itemMap.contains(accumulated)) {
auto *item = new QTreeWidgetItem(parent);
item->setText(0, parts[i]);
item->setExpanded(false);
// Only leaf nodes (last part) are editable
const bool isLeaf = (i == parts.size() - 1);
if (isLeaf) {
item->setFlags(item->flags() | Qt::ItemIsEditable);
item->setToolTip(1, "Double-click to edit and send");
} else {
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
}
m_itemMap[accumulated] = item;
}
parent = m_itemMap[accumulated];
}
}
resizeColumnToContents(0);
m_loading = false;
}
void setValue(const QString &key, const QString &value)
{
if (!m_itemMap.contains(key)) return;
m_loading = true;
m_itemMap[key]->setText(1, value);
m_loading = false;
}
private slots:
void onItemChanged(QTreeWidgetItem *item, int column)
{
if (m_loading || column != 1) return;
// Find the full path for this item
const QString key = fullPath(item);
if (key.isEmpty()) return;
emit valueEdited(key, item->text(1));
}
private:
QString fullPath(QTreeWidgetItem *item) const
{
// Walk up the tree to reconstruct the full slash-separated path
QStringList parts;
QTreeWidgetItem *cur = item;
while (cur && cur != invisibleRootItem()) {
parts.prepend(cur->text(0));
cur = cur->parent();
}
return parts.join('/');
}
QMap<QString, QTreeWidgetItem *> m_itemMap;
bool m_loading = false;
};
// ----------------------------------------------------------------
// WebSocket controller
// ----------------------------------------------------------------
class WebSocketController : public QObject
{
auto *panel = new VersionsPanel(parent);
ctrl->setVersionsPanel(panel);
return panel;
}
Q_OBJECT
public:
explicit WebSocketController(QLineEdit *urlEdit,
QLabel *statusLabel,
QObject *parent = nullptr)
: QObject(parent), m_urlEdit(urlEdit), m_statusLabel(statusLabel)
{}
static QWidget *makeSettingsTab(WebSocketController *ctrl, QWidget *parent)
QWebSocket *socket() { return &m_socket; }
void addLogView(QTextEdit *l) { m_logs.append(l); }
void setSettingsTree(SettingsTree *t)
{
m_settingsTree = t;
// When user edits a value, send set command then verify
connect(t, &SettingsTree::valueEdited,
this, &WebSocketController::onValueEdited);
}
bool isConnected() const
{
return m_socket.state() == QAbstractSocket::ConnectedState;
}
public slots:
void startConnection()
{
const QUrl url(m_urlEdit->text().trimmed());
if (!url.isValid()) { broadcast("Invalid URL"); return; }
broadcast(QString("Connecting to %1").arg(url.toString()));
m_statusLabel->setText("Connecting...");
m_socket.open(url);
}
void closeConnection()
{
broadcast("Closing connection");
m_settingsKeys.clear();
m_socket.close();
}
void sendCommand(const QString &cmd)
{
if (!isConnected()) { broadcast("[not connected]"); return; }
m_socket.sendTextMessage(cmd);
broadcast(QString("TX: %1").arg(cmd));
}
void onConnected()
{
broadcast("Connected");
m_statusLabel->setText("Connected");
sendCommand(QStringLiteral("GBL List"));
}
void onDisconnected()
{
broadcast("Disconnected");
m_statusLabel->setText("Disconnected");
m_settingsKeys.clear();
}
void onTextMessageReceived(const QString &msg)
{
broadcast(QString("RX: %1").arg(msg));
handleProtocol(msg);
}
void onErrorOccurred(QAbstractSocket::SocketError)
{
broadcast(QString("Error: %1").arg(m_socket.errorString()));
m_statusLabel->setText("Error");
}
// Called when user finishes editing a cell in the settings tree
void onValueEdited(const QString &key, const QString &newValue)
{
// Send: GBL key=newvalue
sendCommand(QString("GBL %1=%2").arg(key, newValue));
// Verify: GBL key (machine echoes back confirmed value)
sendCommand(QString("GBL %1").arg(key));
}
private:
void handleProtocol(const QString &msg)
{
if (!m_settingsTree) return;
const QStringList tokens = msg.split(' ', Qt::SkipEmptyParts);
if (tokens.size() < 2) return;
// GBL List key1 key2 ...
if (tokens[0] == "GBL" && tokens[1] == "List" && tokens.size() > 2) {
m_settingsKeys = tokens.mid(2);
m_settingsTree->loadKeys(m_settingsKeys);
for (const QString &key : m_settingsKeys)
sendCommand(QString("GBL %1").arg(key));
return;
}
// GBL key=value
if (tokens[0] == "GBL" && tokens.size() >= 2) {
const QString payload = tokens[1];
const int eqIdx = payload.indexOf('=');
if (eqIdx > 0) {
const QString key = payload.left(eqIdx);
QString value = payload.mid(eqIdx + 1);
if (tokens.size() > 2)
value += ' ' + tokens.mid(2).join(' ');
m_settingsTree->setValue(key, value);
}
}
}
void broadcast(const QString &line)
{
for (auto *log : m_logs)
log->append(line);
}
QLineEdit *m_urlEdit = nullptr;
QLabel *m_statusLabel = nullptr;
QWebSocket m_socket;
QList<QTextEdit *> m_logs;
SettingsTree *m_settingsTree = nullptr;
QStringList m_settingsKeys;
};
// ----------------------------------------------------------------
// Tab builders
// ----------------------------------------------------------------
static QWidget *makeSettingsTab(WebSocketController *controller, QWidget *parent = nullptr)
{
auto *page = new QWidget(parent);
auto *layout = new QVBoxLayout(page);
layout->setContentsMargins(2, 2, 2, 2);
auto *tree = new SettingsTree(page);
auto *ph = new QTreeWidgetItem(tree);
ph->setText(0, "Click the settings tab to load...");
auto *placeholder = new QTreeWidgetItem(tree);
placeholder->setText(0, "Connect to load settings...");
layout->addWidget(tree, 1);
ctrl->setSettingsTree(tree);
controller->setSettingsTree(tree);
return page;
}
static QWidget *makeManualTab(WebSocketController *ctrl, QWidget *parent)
static QWidget *makeManualTab(WebSocketController *controller, QWidget *parent = nullptr)
{
auto *page = new QWidget(parent);
auto *layout = new QVBoxLayout(page);
auto *row = new QHBoxLayout();
auto *page = new QWidget(parent);
auto *layout = new QVBoxLayout(page);
auto *sendRow = new QHBoxLayout();
auto *cmdEdit = new QLineEdit(page);
cmdEdit->setPlaceholderText("Enter command to send...");
auto *sendBtn = new QPushButton("Send", page);
row->addWidget(new QLabel("Command:", page));
row->addWidget(cmdEdit, 1);
row->addWidget(sendBtn);
auto *log = new QTextEdit(page);
log->setReadOnly(true);
layout->addLayout(row);
layout->addWidget(log, 1);
ctrl->addLogView(log);
auto send = [cmdEdit, ctrl]() {
sendRow->addWidget(new QLabel("Command:", page));
sendRow->addWidget(cmdEdit, 1);
sendRow->addWidget(sendBtn);
auto *rxLog = new QTextEdit(page);
rxLog->setReadOnly(true);
rxLog->setPlaceholderText("Received messages will appear here...");
layout->addLayout(sendRow);
layout->addWidget(rxLog, 1);
controller->addLogView(rxLog);
auto send = [cmdEdit, controller]() {
const QString cmd = cmdEdit->text().trimmed();
if (!cmd.isEmpty()) { ctrl->sendCommand(cmd); cmdEdit->clear(); }
if (!cmd.isEmpty()) {
controller->sendCommand(cmd);
cmdEdit->clear();
}
};
QObject::connect(sendBtn, &QPushButton::clicked, page, send);
QObject::connect(cmdEdit, &QLineEdit::returnPressed, page, send);
return page;
}
static QWidget *makePanelsTab(WebSocketController *ctrl, QWidget *parent)
static QWidget *makePanelsTab(WebSocketController *controller, QWidget *parent = nullptr)
{
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);
controller->addLogView(log);
return page;
}
static QWidget *makePlaceholder(const QString &text, QWidget *parent)
static QWidget *makePlaceholder(const QString &text, QWidget *parent = nullptr)
{
auto *page = new QWidget(parent);
auto *layout = new QVBoxLayout(page);
@@ -96,73 +332,77 @@ static QWidget *makePlaceholder(const QString &text, QWidget *parent)
return page;
}
// ----------------------------------------------------------------
// main
// ----------------------------------------------------------------
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget window;
window.setWindowTitle("ESA WebSocket Client");
window.setWindowTitle("Qt WASM WebSocket Client");
auto *mainLayout = new QVBoxLayout(&window);
mainLayout->setContentsMargins(6, 6, 6, 6);
mainLayout->setSpacing(4);
auto *headerRow = new QHBoxLayout();
auto *urlEdit = new QLineEdit(&window);
auto *headerLayout = new QHBoxLayout();
auto *urlLabel = new QLabel("WebSocket URL:", &window);
auto *urlEdit = new QLineEdit(&window);
urlEdit->setPlaceholderText("ws://127.0.0.1:3491/");
urlEdit->setText("ws://127.0.0.1:3491/");
auto *connectBtn = new QPushButton("Connect", &window);
auto *disconnectBtn = new QPushButton("Disconnect", &window);
disconnectBtn->setEnabled(false);
auto *statusLabel = new QLabel("Disconnected", &window);
headerRow->addWidget(new QLabel("WebSocket URL:", &window));
headerRow->addWidget(urlEdit, 1);
headerRow->addWidget(connectBtn);
headerRow->addWidget(disconnectBtn);
headerRow->addWidget(statusLabel);
mainLayout->addLayout(headerRow);
auto *connectButton = new QPushButton("Connect", &window);
auto *disconnectButton = new QPushButton("Disconnect", &window);
disconnectButton->setEnabled(false);
auto *statusLabel = new QLabel("Disconnected", &window);
auto *ctrl = new WebSocketController(urlEdit, statusLabel, &window);
headerLayout->addWidget(urlLabel);
headerLayout->addWidget(urlEdit, 1);
headerLayout->addWidget(connectButton);
headerLayout->addWidget(disconnectButton);
headerLayout->addWidget(statusLabel);
mainLayout->addLayout(headerLayout);
auto *controller = new WebSocketController(urlEdit, statusLabel, &window);
auto *tabs = new QTabWidget(&window);
tabs->addTab(makeGamesTab (ctrl, &window), "games"); // 0
tabs->addTab(makeVersionsTab(ctrl, &window), "versions"); // 1
tabs->addTab(makeManualTab (ctrl, &window), "manual"); // 2
tabs->addTab(makeSettingsTab(ctrl, &window), "settings"); // 3
tabs->addTab(makePlaceholder("Power content here", &window), "power"); // 4
tabs->addTab(makePanelsTab (ctrl, &window), "panels"); // 5
tabs->addTab(makePlaceholder("Games content goes here"), "games");
tabs->addTab(makePlaceholder("Versions content goes here"), "versions");
tabs->addTab(makeManualTab(controller, &window), "manual");
tabs->addTab(makeSettingsTab(controller, &window), "settings");
tabs->addTab(makePlaceholder("Power content goes here"), "power");
tabs->addTab(makePanelsTab(controller, &window), "panels");
mainLayout->addWidget(tabs, 1);
// Lazy load: each tab fires its own data request on FIRST click only
QObject::connect(tabs, &QTabWidget::currentChanged, &window,
[ctrl](int index) {
switch (index) {
case TAB_GAMES: ctrl->requestGamesData(); break;
case TAB_VERSIONS: ctrl->requestVersionsData(); break;
case TAB_SETTINGS: ctrl->requestSettingsData(); break;
default: break;
}
QObject::connect(connectButton, &QPushButton::clicked,
controller, &WebSocketController::startConnection);
QObject::connect(disconnectButton, &QPushButton::clicked,
controller, &WebSocketController::closeConnection);
QObject::connect(controller->socket(), &QWebSocket::connected,
controller, &WebSocketController::onConnected);
QObject::connect(controller->socket(), &QWebSocket::disconnected,
controller, &WebSocketController::onDisconnected);
QObject::connect(controller->socket(), &QWebSocket::textMessageReceived,
controller, &WebSocketController::onTextMessageReceived);
QObject::connect(controller->socket(), &QWebSocket::errorOccurred,
controller, &WebSocketController::onErrorOccurred);
QObject::connect(controller->socket(), &QWebSocket::connected, &window,
[connectButton, disconnectButton]() {
connectButton->setEnabled(false);
disconnectButton->setEnabled(true);
});
QObject::connect(controller->socket(), &QWebSocket::disconnected, &window,
[connectButton, disconnectButton]() {
connectButton->setEnabled(true);
disconnectButton->setEnabled(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]() {
connectBtn->setEnabled(false);
disconnectBtn->setEnabled(true);
});
QObject::connect(ctrl->socket(), &QWebSocket::disconnected, &window, [connectBtn, disconnectBtn]() {
connectBtn->setEnabled(true);
disconnectBtn->setEnabled(false);
});
window.resize(1050, 620);
window.resize(950, 580);
window.show();
return app.exec();
}
#include "main.moc"