Compare commits

...

10 Commits

13 changed files with 922 additions and 295 deletions

View File

@@ -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"

View File

@@ -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));
}

View File

@@ -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
View 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
View 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
View 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
View 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;
};

View File

@@ -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
View 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
```

View File

@@ -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;

View File

@@ -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
View File

409
main.cpp
View File

@@ -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);