터치 패널 PC 버튼 확장 복원

This commit is contained in:
seo committed 2026-08-14 02:26:55 +09:00
1 parent 637739386e
commit 64ded5e566
2 files changed
+117 -6

No files matched your search

+3 -1
View File
@@ -155,7 +155,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 파일을 직접 읽습니다. 화면은 왼쪽 `RASPBERRY` 18칸, 오른쪽 `SYNOLOGY` 18칸 카드로 나눕니다. 왼쪽 하단의 1칸짜리 영역은 아직 기능을 두지 않고 빈칸으로 둡니다.
패널은 Control PHP/API/DB snapshot을 호출하지 않는 standalone 방식입니다. Python이 `/sys/class/net`, `/proc/diskstats`, `/sys/class/thermal`, 팬 hwmon 파일을 직접 읽습니다. 화면은 왼쪽 `RASPBERRY` 18칸, 오른쪽 `SYNOLOGY` 18칸 카드로 나눕니다. 하단에는 좌우 6칸 폭을 한 번에 쓰는 긴 `PC` 버튼을 둡니다.
`RASPBERRY` 카드는 `SSD I/O`, `TEMP`, `FAN`, `THROT`, `LOAD`, `MEM`, `ROOT`, `UPTIME`, `LOCAL`, `UPLINK`, `ETH1`, `5G`, `2.4G`, `DOCKER`, `NGINX`, `MARIADB`, `CONTROL`, `FANPOL`을 표시합니다. 네트워크와 SSD I/O 점멸은 100ms 샘플에서 직전 카운터보다 증가했는지를 보고, CPU 온도/SSD 온도와 팬 RPM은 500ms 간격으로 표시합니다. systemd 서비스 상태는 5초마다 `systemctl is-active`로 확인합니다.
@@ -165,6 +165,8 @@ cd /var/www/control
각 타일에는 글자 아래에 세 개의 작은 표시등을 두며 읽기/업로드는 초록, 쓰기/다운로드는 주황, 오류/드롭 증가는 빨강으로 표시합니다. 활동 표시등은 이벤트 감지 후 120ms 동안 유지하고, 오류 표시등은 2초 동안 유지해 너무 짧은 이벤트도 눈에 보이게 합니다. 네트워크 카드 오른쪽 값은 항상 `D n`으로 드롭 누적을 표시합니다. `LOCAL`은 `lo`, `UPLINK`는 기본 라우트 인터페이스 기준입니다. 라즈베리파이 `ETH1`은 컴퓨터와 연결된 물리 포트 단독 카운터이며 `br0`는 합산하지 않습니다. `SSD I/O`의 값은 `/run/control-rpi-disk-temp` 캐시 파일의 `/dev/sda` Samsung T7 디스크 자체 온도입니다. 캐시 파일은 root로 실행되는 `control-rpi-disk-temp.service`가 몇 초마다 `smartctl -d sntasmedia`로 갱신하므로 GUI 패널은 sudo를 반복 실행하지 않습니다.
하단 `PC` 버튼은 `192.168.50.73`에 1초마다 ping을 보내 켜짐 여부를 표시하고, 꺼져 있을 때 누르면 `74:56:3C:CD:CD:79`로 WOL magic packet을 보냅니다. 주황은 WOL 후 최대 1분 동안 컴퓨터 응답 대기 표시, 빨강은 magic packet 전송 실패 시 5초 또는 1분 대기 후 응답 실패 시 1분 표시입니다.
GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다.
```bash
+114 -5
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import sys
import time
import socket
import os
import shutil
from collections import deque
@@ -28,6 +29,8 @@ NET_GROUPS = {
"wifi5": ["wlan1"],
}
PC_IP = "192.168.50.73"
PC_MAC = "74:56:3C:CD:CD:79"
RPI_SERVICE_UNITS = [
("rpi_docker", "docker.service"),
("rpi_nginx", "nginx.service"),
@@ -340,6 +343,21 @@ def disk_io():
return reads, writes, not found
def send_magic_packet(mac):
clean = mac.replace(":", "").replace("-", "")
if len(clean) != 12:
return False
packet = bytes.fromhex("ff" * 6 + clean * 16)
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
for target in ["192.168.50.255", "255.255.255.255"]:
sock.sendto(packet, (target, 9))
return True
except OSError:
return False
class StatusDot(QWidget):
def __init__(self, active_color):
super().__init__()
@@ -462,6 +480,72 @@ class StatusTile(QFrame):
self.error_dot.set_steady(error_on)
class PcWakeButton(StatusTile):
def __init__(self):
super().__init__("PC")
self.online = False
self.last_wake_at = 0.0
self.wait_until = 0.0
self.error_until = 0.0
self.wait_phase = False
self.blink_timer = QTimer(self)
self.blink_timer.timeout.connect(self.tick_waiting)
self.blink_timer.start(250)
def set_online(self, online):
self.online = online
self.read_dot.set_steady(online)
now = time.monotonic()
self.error_dot.set_steady(now < self.error_until)
if online:
self.wait_until = 0.0
self.wait_phase = False
self.write_dot.set_steady(False)
self.detail.setText("켜짐")
elif now < self.wait_until:
remaining = int(self.wait_until - now)
self.detail.setText(f"대기 {remaining}s")
else:
self.write_dot.set_steady(False)
self.detail.setText("꺼짐")
def tick_waiting(self):
now = time.monotonic()
if not self.online and now < self.wait_until:
self.wait_phase = not self.wait_phase
self.write_dot.set_steady(self.wait_phase)
if not self.online and self.wait_until > 0 and now >= self.wait_until:
self.wait_until = 0.0
self.wait_phase = False
self.write_dot.set_steady(False)
self.error_until = now + 60
self.error_dot.set_steady(True)
self.detail.setText("실패")
if self.error_until > 0 and now >= self.error_until:
self.error_until = 0.0
self.error_dot.set_steady(False)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
now = time.monotonic()
if not self.online and now >= self.wait_until:
self.last_wake_at = now
self.wait_until = now + 60
self.wait_phase = True
self.write_dot.set_steady(True)
self.error_until = 0.0
self.error_dot.set_steady(False)
self.detail.setText("깨우는 중")
if not send_magic_packet(PC_MAC):
self.wait_until = 0.0
self.error_until = now + 5
self.error_dot.set_steady(True)
super().mousePressEvent(event)
def tick(self):
self.error_dot.tick()
class Sparkline(QFrame):
def __init__(self, title, color="#38bdf8", second_color=None, y_unit="", right_unit="", auto_byte_unit=False, max_points=120):
super().__init__()
@@ -610,6 +694,12 @@ class TouchPanel(QMainWindow):
self.fan_timer.timeout.connect(self.update_fan_status)
self.fan_timer.start(FAN_INTERVAL_MS)
self.pc_ping = QProcess(self)
self.pc_ping.finished.connect(self.pc_ping_finished)
self.pc_timer = QTimer(self)
self.pc_timer.timeout.connect(self.ping_pc)
self.pc_timer.start(1000)
self.disk_temp_timer = QTimer(self)
self.disk_temp_timer.timeout.connect(self.update_disk_temp)
self.disk_temp_timer.start(DISK_TEMP_INTERVAL_MS)
@@ -653,6 +743,7 @@ class TouchPanel(QMainWindow):
self.pulse_timer.start(25)
self.sample()
self.update_fan_status()
self.ping_pc()
self.update_disk_temp()
self.update_rpi_services()
self.update_synology_status()
@@ -663,9 +754,13 @@ class TouchPanel(QMainWindow):
def _build_ui(self):
root = QWidget()
layout = QHBoxLayout(root)
layout = QVBoxLayout(root)
layout.setContentsMargins(4, 8, 10, 8)
layout.setSpacing(8)
layout.setSpacing(6)
panel_layout = QHBoxLayout()
panel_layout.setContentsMargins(0, 0, 0, 0)
panel_layout.setSpacing(8)
rpi_rows = [
(("io", "SSD I/O"), ("rpi_temp", "TEMP"), ("rpi_fan", "FAN")),
@@ -684,7 +779,7 @@ class TouchPanel(QMainWindow):
(("synology_sata1", "SATA1 I/O"), ("synology_sata2", "SATA2 I/O"), ("synology_sata4", "SATA4 I/O")),
]
left = self.build_status_panel("RASPBERRY", rpi_rows, fixed_width=398, bottom_blank=True)
left = self.build_status_panel("RASPBERRY", rpi_rows, fixed_width=398)
right = self.build_status_panel("SYNOLOGY", synology_rows)
header = QHBoxLayout()
@@ -697,8 +792,13 @@ class TouchPanel(QMainWindow):
header.addWidget(self.clock)
right.layout().insertLayout(1, header)
layout.addWidget(left)
layout.addWidget(right, 1)
panel_layout.addWidget(left)
panel_layout.addWidget(right, 1)
layout.addLayout(panel_layout, 1)
self.pc_button = PcWakeButton()
self.pc_button.setFixedHeight(48)
layout.addWidget(self.pc_button)
self.setCentralWidget(root)
def build_status_panel(self, title_text, rows, fixed_width=None, bottom_blank=False):
@@ -836,6 +936,15 @@ class TouchPanel(QMainWindow):
detail = "OK" if active else state.upper()[:6]
self.rows[key].set_state(detail, read_steady=active, write_steady=False, error_steady=not active)
def ping_pc(self):
if self.pc_ping.state() != QProcess.NotRunning:
return
self.pc_ping.start("/usr/bin/ping", ["-c", "1", "-W", "1", PC_IP])
def pc_ping_finished(self, exit_code, exit_status):
del exit_status
self.pc_button.set_online(exit_code == 0)
def ping_synology(self):
if self.synology_ping_proc.state() != QProcess.NotRunning:
return