From e89f1da109274a31d31973aa3ab667f20d894247 Mon Sep 17 00:00:00 2001 From: seo Date: Thu, 30 Jul 2026 12:57:49 +0900 Subject: [PATCH] =?UTF-8?q?=ED=84=B0=EC=B9=98=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=ED=91=9C=EC=8B=9C=EB=93=B1=20=EC=8B=A4?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- touch_panel/panel.py | 108 +++++++++++++++++++++++++------------------ 2 files changed, 65 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 1c51974..718a3d8 100644 --- a/README.md +++ b/README.md @@ -146,9 +146,9 @@ cd /var/www/control /usr/bin/python3 touch_panel/panel.py ``` -패널은 Control PHP/API/DB snapshot을 호출하지 않는 standalone 방식입니다. Python이 `/sys/class/net`, `/proc/diskstats`, `/sys/class/thermal`, 팬 hwmon 파일을 직접 읽어 120ms 간격으로 상태를 갱신합니다. 왼쪽 `IO`, `WAN`, `LAN`, `2.4G`, `5G` 원은 장식용 heartbeat가 아니라 실제 커널 카운터 증가에 반응합니다. `IO`는 디스크 read/write 카운터가 증가할 때, 네트워크는 RX/TX byte 또는 packet 카운터가 증가할 때 짧게 점멸합니다. +패널은 Control PHP/API/DB snapshot을 호출하지 않는 standalone 방식입니다. Python이 `/sys/class/net`, `/proc/diskstats`, `/sys/class/thermal`, 팬 hwmon 파일을 직접 읽습니다. 상태패널과 팬패널은 120ms 간격으로 갱신하고, 그래프는 500ms 간격으로 샘플링합니다. 왼쪽 `IO`, `WAN`, `LAN`, `2.4G`, `5G` 표시등은 장식용 heartbeat가 아니라 실제 커널 카운터 증가에 반응합니다. 각 타일에는 두 개의 작은 표시등을 두며 읽기/업로드는 초록, 쓰기/다운로드는 주황으로 점멸합니다. 둘 다 활동이 없으면 둘 다 회색입니다. -오른쪽에는 팬 RPM, CPU 온도, PWM, WAN 실시간 속도와 온도/RPM, WAN/LAN/WiFi 트래픽 그래프를 표시합니다. 팬 버튼과 슬라이더는 Control DB를 거치지 않고 팬 hwmon sysfs에 직접 쓰기를 시도합니다. GUI 사용자의 sysfs 쓰기 권한이 없으면 대시보드는 계속 동작하되 `팬 쓰기 권한 없음`을 표시합니다. +오른쪽에는 팬 RPM, CPU 온도, PWM, WAN 실시간 속도와 온도/RPM, WAN 트래픽 그래프를 표시합니다. 팬 버튼과 슬라이더는 Control DB를 거치지 않고 팬 hwmon sysfs에 직접 쓰기를 시도합니다. GUI 사용자의 sysfs 쓰기 권한이 없으면 대시보드는 계속 동작하되 `팬 쓰기 권한 없음`을 표시합니다. 팬 모드 표시는 hwmon `pwm1_enable` 기준이므로 Control DB가 auto여도 fan policy가 sysfs에 직접 PWM을 쓰는 순간 하드웨어 상태는 manual처럼 보일 수 있습니다. GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다. diff --git a/touch_panel/panel.py b/touch_panel/panel.py index d25f4e4..93e4e63 100755 --- a/touch_panel/panel.py +++ b/touch_panel/panel.py @@ -134,12 +134,12 @@ def write_sys(path, value): class StatusDot(QWidget): - def __init__(self): + def __init__(self, active_color): super().__init__() + self.active_color = QColor(active_color) self.warning = False self.pulse = 0 - self.phase = False - self.setFixedSize(22, 22) + self.setFixedSize(15, 15) def set_warning(self, warning=False): self.warning = warning @@ -147,7 +147,6 @@ class StatusDot(QWidget): def blink(self): self.pulse = 3 - self.phase = not self.phase self.update() def tick(self): @@ -160,21 +159,20 @@ class StatusDot(QWidget): painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) base = QColor("#8fa0aa") - if self.warning: - base = QColor("#f59e0b") if self.pulse > 0: - base = QColor("#86efac") if self.phase else QColor("#16a34a") + base = self.active_color painter.setBrush(base) - painter.setPen(Qt.NoPen) - painter.drawEllipse(3, 3, 16, 16) + painter.setPen(QPen(QColor("#ef4444"), 1) if self.warning else Qt.NoPen) + painter.drawEllipse(2, 2, 11, 11) painter.setBrush(QColor(255, 255, 255, 55)) - painter.drawEllipse(8, 6, 5, 5) + painter.drawEllipse(6, 4, 4, 4) class StatusTile(QFrame): def __init__(self, title): super().__init__() - self.dot = StatusDot() + self.read_dot = StatusDot("#16a34a") + self.write_dot = StatusDot("#f97316") self.title = QLabel(title) self.title.setObjectName("statusTitle") self.detail = QLabel("-") @@ -183,9 +181,15 @@ class StatusTile(QFrame): self.setFixedHeight(58) layout = QHBoxLayout(self) - layout.setContentsMargins(7, 6, 7, 6) - layout.setSpacing(6) - layout.addWidget(self.dot, 0, Qt.AlignLeft | Qt.AlignVCenter) + layout.setContentsMargins(6, 6, 6, 6) + layout.setSpacing(4) + + dots = QVBoxLayout() + dots.setContentsMargins(0, 0, 0, 0) + dots.setSpacing(4) + dots.addWidget(self.read_dot, 0, Qt.AlignCenter) + dots.addWidget(self.write_dot, 0, Qt.AlignCenter) + layout.addLayout(dots) texts = QVBoxLayout() texts.setContentsMargins(0, 0, 0, 0) @@ -195,14 +199,18 @@ class StatusTile(QFrame): layout.addLayout(texts, 1) self.setObjectName("statusTile") - def set_state(self, detail, warning=False, activity=False): - self.dot.set_warning(warning) - if activity: - self.dot.blink() + def set_state(self, detail, warning=False, read_activity=False, write_activity=False): + self.read_dot.set_warning(warning) + self.write_dot.set_warning(warning) + if read_activity: + self.read_dot.blink() + if write_activity: + self.write_dot.blink() self.detail.setText(detail) def tick(self): - self.dot.tick() + self.read_dot.tick() + self.write_dot.tick() class MetricCard(QFrame): @@ -299,10 +307,12 @@ class TouchPanel(QMainWindow): self.last_net = {} self.last_io = None self.last_sample = None + self.last_graph = None + self.current_rates = {"wan": {"rx": 0.0, "tx": 0.0}} self.current_pwm = read_int(self.fan["pwm"]) self.temp_history = deque(maxlen=80) self.rpm_history = deque(maxlen=80) - self.net_history = {key: {"rx": deque(maxlen=80), "tx": deque(maxlen=80)} for key in ["wan", "lan", "wifi"]} + self.net_history = {"wan": {"rx": deque(maxlen=80), "tx": deque(maxlen=80)}} self.setWindowTitle("Standalone Touch Dashboard") self.setCursor(Qt.BlankCursor) @@ -313,6 +323,10 @@ class TouchPanel(QMainWindow): self.sample_timer.timeout.connect(self.sample) self.sample_timer.start(120) + self.graph_timer = QTimer(self) + self.graph_timer.timeout.connect(self.update_graphs) + self.graph_timer.start(500) + self.pulse_timer = QTimer(self) self.pulse_timer.timeout.connect(self.tick_pulses) self.pulse_timer.start(70) @@ -399,11 +413,9 @@ class TouchPanel(QMainWindow): 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) + chart_grid.addWidget(chart, index, 0) right_layout.addLayout(chart_grid, 1) layout.addWidget(left) @@ -422,7 +434,8 @@ class TouchPanel(QMainWindow): percent = pwm / 255 * 100 if pwm > 0 else 0 reads, writes = disk_io() - io_activity = self.last_io is not None and (reads, writes) != self.last_io + io_read_activity = self.last_io is not None and reads != self.last_io[0] + io_write_activity = self.last_io is not None and writes != self.last_io[1] self.last_io = (reads, writes) groups = {key: group_stats(names) for key, names in NET_GROUPS.items()} @@ -431,7 +444,10 @@ class TouchPanel(QMainWindow): for key, row in groups.items(): previous = self.last_net.get(key) current = (row["rx_bytes"], row["tx_bytes"], row["rx_packets"], row["tx_packets"]) - activity[key] = previous is not None and current != previous + activity[key] = { + "read": previous is not None and current[1] != previous[1], + "write": previous is not None and current[0] != previous[0], + } if previous: rates[key] = { "rx": max(0.0, (current[0] - previous[0]) * 8 / dt / 1_000_000), @@ -441,14 +457,15 @@ class TouchPanel(QMainWindow): rates[key] = {"rx": 0.0, "tx": 0.0} self.last_net[key] = current - self.render(temp, rpm, pwm, percent, enable, groups, rates, activity, io_activity) + self.current_rates = rates + self.render(temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity) - def render(self, temp, rpm, pwm, percent, enable, groups, rates, activity, io_activity): - self.rows["io"].set_state(f"{temp:.1f} C", temp >= 75, io_activity) - self.rows["wan"].set_state(self.iface_detail(groups["wan"]), self.iface_warn(groups["wan"]), activity["wan"]) - self.rows["lan"].set_state(self.iface_detail(groups["lan"]), self.iface_warn(groups["lan"]), activity["lan"]) - self.rows["wifi24"].set_state(self.iface_detail(groups["wifi24"]), self.iface_warn(groups["wifi24"]), activity["wifi24"]) - self.rows["wifi5"].set_state(self.iface_detail(groups["wifi5"]), self.iface_warn(groups["wifi5"]), activity["wifi5"]) + def render(self, temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity): + self.rows["io"].set_state(f"{temp:.1f} C", temp >= 75, io_read_activity, io_write_activity) + self.rows["wan"].set_state(self.iface_detail(groups["wan"]), self.iface_warn(groups["wan"]), activity["wan"]["read"], activity["wan"]["write"]) + self.rows["lan"].set_state(self.iface_detail(groups["lan"]), self.iface_warn(groups["lan"]), activity["lan"]["read"], activity["lan"]["write"]) + self.rows["wifi24"].set_state(self.iface_detail(groups["wifi24"]), self.iface_warn(groups["wifi24"]), activity["wifi24"]["read"], activity["wifi24"]["write"]) + self.rows["wifi5"].set_state(self.iface_detail(groups["wifi5"]), self.iface_warn(groups["wifi5"]), activity["wifi5"]["read"], activity["wifi5"]["write"]) self.metrics["rpm"].set_value(f"{rpm:,}") self.metrics["temp"].set_value(f"{temp:.1f} C") @@ -463,23 +480,24 @@ class TouchPanel(QMainWindow): self.mode_buttons["auto"].setChecked(enable not in [0, 1]) self.clock.setText(time.strftime("%H:%M:%S")) + self.latest_temp = temp + self.latest_rpm = rpm + + def update_graphs(self): + temp = getattr(self, "latest_temp", cpu_temp()) + rpm = getattr(self, "latest_rpm", read_int(self.fan["rpm"])) self.temp_history.append(temp) self.rpm_history.append(rpm / 100) self.charts["temp"].set_values(self.temp_history, "C / RPMx100", self.rpm_history) - for key, chart_key in [("wan", "wan"), ("lan", "lan")]: - self.net_history[key]["rx"].append(rates[key]["rx"]) - self.net_history[key]["tx"].append(rates[key]["tx"]) - self.charts[chart_key].set_values( - self.net_history[key]["rx"], - f"{rates[key]['rx']:.2f}/{rates[key]['tx']:.2f} Mbps", - self.net_history[key]["tx"], - ) - wifi_rx = rates["wifi24"]["rx"] + rates["wifi5"]["rx"] - wifi_tx = rates["wifi24"]["tx"] + rates["wifi5"]["tx"] - self.net_history["wifi"]["rx"].append(wifi_rx) - self.net_history["wifi"]["tx"].append(wifi_tx) - self.charts["wifi"].set_values(self.net_history["wifi"]["rx"], f"{wifi_rx:.2f}/{wifi_tx:.2f} Mbps", self.net_history["wifi"]["tx"]) + wan = self.current_rates.get("wan", {"rx": 0.0, "tx": 0.0}) + self.net_history["wan"]["rx"].append(wan["rx"]) + self.net_history["wan"]["tx"].append(wan["tx"]) + self.charts["wan"].set_values( + self.net_history["wan"]["rx"], + f"{wan['rx']:.2f}/{wan['tx']:.2f} Mbps", + self.net_history["wan"]["tx"], + ) def set_fan(self, mode): pwm = self.slider.value()