Tab based websocket comms looking like trad RM
This commit is contained in:
86
GamesPanel.cpp
Normal file
86
GamesPanel.cpp
Normal 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
26
GamesPanel.h
Normal 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
84
SettingsTree.cpp
Normal 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
25
SettingsTree.h
Normal 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
128
VersionsPanel.cpp
Normal 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
34
VersionsPanel.h
Normal 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
52
WebSocketController.h
Normal 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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user