diff --git a/CMakeLists.txt b/CMakeLists.txt index bfed308..03de025 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ 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 diff --git a/LightsPanel.cpp b/LightsPanel.cpp new file mode 100644 index 0000000..c214d3f --- /dev/null +++ b/LightsPanel.cpp @@ -0,0 +1,295 @@ +#include "LightsPanel.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// 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(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(innerR * 0.45))); + p.setFont(cf); + p.setPen(palette().text().color()); + p.drawText(innerRect, Qt::AlignCenter, QString::number(m_count)); +} diff --git a/LightsPanel.h b/LightsPanel.h new file mode 100644 index 0000000..23eac35 --- /dev/null +++ b/LightsPanel.h @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include + +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 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; +}; diff --git a/README.MD b/README.MD index a5cf87c..85565d0 100644 --- a/README.MD +++ b/README.MD @@ -1,3 +1,8 @@ +## ReMote is a WASM Remote Monitor Application + + +### Install +``` sudo apt update sudo apt install -y ninja-build python3 build-essential @@ -19,4 +24,4 @@ cmake --build build-wasm cd build-wasm python3 -m http.server 8000 - +``` diff --git a/WebSocketController.cpp b/WebSocketController.cpp index 2cebbed..e3a9ed0 100644 --- a/WebSocketController.cpp +++ b/WebSocketController.cpp @@ -1,6 +1,7 @@ #include "WebSocketController.h" #include "GamesPanel.h" #include "LogPanel.h" +#include "LightsPanel.h" #include "PanelsPanel.h" #include "PowerPanel.h" #include "SettingsTree.h" @@ -33,6 +34,7 @@ void WebSocketController::setVersionsPanel(VersionsPanel *p) { m_versionsPanel 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 { @@ -168,6 +170,7 @@ void WebSocketController::handleProtocol(const QString &msg) 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)); diff --git a/WebSocketController.h b/WebSocketController.h index 3f436d8..61f6ccf 100644 --- a/WebSocketController.h +++ b/WebSocketController.h @@ -5,6 +5,7 @@ #include class GamesPanel; +class LightsPanel; class LogPanel; class PanelsPanel; class PowerPanel; @@ -31,6 +32,7 @@ public: void setPowerPanel(PowerPanel *panel); void setLogPanel(LogPanel *panel); void setPanelsPanel(PanelsPanel *panel); + void setLightsPanel(LightsPanel *panel); public slots: void startConnection(); @@ -59,6 +61,7 @@ private: PowerPanel *m_powerPanel = nullptr; LogPanel *m_logPanel = nullptr; PanelsPanel *m_panelsPanel = nullptr; + LightsPanel *m_lightsPanel = nullptr; QStringList m_settingsKeys; int m_rnpCount = -1; diff --git a/main.cpp b/main.cpp index 0541117..8638b31 100644 --- a/main.cpp +++ b/main.cpp @@ -21,6 +21,7 @@ #include "GamesPanel.h" #include "LogPanel.h" +#include "LightsPanel.h" #include "PanelsPanel.h" #include "PowerPanel.h" #include "SettingsTree.h" @@ -284,6 +285,15 @@ 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 *panel = new PanelsPanel(parent); @@ -375,6 +385,7 @@ int main(int argc, char *argv[]) tabs->addTab(makePowerTab (ctrl, &window), "Power"); tabs->addTab(makeLogsTab (ctrl, &window), "Logs"); tabs->addTab(makePanelsTab (ctrl, &window), "Panels Impacts"); + tabs->addTab(makeLightsTab (ctrl, &window), "Panel Lights"); mainLayout->addWidget(tabs, 1); // --- Shutdown inline confirm ---