터치 패널 그래프와 상태 점멸 기준 추가

This commit is contained in:
seo committed 2026-07-30 04:47:08 +09:00
1 parent e99fe1021a
commit 209b587eac
2 files changed
+248 -27

No files matched your search

+200 -27
View File
@@ -4,11 +4,12 @@ import subprocess
import sys
from pathlib import Path
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QColor, QFont, QPainter
from PyQt5.QtCore import QPointF, Qt, QTimer
from PyQt5.QtGui import QColor, QFont, QPainter, QPainterPath, QPen
from PyQt5.QtWidgets import (
QApplication,
QFrame,
QGridLayout,
QHBoxLayout,
QLabel,
QMainWindow,
@@ -118,12 +119,105 @@ class FanButton(QPushButton):
self.setMinimumHeight(44)
class MetricCard(QFrame):
def __init__(self, title):
super().__init__()
self.setObjectName("metricCard")
self.title = QLabel(title)
self.title.setObjectName("metricTitle")
self.value = QLabel("-")
self.value.setObjectName("metricValue")
layout = QVBoxLayout(self)
layout.setContentsMargins(10, 8, 10, 8)
layout.setSpacing(2)
layout.addWidget(self.title)
layout.addWidget(self.value)
def set_value(self, text):
self.value.setText(text)
class Sparkline(QFrame):
def __init__(self, title, color="#38bdf8", second_color=None):
super().__init__()
self.setObjectName("chartCard")
self.color = QColor(color)
self.second_color = QColor(second_color) if second_color else None
self.values = []
self.second_values = []
self.title = QLabel(title)
self.title.setObjectName("chartTitle")
self.caption = QLabel("-")
self.caption.setObjectName("chartCaption")
self.canvas = QWidget()
self.canvas.paintEvent = self.paint_chart
self.canvas.setMinimumHeight(58)
layout = QVBoxLayout(self)
layout.setContentsMargins(10, 8, 10, 8)
layout.setSpacing(4)
top = QHBoxLayout()
top.addWidget(self.title)
top.addStretch(1)
top.addWidget(self.caption)
layout.addLayout(top)
layout.addWidget(self.canvas, 1)
def set_values(self, values, caption, second_values=None):
self.values = [float(v or 0) for v in values][-80:]
self.second_values = [float(v or 0) for v in (second_values or [])][-80:]
self.caption.setText(caption)
self.canvas.update()
def paint_chart(self, event):
del event
painter = QPainter(self.canvas)
painter.setRenderHint(QPainter.Antialiasing)
rect = self.canvas.rect().adjusted(2, 4, -2, -4)
painter.fillRect(rect, QColor("#0f172a"))
painter.setPen(QPen(QColor("#263650"), 1))
painter.drawLine(rect.left(), rect.bottom(), rect.right(), rect.bottom())
all_values = self.values + self.second_values
if len(all_values) < 2:
return
low = min(all_values)
high = max(all_values)
if high <= low:
high = low + 1
self.draw_series(painter, rect, self.values, low, high, self.color, 2)
if self.second_color and self.second_values:
self.draw_series(painter, rect, self.second_values, low, high, self.second_color, 2)
@staticmethod
def draw_series(painter, rect, values, low, high, color, width):
if len(values) < 2:
return
path = QPainterPath()
for index, value in enumerate(values):
x = rect.left() + (rect.width() * index / max(1, len(values) - 1))
ratio = (float(value) - low) / (high - low)
y = rect.bottom() - (rect.height() * ratio)
point = QPointF(x, y)
if index == 0:
path.moveTo(point)
else:
path.lineTo(point)
painter.setPen(QPen(color, width))
painter.drawPath(path)
class TouchPanel(QMainWindow):
def __init__(self):
super().__init__()
self.snapshot = {}
self.rows = {}
self.mode_buttons = {}
self.metrics = {}
self.charts = {}
self.last_counters = {}
self.current_mode = "auto"
self.current_pwm = 120
self.in_flight = False
@@ -170,14 +264,13 @@ class TouchPanel(QMainWindow):
left_layout.addWidget(row)
left_layout.addStretch(1)
right = QFrame()
right.setObjectName("panel")
right = QWidget()
right_layout = QVBoxLayout(right)
right_layout.setContentsMargins(16, 14, 16, 14)
right_layout.setSpacing(12)
right_layout.setContentsMargins(0, 0, 0, 0)
right_layout.setSpacing(8)
header = QHBoxLayout()
fan_title = QLabel("팬")
fan_title = QLabel("Control")
fan_title.setObjectName("sectionTitle")
self.clock = QLabel("--:--:--")
self.clock.setObjectName("clock")
@@ -186,15 +279,19 @@ class TouchPanel(QMainWindow):
header.addWidget(self.clock)
right_layout.addLayout(header)
self.fan_rpm = QLabel("- RPM")
self.fan_rpm.setObjectName("bigValue")
self.fan_temp = QLabel("- C")
self.fan_temp.setObjectName("subValue")
self.fan_pwm = QLabel("- PWM")
self.fan_pwm.setObjectName("subValue")
right_layout.addWidget(self.fan_rpm)
right_layout.addWidget(self.fan_temp)
right_layout.addWidget(self.fan_pwm)
metric_grid = QGridLayout()
metric_grid.setHorizontalSpacing(8)
metric_grid.setVerticalSpacing(8)
for index, (key, title) in enumerate([
("rpm", "팬 RPM"),
("temp", "판단 온도"),
("pwm", "PWM"),
("wan", "WAN"),
]):
card = MetricCard(title)
self.metrics[key] = card
metric_grid.addWidget(card, index // 4, index % 4)
right_layout.addLayout(metric_grid)
mode_layout = QHBoxLayout()
for mode in ["auto", "manual", "off"]:
@@ -221,6 +318,19 @@ class TouchPanel(QMainWindow):
slider_wrap.addWidget(plus)
right_layout.addLayout(slider_wrap)
chart_grid = QGridLayout()
chart_grid.setHorizontalSpacing(8)
chart_grid.setVerticalSpacing(8)
for index, (key, chart) in enumerate([
("temp", Sparkline("온도 / RPM", "#f97316", "#38bdf8")),
("wan", Sparkline("WAN 트래픽", "#38bdf8", "#22c55e")),
("lan", Sparkline("LAN 트래픽", "#38bdf8", "#22c55e")),
("wifi", Sparkline("WiFi 트래픽", "#38bdf8", "#22c55e")),
]):
self.charts[key] = chart
chart_grid.addWidget(chart, index // 2, index % 2)
right_layout.addLayout(chart_grid, 1)
self.notice = QLabel("프로토타입")
self.notice.setObjectName("notice")
self.notice.setWordWrap(True)
@@ -275,6 +385,9 @@ class TouchPanel(QMainWindow):
network = data.get("network", {})
wifi = data.get("wifi", {})
touch = data.get("touch_display", {})
io = data.get("io", {})
history = data.get("history", [])
network_history = data.get("network_history", [])
interfaces = {row.get("name"): row for row in network.get("interfaces", [])}
clients = wifi.get("clients", [])
@@ -291,25 +404,74 @@ class TouchPanel(QMainWindow):
w24 = interfaces.get("wlan0") or {}
w5 = interfaces.get("wlan1") or {}
io_active = bool(touch.get("connected")) and bool(fan.get("policy_ok", True))
io_active = bool(touch.get("connected")) and bool(fan.get("policy_ok", True)) and self.counter_activity(
"io",
int(io.get("reads") or 0),
int(io.get("writes") or 0),
)
io_detail = f"{temp:.1f} C"
self.rows["io"].update_state(io_active, io_detail, temp >= 75)
self.rows["wan"].update_state(self.iface_up(wan), self.iface_detail(wan), self.iface_warn(wan))
self.rows["lan"].update_state(self.iface_up(lan), f"{len(lan_clients)}대 {self.iface_detail(lan)}", self.iface_warn(lan))
self.rows["wifi24"].update_state(self.iface_up(w24), f"{len(wifi24)}대 {self.iface_detail(w24)}", self.iface_warn(w24))
self.rows["wifi5"].update_state(self.iface_up(w5), f"{len(wifi5)}대 {self.iface_detail(w5)}", self.iface_warn(w5))
self.rows["wan"].update_state(self.iface_activity("wan", wan), self.iface_detail(wan), self.iface_warn(wan))
self.rows["lan"].update_state(self.iface_activity("lan", lan), f"{len(lan_clients)}대 {self.iface_detail(lan)}", self.iface_warn(lan))
self.rows["wifi24"].update_state(self.iface_activity("wifi24", w24), f"{len(wifi24)}대 {self.iface_detail(w24)}", self.iface_warn(w24))
self.rows["wifi5"].update_state(self.iface_activity("wifi5", w5), f"{len(wifi5)}대 {self.iface_detail(w5)}", self.iface_warn(w5))
self.current_mode = mode
self.current_pwm = pwm
self.fan_rpm.setText(f"{rpm:,} RPM")
self.fan_temp.setText(f"판단 온도 {float(fan.get('control_temp_c') or temp):.1f} C")
self.fan_pwm.setText(f"{pwm} PWM / {float(fan.get('percent') or 0):.1f}%")
control_temp = float(fan.get("control_temp_c") or temp)
self.metrics["rpm"].set_value(f"{rpm:,}")
self.metrics["temp"].set_value(f"{control_temp:.1f} C")
self.metrics["pwm"].set_value(f"{pwm} / {float(fan.get('percent') or 0):.1f}%")
self.metrics["wan"].set_value(self.traffic_caption(wan))
self.slider.blockSignals(True)
self.slider.setValue(pwm)
self.slider.blockSignals(False)
for key, btn in self.mode_buttons.items():
btn.setChecked(key == mode)
self.clock.setText(str(data.get("generated_at", "--:--:--")).split(" ")[-1])
self.update_charts(history, network_history, network.get("default_interface") or "eth0")
def iface_activity(self, key, row):
if not self.iface_up(row):
self.last_counters[key] = None
return False
return self.counter_activity(
key,
int(row.get("rx_bytes") or 0),
int(row.get("tx_bytes") or 0),
int(row.get("rx_packets") or 0),
int(row.get("tx_packets") or 0),
)
def counter_activity(self, key, *values):
current = tuple(values)
previous = self.last_counters.get(key)
self.last_counters[key] = current
return previous is not None and current != previous
def update_charts(self, history, network_history, wan_iface):
temps = [row.get("temp_c") for row in history if isinstance(row, dict)]
rpms = [float(row.get("fan_rpm") or 0) / 100 for row in history if isinstance(row, dict)]
self.charts["temp"].set_values(temps, "C / RPMx100", rpms)
wan_rows = self.iface_history(network_history, [wan_iface])
lan_rows = self.iface_history(network_history, ["br0", "eth1"])
wifi_rows = self.iface_history(network_history, ["wlan0", "wlan1"])
self.set_traffic_chart("wan", wan_rows)
self.set_traffic_chart("lan", lan_rows)
self.set_traffic_chart("wifi", wifi_rows)
def set_traffic_chart(self, key, rows):
rx = [float(row.get("rx_mbps") or 0) for row in rows]
tx = [float(row.get("tx_mbps") or 0) for row in rows]
latest_rx = rx[-1] if rx else 0
latest_tx = tx[-1] if tx else 0
self.charts[key].set_values(rx, f"{latest_rx:.2f}/{latest_tx:.2f} Mbps", tx)
@staticmethod
def iface_history(rows, names):
names = set(names)
return [row for row in rows if isinstance(row, dict) and row.get("iface") in names]
@staticmethod
def iface_up(row):
@@ -327,20 +489,31 @@ class TouchPanel(QMainWindow):
tx_drop = int(row.get("tx_dropped") or 0)
return f"{row.get('state', '-')} D{rx_drop + tx_drop}"
@staticmethod
def traffic_caption(row):
if not row:
return "-"
rx = int(row.get("rx_bytes") or 0)
tx = int(row.get("tx_bytes") or 0)
return f"{rx / 1024 / 1024:.0f}/{tx / 1024 / 1024:.0f}MB"
def main():
app = QApplication(sys.argv)
app.setStyleSheet(
"""
QWidget { background: #0b1220; color: #e5edf7; font-family: "Noto Sans CJK KR", "Noto Sans KR", sans-serif; }
QWidget { background: #0b1220; color: #e5edf7; font-family: "Noto Sans CJK KR", "Noto Sans CJK KR Regular", "Noto Sans KR", sans-serif; }
#panel { background: #111a2e; border: 1px solid #27364f; border-radius: 16px; }
#statusPanel { background: #101827; border-right: 1px solid #263650; }
#sectionTitle { font-size: 20px; font-weight: 700; }
#statusTile { background: #d7d9dc; border: 2px groove #f4f4f4; border-radius: 2px; }
#statusTitle { font-size: 13px; font-weight: 700; color: #111827; background: transparent; }
#statusDetail { font-size: 10px; color: #475569; background: transparent; }
#bigValue { font-size: 62px; font-weight: 800; color: #f8fafc; }
#subValue { font-size: 22px; color: #b8c5d8; }
#metricCard, #chartCard { background: #111a2e; border: 1px solid #27364f; border-radius: 12px; }
#metricTitle { color: #8fa7c5; font-size: 13px; background: transparent; }
#metricValue { color: #f8fafc; font-size: 24px; font-weight: 800; background: transparent; }
#chartTitle { color: #dce8f8; font-size: 14px; font-weight: 700; background: transparent; }
#chartCaption { color: #93a4bd; font-size: 11px; background: transparent; }
#clock { font-size: 18px; color: #8fb3ff; font-weight: 700; }
#notice { padding: 10px; background: #17233a; border: 1px solid #2b3c59; border-radius: 12px; color: #a8b8d0; }
QPushButton { min-height: 42px; border: 1px solid #33445f; border-radius: 14px; background: #1a2942; color: #dce8f8; font-size: 18px; font-weight: 700; }