Tab based websocket comms looking like trad RM

This commit is contained in:
Jon ESA
2026-04-01 19:38:05 +01:00
parent 21a3c35876
commit e10e7127b7
7 changed files with 435 additions and 0 deletions

86
GamesPanel.cpp Normal file
View File

@@ -0,0 +1,86 @@
#include "GamesPanel.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QSplitter>
#include <QVBoxLayout>
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.");
}

26
GamesPanel.h Normal file
View File

@@ -0,0 +1,26 @@
#pragma once
#include <QWidget>
#include <QMap>
#include <QString>
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<QString, QString> m_games; // display name -> 4-letter code
};

84
SettingsTree.cpp Normal file
View File

@@ -0,0 +1,84 @@
#include "SettingsTree.h"
#include <QHeaderView>
#include <QTreeWidgetItem>
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('/');
}

25
SettingsTree.h Normal file
View File

@@ -0,0 +1,25 @@
#pragma once
#include <QMap>
#include <QTreeWidget>
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<QString, QTreeWidgetItem *> m_itemMap;
bool m_loading = false;
};

128
VersionsPanel.cpp Normal file
View File

@@ -0,0 +1,128 @@
#include "VersionsPanel.h"
#include <QHeaderView>
#include <QLabel>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QVBoxLayout>
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));
}

34
VersionsPanel.h Normal file
View File

@@ -0,0 +1,34 @@
#pragma once
#include <QWidget>
#include <QMap>
#include <QString>
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<QString, int> m_rowIndex;
QString m_deviceName;
int m_rnpCount = -1;
int m_collected = 0;
};

52
WebSocketController.h Normal file
View File

@@ -0,0 +1,52 @@
#pragma once
#include <QObject>
#include <QStringList>
#include <QWebSocket>
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<QTextEdit *> m_logs;
SettingsTree *m_settingsTree = nullptr;
GamesPanel *m_gamesPanel = nullptr;
VersionsPanel *m_versionsPanel = nullptr;
QStringList m_settingsKeys;
int m_rnpCount = -1;
};