diff --git a/GamesPanel.cpp b/GamesPanel.cpp new file mode 100644 index 0000000..599faf8 --- /dev/null +++ b/GamesPanel.cpp @@ -0,0 +1,86 @@ +#include "GamesPanel.h" + +#include +#include +#include +#include +#include + +GamesPanel::GamesPanel(QWidget *parent) : QWidget(parent) +{ + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(2, 2, 2, 2); + + auto *splitter = new QSplitter(Qt::Horizontal, this); + + m_gameList = new QListWidget(splitter); + m_gameList->setMinimumWidth(180); + m_gameList->setMaximumWidth(300); + + m_detailWidget = new QWidget(splitter); + auto *detailLayout = new QVBoxLayout(m_detailWidget); + detailLayout->setContentsMargins(8, 8, 8, 8); + + m_nameLabel = new QLabel(m_detailWidget); + m_nameLabel->setStyleSheet("font-size: 14pt; font-weight: bold;"); + m_codeLabel = new QLabel(m_detailWidget); + m_codeLabel->setStyleSheet("font-size: 11pt; color: gray;"); + m_hintLabel = new QLabel("Select a game to view details.", m_detailWidget); + m_hintLabel->setAlignment(Qt::AlignTop); + + detailLayout->addWidget(m_nameLabel); + detailLayout->addWidget(m_codeLabel); + detailLayout->addWidget(m_hintLabel); + detailLayout->addStretch(1); + + splitter->addWidget(m_gameList); + splitter->addWidget(m_detailWidget); + splitter->setStretchFactor(0, 0); + splitter->setStretchFactor(1, 1); + splitter->setSizes({200, 600}); + + layout->addWidget(splitter); + + connect(m_gameList, &QListWidget::currentTextChanged, + this, &GamesPanel::onGameSelected); +} + +void GamesPanel::loadFromResponse(const QString &response) +{ + // Format: "GAM list LEADVision ARCHArchitect ..." + // Each token: 4 uppercase letters = code, rest = name (_S = space) + const QStringList tokens = response.split(' ', Qt::SkipEmptyParts); + if (tokens.size() < 3 || tokens[0] != "GAM" || tokens[1] != "list") return; + + m_games.clear(); + m_gameList->clear(); + + for (const QString &entry : tokens.mid(2)) { + if (entry.isEmpty()) continue; + + int splitPos = 0; + while (splitPos < entry.size() && entry[splitPos].isUpper()) + ++splitPos; + + const QString code = entry.left(qMin(splitPos, 4)); + QString name = entry.mid(code.size()); + name.replace("_S", " "); + + if (code.isEmpty() || name.isEmpty()) continue; + m_games[name] = code; + m_gameList->addItem(name); + } +} + +void GamesPanel::onGameSelected(const QString &name) +{ + if (name.isEmpty() || !m_games.contains(name)) { + m_nameLabel->clear(); + m_codeLabel->clear(); + m_hintLabel->setText("Select a game to view details."); + return; + } + m_nameLabel->setText(name); + m_codeLabel->setText(QString("Code: %1").arg(m_games[name])); + m_hintLabel->setText("Game selected. Future controls will appear here."); +} diff --git a/GamesPanel.h b/GamesPanel.h new file mode 100644 index 0000000..9aa5f30 --- /dev/null +++ b/GamesPanel.h @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include + +class QLabel; +class QListWidget; + +class GamesPanel : public QWidget +{ + Q_OBJECT +public: + explicit GamesPanel(QWidget *parent = nullptr); + void loadFromResponse(const QString &response); + +private slots: + void onGameSelected(const QString &name); + +private: + QListWidget *m_gameList = nullptr; + QWidget *m_detailWidget = nullptr; + QLabel *m_nameLabel = nullptr; + QLabel *m_codeLabel = nullptr; + QLabel *m_hintLabel = nullptr; + QMap m_games; // display name -> 4-letter code +}; diff --git a/SettingsTree.cpp b/SettingsTree.cpp new file mode 100644 index 0000000..29fdd80 --- /dev/null +++ b/SettingsTree.cpp @@ -0,0 +1,84 @@ +#include "SettingsTree.h" + +#include +#include + +SettingsTree::SettingsTree(QWidget *parent) : 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 SettingsTree::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); + 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 SettingsTree::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; +} + +void SettingsTree::onItemChanged(QTreeWidgetItem *item, int column) +{ + if (m_loading || column != 1) return; + const QString key = fullPath(item); + if (key.isEmpty()) return; + emit valueEdited(key, item->text(1)); +} + +QString SettingsTree::fullPath(QTreeWidgetItem *item) const +{ + QStringList parts; + QTreeWidgetItem *cur = item; + while (cur && cur != invisibleRootItem()) { + parts.prepend(cur->text(0)); + cur = cur->parent(); + } + return parts.join('/'); +} diff --git a/SettingsTree.h b/SettingsTree.h new file mode 100644 index 0000000..98538bd --- /dev/null +++ b/SettingsTree.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include + +class SettingsTree : public QTreeWidget +{ + Q_OBJECT +public: + explicit SettingsTree(QWidget *parent = nullptr); + + void loadKeys(const QStringList &keys); + void setValue(const QString &key, const QString &value); + +Q_SIGNALS: + void valueEdited(const QString &key, const QString &value); + +private slots: + void onItemChanged(QTreeWidgetItem *item, int column); + +private: + QString fullPath(QTreeWidgetItem *item) const; + + QMap m_itemMap; + bool m_loading = false; +}; diff --git a/VersionsPanel.cpp b/VersionsPanel.cpp new file mode 100644 index 0000000..d744809 --- /dev/null +++ b/VersionsPanel.cpp @@ -0,0 +1,128 @@ +#include "VersionsPanel.h" + +#include +#include +#include +#include +#include + +VersionsPanel::VersionsPanel(QWidget *parent) : QWidget(parent) +{ + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(4, 4, 4, 4); + layout->setSpacing(6); + + m_deviceNameLabel = new QLabel("Not connected", this); + m_deviceNameLabel->setStyleSheet("font-size: 13pt; font-weight: bold; padding: 4px;"); + layout->addWidget(m_deviceNameLabel); + + m_table = new QTreeWidget(this); + m_table->setColumnCount(3); + m_table->setHeaderLabels({"Board", "Firmware Version", "Hardware UID"}); + m_table->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + m_table->header()->setSectionResizeMode(1, QHeaderView::Stretch); + m_table->header()->setSectionResizeMode(2, QHeaderView::Stretch); + m_table->setAlternatingRowColors(true); + m_table->setRootIsDecorated(false); + m_table->setUniformRowHeights(true); + layout->addWidget(m_table, 1); + + m_statusLabel = new QLabel("Waiting for data...", this); + m_statusLabel->setStyleSheet("color: gray; font-size: 9pt; padding: 2px;"); + layout->addWidget(m_statusLabel); +} + +void VersionsPanel::reset() +{ + m_table->clear(); + m_rowIndex.clear(); + m_deviceName.clear(); + m_rnpCount = -1; + m_collected = 0; + m_deviceNameLabel->setText("Not connected"); + m_statusLabel->setText("Waiting for data..."); +} + +void VersionsPanel::setDeviceName(const QString &name) +{ + m_deviceName = name; + m_deviceNameLabel->setText(name); + + // Update master row label if it already exists + if (m_rowIndex.contains("M")) { + int idx = m_rowIndex["M"]; + auto *item = m_table->topLevelItem(idx); + if (item) item->setText(0, QString("Master (%1)").arg(name)); + } +} + +void VersionsPanel::setRnpCount(int count) +{ + m_rnpCount = count; + m_collected = 0; + + // Pre-allocate master row at index 0 if not already present + if (!m_rowIndex.contains("M")) { + auto *master = new QTreeWidgetItem(); + master->setText(0, QString("Master (%1)").arg(m_deviceName)); + QFont f = master->font(0); + f.setBold(true); + for (int c = 0; c < 3; ++c) master->setFont(c, f); + m_table->insertTopLevelItem(0, master); + m_rowIndex["M"] = 0; + } + + // Pre-allocate one row per slave panel IN ORDER + for (int i = 0; i < count; ++i) { + const QString id = QString::number(i); + if (!m_rowIndex.contains(id)) { + auto *item = new QTreeWidgetItem(); + item->setText(0, QString("Panel %1").arg(i)); + m_table->addTopLevelItem(item); + // Master is always index 0; slaves start at 1 + m_rowIndex[id] = 1 + i; + } + } + + updateStatus(); +} + +void VersionsPanel::setVersion(const QString &boardId, const QString &version) +{ + // If master row doesn't exist yet (NAM/VER arrive before RNP), create it + if (boardId == "M" && !m_rowIndex.contains("M")) { + auto *master = new QTreeWidgetItem(); + master->setText(0, QString("Master (%1)").arg(m_deviceName)); + QFont f = master->font(0); + f.setBold(true); + for (int c = 0; c < 3; ++c) master->setFont(c, f); + m_table->insertTopLevelItem(0, master); + m_rowIndex["M"] = 0; + } + + if (!m_rowIndex.contains(boardId)) return; + auto *item = m_table->topLevelItem(m_rowIndex[boardId]); + if (item) item->setText(1, version); +} + +void VersionsPanel::setUid(const QString &boardId, const QString &uid) +{ + if (!m_rowIndex.contains(boardId)) return; + auto *item = m_table->topLevelItem(m_rowIndex[boardId]); + if (item) item->setText(2, uid); + + if (boardId != "M") { + ++m_collected; + updateStatus(); + } +} + +void VersionsPanel::updateStatus() +{ + if (m_rnpCount < 0) { + m_statusLabel->setText("Waiting for panel count (RNP)..."); + return; + } + m_statusLabel->setText( + QString("Panels: %1 / %2 collected").arg(m_collected).arg(m_rnpCount)); +} diff --git a/VersionsPanel.h b/VersionsPanel.h new file mode 100644 index 0000000..4c6e4b3 --- /dev/null +++ b/VersionsPanel.h @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#include + +class QLabel; +class QTreeWidget; +class QTreeWidgetItem; + +class VersionsPanel : public QWidget +{ + Q_OBJECT +public: + explicit VersionsPanel(QWidget *parent = nullptr); + + void reset(); + void setDeviceName(const QString &name); + void setRnpCount(int count); + void setVersion(const QString &boardId, const QString &version); + void setUid(const QString &boardId, const QString &uid); + +private: + void updateStatus(); + + QLabel *m_deviceNameLabel = nullptr; + QTreeWidget *m_table = nullptr; + QLabel *m_statusLabel = nullptr; + + // boardId -> row index in tree (0 = master) + QMap m_rowIndex; + QString m_deviceName; + int m_rnpCount = -1; + int m_collected = 0; +}; diff --git a/WebSocketController.h b/WebSocketController.h new file mode 100644 index 0000000..047be98 --- /dev/null +++ b/WebSocketController.h @@ -0,0 +1,52 @@ +#pragma once +#include +#include +#include + +class QLabel; +class QLineEdit; +class QTextEdit; +class GamesPanel; +class SettingsTree; +class VersionsPanel; + +class WebSocketController : public QObject +{ + Q_OBJECT +public: + explicit WebSocketController(QLineEdit *urlEdit, + QLabel *statusLabel, + QObject *parent = nullptr); + + QWebSocket *socket(); + void addLogView(QTextEdit *log); + void setSettingsTree(SettingsTree *tree); + void setGamesPanel(GamesPanel *panel); + void setVersionsPanel(VersionsPanel *panel); + bool isConnected() const; + +public slots: + void startConnection(); + void closeConnection(); + void sendCommand(const QString &cmd); + + void onConnected(); + void onDisconnected(); + void onTextMessageReceived(const QString &msg); + void onErrorOccurred(QAbstractSocket::SocketError error); + void onValueEdited(const QString &key, const QString &newValue); + +private: + void handleProtocol(const QString &msg); + void broadcast(const QString &line); + + 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; +};