From 763d2c9e38a055cd135f5da3964a63d61044b3fe Mon Sep 17 00:00:00 2001 From: seo Date: Thu, 30 Jul 2026 22:05:57 +0900 Subject: [PATCH] =?UTF-8?q?=ED=84=B0=EC=B9=98=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EC=A3=BC=EA=B8=B0?= =?UTF-8?q?=EC=99=80=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- touch_panel/panel.py | 55 +++++++++++++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4323ba2..5f016de 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 파일을 직접 읽습니다. 상태패널과 팬패널은 50ms 간격으로 커널 카운터를 따라가고, 그래프는 500ms 간격으로 샘플링합니다. 왼쪽 `DISK I/O`, `WAN`, `LAN`, `2.4G`, `5G` 표시등은 장식용 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 간격으로 커널 카운터를 따라가고, 오른쪽 대시보드 카드/그래프는 1초 간격으로 갱신합니다. 왼쪽 `DISK I/O`, `WAN`, `LAN`, `2.4G`, `5G` 표시등은 장식용 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, WAN 실시간 속도와 온도/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, 메모리 사용률을 표시합니다. 아래에는 온도/RPM 그래프와 WAN 트래픽 그래프를 표시합니다. WAN 속도와 그래프 Y축은 byte/sec 기준으로 `B/s`, `KB/s`, `MB/s`, `GB/s` 중 자동 변환합니다. 위 그래프는 좌측 Y축에 온도, 우측 Y축에 RPM 단위를 표시하고, 아래 WAN 그래프는 기존 형태를 유지합니다. 팬 auto/manual/off 버튼은 없고 PWM 슬라이더는 현재값 위치만 보여 주며 터치 제어는 받지 않습니다. GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다. diff --git a/touch_panel/panel.py b/touch_panel/panel.py index 427456e..c68645c 100755 --- a/touch_panel/panel.py +++ b/touch_panel/panel.py @@ -74,6 +74,22 @@ def cpu_temp(): return round(raw / 1000, 1) if raw > 0 else 0.0 +def memory_percent(): + rows = {} + for line in read_text("/proc/meminfo").splitlines(): + if ":" not in line: + continue + key, value = line.split(":", 1) + parts = value.strip().split() + if parts and parts[0].isdigit(): + rows[key] = int(parts[0]) + total = rows.get("MemTotal", 0) + available = rows.get("MemAvailable", 0) + if total <= 0: + return 0.0 + return round((total - available) / total * 100, 1) + + def fan_paths(): for pwm in Path("/sys/devices/platform/cooling_fan/hwmon").glob("hwmon*/pwm1"): base = pwm.parent @@ -460,6 +476,10 @@ class TouchPanel(QMainWindow): self.current_rates = {"wan": {"rx": 0.0, "tx": 0.0}} self.disk_temp = None self.current_pwm = read_int(self.fan["pwm"]) + self.latest_temp = 0.0 + self.latest_rpm = 0 + self.latest_pwm = self.current_pwm + self.latest_percent = 0.0 self.temp_history = deque(maxlen=80) self.rpm_history = deque(maxlen=80) self.net_history = {"wan": {"rx": deque(maxlen=80), "tx": deque(maxlen=80)}} @@ -473,9 +493,9 @@ class TouchPanel(QMainWindow): self.sample_timer.timeout.connect(self.sample) self.sample_timer.start(50) - self.graph_timer = QTimer(self) - self.graph_timer.timeout.connect(self.update_graphs) - self.graph_timer.start(500) + self.dashboard_timer = QTimer(self) + self.dashboard_timer.timeout.connect(self.update_dashboard) + self.dashboard_timer.start(1000) self.pc_ping = QProcess(self) self.pc_ping.finished.connect(self.pc_ping_finished) @@ -495,6 +515,7 @@ class TouchPanel(QMainWindow): self.sample() self.ping_pc() self.update_disk_temp() + self.update_dashboard() def _build_ui(self): root = QWidget() @@ -541,7 +562,7 @@ class TouchPanel(QMainWindow): metric_grid = QGridLayout() metric_grid.setHorizontalSpacing(8) metric_grid.setVerticalSpacing(8) - for index, (key, title) in enumerate([("rpm", "팬 RPM"), ("temp", "CPU 온도"), ("pwm", "PWM"), ("wan", "WAN")]): + for index, (key, title) in enumerate([("rpm", "팬 RPM"), ("temp", "CPU 온도"), ("pwm", "PWM"), ("mem", "메모리")]): card = MetricCard(title) self.metrics[key] = card metric_grid.addWidget(card, 0, index) @@ -617,9 +638,9 @@ class TouchPanel(QMainWindow): self.last_net[key] = current self.current_rates = rates - self.render(temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity, disk_error) + self.render_status(temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity, disk_error) - def render(self, temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity, disk_error): + def render_status(self, temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity, disk_error): disk_temp_text = f"{self.disk_temp:.1f} C" if self.disk_temp is not None else "- C" self.rows["io"].set_state(disk_temp_text, disk_error, io_read_activity, io_write_activity) self.rows["wan"].set_state(self.iface_detail(groups["wan"]), activity["wan"]["error"], activity["wan"]["read"], activity["wan"]["write"]) @@ -627,22 +648,30 @@ class TouchPanel(QMainWindow): self.rows["wifi24"].set_state(self.iface_detail(groups["wifi24"]), activity["wifi24"]["error"], activity["wifi24"]["read"], activity["wifi24"]["write"]) self.rows["wifi5"].set_state(self.iface_detail(groups["wifi5"]), activity["wifi5"]["error"], activity["wifi5"]["read"], activity["wifi5"]["write"]) + self.latest_temp = temp + self.latest_rpm = rpm + self.latest_pwm = pwm + self.latest_percent = percent + del enable + + def update_dashboard(self): + rpm = getattr(self, "latest_rpm", read_int(self.fan["rpm"])) + temp = getattr(self, "latest_temp", cpu_temp()) + pwm = getattr(self, "latest_pwm", read_int(self.fan["pwm"])) + percent = getattr(self, "latest_percent", pwm / 255 * 100 if pwm > 0 else 0) + mem_percent = memory_percent() + self.metrics["rpm"].set_value(f"{rpm:,}") self.metrics["temp"].set_value(f"{temp:.1f} C") self.metrics["pwm"].set_value(f"{pwm} / {percent:.1f}%") - wan_factor, wan_unit = byte_rate_unit([rates["wan"]["rx"], rates["wan"]["tx"]]) - rx_text = format_byte_rate(rates["wan"]["rx"], wan_factor, wan_unit) - tx_text = format_byte_rate(rates["wan"]["tx"], wan_factor, wan_unit) - self.metrics["wan"].set_value(f"{rx_text}/{tx_text}") + self.metrics["mem"].set_value(f"{mem_percent:.1f}%") self.slider.blockSignals(True) self.slider.setValue(pwm) self.slider.blockSignals(False) - del enable self.clock.setText(time.strftime("%H:%M:%S")) - self.latest_temp = temp - self.latest_rpm = rpm + self.update_graphs() def ping_pc(self): if self.pc_ping.state() != QProcess.NotRunning: