터치 패널 그래프 주기 분리

This commit is contained in:
seo committed 2026-08-01 11:42:02 +09:00
1 parent 3f3198b29f
commit 5f1cc4cc7f
2 files changed
+18 -13

No files matched your search

+1 -1
View File
@@ -147,7 +147,7 @@ cd /var/www/control
패널은 Control PHP/API/DB snapshot을 호출하지 않는 standalone 방식입니다. Python이 `/sys/class/net`, `/proc/diskstats`, `/sys/class/thermal`, 팬 hwmon 파일을 직접 읽습니다. 상태패널의 네트워크/I/O 표시는 50ms 간격으로 갱신하고, 팬 카드와 오른쪽 대시보드 카드/그래프는 500ms 간격으로 갱신합니다. 왼쪽 상태패널은 `RASPBERRY`, `SYNOLOGY`, `CHECK` 3열이며, 라즈베리파이 순서는 `SSD I/O`, `LOCAL`, `UPLINK`, `ETH1`, `5G`, `2.4G`입니다. 시놀로지 열은 `PING`, 정상 디스크 `SATA1 I/O`, `SATA2 I/O`, `SATA4 I/O`, `LOCAL`, `UPLINK`를 표시합니다. `CHECK` 열은 `RPI TEMP`, `RPI FAN`, `RPI THROT`, `SYNO TEMP`, `SYNO FAN`, `SYNO GUARD`를 표시합니다. `RPI TEMP`와 `SYNO TEMP`는 80도 이상부터 빨간 표시등을 500ms 간격으로 점멸하고, 팬 카드는 RPM 0 또는 7000 초과를 빨강, 4500 초과를 주황으로 표시합니다. 시놀로지 `PING`은 장기 실행 `ping -i 0.5` 출력 파싱으로 읽고, 15ms 이하 초록, 30ms 이하 주황, 50ms 이하 빨강, 50ms 초과 빨강 500ms 점멸입니다. `RPI THROT`은 정상 초록, 부팅 후 이력 주황, 현재 언더볼트 빨강, 현재 스로틀 빨강 500ms 점멸, 현재 언더볼트와 스로틀 동시 발생 빨강 100ms 점멸로 표시합니다. `SYNO GUARD`는 `synology-fan-guard.service`가 active이고 DSM 자동제어면 초록 `AUTO`, 고온으로 PWM 최대 강제 중이면 주황 `FULL`, 서비스 실패나 팬 RPM 0이면 빨강 `FAIL`로 표시합니다. `LOCAL`은 `lo`, `UPLINK`는 기본 라우트 인터페이스 기준입니다. 라즈베리파이 `ETH1`은 컴퓨터와 연결된 물리 포트 단독 카운터이며 `br0`는 합산하지 않습니다. 시놀로지는 SSH를 유지한 채 50ms마다 디스크별 sector I/O, `LOCAL`, `UPLINK` 카운터를 스트림으로 가져오고, 각 SATA 카드 값에는 SMART 온도를 표시합니다. 라즈베리파이 CPU/SSD 온도, 시놀로지 CPU 온도/FAN RPM, 시놀로지 SATA SMART 온도는 500ms 간격으로 캐시해 표시합니다. 왼쪽 표시등은 장식용 heartbeat가 아니라 직전 샘플 대비 현재 카운터 증가량이 0 초과인지에 반응합니다. 각 타일에는 글자 아래에 세 개의 작은 표시등을 두며 읽기/업로드은 초록, 쓰기/다운로드는 주황, 오류/드롭 증가는 빨강으로 표시합니다. 활동 표시등은 이벤트 감지 후 120ms 동안 유지하고, 오류 표시등은 2초 동안 유지해 너무 짧은 이벤트도 눈에 보이게 합니다. 네트워크 카드 오른쪽 값은 항상 `D n`으로 드롭 누적을 표시합니다. `SSD I/O`의 값은 `/dev/sda` Samsung T7을 `smartctl -d sntasmedia`로 읽은 디스크 자체 온도입니다. 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 파일을 직접 읽습니다. 상태패널의 네트워크/I/O 표시는 50ms 간격으로 갱신하고, 팬 카드와 오른쪽 대시보드 카드/그래프는 500ms 간격으로 갱신합니다. 왼쪽 상태패널은 `RASPBERRY`, `SYNOLOGY`, `CHECK` 3열이며, 라즈베리파이 순서는 `SSD I/O`, `LOCAL`, `UPLINK`, `ETH1`, `5G`, `2.4G`입니다. 시놀로지 열은 `PING`, 정상 디스크 `SATA1 I/O`, `SATA2 I/O`, `SATA4 I/O`, `LOCAL`, `UPLINK`를 표시합니다. `CHECK` 열은 `RPI TEMP`, `RPI FAN`, `RPI THROT`, `SYNO TEMP`, `SYNO FAN`, `SYNO GUARD`를 표시합니다. `RPI TEMP`와 `SYNO TEMP`는 80도 이상부터 빨간 표시등을 500ms 간격으로 점멸하고, 팬 카드는 RPM 0 또는 7000 초과를 빨강, 4500 초과를 주황으로 표시합니다. 시놀로지 `PING`은 장기 실행 `ping -i 0.5` 출력 파싱으로 읽고, 15ms 이하 초록, 30ms 이하 주황, 50ms 이하 빨강, 50ms 초과 빨강 500ms 점멸입니다. `RPI THROT`은 정상 초록, 부팅 후 이력 주황, 현재 언더볼트 빨강, 현재 스로틀 빨강 500ms 점멸, 현재 언더볼트와 스로틀 동시 발생 빨강 100ms 점멸로 표시합니다. `SYNO GUARD`는 `synology-fan-guard.service`가 active이고 DSM 자동제어면 초록 `AUTO`, 고온으로 PWM 최대 강제 중이면 주황 `FULL`, 서비스 실패나 팬 RPM 0이면 빨강 `FAIL`로 표시합니다. `LOCAL`은 `lo`, `UPLINK`는 기본 라우트 인터페이스 기준입니다. 라즈베리파이 `ETH1`은 컴퓨터와 연결된 물리 포트 단독 카운터이며 `br0`는 합산하지 않습니다. 시놀로지는 SSH를 유지한 채 50ms마다 디스크별 sector I/O, `LOCAL`, `UPLINK` 카운터를 스트림으로 가져오고, 각 SATA 카드 값에는 SMART 온도를 표시합니다. 라즈베리파이 CPU/SSD 온도, 시놀로지 CPU 온도/FAN RPM, 시놀로지 SATA SMART 온도는 500ms 간격으로 캐시해 표시합니다. 왼쪽 표시등은 장식용 heartbeat가 아니라 직전 샘플 대비 현재 카운터 증가량이 0 초과인지에 반응합니다. 각 타일에는 글자 아래에 세 개의 작은 표시등을 두며 읽기/업로드은 초록, 쓰기/다운로드는 주황, 오류/드롭 증가는 빨강으로 표시합니다. 활동 표시등은 이벤트 감지 후 120ms 동안 유지하고, 오류 표시등은 2초 동안 유지해 너무 짧은 이벤트도 눈에 보이게 합니다. 네트워크 카드 오른쪽 값은 항상 `D n`으로 드롭 누적을 표시합니다. `SSD I/O`의 값은 `/dev/sda` Samsung T7을 `smartctl -d sntasmedia`로 읽은 디스크 자체 온도입니다. 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 퍼센트, 메모리 사용률을 표시합니다. 아래에는 최근 10초 기준 온도/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 퍼센트, 메모리 사용률을 표시합니다. 아래에는 최근 60초 기준 온도/RPM 그래프와 최근 10초 기준 WAN 트래픽 그래프를 표시합니다. 온도/RPM 그래프는 500ms 샘플을 사용하고, WAN 그래프는 50ms 샘플을 사용합니다. WAN 속도와 그래프 Y축은 byte/sec 기준으로 `B/s`, `KB/s`, `MB/s`, `GB/s` 중 자동 변환하고, 다운로드 주황/업로드 초록 두 선을 같은 축 범위에 맞춰 그립니다. 그래프 우측 상단 현재값은 고정폭으로 표시합니다. 위 그래프는 좌측 Y축에 온도, 우측 Y축에 RPM 단위를 표시하고, 아래 WAN 그래프는 기존 형태를 유지합니다. 팬 auto/manual/off 버튼은 없고 PWM 슬라이더는 현재값 위치만 보여 주며 터치 제어는 받지 않습니다.
GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다. GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다.
+17 -12
View File
@@ -106,8 +106,10 @@ awk -v cpu="$cpu" -v fan="$fan" -v pwm="$pwm" -v enable="$enable" -v guard="$gua
SAMPLE_INTERVAL_MS = 50 SAMPLE_INTERVAL_MS = 50
DASHBOARD_INTERVAL_MS = 500 DASHBOARD_INTERVAL_MS = 500
FAN_INTERVAL_MS = 500 FAN_INTERVAL_MS = 500
GRAPH_SECONDS = 10 WAN_GRAPH_SECONDS = 10
GRAPH_POINTS = GRAPH_SECONDS * 1000 // DASHBOARD_INTERVAL_MS TEMP_GRAPH_SECONDS = 60
WAN_GRAPH_POINTS = WAN_GRAPH_SECONDS * 1000 // SAMPLE_INTERVAL_MS
TEMP_GRAPH_POINTS = TEMP_GRAPH_SECONDS * 1000 // DASHBOARD_INTERVAL_MS
SYNOLOGY_RESTART_INTERVAL_MS = 1000 SYNOLOGY_RESTART_INTERVAL_MS = 1000
TEMP_INTERVAL_MS = 500 TEMP_INTERVAL_MS = 500
DISK_TEMP_INTERVAL_MS = TEMP_INTERVAL_MS DISK_TEMP_INTERVAL_MS = TEMP_INTERVAL_MS
@@ -488,7 +490,7 @@ class MetricCard(QFrame):
class Sparkline(QFrame): class Sparkline(QFrame):
def __init__(self, title, color="#38bdf8", second_color=None, y_unit="", right_unit="", auto_byte_unit=False): def __init__(self, title, color="#38bdf8", second_color=None, y_unit="", right_unit="", auto_byte_unit=False, max_points=120):
super().__init__() super().__init__()
self.setObjectName("chartCard") self.setObjectName("chartCard")
self.color = QColor(color) self.color = QColor(color)
@@ -496,6 +498,7 @@ class Sparkline(QFrame):
self.y_unit = y_unit self.y_unit = y_unit
self.right_unit = right_unit self.right_unit = right_unit
self.auto_byte_unit = auto_byte_unit self.auto_byte_unit = auto_byte_unit
self.max_points = max_points
self.values = [] self.values = []
self.second_values = [] self.second_values = []
self.title = QLabel(title) self.title = QLabel(title)
@@ -517,8 +520,8 @@ class Sparkline(QFrame):
layout.addWidget(self.canvas, 1) layout.addWidget(self.canvas, 1)
def set_values(self, values, caption, second_values=None): def set_values(self, values, caption, second_values=None):
self.values = list(values)[-GRAPH_POINTS:] self.values = list(values)[-self.max_points:]
self.second_values = list(second_values or [])[-GRAPH_POINTS:] self.second_values = list(second_values or [])[-self.max_points:]
self.caption.setText(caption) self.caption.setText(caption)
self.canvas.update() self.canvas.update()
@@ -611,9 +614,9 @@ class TouchPanel(QMainWindow):
self.latest_rpm = 0 self.latest_rpm = 0
self.latest_pwm = self.current_pwm self.latest_pwm = self.current_pwm
self.latest_percent = 0.0 self.latest_percent = 0.0
self.temp_history = deque(maxlen=GRAPH_POINTS) self.temp_history = deque(maxlen=TEMP_GRAPH_POINTS)
self.rpm_history = deque(maxlen=GRAPH_POINTS) self.rpm_history = deque(maxlen=TEMP_GRAPH_POINTS)
self.net_history = {"wan": {"rx": deque(maxlen=GRAPH_POINTS), "tx": deque(maxlen=GRAPH_POINTS)}} self.net_history = {"wan": {"rx": deque(maxlen=WAN_GRAPH_POINTS), "tx": deque(maxlen=WAN_GRAPH_POINTS)}}
self.setWindowTitle("Standalone Touch Dashboard") self.setWindowTitle("Standalone Touch Dashboard")
self.setCursor(Qt.BlankCursor) self.setCursor(Qt.BlankCursor)
@@ -782,8 +785,8 @@ class TouchPanel(QMainWindow):
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", "rpm")), ("temp", Sparkline("온도 / RPM", "#f97316", "#38bdf8", "C", "rpm", max_points=TEMP_GRAPH_POINTS)),
("wan", Sparkline("WAN 트래픽", "#f97316", "#22c55e", auto_byte_unit=True)), ("wan", Sparkline("WAN 트래픽", "#f97316", "#22c55e", auto_byte_unit=True, max_points=WAN_GRAPH_POINTS)),
]): ]):
self.charts[key] = chart self.charts[key] = chart
chart_grid.addWidget(chart, index, 0) chart_grid.addWidget(chart, index, 0)
@@ -845,6 +848,7 @@ class TouchPanel(QMainWindow):
self.last_net[key] = current self.last_net[key] = current
self.current_rates = rates self.current_rates = rates
self.update_wan_graph()
self.render_status(groups, rates, activity, io_read_activity, io_write_activity, disk_error) self.render_status(groups, rates, activity, io_read_activity, io_write_activity, disk_error)
def render_status(self, groups, rates, activity, io_read_activity, io_write_activity, disk_error): def render_status(self, groups, rates, activity, io_read_activity, io_write_activity, disk_error):
@@ -883,7 +887,7 @@ class TouchPanel(QMainWindow):
self.slider.blockSignals(False) self.slider.blockSignals(False)
self.clock.setText(time.strftime("%H:%M:%S")) self.clock.setText(time.strftime("%H:%M:%S"))
self.update_graphs() self.update_temp_graph()
def ping_pc(self): def ping_pc(self):
if self.pc_ping.state() != QProcess.NotRunning: if self.pc_ping.state() != QProcess.NotRunning:
@@ -1164,13 +1168,14 @@ class TouchPanel(QMainWindow):
error_activity = previous is not None and (errors > previous[2] or drops > previous[3]) error_activity = previous is not None and (errors > previous[2] or drops > previous[3])
self.rows[key].set_state(self.drop_detail(drops, detail), error_activity, read_activity, write_activity) self.rows[key].set_state(self.drop_detail(drops, detail), error_activity, read_activity, write_activity)
def update_graphs(self): def update_temp_graph(self):
temp = getattr(self, "latest_temp", 0.0) temp = getattr(self, "latest_temp", 0.0)
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) self.rpm_history.append(rpm)
self.charts["temp"].set_values(self.temp_history, format_temp_rpm_caption(temp, rpm), self.rpm_history) self.charts["temp"].set_values(self.temp_history, format_temp_rpm_caption(temp, rpm), self.rpm_history)
def update_wan_graph(self):
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"])
self.net_history["wan"]["tx"].append(wan["tx"]) self.net_history["wan"]["tx"].append(wan["tx"])