diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d53e2f..ea93824 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,14 +1,36 @@ -cmake_minimum_required(VERSION 3.21) -project(WasmWebSocketClient LANGUAGES CXX) +cmake_minimum_required(VERSION 3.16) +project(wsapp VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) -find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets WebSockets) +find_package(Qt6 REQUIRED COMPONENTS Core Widgets WebSockets) -qt_standard_project_setup() +qt_add_executable(wsapp + main.cpp + GamesPanel.cpp GamesPanel.h + LogPanel.cpp LogPanel.h + PowerPanel.cpp PowerPanel.h + SettingsTree.cpp SettingsTree.h + VersionsPanel.cpp VersionsPanel.h + WebSocketController.cpp WebSocketController.h +) -qt_add_executable(WasmWebSocketClient main.cpp) +target_link_libraries(wsapp PRIVATE + Qt6::Core + Qt6::Widgets + Qt6::WebSockets +) -target_link_libraries(WasmWebSocketClient - PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::WebSockets) +if(EMSCRIPTEN) + set_target_properties(wsapp PROPERTIES + WIN32_EXECUTABLE TRUE + QT_WASM_INITIAL_MEMORY "50MB" + QT_WASM_MAX_MEMORY "1GB" + ) + target_link_options(wsapp PRIVATE + "SHELL:-s ASYNCIFY=1" + "SHELL:-s ASYNCIFY_STACK_SIZE=65536" + ) +endif() diff --git a/LogPanel.cpp b/LogPanel.cpp new file mode 100644 index 0000000..22d6c31 --- /dev/null +++ b/LogPanel.cpp @@ -0,0 +1,112 @@ +#include "LogPanel.h" +#include "WebSocketController.h" + +#include +#include +#include +#include +#include +#include + +static const QStringList k_logNames = { + "Wireless", "DataBase", "GenericControllers", "ControlPanelRxBusy", + "ErrorChecker", "Main", "PowerControl", "Radar", "LogSerial", + "GameEventHandler", "GameScript", "userList", "SecondarySystems", + "Sounds", "Games", "SecureSocketComms", "CurrentLimit", + "TimerDisplay", "Wall:Controlmessages", "PowerOffTimer", "LogMaster", + "Wall:Sensingsystem", "Globals", "GameTimer", "Nfc", "ControlPanelTx", + "Targetsystem", "GameBase", "LogSyncMessages", "MasterController", + "Wall:Buttons", "ControlPanelRx", "PowerStateChange", "SecureSocket", + "Authentication", "UplinkComms", "Resources", "GameCounter", + "StripInterface", "Uplink", "Wall:Datarequests", "Wirelesswall", + "BeamBreakers", "WebSocketServer", "Wall:Targetdrawing", + "LoopTestMessages", "Gps", "LoopMessages", "NfcRemote", + "LogLoopErrors" +}; + +LogPanel::LogPanel(WebSocketController *ctrl, QWidget *parent) + : QWidget(parent), m_ctrl(ctrl) +{ + auto *outer = new QVBoxLayout(this); + outer->setContentsMargins(6, 6, 6, 6); + outer->setSpacing(6); + + auto *toolbar = new QHBoxLayout(); + auto *refreshBtn = new QPushButton("Refresh", this); + auto *allOnBtn = new QPushButton("All On", this); + auto *allOffBtn = new QPushButton("All Off", this); + toolbar->addWidget(refreshBtn); + toolbar->addWidget(allOnBtn); + toolbar->addWidget(allOffBtn); + toolbar->addStretch(1); + outer->addLayout(toolbar); + + auto *scroll = new QScrollArea(this); + scroll->setWidgetResizable(true); + auto *inner = new QWidget(scroll); + auto *grid = new QGridLayout(inner); + grid->setSpacing(4); + grid->setContentsMargins(4, 4, 4, 4); + + const int cols = 4; + for (int i = 0; i < k_logNames.size(); ++i) { + const QString &name = k_logNames[i]; + auto *cb = new QCheckBox(name, inner); + m_checkboxes[name] = cb; + grid->addWidget(cb, i / cols, i % cols); + + connect(cb, &QCheckBox::toggled, this, [this, name](bool checked) { + onCheckboxToggled(name, checked); + }); + } + + scroll->setWidget(inner); + outer->addWidget(scroll, 1); + + connect(refreshBtn, &QPushButton::clicked, this, &LogPanel::requestRefresh); + connect(allOnBtn, &QPushButton::clicked, this, [this]() { + for (auto *cb : m_checkboxes) + cb->setChecked(true); + }); + connect(allOffBtn, &QPushButton::clicked, this, [this]() { + for (auto *cb : m_checkboxes) + cb->setChecked(false); + }); +} + +void LogPanel::requestRefresh() +{ + if (m_ctrl) + m_ctrl->sendCommand(QStringLiteral("LOG")); +} + +void LogPanel::onCheckboxToggled(const QString &name, bool checked) +{ + if (!m_ctrl) + return; + + const QString state = checked ? "on" : "off"; + m_ctrl->sendCommand(QString("LOG %1=%2").arg(name, state)); + m_ctrl->sendCommand(QStringLiteral("LOG")); +} + +void LogPanel::applyLogResponse(const QString &msg) +{ + for (auto *cb : m_checkboxes) + cb->blockSignals(true); + + for (auto *cb : m_checkboxes) + cb->setChecked(false); + + const QStringList tokens = msg.split(' ', Qt::SkipEmptyParts); + for (int i = 1; i < tokens.size(); ++i) { + const QString &tok = tokens[i]; + const bool isOn = tok.endsWith('*'); + const QString name = isOn ? tok.chopped(1) : tok; + if (m_checkboxes.contains(name)) + m_checkboxes[name]->setChecked(isOn); + } + + for (auto *cb : m_checkboxes) + cb->blockSignals(false); +} diff --git a/LogPanel.h b/LogPanel.h new file mode 100644 index 0000000..d5ba0e7 --- /dev/null +++ b/LogPanel.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include + +class QCheckBox; +class WebSocketController; + +class LogPanel : public QWidget +{ + Q_OBJECT +public: + explicit LogPanel(WebSocketController *ctrl, QWidget *parent = nullptr); + void applyLogResponse(const QString &msg); + +private slots: + void onCheckboxToggled(const QString &name, bool checked); + +private: + void requestRefresh(); + + WebSocketController *m_ctrl = nullptr; + QMap m_checkboxes; +}; diff --git a/PowerPanel.cpp b/PowerPanel.cpp new file mode 100644 index 0000000..1921dd5 --- /dev/null +++ b/PowerPanel.cpp @@ -0,0 +1,127 @@ +#include "PowerPanel.h" + +#include +#include +#include + +static QLabel *makeRow(QWidget *parent) +{ + auto *l = new QLabel(parent); + l->setStyleSheet("font-size: 11pt; padding: 2px 0px;"); + return l; +} + +PowerPanel::PowerPanel(QWidget *parent) : QWidget(parent) +{ + auto *outer = new QVBoxLayout(this); + outer->setContentsMargins(10, 10, 10, 10); + outer->setSpacing(2); + + auto *heading = new QLabel("Status:", this); + QFont hf = heading->font(); + hf.setBold(true); + hf.setPointSize(10); + heading->setFont(hf); + outer->addWidget(heading); + + auto *line = new QFrame(this); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + outer->addWidget(line); + + m_statusLabel = makeRow(this); + m_supplyLabel = makeRow(this); + m_externalLabel = makeRow(this); + m_ratedLabel = makeRow(this); + m_batt1Label = makeRow(this); + m_batt2Label = makeRow(this); + m_batt3Label = makeRow(this); + + outer->addWidget(m_statusLabel); + outer->addWidget(m_supplyLabel); + outer->addWidget(m_externalLabel); + outer->addWidget(m_ratedLabel); + outer->addWidget(m_batt1Label); + outer->addWidget(m_batt2Label); + outer->addWidget(m_batt3Label); + outer->addStretch(1); + + reset(); +} + +void PowerPanel::reset() +{ + const QString style = "font-size: 11pt; padding: 2px 0px;"; + m_statusLabel ->setText("Connection status: --"); m_statusLabel ->setStyleSheet(style); + m_supplyLabel ->setText("Supply voltage: --"); m_supplyLabel ->setStyleSheet(style); + m_externalLabel->setText("External voltage: --"); m_externalLabel->setStyleSheet(style); + m_ratedLabel ->setText("Rated voltage: --"); m_ratedLabel ->setStyleSheet(style); + m_batt1Label ->setText("Battery 1 voltage: --"); m_batt1Label ->setStyleSheet(style); + m_batt2Label ->setText("Battery 2 voltage: --"); m_batt2Label ->setStyleSheet(style); + m_batt3Label ->setText("Battery 3 voltage: --"); m_batt3Label ->setStyleSheet(style); +} + +// #P0-P STA ext +void PowerPanel::setStatus(const QString &raw) +{ + applyLabel(m_statusLabel, + QString("Connection status: %1").arg(statusText(raw)), + statusColour(raw)); +} + +// #P0-P RTV 14.00 +void PowerPanel::setRatedVoltage(const QString &v) +{ + applyLabel(m_ratedLabel, QString("Rated voltage: %1 V").arg(v), "#2196a0"); +} + +// #P0-P VTG supply ext batt1 batt2 batt3 +void PowerPanel::setVoltages(const QStringList &v) +{ + auto set = [&](QLabel *l, const QString &prefix, int idx) { + if (idx >= v.size()) return; + applyLabel(l, QString("%1: %2 V").arg(prefix, v[idx]), voltColour(v[idx])); + }; + set(m_supplyLabel, "Supply voltage", 0); + set(m_externalLabel, "External voltage", 1); + set(m_batt1Label, "Battery 1 voltage", 2); + set(m_batt2Label, "Battery 2 voltage", 3); + set(m_batt3Label, "Battery 3 voltage", 4); +} + +void PowerPanel::applyLabel(QLabel *l, const QString &text, const QString &colour) +{ + l->setText(text); + l->setStyleSheet(QString("font-size: 11pt; padding: 2px 0px; color: %1;").arg(colour)); +} + +QString PowerPanel::statusText(const QString &raw) const +{ + if (raw == "ext") return "External supply"; + if (raw == "bat") return "Battery"; + if (raw == "low") return "Battery low"; + if (raw == "crit") return "Battery critical"; + if (raw == "off") return "Off"; + return raw; +} + +QString PowerPanel::statusColour(const QString &raw) const +{ + if (raw == "ext") return "#2e7d32"; + if (raw == "bat") return "#1565c0"; + if (raw == "low") return "#e65100"; + if (raw == "crit") return "#c62828"; + if (raw == "off") return "#888888"; + return "#888888"; +} + +QString PowerPanel::voltColour(const QString &val) const +{ + bool ok = false; + const double d = val.toDouble(&ok); + if (!ok) return "palette(text)"; + if (d > 10.0) return "#2e7d32"; + if (d > 5.0) return "#e65100"; + if (d > 0.01) return "#c62828"; + return "#888888"; +} diff --git a/PowerPanel.h b/PowerPanel.h new file mode 100644 index 0000000..1de24de --- /dev/null +++ b/PowerPanel.h @@ -0,0 +1,29 @@ +#pragma once +#include + +class QLabel; + +class PowerPanel : public QWidget +{ + Q_OBJECT +public: + explicit PowerPanel(QWidget *parent = nullptr); + void reset(); + void setStatus(const QString &raw); + void setRatedVoltage(const QString &v); + void setVoltages(const QStringList &v); + +private: + QString statusText(const QString &raw) const; + QString statusColour(const QString &raw) const; + QString voltColour(const QString &val) const; + void applyLabel(QLabel *l, const QString &text, const QString &colour); + + QLabel *m_statusLabel = nullptr; + QLabel *m_supplyLabel = nullptr; + QLabel *m_externalLabel = nullptr; + QLabel *m_ratedLabel = nullptr; + QLabel *m_batt1Label = nullptr; + QLabel *m_batt2Label = nullptr; + QLabel *m_batt3Label = nullptr; +}; diff --git a/WebSocketController.cpp b/WebSocketController.cpp index 493a801..9525e85 100644 --- a/WebSocketController.cpp +++ b/WebSocketController.cpp @@ -1,5 +1,7 @@ #include "WebSocketController.h" #include "GamesPanel.h" +#include "LogPanel.h" +#include "PowerPanel.h" #include "SettingsTree.h" #include "VersionsPanel.h" @@ -15,6 +17,7 @@ WebSocketController::WebSocketController(QLineEdit *urlEdit, {} QWebSocket *WebSocketController::socket() { return &m_socket; } + void WebSocketController::addLogView(QTextEdit *log) { m_logs.append(log); } void WebSocketController::setSettingsTree(SettingsTree *tree) @@ -26,49 +29,14 @@ void WebSocketController::setSettingsTree(SettingsTree *tree) void WebSocketController::setGamesPanel(GamesPanel *panel) { m_gamesPanel = panel; } void WebSocketController::setVersionsPanel(VersionsPanel *panel) { m_versionsPanel = panel; } +void WebSocketController::setPowerPanel(PowerPanel *panel) { m_powerPanel = panel; } +void WebSocketController::setLogPanel(LogPanel *panel) { m_logPanel = panel; } bool WebSocketController::isConnected() const { return m_socket.state() == QAbstractSocket::ConnectedState; } -// ---------------------------------------------------------------- -// Lazy loaders — called ONLY when user clicks a tab for first time -// ---------------------------------------------------------------- - -void WebSocketController::requestGamesData() -{ - if (!isConnected() || m_gamesRequested) return; - m_gamesRequested = true; - broadcast("-- Loading games tab --"); - sendCommand(QStringLiteral("GAM list")); -} - -void WebSocketController::requestVersionsData() -{ - if (!isConnected() || m_versionsRequested) return; - m_versionsRequested = true; - broadcast("-- Loading versions tab --"); - sendCommand(QStringLiteral("NAM")); - sendCommand(QStringLiteral("VER")); - sendCommand(QStringLiteral("UID")); - sendCommand(QStringLiteral("RNP")); - // Slave VER/UID loop fires in handleProtocol when RNP reply arrives -} - -void WebSocketController::requestSettingsData() -{ - if (!isConnected() || m_settingsRequested) return; - m_settingsRequested = true; - broadcast("-- Loading settings tab --"); - sendCommand(QStringLiteral("GBL List")); - // Individual GBL loop fires in handleProtocol when List reply arrives -} - -// ---------------------------------------------------------------- -// Connection -// ---------------------------------------------------------------- - void WebSocketController::startConnection() { const QUrl url(m_urlEdit->text().trimmed()); @@ -82,11 +50,9 @@ void WebSocketController::closeConnection() { broadcast("Closing connection"); m_settingsKeys.clear(); - m_rnpCount = -1; - m_gamesRequested = false; - m_versionsRequested = false; - m_settingsRequested = false; + m_rnpCount = -1; if (m_versionsPanel) m_versionsPanel->reset(); + if (m_powerPanel) m_powerPanel->reset(); m_socket.close(); } @@ -99,13 +65,20 @@ void WebSocketController::sendCommand(const QString &cmd) void WebSocketController::onConnected() { - // Send NOTHING here — all data is lazy-loaded on tab click broadcast("Connected"); m_statusLabel->setText("Connected"); - m_rnpCount = -1; - m_gamesRequested = false; - m_versionsRequested = false; - m_settingsRequested = false; + m_rnpCount = -1; + + sendCommand(QStringLiteral("GBL List")); + sendCommand(QStringLiteral("GAM list")); + sendCommand(QStringLiteral("NAM")); + sendCommand(QStringLiteral("VER")); + sendCommand(QStringLiteral("UID")); + sendCommand(QStringLiteral("RNP")); + sendCommand(QStringLiteral("#P0-P STA")); + sendCommand(QStringLiteral("#P0-P RTV")); + sendCommand(QStringLiteral("#P0-P VTG")); + sendCommand(QStringLiteral("LOG")); } void WebSocketController::onDisconnected() @@ -113,10 +86,7 @@ void WebSocketController::onDisconnected() broadcast("Disconnected"); m_statusLabel->setText("Disconnected"); m_settingsKeys.clear(); - m_rnpCount = -1; - m_gamesRequested = false; - m_versionsRequested = false; - m_settingsRequested = false; + m_rnpCount = -1; } void WebSocketController::onTextMessageReceived(const QString &msg) @@ -137,37 +107,54 @@ void WebSocketController::onValueEdited(const QString &key, const QString &newVa sendCommand(QString("GBL %1").arg(key)); } -// ---------------------------------------------------------------- -// Protocol handler -// ---------------------------------------------------------------- - void WebSocketController::handleProtocol(const QString &msg) { const QStringList tokens = msg.split(' ', Qt::SkipEmptyParts); if (tokens.isEmpty()) return; const QString cmd = tokens[0]; - // NAM + // ---- Power: #P0-P STA/RTV/VTG ---- + if (cmd == "#P0-P" && tokens.size() >= 2 && m_powerPanel) { + const QString sub = tokens[1]; + if (sub == "STA" && tokens.size() >= 3) m_powerPanel->setStatus(tokens[2]); + else if (sub == "RTV" && tokens.size() >= 3) m_powerPanel->setRatedVoltage(tokens[2]); + else if (sub == "VTG" && tokens.size() >= 3) m_powerPanel->setVoltages(tokens.mid(2)); + return; + } + + // ---- LOG channel list ---- + // Response: LOG name1 name2* name3 ... (* = enabled) + if (cmd == "LOG" && tokens.size() > 1 && m_logPanel) { + // Only handle the full list response (no '=' in any token) + bool isList = true; + for (int i = 1; i < tokens.size(); ++i) { + if (tokens[i].contains('=')) { isList = false; break; } + } + if (isList) m_logPanel->applyLogResponse(msg); + return; + } + + // NAM if (cmd == "NAM" && tokens.size() >= 2) { if (m_versionsPanel) m_versionsPanel->setDeviceName(tokens[1]); return; } - // VER M or VER + // VER if (cmd == "VER" && tokens.size() >= 3) { if (m_versionsPanel) m_versionsPanel->setVersion(tokens[1], tokens.mid(2).join(' ')); return; } - // UID M or UID + // UID if (cmd == "UID" && tokens.size() >= 3) { if (m_versionsPanel) m_versionsPanel->setUid(tokens[1], tokens.mid(2).join(' ')); return; } - // RNP — triggers the slave loop + // RNP if (cmd == "RNP" && tokens.size() >= 2) { bool ok = false; const int count = tokens[1].toInt(&ok); @@ -182,7 +169,7 @@ void WebSocketController::handleProtocol(const QString &msg) return; } - // GAM list ... + // GAM list if (cmd == "GAM" && tokens.size() >= 2 && tokens[1] == "list") { if (m_gamesPanel) m_gamesPanel->loadFromResponse(msg); return; @@ -204,8 +191,8 @@ void WebSocketController::handleProtocol(const QString &msg) 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); + 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); } diff --git a/WebSocketController.h b/WebSocketController.h index bc0dab0..e65880a 100644 --- a/WebSocketController.h +++ b/WebSocketController.h @@ -7,6 +7,8 @@ class QLabel; class QLineEdit; class QTextEdit; class GamesPanel; +class LogPanel; +class PowerPanel; class SettingsTree; class VersionsPanel; @@ -23,13 +25,10 @@ public: void setSettingsTree(SettingsTree *tree); void setGamesPanel(GamesPanel *panel); void setVersionsPanel(VersionsPanel *panel); + void setPowerPanel(PowerPanel *panel); + void setLogPanel(LogPanel *panel); bool isConnected() const; - // Called by main when user selects a tab for the first time - void requestGamesData(); - void requestVersionsData(); - void requestSettingsData(); - public slots: void startConnection(); void closeConnection(); @@ -45,17 +44,15 @@ private: void handleProtocol(const QString &msg); void broadcast(const QString &line); - QLineEdit *m_urlEdit = nullptr; - QLabel *m_statusLabel = nullptr; - QWebSocket m_socket; + QLineEdit *m_urlEdit = nullptr; + QLabel *m_statusLabel = nullptr; + QWebSocket m_socket; QList m_logs; - SettingsTree *m_settingsTree = nullptr; - GamesPanel *m_gamesPanel = nullptr; - VersionsPanel *m_versionsPanel = nullptr; - QStringList m_settingsKeys; - int m_rnpCount = -1; - - bool m_gamesRequested = false; - bool m_versionsRequested = false; - bool m_settingsRequested = false; + SettingsTree *m_settingsTree = nullptr; + GamesPanel *m_gamesPanel = nullptr; + VersionsPanel *m_versionsPanel = nullptr; + PowerPanel *m_powerPanel = nullptr; + LogPanel *m_logPanel = nullptr; + QStringList m_settingsKeys; + int m_rnpCount = -1; }; diff --git a/build-run.sh b/build-run.sh index c4c74c1..376c0b0 100644 --- a/build-run.sh +++ b/build-run.sh @@ -1,3 +1,6 @@ /home/user/Qt/6.7.3/wasm_singlethread/bin/qt-cmake -S . -B build-wasm -G Ninja cmake --build build-wasm +cd build-wasm +python3 -m http.server 8000 + diff --git a/main.cpp b/main.cpp index adc982d..3c5dea0 100644 --- a/main.cpp +++ b/main.cpp @@ -1,408 +1,189 @@ #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 "PowerPanel.h" +#include "SettingsTree.h" +#include "VersionsPanel.h" +#include "WebSocketController.h" -// ---------------------------------------------------------------- -// Settings tree -// ---------------------------------------------------------------- -class SettingsTree : public QTreeWidget +static QWidget *makeGamesTab(WebSocketController *ctrl, QWidget *parent) { - 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); + auto *panel = new GamesPanel(parent); + ctrl->setGamesPanel(panel); + return panel; +} - 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 m_itemMap; - bool m_loading = false; -}; - -// ---------------------------------------------------------------- -// WebSocket controller -// ---------------------------------------------------------------- -class WebSocketController : public QObject +static QWidget *makeVersionsTab(WebSocketController *ctrl, QWidget *parent) { - Q_OBJECT -public: - explicit WebSocketController(QLineEdit *urlEdit, - QLabel *statusLabel, - QObject *parent = nullptr) - : QObject(parent), m_urlEdit(urlEdit), m_statusLabel(statusLabel) - {} + auto *panel = new VersionsPanel(parent); + ctrl->setVersionsPanel(panel); + return panel; +} - 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 m_logs; - SettingsTree *m_settingsTree = nullptr; - QStringList m_settingsKeys; -}; - -// ---------------------------------------------------------------- -// Tab builders -// ---------------------------------------------------------------- -static QWidget *makeSettingsTab(WebSocketController *controller, QWidget *parent = nullptr) +static QWidget *makeSettingsTab(WebSocketController *ctrl, QWidget *parent) { auto *page = new QWidget(parent); auto *layout = new QVBoxLayout(page); layout->setContentsMargins(2, 2, 2, 2); - auto *tree = new SettingsTree(page); - auto *placeholder = new QTreeWidgetItem(tree); - placeholder->setText(0, "Connect to load settings..."); - + auto *ph = new QTreeWidgetItem(tree); + ph->setText(0, "Connect to load settings..."); layout->addWidget(tree, 1); - controller->setSettingsTree(tree); + ctrl->setSettingsTree(tree); return page; } -static QWidget *makeManualTab(WebSocketController *controller, QWidget *parent = nullptr) +static QWidget *makeManualTab(WebSocketController *ctrl, QWidget *parent) { - auto *page = new QWidget(parent); - auto *layout = new QVBoxLayout(page); - - auto *sendRow = new QHBoxLayout(); + 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 *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); - 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]() { + auto send = [cmdEdit, ctrl]() { const QString cmd = cmdEdit->text().trimmed(); - if (!cmd.isEmpty()) { - controller->sendCommand(cmd); - cmdEdit->clear(); - } + if (!cmd.isEmpty()) { ctrl->sendCommand(cmd); cmdEdit->clear(); } }; - - QObject::connect(sendBtn, &QPushButton::clicked, page, send); - QObject::connect(cmdEdit, &QLineEdit::returnPressed, page, send); + 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 *makePanelsTab(WebSocketController *controller, QWidget *parent = nullptr) +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); - controller->addLogView(log); + ctrl->addLogView(log); return page; } -static QWidget *makePlaceholder(const QString &text, QWidget *parent = nullptr) -{ - auto *page = new QWidget(parent); - auto *layout = new QVBoxLayout(page); - auto *label = new QLabel(text, page); - label->setAlignment(Qt::AlignCenter); - layout->addWidget(label); - return page; -} - -// ---------------------------------------------------------------- -// main -// ---------------------------------------------------------------- int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget window; - window.setWindowTitle("Qt WASM WebSocket Client"); + window.setWindowTitle("ESA WebSocket Client"); auto *mainLayout = new QVBoxLayout(&window); mainLayout->setContentsMargins(6, 6, 6, 6); mainLayout->setSpacing(4); - auto *headerLayout = new QHBoxLayout(); - auto *urlLabel = new QLabel("WebSocket URL:", &window); - auto *urlEdit = new QLineEdit(&window); + // --- Header bar --- + auto *headerRow = new QHBoxLayout(); + 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); + 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; }"); + auto *statusLabel = new QLabel("Disconnected", &window); - auto *connectButton = new QPushButton("Connect", &window); - auto *disconnectButton = new QPushButton("Disconnect", &window); - disconnectButton->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(shutdownBtn); + headerRow->addWidget(statusLabel); + mainLayout->addLayout(headerRow); - 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); + // --- Controller --- + auto *ctrl = new WebSocketController(urlEdit, statusLabel, &window); + // --- Tabs --- auto *tabs = new QTabWidget(&window); - 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"); + 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); - 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); + // --- Shutdown: confirm then send --- + QObject::connect(shutdownBtn, &QPushButton::clicked, &window, [ctrl, &window]() { + QMessageBox msgBox(&window); + msgBox.setWindowTitle("Confirm Shutdown"); + msgBox.setText("Are you sure you want to shut down the machine?"); + msgBox.setInformativeText("This will send the POW ShutDown command immediately."); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + if (msgBox.exec() == QMessageBox::Yes) + ctrl->sendCommand(QStringLiteral("POW ShutDown")); }); - window.resize(950, 580); + // --- 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(); } - -#include "main.moc"