터치 패널 활동 카드 교체

This commit is contained in:
seo committed 2026-08-14 02:38:26 +09:00
1 parent 64ded5e566
commit c22f84e430
2 files changed
+204 -187

No files matched your search

+2 -2
View File
@@ -157,9 +157,9 @@ cd /var/www/control
패널은 Control PHP/API/DB snapshot을 호출하지 않는 standalone 방식입니다. Python이 `/sys/class/net`, `/proc/diskstats`, `/sys/class/thermal`, 팬 hwmon 파일을 직접 읽습니다. 화면은 왼쪽 `RASPBERRY` 18칸, 오른쪽 `SYNOLOGY` 18칸 카드로 나눕니다. 하단에는 좌우 6칸 폭을 한 번에 쓰는 긴 `PC` 버튼을 둡니다. 패널은 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`로 확인합니다. `RASPBERRY` 카드는 `SSD I/O`, `TEMP`, `FAN`, `THROT`, `CPU`, `IRQ`, `CTX`, `FORK`, `LOCAL`, `UPLINK`, `ETH1`, `5G`, `2.4G`, `SOFTIRQ`, `TCP`, `UDP`, `VM`, `RUNQ`를 표시합니다. 네트워크와 SSD I/O 점멸은 100ms 샘플에서 직전 카운터보다 증가했는지를 보고, CPU/IRQ/CTX/FORK/SOFTIRQ/TCP/UDP/VM/RUNQ 카드는 `/proc/stat`, `/proc/net/snmp`, `/proc/vmstat`의 누적 카운터 증가량으로 점멸합니다. CPU 온도/SSD 온도와 팬 RPM은 500ms 간격으로 표시합니다.
`SYNOLOGY` 카드는 `PING`, `TEMP`, `FAN`, `GUARD`, `PWM`, `LOAD`, `MEM`, `SWAP`, `UPTIME`, `ROOT`, `VOL1`, `VOL2`, `LOCAL`, `UPLINK`, `SSH`, `SATA1 I/O`, `SATA2 I/O`, `SATA4 I/O`를 표시합니다. 시놀로지는 SSH를 유지한 채 100ms마다 네트워크, 디스크 sector I/O, load, memory, swap, uptime, filesystem 사용률을 스트림으로 가져옵니다. SATA SMART/온도 직접 조회는 응답 지연 위험 때문에 대시보드에서 실행하지 않고, SATA I/O 카드에는 `조회 중지`를 표시합니다. CPU 온도, 팬 RPM, PWM, fan guard 상태는 500ms 간격으로 별도 조회합니다. `SYNOLOGY` 카드는 `PING`, `TEMP`, `FAN`, `GUARD`, `CPU`, `IRQ`, `CTX`, `FORK`, `SOFTIRQ`, `TCP`, `UDP`, `VM`, `LOCAL`, `UPLINK`, `RUNQ`, `SATA1 I/O`, `SATA2 I/O`, `SATA4 I/O`를 표시합니다. 시놀로지는 SSH를 유지한 채 100ms마다 네트워크, 디스크 sector I/O, CPU/IRQ/CTX/FORK/SOFTIRQ/TCP/UDP/VM/RUNQ 카운터를 스트림으로 가져옵니다. SATA SMART/온도 직접 조회는 응답 지연 위험 때문에 대시보드에서 실행하지 않고, SATA I/O 카드에는 `조회 중지`를 표시합니다. CPU 온도, 팬 RPM, fan guard 상태는 500ms 간격으로 별도 조회합니다.
온도 카드는 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`로 표시합니다. 온도 카드는 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`로 표시합니다.
+202 -185
View File
@@ -2,8 +2,6 @@
import sys import sys
import time import time
import socket import socket
import os
import shutil
from collections import deque from collections import deque
from pathlib import Path from pathlib import Path
@@ -31,13 +29,6 @@ NET_GROUPS = {
PC_IP = "192.168.50.73" PC_IP = "192.168.50.73"
PC_MAC = "74:56:3C:CD:CD:79" PC_MAC = "74:56:3C:CD:CD:79"
RPI_SERVICE_UNITS = [
("rpi_docker", "docker.service"),
("rpi_nginx", "nginx.service"),
("rpi_mariadb", "mariadb.service"),
("rpi_control", "control-websocket.service"),
("rpi_fanpolicy", "fanpanel-apply.service"),
]
SYNOLOGY_PING_HOST = "chaegeon.com" SYNOLOGY_PING_HOST = "chaegeon.com"
SYNOLOGY_SSH = [ SYNOLOGY_SSH = [
"/usr/bin/ssh", "/usr/bin/ssh",
@@ -75,32 +66,30 @@ while :; do
$3 == "sata2" {printf "SATA2_READS=%s\nSATA2_WRITES=%s\n", $6, $10} $3 == "sata2" {printf "SATA2_READS=%s\nSATA2_WRITES=%s\n", $6, $10}
$3 == "sata4" {printf "SATA4_READS=%s\nSATA4_WRITES=%s\n", $6, $10} $3 == "sata4" {printf "SATA4_READS=%s\nSATA4_WRITES=%s\n", $6, $10}
' /proc/diskstats ' /proc/diskstats
awk '{printf "LOAD1=%s\nLOAD5=%s\nLOAD15=%s\n", $1, $2, $3}' /proc/loadavg
awk ' awk '
/MemTotal:/ {mem_total=$2} /^cpu / {
/MemAvailable:/ {mem_avail=$2} total=0
/SwapTotal:/ {swap_total=$2} for (i=2; i<=NF; i++) total += $i
/SwapFree:/ {swap_free=$2} printf "CPU_TOTAL=%s\nCPU_IDLE=%s\nCPU_USER=%s\nCPU_NICE=%s\nCPU_SYSTEM=%s\nCPU_IOWAIT=%s\nCPU_IRQ=%s\nCPU_SOFTIRQ=%s\n", total, $5 + $6, $2, $3, $4, $6, $7, $8
}
/^intr / {printf "INTR=%s\n", $2}
/^ctxt / {printf "CTXT=%s\n", $2}
/^processes / {printf "PROCESSES=%s\n", $2}
/^procs_running / {printf "PROCS_RUNNING=%s\n", $2}
/^procs_blocked / {printf "PROCS_BLOCKED=%s\n", $2}
/^softirq / {printf "SOFTIRQ=%s\n", $2}
' /proc/stat
awk '
$1 == "Tcp:" && tcp_header == 0 {for (i=2; i<=NF; i++) tcp_key[i]=$i; tcp_header=1; next}
$1 == "Tcp:" && tcp_header == 1 {for (i=2; i<=NF; i++) tcp[tcp_key[i]]=$i; tcp_header=0; next}
$1 == "Udp:" && udp_header == 0 {for (i=2; i<=NF; i++) udp_key[i]=$i; udp_header=1; next}
$1 == "Udp:" && udp_header == 1 {for (i=2; i<=NF; i++) udp[udp_key[i]]=$i; udp_header=0; next}
END { END {
if (mem_total > 0) printf "MEM_USED_PCT=%.0f\n", (mem_total - mem_avail) * 100 / mem_total printf "TCP_IN=%s\nTCP_OUT=%s\nTCP_RETRANS=%s\n", tcp["InSegs"] + 0, tcp["OutSegs"] + 0, tcp["RetransSegs"] + 0
if (swap_total > 0) printf "SWAP_USED_PCT=%.0f\n", (swap_total - swap_free) * 100 / swap_total printf "UDP_IN=%s\nUDP_OUT=%s\nUDP_ERRORS=%s\n", udp["InDatagrams"] + 0, udp["OutDatagrams"] + 0, udp["InErrors"] + udp["RcvbufErrors"] + udp["SndbufErrors"] + 0
else printf "SWAP_USED_PCT=0\n"
} }
' /proc/meminfo ' /proc/net/snmp
awk '{printf "UPTIME=%d\n", $1}' /proc/uptime awk '/^(pgfault|pgmajfault|pgpgin|pgpgout) / {key=toupper($1); printf "%s=%s\n", key, $2}' /proc/vmstat
df -P / /volume1 /volume2 2>/dev/null | awk '
NR > 1 {
key = ""
if ($6 == "/") key = "ROOT"
if ($6 == "/volume1") key = "VOL1"
if ($6 == "/volume2") key = "VOL2"
if (key != "") {
pct = $5
gsub("%", "", pct)
printf "%s_FS_PCT=%s\n", key, pct
}
}
'
echo END=1 echo END=1
sleep 0.1 sleep 0.1
done done
@@ -132,7 +121,6 @@ DISK_TEMP_CACHE_FILE = "/run/control-rpi-disk-temp"
DISK_TEMP_CACHE_MAX_AGE_SECONDS = 30 DISK_TEMP_CACHE_MAX_AGE_SECONDS = 30
RPI_THROTTLE_INTERVAL_MS = SAMPLE_INTERVAL_MS RPI_THROTTLE_INTERVAL_MS = SAMPLE_INTERVAL_MS
SYNOLOGY_HEALTH_INTERVAL_MS = FAN_INTERVAL_MS SYNOLOGY_HEALTH_INTERVAL_MS = FAN_INTERVAL_MS
RPI_SERVICE_INTERVAL_MS = 5000
def byte_rate_unit(values): def byte_rate_unit(values):
@@ -172,61 +160,6 @@ def format_temp_rpm_caption(temp, rpm):
return f"{temp:5.1f} C / {int(rpm):5d} rpm" return f"{temp:5.1f} C / {int(rpm):5d} rpm"
def format_uptime(seconds):
seconds = max(0, int(float(seconds or 0)))
days = seconds // 86400
hours = (seconds % 86400) // 3600
minutes = (seconds % 3600) // 60
if days > 0:
return f"{days}d {hours}h"
if hours > 0:
return f"{hours}h {minutes}m"
return f"{minutes}m"
def load_average():
parts = read_text("/proc/loadavg").split()
try:
return float(parts[0])
except (IndexError, ValueError):
return 0.0
def memory_used_percent(swap=False):
values = {}
for line in read_text("/proc/meminfo").splitlines():
if ":" not in line:
continue
key, rest = line.split(":", 1)
parts = rest.strip().split()
if parts and parts[0].isdigit():
values[key] = int(parts[0])
if swap:
total = values.get("SwapTotal", 0)
free = values.get("SwapFree", 0)
return 0.0 if total <= 0 else (total - free) * 100 / total
total = values.get("MemTotal", 0)
available = values.get("MemAvailable", 0)
return 0.0 if total <= 0 else (total - available) * 100 / total
def disk_used_percent(path):
try:
usage = shutil.disk_usage(path)
except OSError:
return None
return usage.used * 100 / usage.total if usage.total > 0 else None
def uptime_seconds():
try:
return float(read_text("/proc/uptime").split()[0])
except (IndexError, ValueError):
return 0.0
def read_text(path, default=""): def read_text(path, default=""):
try: try:
return Path(path).read_text(encoding="utf-8").strip() return Path(path).read_text(encoding="utf-8").strip()
@@ -343,6 +276,69 @@ def disk_io():
return reads, writes, not found return reads, writes, not found
def kernel_activity():
values = {}
for line in read_text("/proc/stat").splitlines():
parts = line.split()
if not parts:
continue
key = parts[0]
if key == "cpu":
ticks = [int(value) for value in parts[1:] if value.isdigit()]
if len(ticks) >= 7:
values["CPU_TOTAL"] = sum(ticks)
values["CPU_IDLE"] = ticks[3] + (ticks[4] if len(ticks) > 4 else 0)
values["CPU_USER"] = ticks[0]
values["CPU_NICE"] = ticks[1]
values["CPU_SYSTEM"] = ticks[2]
values["CPU_IOWAIT"] = ticks[4] if len(ticks) > 4 else 0
values["CPU_IRQ"] = ticks[5] if len(ticks) > 5 else 0
values["CPU_SOFTIRQ"] = ticks[6] if len(ticks) > 6 else 0
elif key in {"intr", "ctxt", "processes", "procs_running", "procs_blocked", "softirq"} and len(parts) > 1:
values[key.upper()] = int(parts[1]) if parts[1].isdigit() else 0
snmp_headers = {}
for line in read_text("/proc/net/snmp").splitlines():
parts = line.split()
if not parts:
continue
family = parts[0].rstrip(":")
if family not in {"Tcp", "Udp"}:
continue
if family not in snmp_headers:
snmp_headers[family] = parts[1:]
continue
row = dict(zip(snmp_headers[family], parts[1:]))
if family == "Tcp":
values["TCP_IN"] = int(row.get("InSegs", "0") or 0)
values["TCP_OUT"] = int(row.get("OutSegs", "0") or 0)
values["TCP_RETRANS"] = int(row.get("RetransSegs", "0") or 0)
elif family == "Udp":
values["UDP_IN"] = int(row.get("InDatagrams", "0") or 0)
values["UDP_OUT"] = int(row.get("OutDatagrams", "0") or 0)
values["UDP_ERRORS"] = (
int(row.get("InErrors", "0") or 0)
+ int(row.get("RcvbufErrors", "0") or 0)
+ int(row.get("SndbufErrors", "0") or 0)
)
del snmp_headers[family]
for line in read_text("/proc/vmstat").splitlines():
parts = line.split()
if len(parts) >= 2 and parts[0] in {"pgfault", "pgmajfault", "pgpgin", "pgpgout"}:
values[parts[0].upper()] = int(parts[1]) if parts[1].isdigit() else 0
return values
def compact_count(value):
value = int(max(0, value))
if value >= 1_000_000:
return f"{value / 1_000_000:.1f}M"
if value >= 1_000:
return f"{value / 1_000:.1f}k"
return str(value)
def send_magic_packet(mac): def send_magic_packet(mac):
clean = mac.replace(":", "").replace("-", "") clean = mac.replace(":", "").replace("-", "")
if len(clean) != 12: if len(clean) != 12:
@@ -656,6 +652,8 @@ class TouchPanel(QMainWindow):
self.last_net = {} self.last_net = {}
self.last_synology = {} self.last_synology = {}
self.last_synology_net_rate = {} self.last_synology_net_rate = {}
self.last_rpi_activity = {}
self.last_synology_activity = {}
self.synology_buffer = "" self.synology_buffer = ""
self.synology_values = {} self.synology_values = {}
self.synology_cpu_temp = 0.0 self.synology_cpu_temp = 0.0
@@ -686,10 +684,6 @@ class TouchPanel(QMainWindow):
self.sample_timer.timeout.connect(self.sample) self.sample_timer.timeout.connect(self.sample)
self.sample_timer.start(SAMPLE_INTERVAL_MS) self.sample_timer.start(SAMPLE_INTERVAL_MS)
self.dashboard_timer = QTimer(self)
self.dashboard_timer.timeout.connect(self.update_dashboard)
self.dashboard_timer.start(DASHBOARD_INTERVAL_MS)
self.fan_timer = QTimer(self) self.fan_timer = QTimer(self)
self.fan_timer.timeout.connect(self.update_fan_status) self.fan_timer.timeout.connect(self.update_fan_status)
self.fan_timer.start(FAN_INTERVAL_MS) self.fan_timer.start(FAN_INTERVAL_MS)
@@ -704,12 +698,6 @@ class TouchPanel(QMainWindow):
self.disk_temp_timer.timeout.connect(self.update_disk_temp) self.disk_temp_timer.timeout.connect(self.update_disk_temp)
self.disk_temp_timer.start(DISK_TEMP_INTERVAL_MS) self.disk_temp_timer.start(DISK_TEMP_INTERVAL_MS)
self.rpi_services_proc = QProcess(self)
self.rpi_services_proc.finished.connect(self.rpi_services_finished)
self.rpi_services_timer = QTimer(self)
self.rpi_services_timer.timeout.connect(self.update_rpi_services)
self.rpi_services_timer.start(RPI_SERVICE_INTERVAL_MS)
self.synology_proc = QProcess(self) self.synology_proc = QProcess(self)
self.synology_proc.readyReadStandardOutput.connect(self.synology_status_ready) self.synology_proc.readyReadStandardOutput.connect(self.synology_status_ready)
self.synology_proc.finished.connect(self.synology_status_finished) self.synology_proc.finished.connect(self.synology_status_finished)
@@ -745,12 +733,10 @@ class TouchPanel(QMainWindow):
self.update_fan_status() self.update_fan_status()
self.ping_pc() self.ping_pc()
self.update_disk_temp() self.update_disk_temp()
self.update_rpi_services()
self.update_synology_status() self.update_synology_status()
self.update_synology_health() self.update_synology_health()
self.ping_synology() self.ping_synology()
self.update_rpi_throttle() self.update_rpi_throttle()
self.update_dashboard()
def _build_ui(self): def _build_ui(self):
root = QWidget() root = QWidget()
@@ -764,34 +750,24 @@ class TouchPanel(QMainWindow):
rpi_rows = [ rpi_rows = [
(("io", "SSD I/O"), ("rpi_temp", "TEMP"), ("rpi_fan", "FAN")), (("io", "SSD I/O"), ("rpi_temp", "TEMP"), ("rpi_fan", "FAN")),
(("rpi_throttle", "THROT"), ("rpi_load", "LOAD"), ("rpi_mem", "MEM")), (("rpi_throttle", "THROT"), ("rpi_cpu_activity", "CPU"), ("rpi_irq", "IRQ")),
(("rpi_root", "ROOT"), ("rpi_uptime", "UPTIME"), ("local", "LOCAL")), (("rpi_ctx", "CTX"), ("rpi_fork", "FORK"), ("local", "LOCAL")),
(("wan", "UPLINK"), ("lan", "ETH1"), ("wifi5", "5G")), (("wan", "UPLINK"), ("lan", "ETH1"), ("wifi5", "5G")),
(("wifi24", "2.4G"), ("rpi_docker", "DOCKER"), ("rpi_nginx", "NGINX")), (("wifi24", "2.4G"), ("rpi_softirq", "SOFTIRQ"), ("rpi_tcp", "TCP")),
(("rpi_mariadb", "MARIADB"), ("rpi_control", "CONTROL"), ("rpi_fanpolicy", "FANPOL")), (("rpi_udp", "UDP"), ("rpi_vm", "VM"), ("rpi_runq", "RUNQ")),
] ]
synology_rows = [ synology_rows = [
(("synology_ping", "PING"), ("synology_temp", "TEMP"), ("synology_fan", "FAN")), (("synology_ping", "PING"), ("synology_temp", "TEMP"), ("synology_fan", "FAN")),
(("synology_guard", "GUARD"), ("synology_pwm", "PWM"), ("synology_load", "LOAD")), (("synology_guard", "GUARD"), ("synology_cpu_activity", "CPU"), ("synology_irq", "IRQ")),
(("synology_mem", "MEM"), ("synology_swap", "SWAP"), ("synology_uptime", "UPTIME")), (("synology_ctx", "CTX"), ("synology_fork", "FORK"), ("synology_softirq", "SOFTIRQ")),
(("synology_root", "ROOT"), ("synology_vol1", "VOL1"), ("synology_vol2", "VOL2")), (("synology_tcp", "TCP"), ("synology_udp", "UDP"), ("synology_vm", "VM")),
(("synology_local", "LOCAL"), ("synology_wan", "UPLINK"), ("synology_ssh", "SSH")), (("synology_local", "LOCAL"), ("synology_wan", "UPLINK"), ("synology_runq", "RUNQ")),
(("synology_sata1", "SATA1 I/O"), ("synology_sata2", "SATA2 I/O"), ("synology_sata4", "SATA4 I/O")), (("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) left = self.build_status_panel("RASPBERRY", rpi_rows, fixed_width=398)
right = self.build_status_panel("SYNOLOGY", synology_rows) right = self.build_status_panel("SYNOLOGY", synology_rows)
header = QHBoxLayout()
self.clock = QLabel("--:--:--")
self.clock.setObjectName("clock")
self.status_text = QLabel("")
self.status_text.setObjectName("statusText")
header.addStretch(1)
header.addWidget(self.status_text)
header.addWidget(self.clock)
right.layout().insertLayout(1, header)
panel_layout.addWidget(left) panel_layout.addWidget(left)
panel_layout.addWidget(right, 1) panel_layout.addWidget(right, 1)
layout.addLayout(panel_layout, 1) layout.addLayout(panel_layout, 1)
@@ -886,6 +862,7 @@ class TouchPanel(QMainWindow):
self.current_rates = rates self.current_rates = rates
self.update_wan_graph() 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)
self.render_rpi_activity()
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):
disk_temp_text = f"{self.disk_temp:.1f} C" if self.disk_temp is not None else "" disk_temp_text = f"{self.disk_temp:.1f} C" if self.disk_temp is not None else ""
@@ -902,39 +879,99 @@ class TouchPanel(QMainWindow):
self.latest_rpm = rpm self.latest_rpm = rpm
def update_dashboard(self): def render_rpi_activity(self):
self.clock.setText(time.strftime("%H:%M:%S")) self.render_activity_tiles("rpi", kernel_activity(), "last_rpi_activity")
self.render_rpi_system()
def render_rpi_system(self): def render_activity_tiles(self, prefix, values, previous_attr):
cores = max(1, os.cpu_count() or 1) row_keys = [
load = load_average() "cpu_activity",
self.rows["rpi_load"].set_state( "irq",
f"{load:.2f}", "ctx",
read_steady=load < cores * 0.7, "fork",
write_steady=cores * 0.7 <= load < cores * 1.2, "softirq",
error_steady=load >= cores * 1.2, "tcp",
) "udp",
self.set_percent_tile("rpi_mem", memory_used_percent(), warn=80, critical=92) "vm",
root_used = disk_used_percent("/") "runq",
self.set_percent_tile("rpi_root", root_used, warn=80, critical=92) ]
self.rows["rpi_uptime"].set_state(format_uptime(uptime_seconds()), read_steady=True, write_steady=False, error_steady=False) if not values:
for suffix in row_keys:
def update_rpi_services(self): self.rows[f"{prefix}_{suffix}"].set_state("-", read_steady=False, write_steady=False, error_steady=True)
if self.rpi_services_proc.state() != QProcess.NotRunning:
return return
units = [unit for _key, unit in RPI_SERVICE_UNITS]
self.rpi_services_proc.start("/usr/bin/systemctl", ["is-active"] + units)
def rpi_services_finished(self, exit_code, exit_status): current = {key: self.int_value(value) for key, value in values.items()}
del exit_code, exit_status previous = getattr(self, previous_attr)
output = bytes(self.rpi_services_proc.readAllStandardOutput()).decode("utf-8", "ignore") has_previous = bool(previous)
states = output.splitlines()
for index, (key, _unit) in enumerate(RPI_SERVICE_UNITS): def delta(name):
state = states[index].strip() if index < len(states) else "unknown" if not has_previous:
active = state == "active" return 0
detail = "OK" if active else state.upper()[:6] return max(0, current.get(name, 0) - previous.get(name, current.get(name, 0)))
self.rows[key].set_state(detail, read_steady=active, write_steady=False, error_steady=not active)
def changed(name):
return has_previous and current.get(name, 0) != previous.get(name, 0)
total_delta = delta("CPU_TOTAL")
idle_delta = delta("CPU_IDLE")
cpu_used = 0.0 if total_delta <= 0 else max(0.0, (total_delta - idle_delta) * 100 / total_delta)
self.rows[f"{prefix}_cpu_activity"].set_state(
f"{cpu_used:.0f}%",
read_activity=delta("CPU_USER") + delta("CPU_NICE") > 0,
write_activity=delta("CPU_SYSTEM") + delta("CPU_IRQ") + delta("CPU_SOFTIRQ") > 0,
error=delta("CPU_IOWAIT") > 0,
)
irq_delta = delta("INTR")
self.rows[f"{prefix}_irq"].set_state(f"+{compact_count(irq_delta)}", read_activity=irq_delta > 0)
ctx_delta = delta("CTXT")
self.rows[f"{prefix}_ctx"].set_state(f"+{compact_count(ctx_delta)}", read_activity=ctx_delta > 0)
fork_delta = delta("PROCESSES")
self.rows[f"{prefix}_fork"].set_state(f"+{compact_count(fork_delta)}", write_activity=fork_delta > 0)
softirq_delta = delta("SOFTIRQ")
self.rows[f"{prefix}_softirq"].set_state(f"+{compact_count(softirq_delta)}", write_activity=softirq_delta > 0)
tcp_in = delta("TCP_IN")
tcp_out = delta("TCP_OUT")
tcp_retrans = delta("TCP_RETRANS")
self.rows[f"{prefix}_tcp"].set_state(
f"I{compact_count(tcp_in)}/O{compact_count(tcp_out)}",
error=tcp_retrans > 0,
read_activity=tcp_in > 0,
write_activity=tcp_out > 0,
)
udp_in = delta("UDP_IN")
udp_out = delta("UDP_OUT")
udp_errors = delta("UDP_ERRORS")
self.rows[f"{prefix}_udp"].set_state(
f"I{compact_count(udp_in)}/O{compact_count(udp_out)}",
error=udp_errors > 0,
read_activity=udp_in > 0,
write_activity=udp_out > 0,
)
page_faults = delta("PGFAULT")
major_faults = delta("PGMAJFAULT")
paging = delta("PGPGIN") + delta("PGPGOUT")
self.rows[f"{prefix}_vm"].set_state(
f"F{compact_count(page_faults)}",
error=major_faults > 0,
read_activity=page_faults > 0,
write_activity=paging > 0,
)
running = current.get("PROCS_RUNNING", 0)
blocked = current.get("PROCS_BLOCKED", 0)
self.rows[f"{prefix}_runq"].set_state(
f"R{running}/B{blocked}",
read_activity=changed("PROCS_RUNNING"),
write_activity=changed("PROCS_BLOCKED"),
error_steady=blocked > 0,
)
setattr(self, previous_attr, current)
def ping_pc(self): def ping_pc(self):
if self.pc_ping.state() != QProcess.NotRunning: if self.pc_ping.state() != QProcess.NotRunning:
@@ -1085,15 +1122,15 @@ class TouchPanel(QMainWindow):
"synology_wan", "synology_wan",
"synology_local", "synology_local",
"synology_guard", "synology_guard",
"synology_pwm", "synology_cpu_activity",
"synology_load", "synology_irq",
"synology_mem", "synology_ctx",
"synology_swap", "synology_fork",
"synology_uptime", "synology_softirq",
"synology_root", "synology_tcp",
"synology_vol1", "synology_udp",
"synology_vol2", "synology_vm",
"synology_ssh", "synology_runq",
]: ]:
self.rows[key].set_state("-", read_steady=False, write_steady=False, error_steady=True) self.rows[key].set_state("-", read_steady=False, write_steady=False, error_steady=True)
@@ -1124,8 +1161,7 @@ class TouchPanel(QMainWindow):
int(values.get("LOCAL_RX_DROP", "0") or 0) + int(values.get("LOCAL_TX_DROP", "0") or 0), int(values.get("LOCAL_RX_DROP", "0") or 0) + int(values.get("LOCAL_TX_DROP", "0") or 0),
"lo", "lo",
) )
self.render_synology_system(values) self.render_activity_tiles("synology", values, "last_synology_activity")
self.rows["synology_ssh"].set_state("OK", read_steady=True, write_steady=False, error_steady=False)
def set_synology_io_tile(self, key, reads, writes, detail): def set_synology_io_tile(self, key, reads, writes, detail):
previous = self.last_synology.get(key) previous = self.last_synology.get(key)
@@ -1177,30 +1213,6 @@ class TouchPanel(QMainWindow):
self.rows["synology_guard"].set_state("FULL", read_steady=False, write_steady=True, error_steady=False) self.rows["synology_guard"].set_state("FULL", read_steady=False, write_steady=True, error_steady=False)
else: else:
self.rows["synology_guard"].set_state("AUTO", read_steady=True, write_steady=False, error_steady=False) self.rows["synology_guard"].set_state("AUTO", read_steady=True, write_steady=False, error_steady=False)
self.rows["synology_pwm"].set_state(
f"{round(pwm * 100 / 255):.0f}%",
read_steady=0 < pwm < 170,
write_steady=170 <= pwm <= 255,
error_steady=pwm <= 0 or enable != 5,
)
def render_synology_system(self, values):
try:
load = float(values.get("LOAD1", "0") or 0)
except ValueError:
load = 0.0
self.rows["synology_load"].set_state(
f"{load:.2f}",
read_steady=load < 2.8,
write_steady=2.8 <= load < 4.8,
error_steady=load >= 4.8,
)
self.set_percent_tile("synology_mem", self.float_value(values.get("MEM_USED_PCT")), warn=80, critical=92)
self.set_percent_tile("synology_swap", self.float_value(values.get("SWAP_USED_PCT")), warn=50, critical=80)
self.set_percent_tile("synology_root", self.float_value(values.get("ROOT_FS_PCT")), warn=80, critical=92)
self.set_percent_tile("synology_vol1", self.float_value(values.get("VOL1_FS_PCT")), warn=80, critical=92)
self.set_percent_tile("synology_vol2", self.float_value(values.get("VOL2_FS_PCT")), warn=80, critical=92)
self.rows["synology_uptime"].set_state(format_uptime(self.float_value(values.get("UPTIME"))), read_steady=True, write_steady=False, error_steady=False)
def render_fan_tile(self, key, rpm): def render_fan_tile(self, key, rpm):
detail = f"{int(rpm):,} rpm" if rpm > 0 else "0 rpm" detail = f"{int(rpm):,} rpm" if rpm > 0 else "0 rpm"
@@ -1307,6 +1319,13 @@ class TouchPanel(QMainWindow):
except (TypeError, ValueError): except (TypeError, ValueError):
return None return None
@staticmethod
def int_value(value):
try:
return int(float(value))
except (TypeError, ValueError):
return 0
def set_percent_tile(self, key, percent, warn=80, critical=92): def set_percent_tile(self, key, percent, warn=80, critical=92):
if percent is None: if percent is None:
self.rows[key].set_state("-", read_steady=False, write_steady=False, error_steady=True) self.rows[key].set_state("-", read_steady=False, write_steady=False, error_steady=True)
@@ -1341,8 +1360,6 @@ def main():
#chartCard { background: #111a2e; border: 1px solid #27364f; border-radius: 12px; } #chartCard { background: #111a2e; border: 1px solid #27364f; border-radius: 12px; }
#chartTitle { color: #dce8f8; font-size: 14px; font-weight: 700; background: transparent; } #chartTitle { color: #dce8f8; font-size: 14px; font-weight: 700; background: transparent; }
#chartCaption { color: #93a4bd; font-size: 11px; background: transparent; font-family: "DejaVu Sans Mono", monospace; } #chartCaption { color: #93a4bd; font-size: 11px; background: transparent; font-family: "DejaVu Sans Mono", monospace; }
#clock { font-size: 18px; color: #8fb3ff; font-weight: 700; }
#statusText { color: #93a4bd; font-size: 13px; background: transparent; }
""" """
) )
win = TouchPanel() win = TouchPanel()