From e82602a22d78900dcdb5d62b124af27bf9ba40ef Mon Sep 17 00:00:00 2001 From: seo Date: Fri, 31 Jul 2026 05:54:56 +0900 Subject: [PATCH] =?UTF-8?q?=ED=84=B0=EC=B9=98=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=20=EC=97=B4=20=EC=98=A8=EB=8F=84=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=EB=A7=8C=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- touch_panel/panel.py | 122 +++++++++---------------------------------- 2 files changed, 25 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 492055b..49fe449 100644 --- a/README.md +++ b/README.md @@ -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 파일을 직접 읽습니다. 상태패널, 팬패널, 오른쪽 대시보드 카드/그래프는 50ms 간격으로 갱신합니다. 왼쪽 상태패널은 `RASPBERRY`, `SYNOLOGY`, `CHECK` 3열이며, 라즈베리파이 순서는 `DISK I/O`, `LOCAL`, `UPLINK`, `ETH1`, `5G`, `2.4G`입니다. 시놀로지 열은 정상 디스크 `SATA1`, `SATA2`, `SATA4`를 개별 카드로 표시한 뒤 `LOCAL`, `UPLINK`를 표시합니다. `CHECK` 열은 `TEMP`, `WEB`, `DB`, `GITEA`, `SYSLOG`, `PC NET`을 표시합니다. `LOCAL`은 `lo`, `UPLINK`는 기본 라우트 인터페이스 기준입니다. 라즈베리파이 `ETH1`은 컴퓨터와 연결된 물리 포트 단독 카운터이며 `br0`는 합산하지 않습니다. 시놀로지는 SSH를 유지한 채 50ms마다 디스크별 sector I/O, `LOCAL`, `UPLINK` 카운터를 스트림으로 가져오고, 각 SATA 카드 값에는 SMART 온도를 표시합니다. 왼쪽 표시등은 장식용 heartbeat가 아니라 직전 샘플 대비 현재 카운터 증가량이 0 초과인지에 반응합니다. 각 타일에는 글자 아래에 세 개의 작은 표시등을 두며 읽기/업로드은 초록, 쓰기/다운로드는 주황, 오류/드롭 증가는 빨강으로 표시합니다. 활동 표시등은 이벤트 감지 후 120ms 동안 유지하고, 오류 표시등은 2초 동안 유지해 너무 짧은 이벤트도 눈에 보이게 합니다. `DISK I/O`의 값은 `/dev/sda` Samsung T7을 `smartctl -d sntasmedia`로 1초마다 읽은 디스크 자체 온도입니다. GUI 사용자 `seo`는 `/etc/sudoers.d/control-touch-panel-smartctl`에서 해당 smartctl 명령만 NOPASSWD로 허용합니다. 네트워크 상태값은 정상, 드롭 누적, 꺼짐으로 표시합니다. 상태패널 최하단의 `PC` 타일은 `192.168.50.73`에 1초마다 ping을 보내 켜짐 여부를 표시하고, 꺼져 있을 때 누르면 `74:56:3C:CD:CD:79`로 WOL magic packet을 보냅니다. PC 타일의 주황은 WOL 후 최대 1분 동안 컴퓨터 응답 대기 표시, 빨강은 magic packet 전송 실패 시 5초 또는 1분 대기 후 응답 실패 시 1분 표시입니다. +패널은 Control PHP/API/DB snapshot을 호출하지 않는 standalone 방식입니다. Python이 `/sys/class/net`, `/proc/diskstats`, `/sys/class/thermal`, 팬 hwmon 파일을 직접 읽습니다. 상태패널, 팬패널, 오른쪽 대시보드 카드/그래프는 50ms 간격으로 갱신합니다. 왼쪽 상태패널은 `RASPBERRY`, `SYNOLOGY`, `CHECK` 3열이며, 라즈베리파이 순서는 `DISK I/O`, `LOCAL`, `UPLINK`, `ETH1`, `5G`, `2.4G`입니다. 시놀로지 열은 정상 디스크 `SATA1`, `SATA2`, `SATA4`를 개별 카드로 표시한 뒤 `LOCAL`, `UPLINK`를 표시합니다. `CHECK` 열은 `RPI TEMP`, `SYNO TEMP`만 표시합니다. `LOCAL`은 `lo`, `UPLINK`는 기본 라우트 인터페이스 기준입니다. 라즈베리파이 `ETH1`은 컴퓨터와 연결된 물리 포트 단독 카운터이며 `br0`는 합산하지 않습니다. 시놀로지는 SSH를 유지한 채 50ms마다 디스크별 sector I/O, `LOCAL`, `UPLINK` 카운터를 스트림으로 가져오고, 각 SATA 카드 값에는 SMART 온도를 표시합니다. 왼쪽 표시등은 장식용 heartbeat가 아니라 직전 샘플 대비 현재 카운터 증가량이 0 초과인지에 반응합니다. 각 타일에는 글자 아래에 세 개의 작은 표시등을 두며 읽기/업로드은 초록, 쓰기/다운로드는 주황, 오류/드롭 증가는 빨강으로 표시합니다. 활동 표시등은 이벤트 감지 후 120ms 동안 유지하고, 오류 표시등은 2초 동안 유지해 너무 짧은 이벤트도 눈에 보이게 합니다. `DISK I/O`의 값은 `/dev/sda` Samsung T7을 `smartctl -d sntasmedia`로 1초마다 읽은 디스크 자체 온도입니다. GUI 사용자 `seo`는 `/etc/sudoers.d/control-touch-panel-smartctl`에서 해당 smartctl 명령만 NOPASSWD로 허용합니다. 네트워크 상태값은 정상, 드롭 누적, 꺼짐으로 표시합니다. 상태패널 최하단의 `PC` 타일은 `192.168.50.73`에 1초마다 ping을 보내 켜짐 여부를 표시하고, 꺼져 있을 때 누르면 `74:56:3C:CD:CD:79`로 WOL magic packet을 보냅니다. PC 타일의 주황은 WOL 후 최대 1분 동안 컴퓨터 응답 대기 표시, 빨강은 magic packet 전송 실패 시 5초 또는 1분 대기 후 응답 실패 시 1분 표시입니다. 오른쪽 상단 카드는 팬 RPM, CPU 온도, PWM 퍼센트, 메모리 사용률을 표시합니다. 아래에는 최근 30초 기준 온도/RPM 그래프와 WAN 트래픽 그래프를 표시합니다. WAN 속도와 그래프 Y축은 byte/sec 기준으로 `B/s`, `KB/s`, `MB/s`, `GB/s` 중 자동 변환하고, 그래프 우측 상단 현재값은 고정폭으로 표시합니다. 위 그래프는 좌측 Y축에 온도, 우측 Y축에 RPM 단위를 표시하고, 아래 WAN 그래프는 기존 형태를 유지합니다. 팬 auto/manual/off 버튼은 없고 PWM 슬라이더는 현재값 위치만 보여 주며 터치 제어는 받지 않습니다. diff --git a/touch_panel/panel.py b/touch_panel/panel.py index d987edf..0054407 100755 --- a/touch_panel/panel.py +++ b/touch_panel/panel.py @@ -90,23 +90,11 @@ awk -F= ' $1 == "sata4" {printf "SATA4_TEMP=%s\n", $2 ? $2 : "-"} ' """ -SYNOLOGY_SERVICE_SCRIPT = r""" -ps -ef | grep 'nginx: master' | grep -v grep >/dev/null && echo WEB=1 || echo WEB=0 -ps -ef | grep -E 'mariadbd|mysqld' | grep -v grep >/dev/null && echo DB=1 || echo DB=0 -ps -ef | grep -E '[/ ]gitea( |$)|gitea web' | grep -v grep >/dev/null && echo GITEA=1 || echo GITEA=0 -""" -LOCAL_LOG_SCRIPT = r""" -active="$(/usr/bin/systemctl is-active rsyslog 2>/dev/null || echo unknown)" -queue="$(find /var/spool/rsyslog -name 'control_synology_fwd*' -type f 2>/dev/null | wc -l)" -echo "RSYSLOG=$active" -echo "QUEUE=$queue" -""" SAMPLE_INTERVAL_MS = 50 GRAPH_SECONDS = 30 GRAPH_POINTS = GRAPH_SECONDS * 1000 // SAMPLE_INTERVAL_MS SYNOLOGY_RESTART_INTERVAL_MS = 1000 SYNOLOGY_DISK_TEMP_INTERVAL_MS = 10000 -SLOW_STATUS_INTERVAL_MS = 5000 def byte_rate_unit(values): @@ -581,7 +569,6 @@ class TouchPanel(QMainWindow): self.synology_buffer = "" self.synology_values = {} self.synology_disk_temps = {} - self.synology_services = {} self.last_io = None self.last_sample = None self.last_graph = None @@ -634,18 +621,6 @@ class TouchPanel(QMainWindow): self.synology_disk_temp_timer.timeout.connect(self.update_synology_disk_temp) self.synology_disk_temp_timer.start(SYNOLOGY_DISK_TEMP_INTERVAL_MS) - self.synology_service_proc = QProcess(self) - self.synology_service_proc.finished.connect(self.synology_service_finished) - self.synology_service_timer = QTimer(self) - self.synology_service_timer.timeout.connect(self.update_synology_services) - self.synology_service_timer.start(SLOW_STATUS_INTERVAL_MS) - - self.local_log_proc = QProcess(self) - self.local_log_proc.finished.connect(self.local_log_finished) - self.local_log_timer = QTimer(self) - self.local_log_timer.timeout.connect(self.update_local_log_status) - self.local_log_timer.start(SLOW_STATUS_INTERVAL_MS) - self.pulse_timer = QTimer(self) self.pulse_timer.timeout.connect(self.tick_pulses) self.pulse_timer.start(25) @@ -654,8 +629,6 @@ class TouchPanel(QMainWindow): self.update_disk_temp() self.update_synology_status() self.update_synology_disk_temp() - self.update_synology_services() - self.update_local_log_status() self.update_dashboard() def _build_ui(self): @@ -689,12 +662,12 @@ class TouchPanel(QMainWindow): status_grid.addWidget(extra_title, 0, 2) status_rows = [ - (("io", "DISK I/O"), ("synology_sata1", "SATA1"), ("warn_temp", "TEMP")), - (("local", "LOCAL"), ("synology_sata2", "SATA2"), ("svc_web", "WEB")), - (("wan", "UPLINK"), ("synology_sata4", "SATA4"), ("svc_db", "DB")), - (("lan", "ETH1"), ("synology_local", "LOCAL"), ("svc_gitea", "GITEA")), - (("wifi5", "5G"), ("synology_wan", "UPLINK"), ("syslog_link", "SYSLOG")), - (("wifi24", "2.4G"), None, ("pc_net", "PC NET")), + (("io", "DISK I/O"), ("synology_sata1", "SATA1"), ("rpi_temp", "RPI TEMP")), + (("local", "LOCAL"), ("synology_sata2", "SATA2"), ("synology_temp", "SYNO TEMP")), + (("wan", "UPLINK"), ("synology_sata4", "SATA4"), None), + (("lan", "ETH1"), ("synology_local", "LOCAL"), None), + (("wifi5", "5G"), ("synology_wan", "UPLINK"), None), + (("wifi24", "2.4G"), None, None), ] for index, (pi_row, synology_row, extra_row) in enumerate(status_rows, 1): key, label = pi_row @@ -827,13 +800,7 @@ class TouchPanel(QMainWindow): self.rows["lan"].set_state(self.iface_detail(groups["lan"], "eth1"), activity["lan"]["error"], activity["lan"]["read"], activity["lan"]["write"]) self.rows["wifi24"].set_state(self.iface_detail(groups["wifi24"], "wlan0"), activity["wifi24"]["error"], activity["wifi24"]["read"], activity["wifi24"]["write"]) self.rows["wifi5"].set_state(self.iface_detail(groups["wifi5"], "wlan1"), activity["wifi5"]["error"], activity["wifi5"]["read"], activity["wifi5"]["write"]) - self.render_temp_warning(temp) - self.rows["pc_net"].set_state( - "eth1" if self.pc_button.online else "꺼짐", - read_activity=self.pc_button.online and activity["lan"]["read"], - write_activity=self.pc_button.online and activity["lan"]["write"], - error_steady=False, - ) + self.render_rpi_temp(temp) self.latest_temp = temp self.latest_rpm = rpm @@ -883,6 +850,7 @@ class TouchPanel(QMainWindow): for part in parts: if part.isdigit(): self.disk_temp = float(part) + self.render_rpi_temp(getattr(self, "latest_temp", cpu_temp())) return def update_synology_status(self): @@ -897,16 +865,6 @@ class TouchPanel(QMainWindow): return self.synology_disk_temp_proc.start(SYNOLOGY_SSH[0], SYNOLOGY_SSH[1:] + [SYNOLOGY_DISK_TEMP_SCRIPT]) - def update_synology_services(self): - if self.synology_service_proc.state() != QProcess.NotRunning: - return - self.synology_service_proc.start(SYNOLOGY_SSH[0], SYNOLOGY_SSH[1:] + [SYNOLOGY_SERVICE_SCRIPT]) - - def update_local_log_status(self): - if self.local_log_proc.state() != QProcess.NotRunning: - return - self.local_log_proc.start("/bin/sh", ["-c", LOCAL_LOG_SCRIPT]) - def synology_disk_temp_finished(self, exit_code, exit_status): del exit_status if exit_code != 0: @@ -919,30 +877,7 @@ class TouchPanel(QMainWindow): key, value = line.split("=", 1) temps[key.strip()] = value.strip() self.synology_disk_temps = temps - self.render_temp_warning(getattr(self, "latest_temp", cpu_temp())) - - def synology_service_finished(self, exit_code, exit_status): - del exit_status - output = bytes(self.synology_service_proc.readAllStandardOutput()).decode("utf-8", "ignore") - values = self.parse_status_output(output) if exit_code == 0 else {} - self.synology_services = values - self.render_service_tile("svc_web", "nginx", values.get("WEB") == "1", bool(values)) - self.render_service_tile("svc_db", "MariaDB", values.get("DB") == "1", bool(values)) - self.render_service_tile("svc_gitea", "Gitea", values.get("GITEA") == "1", bool(values)) - - def local_log_finished(self, exit_code, exit_status): - del exit_status - output = bytes(self.local_log_proc.readAllStandardOutput()).decode("utf-8", "ignore") - values = self.parse_status_output(output) if exit_code == 0 else {} - rsyslog_ok = values.get("RSYSLOG") == "active" - queue_count = int(values.get("QUEUE", "0") or 0) - detail = f"Q {queue_count}" if rsyslog_ok else values.get("RSYSLOG", "down") - self.rows["syslog_link"].set_state( - detail, - read_steady=rsyslog_ok and queue_count == 0, - write_steady=rsyslog_ok and queue_count > 0, - error_steady=not rsyslog_ok, - ) + self.render_synology_temp() def synology_status_ready(self): output = bytes(self.synology_proc.readAllStandardOutput()).decode("utf-8", "ignore") @@ -1001,28 +936,29 @@ class TouchPanel(QMainWindow): write_activity = previous is not None and writes > previous[1] self.rows[key].set_state(detail, read_activity=read_activity, write_activity=write_activity, error_steady=False) - def render_temp_warning(self, pi_temp): - synology_temps = [] - for value in self.synology_disk_temps.values(): - if value.isdigit(): - synology_temps.append(float(value)) - synology_max = max(synology_temps) if synology_temps else 0.0 + def render_rpi_temp(self, pi_temp): pi_disk = self.disk_temp or 0.0 - hottest = max(float(pi_temp), pi_disk, synology_max) + hottest = max(float(pi_temp), pi_disk) detail = f"{hottest:.0f} C" if hottest > 0 else "- C" - self.rows["warn_temp"].set_state( + self.rows["rpi_temp"].set_state( detail, read_steady=0 < hottest < 60, write_steady=60 <= hottest < 70, error_steady=hottest >= 70, ) - def render_service_tile(self, key, detail, ok, reachable): - self.rows[key].set_state( - detail if reachable else "끊김", - read_steady=ok, - write_steady=False, - error_steady=not ok, + def render_synology_temp(self): + synology_temps = [] + for value in self.synology_disk_temps.values(): + if value.isdigit(): + synology_temps.append(float(value)) + synology_max = max(synology_temps) if synology_temps else 0.0 + detail = f"{synology_max:.0f} C" if synology_max > 0 else "- C" + self.rows["synology_temp"].set_state( + detail, + read_steady=0 < synology_max < 60, + write_steady=60 <= synology_max < 70, + error_steady=synology_max >= 70, ) def synology_disk_temp_detail(self, disk): @@ -1031,16 +967,6 @@ class TouchPanel(QMainWindow): return "- C" return f"{value} C" - @staticmethod - def parse_status_output(output): - values = {} - for line in output.splitlines(): - if "=" not in line: - continue - key, value = line.split("=", 1) - values[key.strip()] = value.strip() - return values - def set_synology_net_tile(self, key, rx, tx, errors, drops, detail): previous = self.last_synology.get(key) self.last_synology[key] = (rx, tx, errors, drops)