터치 패널 대시보드 주기와 메모리 카드 정리

This commit is contained in:
seo committed 2026-07-30 22:05:57 +09:00
1 parent a2a45a0118
commit 763d2c9e38
2 files changed
+44 -15

No files matched your search

+2 -2
View File
@@ -146,9 +146,9 @@ 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 간격으로 커널 카운터를 따라가고, 그래프는 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차 기준입니다. GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다.
+42 -13
View File
@@ -74,6 +74,22 @@ def cpu_temp():
return round(raw / 1000, 1) if raw > 0 else 0.0 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(): def fan_paths():
for pwm in Path("/sys/devices/platform/cooling_fan/hwmon").glob("hwmon*/pwm1"): for pwm in Path("/sys/devices/platform/cooling_fan/hwmon").glob("hwmon*/pwm1"):
base = pwm.parent base = pwm.parent
@@ -460,6 +476,10 @@ class TouchPanel(QMainWindow):
self.current_rates = {"wan": {"rx": 0.0, "tx": 0.0}} self.current_rates = {"wan": {"rx": 0.0, "tx": 0.0}}
self.disk_temp = None self.disk_temp = None
self.current_pwm = read_int(self.fan["pwm"]) 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.temp_history = deque(maxlen=80)
self.rpm_history = deque(maxlen=80) self.rpm_history = deque(maxlen=80)
self.net_history = {"wan": {"rx": deque(maxlen=80), "tx": 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.timeout.connect(self.sample)
self.sample_timer.start(50) self.sample_timer.start(50)
self.graph_timer = QTimer(self) self.dashboard_timer = QTimer(self)
self.graph_timer.timeout.connect(self.update_graphs) self.dashboard_timer.timeout.connect(self.update_dashboard)
self.graph_timer.start(500) self.dashboard_timer.start(1000)
self.pc_ping = QProcess(self) self.pc_ping = QProcess(self)
self.pc_ping.finished.connect(self.pc_ping_finished) self.pc_ping.finished.connect(self.pc_ping_finished)
@@ -495,6 +515,7 @@ class TouchPanel(QMainWindow):
self.sample() self.sample()
self.ping_pc() self.ping_pc()
self.update_disk_temp() self.update_disk_temp()
self.update_dashboard()
def _build_ui(self): def _build_ui(self):
root = QWidget() root = QWidget()
@@ -541,7 +562,7 @@ class TouchPanel(QMainWindow):
metric_grid = QGridLayout() metric_grid = QGridLayout()
metric_grid.setHorizontalSpacing(8) metric_grid.setHorizontalSpacing(8)
metric_grid.setVerticalSpacing(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) card = MetricCard(title)
self.metrics[key] = card self.metrics[key] = card
metric_grid.addWidget(card, 0, index) metric_grid.addWidget(card, 0, index)
@@ -617,9 +638,9 @@ class TouchPanel(QMainWindow):
self.last_net[key] = current self.last_net[key] = current
self.current_rates = rates 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" 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["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"]) 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["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.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["rpm"].set_value(f"{rpm:,}")
self.metrics["temp"].set_value(f"{temp:.1f} C") self.metrics["temp"].set_value(f"{temp:.1f} C")
self.metrics["pwm"].set_value(f"{pwm} / {percent:.1f}%") self.metrics["pwm"].set_value(f"{pwm} / {percent:.1f}%")
wan_factor, wan_unit = byte_rate_unit([rates["wan"]["rx"], rates["wan"]["tx"]]) self.metrics["mem"].set_value(f"{mem_percent:.1f}%")
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.slider.blockSignals(True) self.slider.blockSignals(True)
self.slider.setValue(pwm) self.slider.setValue(pwm)
self.slider.blockSignals(False) self.slider.blockSignals(False)
del enable
self.clock.setText(time.strftime("%H:%M:%S")) self.clock.setText(time.strftime("%H:%M:%S"))
self.latest_temp = temp self.update_graphs()
self.latest_rpm = rpm
def ping_pc(self): def ping_pc(self):
if self.pc_ping.state() != QProcess.NotRunning: if self.pc_ping.state() != QProcess.NotRunning: