터치 패널 삼색 상태등과 PC 깨우기 추가
This commit is contained in:
1 parent
e89f1da109
commit
9388122892
2 files changed
+98
-29
No files matched your search
@@ -146,7 +146,7 @@ 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 간격으로 갱신하고, 그래프는 500ms 간격으로 샘플링합니다. 왼쪽 `IO`, `WAN`, `LAN`, `2.4G`, `5G` 표시등은 장식용 heartbeat가 아니라 실제 커널 카운터 증가에 반응합니다. 각 타일에는 두 개의 작은 표시등을 두며 읽기/업로드는 초록, 쓰기/다운로드는 주황으로 점멸합니다. 둘 다 활동이 없으면 둘 다 회색입니다.
|
||||
패널은 Control PHP/API/DB snapshot을 호출하지 않는 standalone 방식입니다. Python이 `/sys/class/net`, `/proc/diskstats`, `/sys/class/thermal`, 팬 hwmon 파일을 직접 읽습니다. 상태패널과 팬패널은 120ms 간격으로 갱신하고, 그래프는 500ms 간격으로 샘플링합니다. 왼쪽 `DISK I/O`, `WAN`, `LAN`, `2.4G`, `5G` 표시등은 장식용 heartbeat가 아니라 실제 커널 카운터 증가에 반응합니다. 각 타일에는 글자 아래에 세 개의 작은 표시등을 두며 읽기/업로드는 초록, 쓰기/다운로드는 주황, 오류는 빨강으로 표시합니다. 활동이 없으면 초록/주황은 회색입니다. 상태패널 최하단 왼쪽의 `PC` 표시등은 `192.168.50.73`에 1초마다 ping을 보내 켜짐 여부를 표시하고, 꺼져 있을 때 누르면 `74:56:3C:CD:CD:79`로 WOL magic packet을 보냅니다.
|
||||
|
||||
오른쪽에는 팬 RPM, CPU 온도, PWM, WAN 실시간 속도와 온도/RPM, WAN 트래픽 그래프를 표시합니다. 팬 버튼과 슬라이더는 Control DB를 거치지 않고 팬 hwmon sysfs에 직접 쓰기를 시도합니다. GUI 사용자의 sysfs 쓰기 권한이 없으면 대시보드는 계속 동작하되 `팬 쓰기 권한 없음`을 표시합니다. 팬 모드 표시는 hwmon `pwm1_enable` 기준이므로 Control DB가 auto여도 fan policy가 sysfs에 직접 PWM을 쓰는 순간 하드웨어 상태는 manual처럼 보일 수 있습니다.
|
||||
|
||||
|
||||
+97
-28
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5.QtCore import QPointF, Qt, QTimer
|
||||
from PyQt5.QtCore import QPointF, QProcess, Qt, QTimer
|
||||
from PyQt5.QtGui import QColor, QPainter, QPainterPath, QPen
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -27,6 +28,9 @@ NET_GROUPS = {
|
||||
"wifi5": ["wlan1"],
|
||||
}
|
||||
|
||||
PC_IP = "192.168.50.73"
|
||||
PC_MAC = "74:56:3C:CD:CD:79"
|
||||
|
||||
|
||||
def read_text(path, default=""):
|
||||
try:
|
||||
@@ -111,6 +115,7 @@ def group_stats(names):
|
||||
def disk_io():
|
||||
reads = 0
|
||||
writes = 0
|
||||
found = False
|
||||
for line in read_text("/proc/diskstats").splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) < 14:
|
||||
@@ -118,14 +123,30 @@ def disk_io():
|
||||
name = parts[2]
|
||||
if name.startswith(("loop", "ram", "zram", "dm-")):
|
||||
continue
|
||||
found = True
|
||||
reads += int(parts[3])
|
||||
writes += int(parts[7])
|
||||
return reads, writes
|
||||
return reads, writes, not found
|
||||
|
||||
|
||||
def write_sys(path, value):
|
||||
if not path:
|
||||
return False
|
||||
|
||||
|
||||
def send_magic_packet(mac):
|
||||
clean = mac.replace(":", "").replace("-", "")
|
||||
if len(clean) != 12:
|
||||
return False
|
||||
packet = bytes.fromhex("ff" * 6 + clean * 16)
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
for target in ["192.168.50.255", "255.255.255.255"]:
|
||||
sock.sendto(packet, (target, 9))
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
try:
|
||||
Path(path).write_text(str(value) + "\n", encoding="utf-8")
|
||||
return True
|
||||
@@ -137,12 +158,12 @@ class StatusDot(QWidget):
|
||||
def __init__(self, active_color):
|
||||
super().__init__()
|
||||
self.active_color = QColor(active_color)
|
||||
self.warning = False
|
||||
self.steady = False
|
||||
self.pulse = 0
|
||||
self.setFixedSize(15, 15)
|
||||
|
||||
def set_warning(self, warning=False):
|
||||
self.warning = warning
|
||||
def set_steady(self, steady=False):
|
||||
self.steady = steady
|
||||
self.update()
|
||||
|
||||
def blink(self):
|
||||
@@ -159,10 +180,10 @@ class StatusDot(QWidget):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
base = QColor("#8fa0aa")
|
||||
if self.pulse > 0:
|
||||
if self.steady or self.pulse > 0:
|
||||
base = self.active_color
|
||||
painter.setBrush(base)
|
||||
painter.setPen(QPen(QColor("#ef4444"), 1) if self.warning else Qt.NoPen)
|
||||
painter.setPen(Qt.NoPen)
|
||||
painter.drawEllipse(2, 2, 11, 11)
|
||||
painter.setBrush(QColor(255, 255, 255, 55))
|
||||
painter.drawEllipse(6, 4, 4, 4)
|
||||
@@ -173,6 +194,7 @@ class StatusTile(QFrame):
|
||||
super().__init__()
|
||||
self.read_dot = StatusDot("#16a34a")
|
||||
self.write_dot = StatusDot("#f97316")
|
||||
self.error_dot = StatusDot("#dc2626")
|
||||
self.title = QLabel(title)
|
||||
self.title.setObjectName("statusTitle")
|
||||
self.detail = QLabel("-")
|
||||
@@ -180,28 +202,28 @@ class StatusTile(QFrame):
|
||||
self.detail.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
self.setFixedHeight(58)
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
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)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(6, 5, 6, 5)
|
||||
layout.setSpacing(2)
|
||||
texts = QVBoxLayout()
|
||||
texts.setContentsMargins(0, 0, 0, 0)
|
||||
texts.setSpacing(1)
|
||||
texts.setSpacing(0)
|
||||
texts.addWidget(self.title)
|
||||
texts.addWidget(self.detail)
|
||||
layout.addLayout(texts, 1)
|
||||
layout.addLayout(texts)
|
||||
|
||||
dots = QHBoxLayout()
|
||||
dots.setContentsMargins(0, 0, 0, 0)
|
||||
dots.setSpacing(5)
|
||||
dots.addWidget(self.read_dot, 0, Qt.AlignLeft)
|
||||
dots.addWidget(self.write_dot, 0, Qt.AlignLeft)
|
||||
dots.addWidget(self.error_dot, 0, Qt.AlignLeft)
|
||||
dots.addStretch(1)
|
||||
layout.addLayout(dots)
|
||||
self.setObjectName("statusTile")
|
||||
|
||||
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)
|
||||
def set_state(self, detail, error=False, read_activity=False, write_activity=False):
|
||||
self.error_dot.set_steady(error)
|
||||
if read_activity:
|
||||
self.read_dot.blink()
|
||||
if write_activity:
|
||||
@@ -211,6 +233,33 @@ class StatusTile(QFrame):
|
||||
def tick(self):
|
||||
self.read_dot.tick()
|
||||
self.write_dot.tick()
|
||||
self.error_dot.tick()
|
||||
|
||||
|
||||
class PcWakeButton(QFrame):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setObjectName("pcWake")
|
||||
self.online = False
|
||||
self.dot = StatusDot("#16a34a")
|
||||
self.label = QLabel("PC")
|
||||
self.label.setObjectName("pcLabel")
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(5, 4, 5, 4)
|
||||
layout.setSpacing(5)
|
||||
layout.addWidget(self.dot)
|
||||
layout.addWidget(self.label)
|
||||
layout.addStretch(1)
|
||||
self.setFixedHeight(28)
|
||||
|
||||
def set_online(self, online):
|
||||
self.online = online
|
||||
self.dot.set_steady(online)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.LeftButton and not self.online:
|
||||
send_magic_packet(PC_MAC)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
|
||||
class MetricCard(QFrame):
|
||||
@@ -327,10 +376,17 @@ class TouchPanel(QMainWindow):
|
||||
self.graph_timer.timeout.connect(self.update_graphs)
|
||||
self.graph_timer.start(500)
|
||||
|
||||
self.pc_ping = QProcess(self)
|
||||
self.pc_ping.finished.connect(self.pc_ping_finished)
|
||||
self.pc_timer = QTimer(self)
|
||||
self.pc_timer.timeout.connect(self.ping_pc)
|
||||
self.pc_timer.start(1000)
|
||||
|
||||
self.pulse_timer = QTimer(self)
|
||||
self.pulse_timer.timeout.connect(self.tick_pulses)
|
||||
self.pulse_timer.start(70)
|
||||
self.sample()
|
||||
self.ping_pc()
|
||||
|
||||
def _build_ui(self):
|
||||
root = QWidget()
|
||||
@@ -348,11 +404,13 @@ class TouchPanel(QMainWindow):
|
||||
title.setObjectName("sectionTitle")
|
||||
left_layout.addWidget(title)
|
||||
|
||||
for key, label in [("io", "IO"), ("wan", "WAN"), ("lan", "LAN"), ("wifi24", "2.4G"), ("wifi5", "5G")]:
|
||||
for key, label in [("io", "DISK I/O"), ("wan", "WAN"), ("lan", "LAN"), ("wifi24", "2.4G"), ("wifi5", "5G")]:
|
||||
tile = StatusTile(label)
|
||||
self.rows[key] = tile
|
||||
left_layout.addWidget(tile)
|
||||
left_layout.addStretch(1)
|
||||
self.pc_button = PcWakeButton()
|
||||
left_layout.addWidget(self.pc_button, 0, Qt.AlignLeft | Qt.AlignBottom)
|
||||
|
||||
right = QWidget()
|
||||
right_layout = QVBoxLayout(right)
|
||||
@@ -433,7 +491,7 @@ class TouchPanel(QMainWindow):
|
||||
enable = read_int(self.fan["enable"], 2)
|
||||
percent = pwm / 255 * 100 if pwm > 0 else 0
|
||||
|
||||
reads, writes = disk_io()
|
||||
reads, writes, disk_error = disk_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)
|
||||
@@ -458,10 +516,10 @@ class TouchPanel(QMainWindow):
|
||||
self.last_net[key] = current
|
||||
|
||||
self.current_rates = rates
|
||||
self.render(temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity)
|
||||
self.render(temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity, disk_error)
|
||||
|
||||
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)
|
||||
def render(self, temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity, disk_error):
|
||||
self.rows["io"].set_state(f"{temp:.1f} C", disk_error, 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"])
|
||||
@@ -483,6 +541,15 @@ class TouchPanel(QMainWindow):
|
||||
self.latest_temp = temp
|
||||
self.latest_rpm = rpm
|
||||
|
||||
def ping_pc(self):
|
||||
if self.pc_ping.state() != QProcess.NotRunning:
|
||||
return
|
||||
self.pc_ping.start("/usr/bin/ping", ["-c", "1", "-W", "1", PC_IP])
|
||||
|
||||
def pc_ping_finished(self, exit_code, exit_status):
|
||||
del exit_status
|
||||
self.pc_button.set_online(exit_code == 0)
|
||||
|
||||
def update_graphs(self):
|
||||
temp = getattr(self, "latest_temp", cpu_temp())
|
||||
rpm = getattr(self, "latest_rpm", read_int(self.fan["rpm"]))
|
||||
@@ -545,6 +612,8 @@ def main():
|
||||
#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; }
|
||||
#pcWake { background: transparent; border: 0; }
|
||||
#pcLabel { color: #dce8f8; font-size: 13px; font-weight: 700; background: transparent; }
|
||||
#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; }
|
||||
|
||||
Reference in new issue
Block a user