Compare commits
10 Commits
693e1d8e37
...
6e5fd60bc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e5fd60bc2 | ||
|
|
9671785a14 | ||
|
|
3dc9ee91d4 | ||
|
|
c5f58b5603 | ||
|
|
bea6c7c885 | ||
|
|
d4ad6f4258 | ||
|
|
81e8cf74c4 | ||
|
|
d75c80ef03 | ||
|
|
96079b493a | ||
|
|
90493a3864 |
@@ -10,7 +10,9 @@ find_package(Qt6 REQUIRED COMPONENTS Core Widgets WebSockets)
|
||||
qt_add_executable(wsapp
|
||||
main.cpp
|
||||
GamesPanel.cpp GamesPanel.h
|
||||
LightsPanel.cpp LightsPanel.h
|
||||
LogPanel.cpp LogPanel.h
|
||||
PanelsPanel.cpp PanelsPanel.h
|
||||
PowerPanel.cpp PowerPanel.h
|
||||
SettingsTree.cpp SettingsTree.h
|
||||
VersionsPanel.cpp VersionsPanel.h
|
||||
@@ -27,7 +29,7 @@ if(EMSCRIPTEN)
|
||||
set_target_properties(wsapp PROPERTIES
|
||||
WIN32_EXECUTABLE TRUE
|
||||
QT_WASM_INITIAL_MEMORY "50MB"
|
||||
QT_WASM_MAX_MEMORY "1GB"
|
||||
QT_WASM_MAX_MEMORY "4GB"
|
||||
)
|
||||
target_link_options(wsapp PRIVATE
|
||||
"SHELL:-s ASYNCIFY=1"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QSet>
|
||||
@@ -22,7 +23,7 @@ static QMap<QString,QString> parseGamResponse(const QString &response,
|
||||
return result;
|
||||
|
||||
for (const QString &entry : tokens.mid(2)) {
|
||||
if (entry.size() < 5) continue; // need at least 4-char code + 1-char name
|
||||
if (entry.size() < 5) continue;
|
||||
const QString code = entry.left(4);
|
||||
QString name = entry.mid(4);
|
||||
name.replace("_S", " ");
|
||||
@@ -59,7 +60,7 @@ GamesPanel::GamesPanel(QWidget *parent) : QWidget(parent)
|
||||
leftLayout->addWidget(m_availableList, 1);
|
||||
leftLayout->addWidget(m_addBtn);
|
||||
|
||||
// ---- Right pane: Installed games + Start/Stop ----
|
||||
// ---- Right pane: Installed games + Start/Stop + time/countdown inputs ----
|
||||
auto *rightWidget = new QWidget(splitter);
|
||||
auto *rightLayout = new QVBoxLayout(rightWidget);
|
||||
rightLayout->setContentsMargins(4, 4, 4, 4);
|
||||
@@ -67,13 +68,32 @@ GamesPanel::GamesPanel(QWidget *parent) : QWidget(parent)
|
||||
|
||||
auto *instLabel = new QLabel("<b>Installed Games</b>", rightWidget);
|
||||
instLabel->setStyleSheet("color: #555;");
|
||||
m_installedList = new QListWidget(rightWidget);
|
||||
m_installedList = new QListWidget(rightWidget);
|
||||
m_installedList->addItem("Connect to load installed games...");
|
||||
|
||||
// Parameter inputs row
|
||||
auto *paramRow = new QHBoxLayout();
|
||||
auto *timeLabel = new QLabel("Time:", rightWidget);
|
||||
m_timeEdit = new QLineEdit(rightWidget);
|
||||
m_timeEdit->setPlaceholderText("seconds");
|
||||
m_timeEdit->setFixedWidth(72);
|
||||
m_timeEdit->setToolTip("Optional: game duration in seconds (adds ,t<value>)");
|
||||
auto *countLabel = new QLabel("Countdown:", rightWidget);
|
||||
m_countdownEdit = new QLineEdit(rightWidget);
|
||||
m_countdownEdit->setPlaceholderText("seconds");
|
||||
m_countdownEdit->setFixedWidth(72);
|
||||
m_countdownEdit->setToolTip("Optional: countdown duration in seconds (adds ,c<value>)");
|
||||
paramRow->addWidget(timeLabel);
|
||||
paramRow->addWidget(m_timeEdit);
|
||||
paramRow->addSpacing(8);
|
||||
paramRow->addWidget(countLabel);
|
||||
paramRow->addWidget(m_countdownEdit);
|
||||
paramRow->addStretch(1);
|
||||
|
||||
// Start / Stop button row
|
||||
auto *btnRow = new QHBoxLayout();
|
||||
m_startBtn = new QPushButton("▶ Start Game", rightWidget);
|
||||
m_stopBtn = new QPushButton("■ Stop Game", rightWidget);
|
||||
auto *btnRow = new QHBoxLayout();
|
||||
m_startBtn = new QPushButton("▶ Start Game", rightWidget);
|
||||
m_stopBtn = new QPushButton("■ Stop Game", rightWidget);
|
||||
m_startBtn->setEnabled(false);
|
||||
m_startBtn->setStyleSheet(
|
||||
"QPushButton { color: white; background-color: #2e7d32;"
|
||||
@@ -90,6 +110,7 @@ GamesPanel::GamesPanel(QWidget *parent) : QWidget(parent)
|
||||
|
||||
rightLayout->addWidget(instLabel);
|
||||
rightLayout->addWidget(m_installedList, 1);
|
||||
rightLayout->addLayout(paramRow);
|
||||
rightLayout->addLayout(btnRow);
|
||||
|
||||
splitter->addWidget(leftWidget);
|
||||
@@ -154,7 +175,7 @@ void GamesPanel::rebuildAvailable()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Add selected available game to installed list
|
||||
// Add selected available game
|
||||
// ---------------------------------------------------------------------------
|
||||
void GamesPanel::onAddClicked()
|
||||
{
|
||||
@@ -166,7 +187,11 @@ void GamesPanel::onAddClicked()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Start selected installed game: GST <code> ,p
|
||||
// Start selected installed game:
|
||||
// GST <code> ,p (no params)
|
||||
// GST <code> ,p,t<time> (time only)
|
||||
// GST <code> ,p,c<countdown> (countdown only)
|
||||
// GST <code> ,p,t<time>,c<countdown> (both)
|
||||
// ---------------------------------------------------------------------------
|
||||
void GamesPanel::onStartClicked()
|
||||
{
|
||||
@@ -174,5 +199,14 @@ void GamesPanel::onStartClicked()
|
||||
if (!item) return;
|
||||
const QString name = item->text();
|
||||
if (!m_installedGames.contains(name)) return;
|
||||
emit commandRequested(QString("GST %1 ,p").arg(m_installedGames[name]));
|
||||
|
||||
const QString code = m_installedGames[name];
|
||||
const QString timeVal = m_timeEdit->text().trimmed();
|
||||
const QString countVal = m_countdownEdit->text().trimmed();
|
||||
|
||||
QString params = ",p";
|
||||
if (!timeVal.isEmpty()) params += QString(",t%1").arg(timeVal);
|
||||
if (!countVal.isEmpty()) params += QString(",c%1").arg(countVal);
|
||||
|
||||
emit commandRequested(QString("GST %1 %2").arg(code, params));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QWidget>
|
||||
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
|
||||
@@ -31,6 +32,8 @@ private:
|
||||
|
||||
// Right pane
|
||||
QListWidget *m_installedList = nullptr;
|
||||
QLineEdit *m_timeEdit = nullptr;
|
||||
QLineEdit *m_countdownEdit = nullptr;
|
||||
QPushButton *m_startBtn = nullptr;
|
||||
QPushButton *m_stopBtn = nullptr;
|
||||
|
||||
|
||||
295
LightsPanel.cpp
Normal file
295
LightsPanel.cpp
Normal file
@@ -0,0 +1,295 @@
|
||||
#include "LightsPanel.h"
|
||||
|
||||
#include <QColorDialog>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <QtMath>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
static QString toDecRGB(const QColor &c)
|
||||
{
|
||||
return QString("%1 %2 %3").arg(c.red()).arg(c.green()).arg(c.blue());
|
||||
}
|
||||
|
||||
static void applyColorToBtn(QPushButton *btn, const QColor &c)
|
||||
{
|
||||
const bool dark = (c.red() * 299 + c.green() * 587 + c.blue() * 114) < 128000;
|
||||
btn->setStyleSheet(QString(
|
||||
"QPushButton { background:%1; color:%2; border:1px solid #555;"
|
||||
" border-radius:4px; padding:3px 10px; }"
|
||||
"QPushButton:hover { border:2px solid #fff; }")
|
||||
.arg(c.name())
|
||||
.arg(dark ? "#ffffff" : "#000000"));
|
||||
btn->setText(QString("%1, %2, %3").arg(c.red()).arg(c.green()).arg(c.blue()));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// ---------------------------------------------------------------------------
|
||||
LightsPanel::LightsPanel(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
auto *root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(4, 4, 4, 4);
|
||||
root->setSpacing(4);
|
||||
|
||||
m_statusLabel = new QLabel("Connect to load panels...", this);
|
||||
m_statusLabel->setAlignment(Qt::AlignHCenter | Qt::AlignTop);
|
||||
m_statusLabel->setStyleSheet("color: #888; font-style: italic;");
|
||||
root->addWidget(m_statusLabel);
|
||||
|
||||
root->addStretch(1);
|
||||
|
||||
// --- Control bar ---
|
||||
auto *bar = new QHBoxLayout();
|
||||
bar->setSpacing(8);
|
||||
|
||||
m_selectedLabel = new QLabel("No panel selected", this);
|
||||
m_selectedLabel->setStyleSheet("font-weight: bold; min-width: 140px;");
|
||||
|
||||
auto *colorLabel = new QLabel("Colour:", this);
|
||||
m_colorBtn = new QPushButton(this);
|
||||
m_colorBtn->setFixedWidth(130);
|
||||
applyColorToBtn(m_colorBtn, m_ledColor);
|
||||
|
||||
m_sendBtn = new QPushButton("Send to Panel", this);
|
||||
m_sendBtn->setEnabled(false);
|
||||
m_sendBtn->setStyleSheet(
|
||||
"QPushButton { background:#01696f; color:white; border-radius:4px; padding:4px 10px; }"
|
||||
"QPushButton:hover { background:#0c4e54; }"
|
||||
"QPushButton:disabled { background:#888; color:#ccc; }");
|
||||
|
||||
m_sendAllBtn = new QPushButton("Send to All", this);
|
||||
m_sendAllBtn->setEnabled(false);
|
||||
m_sendAllBtn->setStyleSheet(
|
||||
"QPushButton { background:#437a22; color:white; border-radius:4px; padding:4px 10px; }"
|
||||
"QPushButton:hover { background:#2e5c10; }"
|
||||
"QPushButton:disabled { background:#888; color:#ccc; }");
|
||||
|
||||
m_clearAllBtn = new QPushButton("Clear All", this);
|
||||
m_clearAllBtn->setEnabled(false);
|
||||
m_clearAllBtn->setStyleSheet(
|
||||
"QPushButton { background:#555; color:white; border-radius:4px; padding:4px 10px; }"
|
||||
"QPushButton:hover { background:#333; }"
|
||||
"QPushButton:disabled { background:#888; color:#ccc; }");
|
||||
|
||||
bar->addWidget(m_selectedLabel);
|
||||
bar->addStretch(1);
|
||||
bar->addWidget(colorLabel);
|
||||
bar->addWidget(m_colorBtn);
|
||||
bar->addSpacing(8);
|
||||
bar->addWidget(m_sendBtn);
|
||||
bar->addWidget(m_sendAllBtn);
|
||||
bar->addWidget(m_clearAllBtn);
|
||||
root->addLayout(bar);
|
||||
|
||||
connect(m_colorBtn, &QPushButton::clicked, this, &LightsPanel::pickColor);
|
||||
connect(m_sendBtn, &QPushButton::clicked, this, [this](){
|
||||
if (m_selected >= 0) sendLedCommand(m_selected, m_ledColor);
|
||||
});
|
||||
connect(m_sendAllBtn, &QPushButton::clicked, this, [this](){
|
||||
for (int i = 0; i < m_count; ++i) sendLedCommand(i, m_ledColor);
|
||||
});
|
||||
connect(m_clearAllBtn, &QPushButton::clicked, this, [this](){
|
||||
const QColor black(0, 0, 0);
|
||||
for (int i = 0; i < m_count; ++i) sendLedCommand(i, black);
|
||||
});
|
||||
|
||||
setMinimumSize(220, 260);
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
void LightsPanel::setRnpCount(int count)
|
||||
{
|
||||
m_count = count;
|
||||
m_selected = -1;
|
||||
m_panelColors.clear();
|
||||
m_statusLabel->setText(count > 0
|
||||
? QString("%1 panel%2 — click a segment to select").arg(count).arg(count == 1 ? "" : "s")
|
||||
: "No panels reported");
|
||||
const bool ok = count > 0;
|
||||
m_sendAllBtn->setEnabled(ok);
|
||||
m_clearAllBtn->setEnabled(ok);
|
||||
updateSendBtn();
|
||||
update();
|
||||
}
|
||||
|
||||
void LightsPanel::reset()
|
||||
{
|
||||
m_count = 0;
|
||||
m_selected = -1;
|
||||
m_panelColors.clear();
|
||||
m_statusLabel->setText("Connect to load panels...");
|
||||
m_sendBtn->setEnabled(false);
|
||||
m_sendAllBtn->setEnabled(false);
|
||||
m_clearAllBtn->setEnabled(false);
|
||||
update();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal
|
||||
// ---------------------------------------------------------------------------
|
||||
void LightsPanel::updateSendBtn()
|
||||
{
|
||||
m_sendBtn->setEnabled(m_selected >= 0);
|
||||
m_sendBtn->setText(m_selected >= 0
|
||||
? QString("Send to Panel %1").arg(m_selected)
|
||||
: "Send to Panel");
|
||||
}
|
||||
|
||||
void LightsPanel::pickColor()
|
||||
{
|
||||
auto *dlg = new QColorDialog(m_ledColor, this);
|
||||
dlg->setOption(QColorDialog::DontUseNativeDialog);
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||||
connect(dlg, &QColorDialog::colorSelected, this, [this](const QColor &c) {
|
||||
m_ledColor = c;
|
||||
applyColorToBtn(m_colorBtn, c);
|
||||
update();
|
||||
});
|
||||
dlg->open();
|
||||
}
|
||||
|
||||
void LightsPanel::sendLedCommand(int panel, const QColor &color)
|
||||
{
|
||||
emit commandRequested(QString("LED %1 %2 A").arg(panel).arg(toDecRGB(color)));
|
||||
m_panelColors[panel] = color;
|
||||
update();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Geometry
|
||||
// ---------------------------------------------------------------------------
|
||||
static void calcGeom(const QRectF &widgetRect,
|
||||
double &cx, double &cy, double &outerR, double &innerR)
|
||||
{
|
||||
const QRectF draw = widgetRect.adjusted(0, 28, 0, 44);
|
||||
const double side = qMin(draw.width(), draw.height()) * 0.88;
|
||||
cx = draw.center().x();
|
||||
cy = draw.center().y();
|
||||
outerR = side / 2.0;
|
||||
innerR = outerR * 0.44;
|
||||
}
|
||||
|
||||
int LightsPanel::segmentAt(const QPointF &pos) const
|
||||
{
|
||||
if (m_count <= 0) return -1;
|
||||
double cx, cy, outerR, innerR;
|
||||
calcGeom(rect(), cx, cy, outerR, innerR);
|
||||
|
||||
const double dx = pos.x() - cx;
|
||||
const double dy = -(pos.y() - cy);
|
||||
const double dist = qSqrt(dx*dx + dy*dy);
|
||||
if (dist < innerR || dist > outerR) return -1;
|
||||
|
||||
double cwFromTop = 90.0 - qRadiansToDegrees(qAtan2(dy, dx));
|
||||
if (cwFromTop < 0) cwFromTop += 360.0;
|
||||
if (cwFromTop >= 360) cwFromTop -= 360.0;
|
||||
|
||||
const double gapDeg = (m_count > 1) ? 3.0 : 0.0;
|
||||
const double segSpan = (360.0 - m_count * gapDeg) / m_count;
|
||||
for (int i = 0; i < m_count; ++i) {
|
||||
const double start = i * (segSpan + gapDeg);
|
||||
if (cwFromTop >= start && cwFromTop < start + segSpan) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mouse
|
||||
// ---------------------------------------------------------------------------
|
||||
void LightsPanel::mousePressEvent(QMouseEvent *e)
|
||||
{
|
||||
const int idx = segmentAt(e->pos());
|
||||
if (idx < 0) { QWidget::mousePressEvent(e); return; }
|
||||
m_selected = idx;
|
||||
m_selectedLabel->setText(QString("Panel %1 selected").arg(idx));
|
||||
updateSendBtn();
|
||||
update();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Paint
|
||||
// ---------------------------------------------------------------------------
|
||||
void LightsPanel::paintEvent(QPaintEvent *)
|
||||
{
|
||||
if (m_count <= 0) return;
|
||||
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
double cx, cy, outerR, innerR;
|
||||
calcGeom(rect(), cx, cy, outerR, innerR);
|
||||
const double side = outerR * 2.0;
|
||||
|
||||
const QRectF outerRect(cx - outerR, cy - outerR, side, side);
|
||||
const QRectF innerRect(cx - innerR, cy - innerR, innerR * 2, innerR * 2);
|
||||
|
||||
const double gapDeg = (m_count > 1) ? 3.0 : 0.0;
|
||||
const double segSpan = (360.0 - m_count * gapDeg) / m_count;
|
||||
|
||||
static const QColor kDefault(70, 70, 80);
|
||||
static const QColor kSelected(40, 120, 140);
|
||||
|
||||
for (int i = 0; i < m_count; ++i) {
|
||||
const double startAngle = 90.0 - i * (segSpan + gapDeg);
|
||||
const double sweep = -segSpan;
|
||||
const double midAngleRad = qDegreesToRadians(startAngle + sweep / 2.0);
|
||||
|
||||
QPainterPath path;
|
||||
path.arcMoveTo(outerRect, startAngle);
|
||||
path.arcTo(outerRect, startAngle, sweep);
|
||||
path.arcTo(innerRect, startAngle + sweep, -sweep);
|
||||
path.closeSubpath();
|
||||
|
||||
QColor fill = m_panelColors.value(i,
|
||||
i == m_selected ? kSelected : kDefault);
|
||||
p.setBrush(fill);
|
||||
p.setPen(QPen(i == m_selected ? Qt::white : QColor(255,255,255,80),
|
||||
i == m_selected ? 3.0 : 1.5));
|
||||
p.drawPath(path);
|
||||
|
||||
// Colour preview dot on selected segment (outer rim)
|
||||
if (i == m_selected) {
|
||||
const double previewR = qMin((outerR - innerR) * 0.22,
|
||||
outerR * qSin(qDegreesToRadians(segSpan / 2.0)) * 0.55);
|
||||
const double bCentR = outerR - previewR - 2.0;
|
||||
const QPointF bc(cx + bCentR * qCos(midAngleRad),
|
||||
cy - bCentR * qSin(midAngleRad));
|
||||
const QRectF pr(bc.x()-previewR, bc.y()-previewR, previewR*2, previewR*2);
|
||||
p.setBrush(m_ledColor);
|
||||
p.setPen(QPen(Qt::white, 1.5));
|
||||
p.drawEllipse(pr);
|
||||
}
|
||||
|
||||
// Panel index label
|
||||
const double midR = (outerR + innerR) / 2.0;
|
||||
const QPointF lp(cx + midR * qCos(midAngleRad),
|
||||
cy - midR * qSin(midAngleRad));
|
||||
QFont font = p.font();
|
||||
font.setBold(true);
|
||||
font.setPointSize(qMax(7, qMin(12, static_cast<int>(outerR / (m_count * 0.55 + 2)))));
|
||||
p.setFont(font);
|
||||
const QColor bg = m_panelColors.value(i, i == m_selected ? kSelected : kDefault);
|
||||
const bool darkBg = (bg.red()*299 + bg.green()*587 + bg.blue()*114) < 128000;
|
||||
p.setPen(darkBg ? Qt::white : Qt::black);
|
||||
p.drawText(QRectF(lp.x()-22, lp.y()-11, 44, 22), Qt::AlignCenter, QString::number(i));
|
||||
}
|
||||
|
||||
// Centre count
|
||||
QFont cf = p.font();
|
||||
cf.setBold(true);
|
||||
cf.setPointSize(qMax(8, static_cast<int>(innerR * 0.45)));
|
||||
p.setFont(cf);
|
||||
p.setPen(palette().text().color());
|
||||
p.drawText(innerRect, Qt::AlignCenter, QString::number(m_count));
|
||||
}
|
||||
43
LightsPanel.h
Normal file
43
LightsPanel.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include <QColor>
|
||||
#include <QMap>
|
||||
#include <QWidget>
|
||||
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
|
||||
class LightsPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LightsPanel(QWidget *parent = nullptr);
|
||||
|
||||
void setRnpCount(int count);
|
||||
void reset();
|
||||
|
||||
signals:
|
||||
void commandRequested(const QString &cmd);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
int segmentAt(const QPointF &pos) const;
|
||||
void sendLedCommand(int panel, const QColor &color);
|
||||
void pickColor();
|
||||
void updateSendBtn();
|
||||
|
||||
int m_count = 0;
|
||||
int m_selected = -1;
|
||||
QColor m_ledColor = QColor(255, 0, 0);
|
||||
|
||||
QMap<int, QColor> m_panelColors;
|
||||
|
||||
QLabel *m_statusLabel = nullptr;
|
||||
QLabel *m_selectedLabel = nullptr;
|
||||
QPushButton *m_colorBtn = nullptr;
|
||||
QPushButton *m_sendBtn = nullptr;
|
||||
QPushButton *m_sendAllBtn = nullptr;
|
||||
QPushButton *m_clearAllBtn = nullptr;
|
||||
};
|
||||
202
PanelsPanel.cpp
Normal file
202
PanelsPanel.cpp
Normal file
@@ -0,0 +1,202 @@
|
||||
#include "PanelsPanel.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QtMath>
|
||||
|
||||
PanelsPanel::PanelsPanel(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(4, 4, 4, 4);
|
||||
|
||||
m_statusLabel = new QLabel("Connect to load panels...", this);
|
||||
m_statusLabel->setAlignment(Qt::AlignHCenter | Qt::AlignTop);
|
||||
m_statusLabel->setStyleSheet("color: #888; font-style: italic;");
|
||||
layout->addWidget(m_statusLabel);
|
||||
layout->addStretch(1);
|
||||
|
||||
setMinimumSize(200, 200);
|
||||
}
|
||||
|
||||
void PanelsPanel::setRnpCount(int count)
|
||||
{
|
||||
m_count = count;
|
||||
m_colors.clear();
|
||||
m_hits.clear();
|
||||
m_statusLabel->setText(count > 0
|
||||
? QString("%1 panel%2").arg(count).arg(count == 1 ? "" : "s")
|
||||
: "No panels reported");
|
||||
update();
|
||||
}
|
||||
|
||||
void PanelsPanel::setPanelColor(int index, const QColor &color)
|
||||
{
|
||||
m_colors[index] = color;
|
||||
update();
|
||||
}
|
||||
|
||||
void PanelsPanel::impactPanel(int index, int power)
|
||||
{
|
||||
m_hits[index] = power;
|
||||
update();
|
||||
QTimer::singleShot(3000, this, [this, index]() {
|
||||
m_hits.remove(index);
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
void PanelsPanel::reset()
|
||||
{
|
||||
m_count = 0;
|
||||
m_colors.clear();
|
||||
m_hits.clear();
|
||||
m_statusLabel->setText("Connect to load panels...");
|
||||
update();
|
||||
}
|
||||
|
||||
static double bubbleScale(int power)
|
||||
{
|
||||
if (power < 150) return 0.20;
|
||||
if (power < 500) return 0.40;
|
||||
return 0.80;
|
||||
}
|
||||
|
||||
// Draw a radial-gradient bubble at bCentre with radius circR
|
||||
static void drawBubble(QPainter &p, const QPointF &bCentre, double circR,
|
||||
bool isHighest, int power)
|
||||
{
|
||||
const QRectF circRect(bCentre.x() - circR, bCentre.y() - circR,
|
||||
circR * 2, circR * 2);
|
||||
|
||||
// Drop shadow
|
||||
p.setBrush(QColor(0, 0, 0, 55));
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawEllipse(circRect.adjusted(2, 3, 2, 3));
|
||||
|
||||
// Colour scheme: red = highest, orange/amber = others
|
||||
QColor bright, dark, rim;
|
||||
if (isHighest) {
|
||||
bright = QColor(255, 100, 100);
|
||||
dark = QColor(160, 10, 10);
|
||||
rim = QColor(255, 200, 200);
|
||||
} else {
|
||||
bright = QColor(255, 210, 60);
|
||||
dark = QColor(200, 100, 0);
|
||||
rim = QColor(255, 240, 180);
|
||||
}
|
||||
|
||||
QRadialGradient grad(bCentre, circR,
|
||||
QPointF(bCentre.x() - circR * 0.3,
|
||||
bCentre.y() - circR * 0.3));
|
||||
grad.setColorAt(0.0, bright);
|
||||
grad.setColorAt(1.0, dark);
|
||||
p.setBrush(grad);
|
||||
p.setPen(QPen(rim, 1.5));
|
||||
p.drawEllipse(circRect);
|
||||
|
||||
// Power text
|
||||
QFont pf = p.font();
|
||||
pf.setBold(true);
|
||||
pf.setPointSize(qMax(5, qMin(11, static_cast<int>(circR * 0.68))));
|
||||
p.setFont(pf);
|
||||
p.setPen(Qt::white);
|
||||
p.drawText(circRect, Qt::AlignCenter, QString::number(power));
|
||||
}
|
||||
|
||||
void PanelsPanel::paintEvent(QPaintEvent *)
|
||||
{
|
||||
if (m_count <= 0) return;
|
||||
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
const QRectF bounds = rect();
|
||||
const QRectF drawArea = bounds.adjusted(0, 28, 0, 0);
|
||||
const double side = qMin(drawArea.width(), drawArea.height()) * 0.88;
|
||||
const double cx = drawArea.center().x();
|
||||
const double cy = drawArea.center().y();
|
||||
const double outerR = side / 2.0;
|
||||
const double innerR = outerR * 0.44;
|
||||
const double bandW = outerR - innerR;
|
||||
|
||||
const QRectF outerRect(cx - outerR, cy - outerR, side, side);
|
||||
const QRectF innerRect(cx - innerR, cy - innerR, innerR * 2, innerR * 2);
|
||||
|
||||
const double gapDeg = (m_count > 1) ? 3.0 : 0.0;
|
||||
const double segSpan = (360.0 - m_count * gapDeg) / m_count;
|
||||
|
||||
const double maxCircR = qMin(
|
||||
bandW * 0.46,
|
||||
outerR * qSin(qDegreesToRadians(segSpan / 2.0)) * 0.82
|
||||
);
|
||||
|
||||
// Find the highest-power panel index among active hits
|
||||
int topIndex = -1;
|
||||
int topPower = -1;
|
||||
for (auto it = m_hits.cbegin(); it != m_hits.cend(); ++it) {
|
||||
if (it.value() > topPower) { topPower = it.value(); topIndex = it.key(); }
|
||||
}
|
||||
|
||||
static const QColor kDefault(100, 140, 180);
|
||||
static const QColor kHit(200, 50, 50);
|
||||
|
||||
for (int i = 0; i < m_count; ++i) {
|
||||
const bool isHit = m_hits.contains(i);
|
||||
const double startAngle = 90.0 - i * (segSpan + gapDeg);
|
||||
const double sweep = -segSpan;
|
||||
const double midAngleRad = qDegreesToRadians(startAngle + sweep / 2.0);
|
||||
|
||||
// --- Segment ---
|
||||
QPainterPath path;
|
||||
path.arcMoveTo(outerRect, startAngle);
|
||||
path.arcTo(outerRect, startAngle, sweep);
|
||||
path.arcTo(innerRect, startAngle + sweep, -sweep);
|
||||
path.closeSubpath();
|
||||
|
||||
p.setBrush(isHit ? kHit : m_colors.value(i, kDefault));
|
||||
p.setPen(QPen(Qt::white, isHit ? 3 : 2));
|
||||
p.drawPath(path);
|
||||
|
||||
// --- Panel index label at midpoint ---
|
||||
const double midR = (outerR + innerR) / 2.0;
|
||||
const QPointF labelPt(cx + midR * qCos(midAngleRad),
|
||||
cy - midR * qSin(midAngleRad));
|
||||
{
|
||||
QFont font = p.font();
|
||||
font.setBold(true);
|
||||
font.setPointSize(qMax(7, qMin(12, static_cast<int>(outerR / (m_count * 0.55 + 2)))));
|
||||
p.setFont(font);
|
||||
p.setPen(Qt::white);
|
||||
p.drawText(QRectF(labelPt.x() - 22, labelPt.y() - 11, 44, 22),
|
||||
Qt::AlignCenter, QString::number(i));
|
||||
}
|
||||
|
||||
if (isHit) {
|
||||
const int power = m_hits[i];
|
||||
const bool isTop = (i == topIndex);
|
||||
const double circR = maxCircR * bubbleScale(power);
|
||||
const double bCentR = outerR - circR - 2.0;
|
||||
const QPointF bCentre(cx + bCentR * qCos(midAngleRad),
|
||||
cy - bCentR * qSin(midAngleRad));
|
||||
|
||||
// Glow halo on segment
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.setPen(QPen(isTop ? QColor(255, 80, 80, 120)
|
||||
: QColor(255, 200, 50, 100), 5));
|
||||
p.drawPath(path);
|
||||
|
||||
drawBubble(p, bCentre, circR, isTop, power);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Centre hole: total count ---
|
||||
QFont cf = p.font();
|
||||
cf.setBold(true);
|
||||
cf.setPointSize(qMax(8, static_cast<int>(innerR * 0.45)));
|
||||
p.setFont(cf);
|
||||
p.setPen(palette().text().color());
|
||||
p.drawText(innerRect, Qt::AlignCenter, QString::number(m_count));
|
||||
}
|
||||
27
PanelsPanel.h
Normal file
27
PanelsPanel.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
#include <QColor>
|
||||
#include <QMap>
|
||||
#include <QWidget>
|
||||
|
||||
class QLabel;
|
||||
|
||||
class PanelsPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PanelsPanel(QWidget *parent = nullptr);
|
||||
|
||||
void setRnpCount(int count);
|
||||
void setPanelColor(int index, const QColor &color);
|
||||
void impactPanel(int index, int power);
|
||||
void reset();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
int m_count = 0;
|
||||
QMap<int,QColor> m_colors;
|
||||
QMap<int,int> m_hits; // panel index → impact power (active for 3 s)
|
||||
QLabel *m_statusLabel = nullptr;
|
||||
};
|
||||
@@ -17,13 +17,6 @@ PowerPanel::PowerPanel(QWidget *parent) : QWidget(parent)
|
||||
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);
|
||||
@@ -52,13 +45,13 @@ PowerPanel::PowerPanel(QWidget *parent) : QWidget(parent)
|
||||
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);
|
||||
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
|
||||
@@ -82,8 +75,8 @@ void PowerPanel::setVoltages(const QStringList &v)
|
||||
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_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);
|
||||
@@ -119,9 +112,9 @@ 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";
|
||||
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";
|
||||
}
|
||||
|
||||
58
README.MD
Normal file
58
README.MD
Normal file
@@ -0,0 +1,58 @@
|
||||
## ReMote is a WASM Remote Monitor Application
|
||||
|
||||
|
||||
### Install Linux
|
||||
```
|
||||
sudo apt update
|
||||
sudo apt install -y ninja-build python3 build-essential
|
||||
|
||||
cd ~
|
||||
git clone https://github.com/emscripten-core/emsdk.git
|
||||
cd emsdk
|
||||
export EMSDK_KEEP_DOWNLOADS=1
|
||||
curl -L https://storage.googleapis.com/webassembly/emscripten-releases-builds/linux/2ce4170cef5ce46f337f9fd907b614a8db772c7d/wasm-binaries.tar.xz -o downloads/2ce4170cef5ce46f337f9fd907b614a8db772c7d-wasm-binaries.tar.xz
|
||||
./emsdk install 3.1.50
|
||||
./emsdk activate 3.1.50
|
||||
source ./emsdk_env.sh
|
||||
|
||||
echo "$EMSDK"
|
||||
em++ --version
|
||||
ninja --version
|
||||
|
||||
/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
|
||||
```
|
||||
|
||||
|
||||
### Install MacOS
|
||||
|
||||
```
|
||||
git clone https://github.com/emscripten-core/emsdk.git
|
||||
cd emsdk
|
||||
./emsdk install latest
|
||||
./emsdk activate latest
|
||||
source "/Users/tech/code/emsdk/emsdk_env.sh"
|
||||
|
||||
(
|
||||
note if latest does not work use 4.0.7 explicitly.
|
||||
"your em is built with 5.1........"
|
||||
|
||||
./emsdk install 4.0.7
|
||||
./emsdk activate 4.0.7
|
||||
source "/Users/tech/code/emsdk/emsdk_env.sh"
|
||||
)
|
||||
|
||||
|
||||
brew install cmake ninja
|
||||
|
||||
|
||||
cat code/esa-remote/build-mac.sh
|
||||
/Users/tech/Qt/6.11.0/wasm_singlethread/bin/qt-cmake -S . -B build-wasm -G Ninja
|
||||
cmake --build build-wasm
|
||||
|
||||
cd build-wasm
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "WebSocketController.h"
|
||||
#include "GamesPanel.h"
|
||||
#include "LogPanel.h"
|
||||
#include "LightsPanel.h"
|
||||
#include "PanelsPanel.h"
|
||||
#include "PowerPanel.h"
|
||||
#include "SettingsTree.h"
|
||||
#include "VersionsPanel.h"
|
||||
@@ -27,10 +29,12 @@ void WebSocketController::setSettingsTree(SettingsTree *tree)
|
||||
this, &WebSocketController::onValueEdited);
|
||||
}
|
||||
|
||||
void WebSocketController::setGamesPanel(GamesPanel *panel) { m_gamesPanel = panel; }
|
||||
void WebSocketController::setVersionsPanel(VersionsPanel *p) { m_versionsPanel = p; }
|
||||
void WebSocketController::setPowerPanel(PowerPanel *panel) { m_powerPanel = panel; }
|
||||
void WebSocketController::setLogPanel(LogPanel *panel) { m_logPanel = panel; }
|
||||
void WebSocketController::setGamesPanel(GamesPanel *panel) { m_gamesPanel = panel; }
|
||||
void WebSocketController::setVersionsPanel(VersionsPanel *p) { m_versionsPanel = p; }
|
||||
void WebSocketController::setPowerPanel(PowerPanel *panel) { m_powerPanel = panel; }
|
||||
void WebSocketController::setLogPanel(LogPanel *panel) { m_logPanel = panel; }
|
||||
void WebSocketController::setPanelsPanel(PanelsPanel *panel) { m_panelsPanel = panel; }
|
||||
void WebSocketController::setLightsPanel(LightsPanel *panel) { m_lightsPanel = panel; }
|
||||
|
||||
bool WebSocketController::isConnected() const
|
||||
{
|
||||
@@ -53,6 +57,7 @@ void WebSocketController::closeConnection()
|
||||
m_rnpCount = -1;
|
||||
if (m_versionsPanel) m_versionsPanel->reset();
|
||||
if (m_powerPanel) m_powerPanel->reset();
|
||||
if (m_panelsPanel) m_panelsPanel->reset();
|
||||
m_socket.close();
|
||||
}
|
||||
|
||||
@@ -71,7 +76,7 @@ void WebSocketController::onConnected()
|
||||
|
||||
sendCommand(QStringLiteral("GBL List"));
|
||||
sendCommand(QStringLiteral("GAM list"));
|
||||
sendCommand(QStringLiteral("GAM listall")); // <-- fetch full catalogue
|
||||
sendCommand(QStringLiteral("GAM listall"));
|
||||
sendCommand(QStringLiteral("NAM"));
|
||||
sendCommand(QStringLiteral("VER"));
|
||||
sendCommand(QStringLiteral("UID"));
|
||||
@@ -80,6 +85,12 @@ void WebSocketController::onConnected()
|
||||
sendCommand(QStringLiteral("#P0-P RTV"));
|
||||
sendCommand(QStringLiteral("#P0-P VTG"));
|
||||
sendCommand(QStringLiteral("LOG"));
|
||||
sendCommand(QStringLiteral("GBL brightnessMin"));
|
||||
sendCommand(QStringLiteral("GBL brightnessMax"));
|
||||
sendCommand(QStringLiteral("GBL brightness"));
|
||||
sendCommand(QStringLiteral("GBL sound/volumeMin"));
|
||||
sendCommand(QStringLiteral("GBL sound/volumeMax"));
|
||||
sendCommand(QStringLiteral("GBL sound/volume"));
|
||||
}
|
||||
|
||||
void WebSocketController::onDisconnected()
|
||||
@@ -88,6 +99,7 @@ void WebSocketController::onDisconnected()
|
||||
m_statusLabel->setText("Disconnected");
|
||||
m_settingsKeys.clear();
|
||||
m_rnpCount = -1;
|
||||
if (m_panelsPanel) m_panelsPanel->reset();
|
||||
}
|
||||
|
||||
void WebSocketController::onTextMessageReceived(const QString &msg)
|
||||
@@ -114,7 +126,7 @@ void WebSocketController::handleProtocol(const QString &msg)
|
||||
if (tokens.isEmpty()) return;
|
||||
const QString cmd = tokens[0];
|
||||
|
||||
// ---- Power: #P0-P STA/RTV/VTG ----
|
||||
// ---- Power ----
|
||||
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]);
|
||||
@@ -123,7 +135,7 @@ void WebSocketController::handleProtocol(const QString &msg)
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- LOG channel list ----
|
||||
// ---- LOG ----
|
||||
if (cmd == "LOG" && tokens.size() > 1 && m_logPanel) {
|
||||
bool isList = true;
|
||||
for (int i = 1; i < tokens.size(); ++i)
|
||||
@@ -140,15 +152,13 @@ void WebSocketController::handleProtocol(const QString &msg)
|
||||
|
||||
// ---- VER ----
|
||||
if (cmd == "VER" && tokens.size() >= 3) {
|
||||
if (m_versionsPanel)
|
||||
m_versionsPanel->setVersion(tokens[1], tokens.mid(2).join(' '));
|
||||
if (m_versionsPanel) m_versionsPanel->setVersion(tokens[1], tokens.mid(2).join(' '));
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- UID ----
|
||||
if (cmd == "UID" && tokens.size() >= 3) {
|
||||
if (m_versionsPanel)
|
||||
m_versionsPanel->setUid(tokens[1], tokens.mid(2).join(' '));
|
||||
if (m_versionsPanel) m_versionsPanel->setUid(tokens[1], tokens.mid(2).join(' '));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,6 +169,8 @@ void WebSocketController::handleProtocol(const QString &msg)
|
||||
if (ok && count > 0) {
|
||||
m_rnpCount = count;
|
||||
if (m_versionsPanel) m_versionsPanel->setRnpCount(count);
|
||||
if (m_panelsPanel) m_panelsPanel->setRnpCount(count);
|
||||
if (m_lightsPanel) m_lightsPanel->setRnpCount(count);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
sendCommand(QString("VER %1").arg(i));
|
||||
sendCommand(QString("UID %1").arg(i));
|
||||
@@ -167,13 +179,24 @@ void WebSocketController::handleProtocol(const QString &msg)
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- GAM listall — full catalogue ----
|
||||
// ---- IMP (panel impact) ----
|
||||
// Format: IMP <panel_idx> <power> <x> <y> <z>
|
||||
if (cmd == "IMP" && tokens.size() >= 3) {
|
||||
bool idxOk = false, pwrOk = false;
|
||||
const int idx = tokens[1].toInt(&idxOk);
|
||||
const int power = tokens[2].toInt(&pwrOk);
|
||||
if (idxOk && pwrOk && m_panelsPanel)
|
||||
m_panelsPanel->impactPanel(idx, power);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- GAM listall ----
|
||||
if (cmd == "GAM" && tokens.size() >= 2 && tokens[1] == "listall") {
|
||||
if (m_gamesPanel) m_gamesPanel->loadAllFromResponse(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- GAM list — installed games ----
|
||||
// ---- GAM list ----
|
||||
if (cmd == "GAM" && tokens.size() >= 2 && tokens[1] == "list") {
|
||||
if (m_gamesPanel) m_gamesPanel->loadFromResponse(msg);
|
||||
return;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QWebSocket>
|
||||
|
||||
class GamesPanel;
|
||||
class LightsPanel;
|
||||
class LogPanel;
|
||||
class PanelsPanel;
|
||||
class PowerPanel;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QTextEdit;
|
||||
class GamesPanel;
|
||||
class LogPanel;
|
||||
class PowerPanel;
|
||||
class SettingsTree;
|
||||
class VersionsPanel;
|
||||
|
||||
@@ -16,43 +19,50 @@ class WebSocketController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit WebSocketController(QLineEdit *urlEdit,
|
||||
QLabel *statusLabel,
|
||||
QObject *parent = nullptr);
|
||||
explicit WebSocketController(QLineEdit *urlEdit, QLabel *statusLabel,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
QWebSocket *socket();
|
||||
bool isConnected() const;
|
||||
|
||||
void addLogView(QTextEdit *log);
|
||||
void setSettingsTree(SettingsTree *tree);
|
||||
void setGamesPanel(GamesPanel *panel);
|
||||
void setVersionsPanel(VersionsPanel *panel);
|
||||
void setPowerPanel(PowerPanel *panel);
|
||||
void setLogPanel(LogPanel *panel);
|
||||
bool isConnected() const;
|
||||
void setPanelsPanel(PanelsPanel *panel);
|
||||
void setLightsPanel(LightsPanel *panel);
|
||||
|
||||
public slots:
|
||||
void startConnection();
|
||||
void closeConnection();
|
||||
void sendCommand(const QString &cmd);
|
||||
|
||||
void onConnected();
|
||||
void onDisconnected();
|
||||
void onTextMessageReceived(const QString &msg);
|
||||
void onTextMessageReceived(const QString &message);
|
||||
void onErrorOccurred(QAbstractSocket::SocketError error);
|
||||
|
||||
private slots:
|
||||
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;
|
||||
QWebSocket m_socket;
|
||||
QLineEdit *m_urlEdit = nullptr;
|
||||
QLabel *m_statusLabel = nullptr;
|
||||
QList<QTextEdit*> m_logs;
|
||||
|
||||
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;
|
||||
LogPanel *m_logPanel = nullptr;
|
||||
PanelsPanel *m_panelsPanel = nullptr;
|
||||
LightsPanel *m_lightsPanel = nullptr;
|
||||
|
||||
QStringList m_settingsKeys;
|
||||
int m_rnpCount = -1;
|
||||
};
|
||||
|
||||
0
build-run.sh
Normal file → Executable file
0
build-run.sh
Normal file → Executable file
409
main.cpp
409
main.cpp
@@ -1,10 +1,11 @@
|
||||
#include <QApplication>
|
||||
#include <QButtonGroup>
|
||||
#include <QCheckBox>
|
||||
#include <QClipboard>
|
||||
#include <QFont>
|
||||
#include <QGridLayout>
|
||||
#include <QCheckBox>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHash>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
@@ -20,16 +21,63 @@
|
||||
|
||||
#include "GamesPanel.h"
|
||||
#include "LogPanel.h"
|
||||
#include "LightsPanel.h"
|
||||
#include "PanelsPanel.h"
|
||||
#include "PowerPanel.h"
|
||||
#include "SettingsTree.h"
|
||||
#include "VersionsPanel.h"
|
||||
#include "WebSocketController.h"
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OS clipboard bridge (works from button clicks in WASM via navigator.clipboard)
|
||||
// ---------------------------------------------------------------------------
|
||||
static void copyToClipboard(const QString &text)
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
QByteArray utf8 = text.toUtf8();
|
||||
EM_ASM({
|
||||
var txt = UTF8ToString($0, $1);
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(txt).catch(function() {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = txt; ta.style.position='fixed'; ta.style.opacity='0';
|
||||
document.body.appendChild(ta); ta.focus(); ta.select();
|
||||
document.execCommand('copy'); document.body.removeChild(ta);
|
||||
});
|
||||
} else {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = txt; ta.style.position='fixed'; ta.style.opacity='0';
|
||||
document.body.appendChild(ta); ta.focus(); ta.select();
|
||||
document.execCommand('copy'); document.body.removeChild(ta);
|
||||
}
|
||||
}, utf8.constData(), utf8.size());
|
||||
#else
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ICON number → IP address
|
||||
// ---------------------------------------------------------------------------
|
||||
static const auto iconToIp = [](int icon) -> QString {
|
||||
static const QHash<int,QString> overrides = {
|
||||
{280, QStringLiteral("10.10.1.30")},
|
||||
};
|
||||
if (overrides.contains(icon)) return overrides[icon];
|
||||
const int subnet = (icon - 1) / 254;
|
||||
const int host = ((icon - 1) % 254) + 1;
|
||||
return QString("10.10.%1.%2").arg(subnet).arg(host);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
static QWidget *makeGamesTab(WebSocketController *ctrl, QWidget *parent)
|
||||
{
|
||||
auto *panel = new GamesPanel(parent);
|
||||
ctrl->setGamesPanel(panel);
|
||||
// Route GAM add commands back through the controller
|
||||
QObject::connect(panel, &GamesPanel::commandRequested,
|
||||
ctrl, &WebSocketController::sendCommand);
|
||||
return panel;
|
||||
@@ -49,25 +97,20 @@ static QWidget *makeSettingsTab(WebSocketController *ctrl, QWidget *parent)
|
||||
layout->setContentsMargins(2, 2, 2, 2);
|
||||
layout->setSpacing(4);
|
||||
|
||||
// --- Sliders row: brightness and volume side by side (top) ---
|
||||
auto *slidersRow = new QHBoxLayout();
|
||||
|
||||
auto *brightnessLabel = new QLabel("Brightness:", page);
|
||||
// --- Sliders row (top) ---
|
||||
auto *slidersRow = new QHBoxLayout();
|
||||
auto *brightnessLabel = new QLabel("Brightness:", page);
|
||||
auto *brightnessSlider = new QSlider(Qt::Horizontal, page);
|
||||
brightnessSlider->setMinimum(1);
|
||||
brightnessSlider->setMaximum(12);
|
||||
brightnessSlider->setValue(1);
|
||||
brightnessSlider->setEnabled(false);
|
||||
auto *brightnessVal = new QLabel("–", page);
|
||||
brightnessSlider->setMinimum(1); brightnessSlider->setMaximum(12);
|
||||
brightnessSlider->setValue(1); brightnessSlider->setEnabled(false);
|
||||
auto *brightnessVal = new QLabel("â", page);
|
||||
brightnessVal->setFixedWidth(28);
|
||||
|
||||
auto *volumeLabel = new QLabel("Volume:", page);
|
||||
auto *volumeSlider = new QSlider(Qt::Horizontal, page);
|
||||
volumeSlider->setMinimum(1);
|
||||
volumeSlider->setMaximum(6);
|
||||
volumeSlider->setValue(1);
|
||||
volumeSlider->setEnabled(false);
|
||||
auto *volumeVal = new QLabel("–", page);
|
||||
volumeSlider->setMinimum(1); volumeSlider->setMaximum(6);
|
||||
volumeSlider->setValue(1); volumeSlider->setEnabled(false);
|
||||
auto *volumeVal = new QLabel("â", page);
|
||||
volumeVal->setFixedWidth(28);
|
||||
|
||||
slidersRow->addWidget(brightnessLabel);
|
||||
@@ -79,57 +122,45 @@ static QWidget *makeSettingsTab(WebSocketController *ctrl, QWidget *parent)
|
||||
slidersRow->addWidget(volumeVal);
|
||||
layout->addLayout(slidersRow);
|
||||
|
||||
// --- Tree (takes all spare vertical space) ---
|
||||
// --- Tree ---
|
||||
auto *tree = new SettingsTree(page);
|
||||
auto *ph = new QTreeWidgetItem(tree);
|
||||
ph->setText(0, "Connect to load settings...");
|
||||
layout->addWidget(tree, 1);
|
||||
ctrl->setSettingsTree(tree);
|
||||
|
||||
// --- Single compact toolbar row: name input + 3 GBL buttons + checkbox (below tree) ---
|
||||
// --- Toolbar (below tree) ---
|
||||
auto *toolbar = new QHBoxLayout();
|
||||
auto *nameEdit = new QLineEdit(page);
|
||||
nameEdit->setPlaceholderText("Config name...");
|
||||
nameEdit->setMaximumWidth(160);
|
||||
auto *saveBtn = new QPushButton("Save", page);
|
||||
auto *loadBtn = new QPushButton("Restore From File", page);
|
||||
auto *resetBtn = new QPushButton("Restore Defaults", page);
|
||||
auto *saveBtn = new QPushButton("Save to Device", page);
|
||||
auto *loadBtn = new QPushButton("Restore from Device", page);
|
||||
auto *resetBtn = new QPushButton("Restore Defaults", page);
|
||||
resetBtn->setStyleSheet(
|
||||
"QPushButton { color: white; background-color: #c62828; border-radius: 4px; padding: 3px 8px; }"
|
||||
"QPushButton:hover { background-color: #b71c1c; }"
|
||||
"QPushButton:disabled { background-color: #888888; color: #cccccc; }");
|
||||
"QPushButton { color:white; background:#c62828; border-radius:4px; padding:3px 8px; }"
|
||||
"QPushButton:hover { background:#b71c1c; }"
|
||||
"QPushButton:disabled { background:#888; color:#ccc; }");
|
||||
auto *autoPowerOff = new QCheckBox("Auto Power Off", page);
|
||||
toolbar->addWidget(nameEdit);
|
||||
toolbar->addWidget(saveBtn);
|
||||
toolbar->addWidget(loadBtn);
|
||||
toolbar->addWidget(resetBtn);
|
||||
toolbar->addStretch(1);
|
||||
toolbar->addWidget(autoPowerOff);
|
||||
toolbar->addWidget(nameEdit); toolbar->addWidget(saveBtn);
|
||||
toolbar->addWidget(loadBtn); toolbar->addWidget(resetBtn);
|
||||
toolbar->addStretch(1); toolbar->addWidget(autoPowerOff);
|
||||
layout->addLayout(toolbar);
|
||||
|
||||
// --- Reset to defaults (inline confirm row — no QMessageBox::exec on WASM) ---
|
||||
// --- Reset confirm row ---
|
||||
auto *confirmRow = new QHBoxLayout();
|
||||
auto *confirmLabel = new QLabel(page);
|
||||
confirmLabel->setText("Click again to confirm reset:");
|
||||
confirmLabel->setStyleSheet("color: #c62828;");
|
||||
confirmLabel->setVisible(false);
|
||||
auto *confirmLabel = new QLabel("Click again to confirm reset:", page);
|
||||
confirmLabel->setStyleSheet("color:#c62828;"); confirmLabel->setVisible(false);
|
||||
auto *confirmYesBtn = new QPushButton("Yes, Reset", page);
|
||||
confirmYesBtn->setStyleSheet(
|
||||
"QPushButton { color: white; background-color: #c62828; border-radius: 4px; padding: 2px 8px; }");
|
||||
confirmYesBtn->setStyleSheet("QPushButton{color:white;background:#c62828;border-radius:4px;padding:2px 8px;}");
|
||||
confirmYesBtn->setVisible(false);
|
||||
auto *confirmNoBtn = new QPushButton("Cancel", page);
|
||||
confirmNoBtn->setVisible(false);
|
||||
confirmRow->addWidget(confirmLabel);
|
||||
confirmRow->addWidget(confirmYesBtn);
|
||||
confirmRow->addWidget(confirmNoBtn);
|
||||
confirmRow->addStretch(1);
|
||||
confirmRow->addWidget(confirmLabel); confirmRow->addWidget(confirmYesBtn);
|
||||
confirmRow->addWidget(confirmNoBtn); confirmRow->addStretch(1);
|
||||
layout->addLayout(confirmRow);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Connections
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// Save config
|
||||
// Save
|
||||
auto doSave = [ctrl, nameEdit]() {
|
||||
QString name = nameEdit->text().trimmed();
|
||||
name.remove(QRegularExpression("[^A-Za-z0-9]"));
|
||||
@@ -139,44 +170,31 @@ static QWidget *makeSettingsTab(WebSocketController *ctrl, QWidget *parent)
|
||||
};
|
||||
QObject::connect(saveBtn, &QPushButton::clicked, page, doSave);
|
||||
QObject::connect(nameEdit, &QLineEdit::returnPressed, page, doSave);
|
||||
|
||||
// Restore from file
|
||||
QObject::connect(loadBtn, &QPushButton::clicked, page, [ctrl, nameEdit]() {
|
||||
QObject::connect(loadBtn, &QPushButton::clicked, page, [ctrl, nameEdit]() {
|
||||
QString name = nameEdit->text().trimmed();
|
||||
name.remove(QRegularExpression("[^A-Za-z0-9]"));
|
||||
if (name.isEmpty()) return;
|
||||
ctrl->sendCommand(QString("GBL LoadFromFile %1").arg(name));
|
||||
nameEdit->clear();
|
||||
});
|
||||
|
||||
// Reset confirm
|
||||
QObject::connect(resetBtn, &QPushButton::clicked, page,
|
||||
[confirmLabel, confirmYesBtn, confirmNoBtn]() {
|
||||
confirmLabel->setVisible(true);
|
||||
confirmYesBtn->setVisible(true);
|
||||
confirmNoBtn->setVisible(true);
|
||||
confirmLabel->setVisible(true); confirmYesBtn->setVisible(true); confirmNoBtn->setVisible(true);
|
||||
});
|
||||
QObject::connect(confirmYesBtn, &QPushButton::clicked, page,
|
||||
[ctrl, confirmLabel, confirmYesBtn, confirmNoBtn]() {
|
||||
ctrl->sendCommand(QStringLiteral("GBL ResetAllToDefaults"));
|
||||
confirmLabel->setVisible(false);
|
||||
confirmYesBtn->setVisible(false);
|
||||
confirmNoBtn->setVisible(false);
|
||||
confirmLabel->setVisible(false); confirmYesBtn->setVisible(false); confirmNoBtn->setVisible(false);
|
||||
});
|
||||
QObject::connect(confirmNoBtn, &QPushButton::clicked, page,
|
||||
[confirmLabel, confirmYesBtn, confirmNoBtn]() {
|
||||
confirmLabel->setVisible(false);
|
||||
confirmYesBtn->setVisible(false);
|
||||
confirmNoBtn->setVisible(false);
|
||||
confirmLabel->setVisible(false); confirmYesBtn->setVisible(false); confirmNoBtn->setVisible(false);
|
||||
});
|
||||
|
||||
// Auto power off
|
||||
QObject::connect(autoPowerOff, &QCheckBox::toggled, page, [ctrl](bool checked) {
|
||||
ctrl->sendCommand(checked ? QStringLiteral("POW EnableAutoOff")
|
||||
: QStringLiteral("POW KeepOn"));
|
||||
ctrl->sendCommand(checked ? QStringLiteral("POW EnableAutoOff") : QStringLiteral("POW KeepOn"));
|
||||
});
|
||||
|
||||
// Query brightness/volume ranges+values on connect; disable on disconnect
|
||||
// Slider: query ranges on connect, disable on disconnect
|
||||
QObject::connect(ctrl->socket(), &QWebSocket::connected, page, [ctrl]() {
|
||||
ctrl->sendCommand(QStringLiteral("GBL brightnessMin"));
|
||||
ctrl->sendCommand(QStringLiteral("GBL brightnessMax"));
|
||||
@@ -187,57 +205,28 @@ static QWidget *makeSettingsTab(WebSocketController *ctrl, QWidget *parent)
|
||||
});
|
||||
QObject::connect(ctrl->socket(), &QWebSocket::disconnected, page,
|
||||
[brightnessSlider, brightnessVal, volumeSlider, volumeVal]() {
|
||||
brightnessSlider->setEnabled(false);
|
||||
brightnessVal->setText(QStringLiteral("–"));
|
||||
volumeSlider->setEnabled(false);
|
||||
volumeVal->setText(QStringLiteral("–"));
|
||||
brightnessSlider->setEnabled(false); brightnessVal->setText("â");
|
||||
volumeSlider->setEnabled(false); volumeVal->setText("â");
|
||||
});
|
||||
|
||||
// Parse incoming GBL brightness / sound/volume responses
|
||||
QObject::connect(ctrl->socket(), &QWebSocket::textMessageReceived, page,
|
||||
[brightnessSlider, brightnessVal, volumeSlider, volumeVal](const QString &msg) {
|
||||
// helper: parse "GBL <key>=<int>" and return {ok, value}
|
||||
auto parseGbl = [&](const QString &key) -> std::pair<bool,int> {
|
||||
const QString prefix = QString("GBL %1=").arg(key);
|
||||
if (!msg.startsWith(prefix)) return {false, 0};
|
||||
bool ok = false;
|
||||
int v = msg.mid(prefix.length()).toInt(&ok);
|
||||
return {ok, v};
|
||||
if (!msg.startsWith(prefix)) return {false,0};
|
||||
bool ok=false; int v=msg.mid(prefix.length()).toInt(&ok);
|
||||
return {ok,v};
|
||||
};
|
||||
|
||||
if (auto [ok, v] = parseGbl("brightness"); ok) {
|
||||
brightnessSlider->setValue(v);
|
||||
brightnessVal->setText(QString::number(v));
|
||||
brightnessSlider->setEnabled(true);
|
||||
} else if (auto [ok, v] = parseGbl("brightnessMin"); ok) {
|
||||
brightnessSlider->setMinimum(v);
|
||||
} else if (auto [ok, v] = parseGbl("brightnessMax"); ok) {
|
||||
brightnessSlider->setMaximum(v);
|
||||
} else if (auto [ok, v] = parseGbl("sound/volume"); ok) {
|
||||
volumeSlider->setValue(v);
|
||||
volumeVal->setText(QString::number(v));
|
||||
volumeSlider->setEnabled(true);
|
||||
} else if (auto [ok, v] = parseGbl("sound/volumeMin"); ok) {
|
||||
volumeSlider->setMinimum(v);
|
||||
} else if (auto [ok, v] = parseGbl("sound/volumeMax"); ok) {
|
||||
volumeSlider->setMaximum(v);
|
||||
}
|
||||
});
|
||||
|
||||
// Live label update while dragging; send command only on release
|
||||
QObject::connect(brightnessSlider, &QSlider::valueChanged, page,
|
||||
[brightnessVal](int v) { brightnessVal->setText(QString::number(v)); });
|
||||
QObject::connect(brightnessSlider, &QSlider::sliderReleased, page,
|
||||
[ctrl, brightnessSlider]() {
|
||||
ctrl->sendCommand(QString("GBL brightness=%1").arg(brightnessSlider->value()));
|
||||
});
|
||||
|
||||
QObject::connect(volumeSlider, &QSlider::valueChanged, page,
|
||||
[volumeVal](int v) { volumeVal->setText(QString::number(v)); });
|
||||
QObject::connect(volumeSlider, &QSlider::sliderReleased, page,
|
||||
[ctrl, volumeSlider]() {
|
||||
ctrl->sendCommand(QString("GBL sound/volume=%1").arg(volumeSlider->value()));
|
||||
if (auto [ok,v]=parseGbl("brightness"); ok) { brightnessSlider->setValue(v); brightnessVal->setText(QString::number(v)); brightnessSlider->setEnabled(true); }
|
||||
else if (auto [ok,v]=parseGbl("brightnessMin"); ok) { brightnessSlider->setMinimum(v); }
|
||||
else if (auto [ok,v]=parseGbl("brightnessMax"); ok) { brightnessSlider->setMaximum(v); }
|
||||
else if (auto [ok,v]=parseGbl("sound/volume"); ok) { volumeSlider->setValue(v); volumeVal->setText(QString::number(v)); volumeSlider->setEnabled(true); }
|
||||
else if (auto [ok,v]=parseGbl("sound/volumeMin"); ok) { volumeSlider->setMinimum(v); }
|
||||
else if (auto [ok,v]=parseGbl("sound/volumeMax"); ok) { volumeSlider->setMaximum(v); }
|
||||
});
|
||||
QObject::connect(brightnessSlider, &QSlider::valueChanged, page, [brightnessVal](int v){ brightnessVal->setText(QString::number(v)); });
|
||||
QObject::connect(brightnessSlider, &QSlider::sliderReleased,page, [ctrl,brightnessSlider](){ ctrl->sendCommand(QString("GBL brightness=%1").arg(brightnessSlider->value())); });
|
||||
QObject::connect(volumeSlider, &QSlider::valueChanged, page, [volumeVal](int v){ volumeVal->setText(QString::number(v)); });
|
||||
QObject::connect(volumeSlider, &QSlider::sliderReleased,page, [ctrl,volumeSlider](){ ctrl->sendCommand(QString("GBL sound/volume=%1").arg(volumeSlider->value())); });
|
||||
|
||||
return page;
|
||||
}
|
||||
@@ -249,13 +238,7 @@ static QWidget *makeManualTab(WebSocketController *ctrl, QWidget *parent)
|
||||
layout->setContentsMargins(2, 2, 2, 2);
|
||||
layout->setSpacing(4);
|
||||
|
||||
// 4 independent command input boxes (Enter to send, no Send button)
|
||||
const QStringList placeholders = {
|
||||
"Command 1...",
|
||||
"Command 2...",
|
||||
"Command 3...",
|
||||
"Command 4...",
|
||||
};
|
||||
const QStringList placeholders = {"Command 1...","Command 2...","Command 3...","Command 4..."};
|
||||
auto *inputGrid = new QGridLayout();
|
||||
inputGrid->setSpacing(4);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
@@ -270,17 +253,21 @@ static QWidget *makeManualTab(WebSocketController *ctrl, QWidget *parent)
|
||||
}
|
||||
layout->addLayout(inputGrid);
|
||||
|
||||
// Log view + clear button
|
||||
auto *clearBtn = new QPushButton("Clear Log", page);
|
||||
clearBtn->setMaximumWidth(100);
|
||||
auto *log = new QTextEdit(page);
|
||||
log->setReadOnly(true);
|
||||
log->setFont(QFont("Courier", 9));
|
||||
auto *logBtnRow = new QHBoxLayout();
|
||||
auto *copyLogBtn = new QPushButton("Copy Log", page);
|
||||
auto *clearBtn = new QPushButton("Clear Log", page);
|
||||
copyLogBtn->setMaximumWidth(110);
|
||||
clearBtn->setMaximumWidth(100);
|
||||
logBtnRow->addWidget(copyLogBtn); logBtnRow->addWidget(clearBtn); logBtnRow->addStretch(1);
|
||||
layout->addWidget(log, 1);
|
||||
layout->addWidget(clearBtn);
|
||||
layout->addLayout(logBtnRow);
|
||||
ctrl->addLogView(log);
|
||||
|
||||
QObject::connect(clearBtn, &QPushButton::clicked, log, &QTextEdit::clear);
|
||||
QObject::connect(clearBtn, &QPushButton::clicked, log, &QTextEdit::clear);
|
||||
QObject::connect(copyLogBtn, &QPushButton::clicked, page, [log](){ copyToClipboard(log->toPlainText()); });
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -298,15 +285,20 @@ static QWidget *makeLogsTab(WebSocketController *ctrl, QWidget *parent)
|
||||
return panel;
|
||||
}
|
||||
|
||||
static QWidget *makeLightsTab(WebSocketController *ctrl, QWidget *parent)
|
||||
{
|
||||
auto *panel = new LightsPanel(parent);
|
||||
ctrl->setLightsPanel(panel);
|
||||
QObject::connect(panel, &LightsPanel::commandRequested,
|
||||
ctrl, &WebSocketController::sendCommand);
|
||||
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);
|
||||
ctrl->addLogView(log);
|
||||
return page;
|
||||
auto *panel = new PanelsPanel(parent);
|
||||
ctrl->setPanelsPanel(panel);
|
||||
return panel;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
@@ -320,108 +312,66 @@ int main(int argc, char *argv[])
|
||||
mainLayout->setContentsMargins(6, 6, 6, 6);
|
||||
mainLayout->setSpacing(4);
|
||||
|
||||
// --- ICON → IP helper (formula + hardcoded overrides) ---
|
||||
// Returns the IP string for a given ICON number.
|
||||
static const auto iconToIp = [](int icon) -> QString {
|
||||
static const QHash<int,QString> overrides = {
|
||||
{280, QStringLiteral("10.10.1.30")},
|
||||
};
|
||||
if (overrides.contains(icon))
|
||||
return overrides[icon];
|
||||
const int subnet = (icon - 1) / 254;
|
||||
const int host = ((icon - 1) % 254) + 1;
|
||||
return QString("10.10.%1.%2").arg(subnet).arg(host);
|
||||
};
|
||||
|
||||
// --- Hidden urlEdit still used by WebSocketController::startConnection() ---
|
||||
auto *urlEdit = new QLineEdit(&window);
|
||||
// --- Hidden urlEdit used by WebSocketController ---
|
||||
auto *urlEdit = new QLineEdit(&window);
|
||||
urlEdit->setVisible(false);
|
||||
|
||||
auto *statusLabel = new QLabel("Disconnected", &window);
|
||||
|
||||
// --- Network connection row ---
|
||||
auto *headerRow = new QHBoxLayout();
|
||||
auto *headerRow = new QHBoxLayout();
|
||||
headerRow->addWidget(new QLabel("Network connection:", &window));
|
||||
|
||||
// Option 1 – fixed IP
|
||||
auto *radioFixed = new QRadioButton("10.110.110.10", &window);
|
||||
|
||||
// Option 2 – ICON number → derived IP
|
||||
auto *radioIcon = new QRadioButton(&window);
|
||||
auto *iconSpin = new QSpinBox(&window);
|
||||
iconSpin->setRange(1, 9999);
|
||||
iconSpin->setValue(280);
|
||||
iconSpin->setFixedWidth(68);
|
||||
iconSpin->setAlignment(Qt::AlignRight);
|
||||
auto *iconIpLabel = new QLabel(iconToIp(280), &window);
|
||||
iconIpLabel->setStyleSheet("color: #555; font-style: italic;");
|
||||
|
||||
// Option 3 – free text (hostname or raw IP)
|
||||
auto *radioOther = new QRadioButton("other:", &window);
|
||||
auto *otherEdit = new QLineEdit(&window);
|
||||
otherEdit->setText("ftsDevkit4.local");
|
||||
otherEdit->setFixedWidth(160);
|
||||
otherEdit->setEnabled(false); // only active when radioOther selected
|
||||
auto *radioFixed = new QRadioButton("10.110.110.10", &window);
|
||||
auto *radioIcon = new QRadioButton(&window);
|
||||
auto *iconSpin = new QSpinBox(&window);
|
||||
iconSpin->setRange(1, 9999); iconSpin->setValue(280);
|
||||
iconSpin->setFixedWidth(68); iconSpin->setAlignment(Qt::AlignRight);
|
||||
auto *iconIpLabel = new QLabel(iconToIp(280), &window);
|
||||
iconIpLabel->setStyleSheet("color:#555; font-style:italic;");
|
||||
auto *radioOther = new QRadioButton("other:", &window);
|
||||
auto *otherEdit = new QLineEdit(&window);
|
||||
otherEdit->setText("ftsDevkit4.local"); otherEdit->setFixedWidth(160);
|
||||
otherEdit->setEnabled(false);
|
||||
|
||||
auto *btnGroup = new QButtonGroup(&window);
|
||||
btnGroup->addButton(radioFixed, 0);
|
||||
btnGroup->addButton(radioIcon, 1);
|
||||
btnGroup->addButton(radioOther, 2);
|
||||
radioIcon->setChecked(true); // default selection
|
||||
radioIcon->setChecked(true);
|
||||
|
||||
headerRow->addWidget(radioFixed);
|
||||
headerRow->addWidget(radioIcon);
|
||||
headerRow->addWidget(iconSpin);
|
||||
headerRow->addWidget(iconIpLabel);
|
||||
headerRow->addWidget(radioIcon); headerRow->addWidget(iconSpin); headerRow->addWidget(iconIpLabel);
|
||||
headerRow->addSpacing(8);
|
||||
headerRow->addWidget(radioOther);
|
||||
headerRow->addWidget(otherEdit);
|
||||
headerRow->addWidget(radioOther); headerRow->addWidget(otherEdit);
|
||||
headerRow->addStretch(1);
|
||||
|
||||
// Buttons
|
||||
auto *connectBtn = new QPushButton("Connect", &window);
|
||||
auto *disconnectBtn = new QPushButton("Disconnect", &window);
|
||||
auto *shutdownBtn = new QPushButton("Shutdown", &window);
|
||||
disconnectBtn->setEnabled(false);
|
||||
shutdownBtn->setEnabled(false);
|
||||
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; }");
|
||||
headerRow->addWidget(connectBtn);
|
||||
headerRow->addWidget(disconnectBtn);
|
||||
headerRow->addWidget(shutdownBtn);
|
||||
headerRow->addWidget(statusLabel);
|
||||
"QPushButton{color:white;background:#c62828;border-radius:4px;padding:3px 10px;}"
|
||||
"QPushButton:hover{background:#b71c1c;}"
|
||||
"QPushButton:disabled{background:#888;color:#ccc;}");
|
||||
headerRow->addWidget(connectBtn); headerRow->addWidget(disconnectBtn);
|
||||
headerRow->addWidget(shutdownBtn); headerRow->addWidget(statusLabel);
|
||||
mainLayout->addLayout(headerRow);
|
||||
|
||||
// --- Build URL from current selection into the hidden urlEdit ---
|
||||
auto updateUrl = [=]() {
|
||||
const int port = 5424;
|
||||
QString host;
|
||||
if (radioFixed->isChecked()) {
|
||||
host = QStringLiteral("10.110.110.10");
|
||||
} else if (radioIcon->isChecked()) {
|
||||
host = iconToIp(iconSpin->value());
|
||||
} else {
|
||||
host = otherEdit->text().trimmed();
|
||||
if (host.isEmpty()) host = QStringLiteral("127.0.0.1");
|
||||
}
|
||||
if (radioFixed->isChecked()) host = QStringLiteral("10.110.110.10");
|
||||
else if (radioIcon->isChecked()) host = iconToIp(iconSpin->value());
|
||||
else { host = otherEdit->text().trimmed(); if (host.isEmpty()) host = "127.0.0.1"; }
|
||||
urlEdit->setText(QString("ws://%1:%2/").arg(host).arg(port));
|
||||
};
|
||||
updateUrl(); // set initial value
|
||||
|
||||
// Keep iconIpLabel in sync and rebuild URL whenever ICON changes
|
||||
updateUrl();
|
||||
QObject::connect(iconSpin, QOverload<int>::of(&QSpinBox::valueChanged), &window,
|
||||
[=](int v) { iconIpLabel->setText(iconToIp(v)); updateUrl(); });
|
||||
[=](int v){ iconIpLabel->setText(iconToIp(v)); updateUrl(); });
|
||||
QObject::connect(btnGroup, QOverload<int>::of(&QButtonGroup::idClicked), &window,
|
||||
[=](int) {
|
||||
otherEdit->setEnabled(radioOther->isChecked());
|
||||
updateUrl();
|
||||
});
|
||||
QObject::connect(otherEdit, &QLineEdit::textChanged, &window, [=]() {
|
||||
if (radioOther->isChecked()) updateUrl();
|
||||
});
|
||||
[=](int){ otherEdit->setEnabled(radioOther->isChecked()); updateUrl(); });
|
||||
QObject::connect(otherEdit, &QLineEdit::textChanged, &window,
|
||||
[=](){ if (radioOther->isChecked()) updateUrl(); });
|
||||
|
||||
// --- Controller ---
|
||||
auto *ctrl = new WebSocketController(urlEdit, statusLabel, &window);
|
||||
@@ -430,67 +380,54 @@ int main(int argc, char *argv[])
|
||||
auto *tabs = new QTabWidget(&window);
|
||||
tabs->addTab(makeGamesTab (ctrl, &window), "Games");
|
||||
tabs->addTab(makeVersionsTab(ctrl, &window), "Versions");
|
||||
tabs->addTab(makeManualTab (ctrl, &window), "Manual");
|
||||
tabs->addTab(makeManualTab (ctrl, &window), "Manual Websocket");
|
||||
tabs->addTab(makeSettingsTab(ctrl, &window), "Settings");
|
||||
tabs->addTab(makePowerTab (ctrl, &window), "Power");
|
||||
tabs->addTab(makeLogsTab (ctrl, &window), "Logs");
|
||||
tabs->addTab(makePanelsTab (ctrl, &window), "Panels");
|
||||
tabs->addTab(makePanelsTab (ctrl, &window), "Panels Impacts");
|
||||
tabs->addTab(makeLightsTab (ctrl, &window), "Panel Lights");
|
||||
mainLayout->addWidget(tabs, 1);
|
||||
|
||||
// --- Shutdown: inline confirm (no QMessageBox::exec on WASM) ---
|
||||
// --- Shutdown inline confirm ---
|
||||
auto *shutdownConfirmLabel = new QLabel("Confirm shutdown:", &window);
|
||||
auto *shutdownConfirmYes = new QPushButton("Yes, Shutdown", &window);
|
||||
auto *shutdownConfirmNo = new QPushButton("Cancel", &window);
|
||||
shutdownConfirmLabel->setStyleSheet("color: #c62828; font-weight: bold;");
|
||||
shutdownConfirmYes->setStyleSheet(
|
||||
"QPushButton { color: white; background-color: #c62828; border-radius: 4px; padding: 2px 8px; }");
|
||||
shutdownConfirmLabel->setVisible(false);
|
||||
shutdownConfirmYes->setVisible(false);
|
||||
shutdownConfirmNo->setVisible(false);
|
||||
shutdownConfirmLabel->setStyleSheet("color:#c62828; font-weight:bold;");
|
||||
shutdownConfirmYes->setStyleSheet("QPushButton{color:white;background:#c62828;border-radius:4px;padding:2px 8px;}");
|
||||
shutdownConfirmLabel->setVisible(false); shutdownConfirmYes->setVisible(false); shutdownConfirmNo->setVisible(false);
|
||||
headerRow->addWidget(shutdownConfirmLabel);
|
||||
headerRow->addWidget(shutdownConfirmYes);
|
||||
headerRow->addWidget(shutdownConfirmNo);
|
||||
|
||||
QObject::connect(shutdownBtn, &QPushButton::clicked, &window,
|
||||
[shutdownConfirmLabel, shutdownConfirmYes, shutdownConfirmNo]() {
|
||||
shutdownConfirmLabel->setVisible(true);
|
||||
shutdownConfirmYes->setVisible(true);
|
||||
shutdownConfirmNo->setVisible(true);
|
||||
[shutdownConfirmLabel, shutdownConfirmYes, shutdownConfirmNo](){
|
||||
shutdownConfirmLabel->setVisible(true); shutdownConfirmYes->setVisible(true); shutdownConfirmNo->setVisible(true);
|
||||
});
|
||||
QObject::connect(shutdownConfirmYes, &QPushButton::clicked, &window,
|
||||
[ctrl, shutdownConfirmLabel, shutdownConfirmYes, shutdownConfirmNo]() {
|
||||
[ctrl, shutdownConfirmLabel, shutdownConfirmYes, shutdownConfirmNo](){
|
||||
ctrl->sendCommand(QStringLiteral("POW ShutDown"));
|
||||
shutdownConfirmLabel->setVisible(false);
|
||||
shutdownConfirmYes->setVisible(false);
|
||||
shutdownConfirmNo->setVisible(false);
|
||||
shutdownConfirmLabel->setVisible(false); shutdownConfirmYes->setVisible(false); shutdownConfirmNo->setVisible(false);
|
||||
});
|
||||
QObject::connect(shutdownConfirmNo, &QPushButton::clicked, &window,
|
||||
[shutdownConfirmLabel, shutdownConfirmYes, shutdownConfirmNo]() {
|
||||
shutdownConfirmLabel->setVisible(false);
|
||||
shutdownConfirmYes->setVisible(false);
|
||||
shutdownConfirmNo->setVisible(false);
|
||||
[shutdownConfirmLabel, shutdownConfirmYes, shutdownConfirmNo](){
|
||||
shutdownConfirmLabel->setVisible(false); shutdownConfirmYes->setVisible(false); shutdownConfirmNo->setVisible(false);
|
||||
});
|
||||
|
||||
// --- 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, 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);
|
||||
[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);
|
||||
[connectBtn, disconnectBtn, shutdownBtn](){
|
||||
connectBtn->setEnabled(true); disconnectBtn->setEnabled(false); shutdownBtn->setEnabled(false);
|
||||
});
|
||||
|
||||
window.resize(1050, 620);
|
||||
|
||||
Reference in New Issue
Block a user