터치 패널 관찰 전용 슬라이더와 이중 축 개선

This commit is contained in:
seo committed 2026-07-30 13:14:06 +09:00
1 parent e0b384838d
commit 6aa8e7277f
2 files changed
+29 -72

No files matched your search

+2 -2
View File
@@ -146,9 +146,9 @@ cd /var/www/control
/usr/bin/python3 touch_panel/panel.py /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 간격으로 샘플링합니다. 왼쪽 `DISK I/O`, `WAN`, `LAN`, `2.4G`, `5G` 표시등은 장식용 heartbeat가 아니라 실제 커널 카운터 증가에 반응합니다. 각 타일에는 글자 아래에 세 개의 작은 표시등을 두며 읽기/업로드는 초록, 쓰기/다운로드는 주황, 오류는 빨강으로 표시합니다. 활동이 없으면 초록/주황은 회색입니다. `DISK I/O`의 값은 `/dev/sda` Samsung T7을 `smartctl -d sntasmedia`로 읽은 디스크 자체 온도입니다. 네트워크 상태값은 정상, 드롭 누적, 꺼짐으로 표시합니다. 상태패널 최하단의 `PC` 타일은 `192.168.50.73`에 1초마다 ping을 보내 켜짐 여부를 표시하고, 꺼져 있을 때 누르면 `74:56:3C:CD:CD:79`로 WOL magic packet을 보냅니다. WOL은 한 번 누르면 30초 동안 추가 전송을 막습니다. 패널은 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가 아니라 실제 커널 카운터 증가에 반응합니다. 각 타일에는 글자 아래에 세 개의 작은 표시등을 두며 읽기/업로드는 초록, 쓰기/다운로드는 주황, 오류는 빨강으로 표시합니다. 활동이 없으면 초록/주황은 회색입니다. `DISK I/O`의 값은 `/dev/sda` Samsung T7을 `smartctl -d sntasmedia`로 1초마다 읽은 디스크 자체 온도입니다. 네트워크 상태값은 정상, 드롭 누적, 꺼짐으로 표시합니다. 상태패널 최하단의 `PC` 타일은 `192.168.50.73`에 1초마다 ping을 보내 켜짐 여부를 표시하고, 꺼져 있을 때 누르면 `74:56:3C:CD:CD:79`로 WOL magic packet을 보냅니다. PC 타일의 주황은 터치/WOL 전송 시도 표시, 빨강은 magic packet 전송 실패 표시입니다. WOL은 한 번 누르면 30초 동안 추가 전송을 막습니다.
오른쪽에는 팬 RPM, CPU 온도, PWM, WAN 실시간 속도와 온도/RPM, WAN 트래픽 그래프를 표시합니다. 그래프는 기존 범례/캡션을 유지하고 Y축에 단위를 함께 표시합니다. 팬 버튼과 슬라이더는 Control DB를 거치지 않고 팬 hwmon sysfs에 직접 쓰기를 시도합니다. GUI 사용자의 sysfs 쓰기 권한이 없으면 대시보드는 계속 동작하되 `팬 쓰기 권한 없음`을 표시합니다. 팬 모드 표시는 hwmon `pwm1_enable` 기준이므로 Control DB가 auto여도 fan policy가 sysfs에 직접 PWM을 쓰는 순간 하드웨어 상태는 manual처럼 보일 수 있습니다. 오른쪽에는 팬 RPM, CPU 온도, PWM, WAN 실시간 속도와 온도/RPM, WAN 트래픽 그래프를 표시합니다. 위 그래프는 좌측 Y축에 온도, 우측 Y축에 RPM 단위를 표시하고, 아래 WAN 그래프는 기존 형태를 유지합니다. 팬 auto/manual/off 버튼은 없고 PWM 슬라이더는 현재값 위치만 보여 주며 터치 제어는 받지 않습니다.
GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다. GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다.
+27 -70
View File
@@ -14,7 +14,6 @@ from PyQt5.QtWidgets import (
QHBoxLayout, QHBoxLayout,
QLabel, QLabel,
QMainWindow, QMainWindow,
QPushButton,
QSlider, QSlider,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
@@ -129,16 +128,6 @@ def disk_io():
return reads, writes, not found return reads, writes, not found
def write_sys(path, value):
if not path:
return False
try:
Path(path).write_text(str(value) + "\n", encoding="utf-8")
return True
except OSError:
return False
def send_magic_packet(mac): def send_magic_packet(mac):
clean = mac.replace(":", "").replace("-", "") clean = mac.replace(":", "").replace("-", "")
if len(clean) != 12: if len(clean) != 12:
@@ -167,7 +156,7 @@ class StatusDot(QWidget):
self.update() self.update()
def blink(self): def blink(self):
self.pulse = 3 self.pulse = 1
self.update() self.update()
def tick(self): def tick(self):
@@ -284,12 +273,13 @@ class MetricCard(QFrame):
class Sparkline(QFrame): class Sparkline(QFrame):
def __init__(self, title, color="#38bdf8", second_color=None, y_unit=""): def __init__(self, title, color="#38bdf8", second_color=None, y_unit="", right_unit=""):
super().__init__() super().__init__()
self.setObjectName("chartCard") self.setObjectName("chartCard")
self.color = QColor(color) self.color = QColor(color)
self.second_color = QColor(second_color) if second_color else None self.second_color = QColor(second_color) if second_color else None
self.y_unit = y_unit self.y_unit = y_unit
self.right_unit = right_unit
self.values = [] self.values = []
self.second_values = [] self.second_values = []
self.title = QLabel(title) self.title = QLabel(title)
@@ -320,15 +310,15 @@ class Sparkline(QFrame):
del event del event
painter = QPainter(self.canvas) painter = QPainter(self.canvas)
painter.setRenderHint(QPainter.Antialiasing) painter.setRenderHint(QPainter.Antialiasing)
rect = self.canvas.rect().adjusted(34, 4, -2, -13) right_margin = 34 if self.second_values and self.right_unit else 2
rect = self.canvas.rect().adjusted(34, 4, -right_margin, -13)
painter.fillRect(rect, QColor("#0f172a")) painter.fillRect(rect, QColor("#0f172a"))
painter.setPen(QPen(QColor("#263650"), 1)) painter.setPen(QPen(QColor("#263650"), 1))
painter.drawLine(rect.left(), rect.bottom(), rect.right(), rect.bottom()) painter.drawLine(rect.left(), rect.bottom(), rect.right(), rect.bottom())
all_values = self.values + self.second_values if len(self.values) < 2:
if len(all_values) < 2:
return return
low = min(all_values) low = min(self.values)
high = max(all_values) high = max(self.values)
if high <= low: if high <= low:
high = low + 1 high = low + 1
painter.setPen(QColor("#93a4bd")) painter.setPen(QColor("#93a4bd"))
@@ -336,16 +326,25 @@ class Sparkline(QFrame):
painter.drawText(1, rect.bottom(), self.axis_text(low)) painter.drawText(1, rect.bottom(), self.axis_text(low))
self.draw_series(painter, rect, self.values, low, high, self.color) self.draw_series(painter, rect, self.values, low, high, self.color)
if self.second_color and self.second_values: if self.second_color and self.second_values:
self.draw_series(painter, rect, self.second_values, low, high, self.second_color) second_low = min(self.second_values)
second_high = max(self.second_values)
if second_high <= second_low:
second_high = second_low + 1
if self.right_unit:
painter.setPen(QColor("#93a4bd"))
right_x = self.canvas.rect().right() - 31
painter.drawText(right_x, rect.top() + 10, self.axis_text(second_high, self.right_unit))
painter.drawText(right_x, rect.bottom(), self.axis_text(second_low, self.right_unit))
self.draw_series(painter, rect, self.second_values, second_low, second_high, self.second_color)
def axis_text(self, value): def axis_text(self, value, unit=None):
if abs(value) >= 100: if abs(value) >= 100:
text = f"{value:.0f}" text = f"{value:.0f}"
elif abs(value) >= 10: elif abs(value) >= 10:
text = f"{value:.1f}" text = f"{value:.1f}"
else: else:
text = f"{value:.2f}" text = f"{value:.2f}"
return f"{text}{self.y_unit}" return f"{text}{self.y_unit if unit is None else unit}"
@staticmethod @staticmethod
def draw_series(painter, rect, values, low, high, color): def draw_series(painter, rect, values, low, high, color):
@@ -365,7 +364,6 @@ class TouchPanel(QMainWindow):
self.rows = {} self.rows = {}
self.metrics = {} self.metrics = {}
self.charts = {} self.charts = {}
self.mode_buttons = {}
self.fan = fan_paths() self.fan = fan_paths()
self.wan_iface = default_iface() self.wan_iface = default_iface()
NET_GROUPS["wan"] = [self.wan_iface] NET_GROUPS["wan"] = [self.wan_iface]
@@ -403,7 +401,7 @@ class TouchPanel(QMainWindow):
self.disk_temp_proc.finished.connect(self.disk_temp_finished) self.disk_temp_proc.finished.connect(self.disk_temp_finished)
self.disk_temp_timer = QTimer(self) self.disk_temp_timer = QTimer(self)
self.disk_temp_timer.timeout.connect(self.update_disk_temp) self.disk_temp_timer.timeout.connect(self.update_disk_temp)
self.disk_temp_timer.start(5000) self.disk_temp_timer.start(1000)
self.pulse_timer = QTimer(self) self.pulse_timer = QTimer(self)
self.pulse_timer.timeout.connect(self.tick_pulses) self.pulse_timer.timeout.connect(self.tick_pulses)
@@ -463,37 +461,20 @@ class TouchPanel(QMainWindow):
metric_grid.addWidget(card, 0, index) metric_grid.addWidget(card, 0, index)
right_layout.addLayout(metric_grid) right_layout.addLayout(metric_grid)
mode_layout = QHBoxLayout()
for mode, label in [("auto", "auto"), ("manual", "manual"), ("off", "off")]:
btn = QPushButton(label)
btn.setCheckable(True)
btn.clicked.connect(lambda checked=False, m=mode: self.set_fan(m))
self.mode_buttons[mode] = btn
mode_layout.addWidget(btn)
right_layout.addLayout(mode_layout)
slider_wrap = QHBoxLayout() slider_wrap = QHBoxLayout()
minus = QPushButton("-")
minus.setObjectName("stepButton")
minus.clicked.connect(lambda: self.step_pwm(-10))
plus = QPushButton("+")
plus.setObjectName("stepButton")
plus.clicked.connect(lambda: self.step_pwm(10))
self.slider = QSlider(Qt.Horizontal) self.slider = QSlider(Qt.Horizontal)
self.slider.setRange(0, 255) self.slider.setRange(0, 255)
self.slider.setValue(self.current_pwm) self.slider.setValue(self.current_pwm)
self.slider.valueChanged.connect(self.slider_changed) self.slider.setFocusPolicy(Qt.NoFocus)
self.slider.sliderReleased.connect(lambda: self.set_fan("manual")) self.slider.setAttribute(Qt.WA_TransparentForMouseEvents, True)
slider_wrap.addWidget(minus)
slider_wrap.addWidget(self.slider, 1) slider_wrap.addWidget(self.slider, 1)
slider_wrap.addWidget(plus)
right_layout.addLayout(slider_wrap) right_layout.addLayout(slider_wrap)
chart_grid = QGridLayout() chart_grid = QGridLayout()
chart_grid.setHorizontalSpacing(8) chart_grid.setHorizontalSpacing(8)
chart_grid.setVerticalSpacing(8) chart_grid.setVerticalSpacing(8)
for index, (key, chart) in enumerate([ for index, (key, chart) in enumerate([
("temp", Sparkline("온도 / RPM", "#f97316", "#38bdf8", "C")), ("temp", Sparkline("온도 / RPM", "#f97316", "#38bdf8", "C", "rpm")),
("wan", Sparkline("WAN 트래픽", "#38bdf8", "#22c55e", "M")), ("wan", Sparkline("WAN 트래픽", "#38bdf8", "#22c55e", "M")),
]): ]):
self.charts[key] = chart self.charts[key] = chart
@@ -558,9 +539,7 @@ class TouchPanel(QMainWindow):
self.slider.blockSignals(True) self.slider.blockSignals(True)
self.slider.setValue(pwm) self.slider.setValue(pwm)
self.slider.blockSignals(False) self.slider.blockSignals(False)
self.mode_buttons["off"].setChecked(enable == 0 or pwm == 0) del enable
self.mode_buttons["manual"].setChecked(enable == 1 and pwm > 0)
self.mode_buttons["auto"].setChecked(enable not in [0, 1])
self.clock.setText(time.strftime("%H:%M:%S")) self.clock.setText(time.strftime("%H:%M:%S"))
self.latest_temp = temp self.latest_temp = temp
@@ -595,8 +574,8 @@ class TouchPanel(QMainWindow):
temp = getattr(self, "latest_temp", cpu_temp()) temp = getattr(self, "latest_temp", cpu_temp())
rpm = getattr(self, "latest_rpm", read_int(self.fan["rpm"])) rpm = getattr(self, "latest_rpm", read_int(self.fan["rpm"]))
self.temp_history.append(temp) self.temp_history.append(temp)
self.rpm_history.append(rpm / 100) self.rpm_history.append(rpm)
self.charts["temp"].set_values(self.temp_history, "C / RPMx100", self.rpm_history) self.charts["temp"].set_values(self.temp_history, "C / RPM", self.rpm_history)
wan = self.current_rates.get("wan", {"rx": 0.0, "tx": 0.0}) wan = self.current_rates.get("wan", {"rx": 0.0, "tx": 0.0})
self.net_history["wan"]["rx"].append(wan["rx"]) self.net_history["wan"]["rx"].append(wan["rx"])
@@ -607,25 +586,6 @@ class TouchPanel(QMainWindow):
self.net_history["wan"]["tx"], self.net_history["wan"]["tx"],
) )
def set_fan(self, mode):
pwm = self.slider.value()
ok = True
if mode == "off":
ok = write_sys(self.fan["enable"], 0) and write_sys(self.fan["pwm"], 0)
elif mode == "manual":
ok = write_sys(self.fan["enable"], 1) and write_sys(self.fan["pwm"], pwm)
elif mode == "auto":
ok = write_sys(self.fan["enable"], 2)
self.status_text.setText("" if ok else "팬 쓰기 권한 없음")
def step_pwm(self, delta):
self.slider.setValue(max(0, min(255, self.slider.value() + delta)))
self.set_fan("manual")
def slider_changed(self, value):
self.current_pwm = value
self.metrics["pwm"].set_value(f"{value} / {value / 255 * 100:.1f}%")
def tick_pulses(self): def tick_pulses(self):
for row in self.rows.values(): for row in self.rows.values():
row.tick() row.tick()
@@ -662,9 +622,6 @@ def main():
#chartCaption { color: #93a4bd; font-size: 11px; background: transparent; } #chartCaption { color: #93a4bd; font-size: 11px; background: transparent; }
#clock { font-size: 18px; color: #8fb3ff; font-weight: 700; } #clock { font-size: 18px; color: #8fb3ff; font-weight: 700; }
#statusText { color: #93a4bd; font-size: 13px; background: transparent; } #statusText { color: #93a4bd; font-size: 13px; background: transparent; }
QPushButton { min-height: 42px; border: 1px solid #33445f; border-radius: 14px; background: #1a2942; color: #dce8f8; font-size: 18px; font-weight: 700; }
QPushButton:checked { background: #16a34a; color: white; border-color: #22c55e; }
QPushButton#stepButton { min-width: 56px; font-size: 28px; }
QSlider::groove:horizontal { height: 10px; background: #263650; border-radius: 5px; } QSlider::groove:horizontal { height: 10px; background: #263650; border-radius: 5px; }
QSlider::sub-page:horizontal { background: #22c55e; border-radius: 5px; } QSlider::sub-page:horizontal { background: #22c55e; border-radius: 5px; }
QSlider::handle:horizontal { width: 28px; margin: -10px 0; border-radius: 14px; background: #f8fafc; } QSlider::handle:horizontal { width: 28px; margin: -10px 0; border-radius: 14px; background: #f8fafc; }