diff --git a/README.md b/README.md index 996e9cf..9dd5b87 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 간격으로 갱신하고, 그래프는 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차 기준입니다. diff --git a/touch_panel/panel.py b/touch_panel/panel.py index 86c38d0..6c05ea6 100755 --- a/touch_panel/panel.py +++ b/touch_panel/panel.py @@ -14,7 +14,6 @@ from PyQt5.QtWidgets import ( QHBoxLayout, QLabel, QMainWindow, - QPushButton, QSlider, QVBoxLayout, QWidget, @@ -129,16 +128,6 @@ def disk_io(): 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): clean = mac.replace(":", "").replace("-", "") if len(clean) != 12: @@ -167,7 +156,7 @@ class StatusDot(QWidget): self.update() def blink(self): - self.pulse = 3 + self.pulse = 1 self.update() def tick(self): @@ -284,12 +273,13 @@ class MetricCard(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__() self.setObjectName("chartCard") self.color = QColor(color) self.second_color = QColor(second_color) if second_color else None self.y_unit = y_unit + self.right_unit = right_unit self.values = [] self.second_values = [] self.title = QLabel(title) @@ -320,15 +310,15 @@ class Sparkline(QFrame): del event painter = QPainter(self.canvas) 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.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: + if len(self.values) < 2: return - low = min(all_values) - high = max(all_values) + low = min(self.values) + high = max(self.values) if high <= low: high = low + 1 painter.setPen(QColor("#93a4bd")) @@ -336,16 +326,25 @@ class Sparkline(QFrame): painter.drawText(1, rect.bottom(), self.axis_text(low)) self.draw_series(painter, rect, self.values, low, high, self.color) 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: text = f"{value:.0f}" elif abs(value) >= 10: text = f"{value:.1f}" else: text = f"{value:.2f}" - return f"{text}{self.y_unit}" + return f"{text}{self.y_unit if unit is None else unit}" @staticmethod def draw_series(painter, rect, values, low, high, color): @@ -365,7 +364,6 @@ class TouchPanel(QMainWindow): self.rows = {} self.metrics = {} self.charts = {} - self.mode_buttons = {} self.fan = fan_paths() self.wan_iface = default_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_timer = QTimer(self) 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.timeout.connect(self.tick_pulses) @@ -463,37 +461,20 @@ class TouchPanel(QMainWindow): metric_grid.addWidget(card, 0, index) 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() - 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.setRange(0, 255) self.slider.setValue(self.current_pwm) - self.slider.valueChanged.connect(self.slider_changed) - self.slider.sliderReleased.connect(lambda: self.set_fan("manual")) - slider_wrap.addWidget(minus) + self.slider.setFocusPolicy(Qt.NoFocus) + self.slider.setAttribute(Qt.WA_TransparentForMouseEvents, True) slider_wrap.addWidget(self.slider, 1) - 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", "C")), + ("temp", Sparkline("온도 / RPM", "#f97316", "#38bdf8", "C", "rpm")), ("wan", Sparkline("WAN 트래픽", "#38bdf8", "#22c55e", "M")), ]): self.charts[key] = chart @@ -558,9 +539,7 @@ class TouchPanel(QMainWindow): self.slider.blockSignals(True) self.slider.setValue(pwm) self.slider.blockSignals(False) - self.mode_buttons["off"].setChecked(enable == 0 or pwm == 0) - self.mode_buttons["manual"].setChecked(enable == 1 and pwm > 0) - self.mode_buttons["auto"].setChecked(enable not in [0, 1]) + del enable self.clock.setText(time.strftime("%H:%M:%S")) self.latest_temp = temp @@ -595,8 +574,8 @@ class TouchPanel(QMainWindow): 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) + self.rpm_history.append(rpm) + 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}) self.net_history["wan"]["rx"].append(wan["rx"]) @@ -607,25 +586,6 @@ class TouchPanel(QMainWindow): 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): for row in self.rows.values(): row.tick() @@ -662,9 +622,6 @@ def main(): #chartCaption { color: #93a4bd; font-size: 11px; background: transparent; } #clock { font-size: 18px; color: #8fb3ff; font-weight: 700; } #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::sub-page:horizontal { background: #22c55e; border-radius: 5px; } QSlider::handle:horizontal { width: 28px; margin: -10px 0; border-radius: 14px; background: #f8fafc; }