터치 패널 체크 열에 팬 상태 추가

This commit is contained in:
seo committed 2026-07-31 06:04:11 +09:00
1 parent e82602a22d
commit 3eda77c741
2 files changed
+65 -13

No files matched your search

+1 -1
View File
@@ -146,7 +146,7 @@ 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 파일을 직접 읽습니다. 상태패널, 팬패널, 오른쪽 대시보드 카드/그래프는 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분 표시입니다. 패널은 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`, `RPI FAN`, `SYNO TEMP`, `SYNO FAN`을 표시합니다. `SYNO TEMP`는 시놀로지 CPU 온도 기준이고, 팬 카드는 RPM 0 또는 7000 초과를 빨강, 4500 초과를 주황으로 표시합니다. `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 슬라이더는 현재값 위치만 보여 주며 터치 제어는 받지 않습니다. 오른쪽 상단 카드는 팬 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 슬라이더는 현재값 위치만 보여 주며 터치 제어는 받지 않습니다.
+64 -12
View File
@@ -90,11 +90,19 @@ awk -F= '
$1 == "sata4" {printf "SATA4_TEMP=%s\n", $2 ? $2 : "-"} $1 == "sata4" {printf "SATA4_TEMP=%s\n", $2 ? $2 : "-"}
' '
""" """
SYNOLOGY_HEALTH_SCRIPT = r"""
cpu="$(cat /sys/class/hwmon/hwmon0/temp1_input 2>/dev/null)"
fan="$(cat /sys/class/hwmon/hwmon1/fan2_input 2>/dev/null)"
case "$cpu" in ''|*[!0-9]*) cpu=0 ;; esac
case "$fan" in ''|*[!0-9]*) fan=0 ;; esac
awk -v cpu="$cpu" -v fan="$fan" 'BEGIN {printf "SYNO_CPU=%.1f\nSYNO_FAN=%d\n", cpu / 1000, fan}'
"""
SAMPLE_INTERVAL_MS = 50 SAMPLE_INTERVAL_MS = 50
GRAPH_SECONDS = 30 GRAPH_SECONDS = 30
GRAPH_POINTS = GRAPH_SECONDS * 1000 // SAMPLE_INTERVAL_MS GRAPH_POINTS = GRAPH_SECONDS * 1000 // SAMPLE_INTERVAL_MS
SYNOLOGY_RESTART_INTERVAL_MS = 1000 SYNOLOGY_RESTART_INTERVAL_MS = 1000
SYNOLOGY_DISK_TEMP_INTERVAL_MS = 10000 SYNOLOGY_DISK_TEMP_INTERVAL_MS = 10000
SYNOLOGY_HEALTH_INTERVAL_MS = 5000
def byte_rate_unit(values): def byte_rate_unit(values):
@@ -569,6 +577,8 @@ class TouchPanel(QMainWindow):
self.synology_buffer = "" self.synology_buffer = ""
self.synology_values = {} self.synology_values = {}
self.synology_disk_temps = {} self.synology_disk_temps = {}
self.synology_cpu_temp = 0.0
self.synology_fan_rpm = 0
self.last_io = None self.last_io = None
self.last_sample = None self.last_sample = None
self.last_graph = None self.last_graph = None
@@ -621,6 +631,12 @@ class TouchPanel(QMainWindow):
self.synology_disk_temp_timer.timeout.connect(self.update_synology_disk_temp) 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_disk_temp_timer.start(SYNOLOGY_DISK_TEMP_INTERVAL_MS)
self.synology_health_proc = QProcess(self)
self.synology_health_proc.finished.connect(self.synology_health_finished)
self.synology_health_timer = QTimer(self)
self.synology_health_timer.timeout.connect(self.update_synology_health)
self.synology_health_timer.start(SYNOLOGY_HEALTH_INTERVAL_MS)
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)
self.pulse_timer.start(25) self.pulse_timer.start(25)
@@ -629,6 +645,7 @@ class TouchPanel(QMainWindow):
self.update_disk_temp() self.update_disk_temp()
self.update_synology_status() self.update_synology_status()
self.update_synology_disk_temp() self.update_synology_disk_temp()
self.update_synology_health()
self.update_dashboard() self.update_dashboard()
def _build_ui(self): def _build_ui(self):
@@ -663,9 +680,9 @@ class TouchPanel(QMainWindow):
status_rows = [ status_rows = [
(("io", "DISK I/O"), ("synology_sata1", "SATA1"), ("rpi_temp", "RPI TEMP")), (("io", "DISK I/O"), ("synology_sata1", "SATA1"), ("rpi_temp", "RPI TEMP")),
(("local", "LOCAL"), ("synology_sata2", "SATA2"), ("synology_temp", "SYNO TEMP")), (("local", "LOCAL"), ("synology_sata2", "SATA2"), ("rpi_fan", "RPI FAN")),
(("wan", "UPLINK"), ("synology_sata4", "SATA4"), None), (("wan", "UPLINK"), ("synology_sata4", "SATA4"), ("synology_temp", "SYNO TEMP")),
(("lan", "ETH1"), ("synology_local", "LOCAL"), None), (("lan", "ETH1"), ("synology_local", "LOCAL"), ("synology_fan", "SYNO FAN")),
(("wifi5", "5G"), ("synology_wan", "UPLINK"), None), (("wifi5", "5G"), ("synology_wan", "UPLINK"), None),
(("wifi24", "2.4G"), None, None), (("wifi24", "2.4G"), None, None),
] ]
@@ -801,6 +818,7 @@ class TouchPanel(QMainWindow):
self.rows["wifi24"].set_state(self.iface_detail(groups["wifi24"], "wlan0"), activity["wifi24"]["error"], activity["wifi24"]["read"], activity["wifi24"]["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.rows["wifi5"].set_state(self.iface_detail(groups["wifi5"], "wlan1"), activity["wifi5"]["error"], activity["wifi5"]["read"], activity["wifi5"]["write"])
self.render_rpi_temp(temp) self.render_rpi_temp(temp)
self.render_rpi_fan(rpm)
self.latest_temp = temp self.latest_temp = temp
self.latest_rpm = rpm self.latest_rpm = rpm
@@ -865,6 +883,11 @@ class TouchPanel(QMainWindow):
return return
self.synology_disk_temp_proc.start(SYNOLOGY_SSH[0], SYNOLOGY_SSH[1:] + [SYNOLOGY_DISK_TEMP_SCRIPT]) self.synology_disk_temp_proc.start(SYNOLOGY_SSH[0], SYNOLOGY_SSH[1:] + [SYNOLOGY_DISK_TEMP_SCRIPT])
def update_synology_health(self):
if self.synology_health_proc.state() != QProcess.NotRunning:
return
self.synology_health_proc.start(SYNOLOGY_SSH[0], SYNOLOGY_SSH[1:] + [SYNOLOGY_HEALTH_SCRIPT])
def synology_disk_temp_finished(self, exit_code, exit_status): def synology_disk_temp_finished(self, exit_code, exit_status):
del exit_status del exit_status
if exit_code != 0: if exit_code != 0:
@@ -877,7 +900,25 @@ class TouchPanel(QMainWindow):
key, value = line.split("=", 1) key, value = line.split("=", 1)
temps[key.strip()] = value.strip() temps[key.strip()] = value.strip()
self.synology_disk_temps = temps self.synology_disk_temps = temps
def synology_health_finished(self, exit_code, exit_status):
del exit_status
if exit_code != 0:
return
output = bytes(self.synology_health_proc.readAllStandardOutput()).decode("utf-8", "ignore")
values = {}
for line in output.splitlines():
if "=" not in line:
continue
key, value = line.split("=", 1)
values[key.strip()] = value.strip()
try:
self.synology_cpu_temp = float(values.get("SYNO_CPU", "0") or 0)
except ValueError:
self.synology_cpu_temp = 0.0
self.synology_fan_rpm = int(values.get("SYNO_FAN", "0") or 0)
self.render_synology_temp() self.render_synology_temp()
self.render_synology_fan()
def synology_status_ready(self): def synology_status_ready(self):
output = bytes(self.synology_proc.readAllStandardOutput()).decode("utf-8", "ignore") output = bytes(self.synology_proc.readAllStandardOutput()).decode("utf-8", "ignore")
@@ -947,18 +988,29 @@ class TouchPanel(QMainWindow):
error_steady=hottest >= 70, error_steady=hottest >= 70,
) )
def render_rpi_fan(self, rpm):
self.render_fan_tile("rpi_fan", rpm)
def render_synology_temp(self): def render_synology_temp(self):
synology_temps = [] temp = self.synology_cpu_temp
for value in self.synology_disk_temps.values(): detail = f"{temp:.0f} C" if temp > 0 else "- C"
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( self.rows["synology_temp"].set_state(
detail, detail,
read_steady=0 < synology_max < 60, read_steady=0 < temp < 75,
write_steady=60 <= synology_max < 70, write_steady=75 <= temp < 90,
error_steady=synology_max >= 70, error_steady=temp >= 90,
)
def render_synology_fan(self):
self.render_fan_tile("synology_fan", self.synology_fan_rpm)
def render_fan_tile(self, key, rpm):
detail = f"{int(rpm):,} rpm" if rpm > 0 else "0 rpm"
self.rows[key].set_state(
detail,
read_steady=500 <= rpm <= 4500,
write_steady=4500 < rpm <= 7000,
error_steady=rpm == 0 or rpm > 7000,
) )
def synology_disk_temp_detail(self, disk): def synology_disk_temp_detail(self, disk):