터치 패널 활동 카드 교체
This commit is contained in:
1 parent
64ded5e566
commit
c22f84e430
2 files changed
+204
-187
No files matched your search
@@ -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` 버튼을 둡니다.
|
||||
|
||||
`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`로 표시합니다.
|
||||
|
||||
|
||||
+202
-185
@@ -2,8 +2,6 @@
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import os
|
||||
import shutil
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
|
||||
@@ -31,13 +29,6 @@ NET_GROUPS = {
|
||||
|
||||
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"),
|
||||
("rpi_mariadb", "mariadb.service"),
|
||||
("rpi_control", "control-websocket.service"),
|
||||
("rpi_fanpolicy", "fanpanel-apply.service"),
|
||||
]
|
||||
SYNOLOGY_PING_HOST = "chaegeon.com"
|
||||
SYNOLOGY_SSH = [
|
||||
"/usr/bin/ssh",
|
||||
@@ -75,32 +66,30 @@ while :; do
|
||||
$3 == "sata2" {printf "SATA2_READS=%s\nSATA2_WRITES=%s\n", $6, $10}
|
||||
$3 == "sata4" {printf "SATA4_READS=%s\nSATA4_WRITES=%s\n", $6, $10}
|
||||
' /proc/diskstats
|
||||
awk '{printf "LOAD1=%s\nLOAD5=%s\nLOAD15=%s\n", $1, $2, $3}' /proc/loadavg
|
||||
awk '
|
||||
/MemTotal:/ {mem_total=$2}
|
||||
/MemAvailable:/ {mem_avail=$2}
|
||||
/SwapTotal:/ {swap_total=$2}
|
||||
/SwapFree:/ {swap_free=$2}
|
||||
/^cpu / {
|
||||
total=0
|
||||
for (i=2; i<=NF; i++) total += $i
|
||||
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 {
|
||||
if (mem_total > 0) printf "MEM_USED_PCT=%.0f\n", (mem_total - mem_avail) * 100 / mem_total
|
||||
if (swap_total > 0) printf "SWAP_USED_PCT=%.0f\n", (swap_total - swap_free) * 100 / swap_total
|
||||
else printf "SWAP_USED_PCT=0\n"
|
||||
printf "TCP_IN=%s\nTCP_OUT=%s\nTCP_RETRANS=%s\n", tcp["InSegs"] + 0, tcp["OutSegs"] + 0, tcp["RetransSegs"] + 0
|
||||
printf "UDP_IN=%s\nUDP_OUT=%s\nUDP_ERRORS=%s\n", udp["InDatagrams"] + 0, udp["OutDatagrams"] + 0, udp["InErrors"] + udp["RcvbufErrors"] + udp["SndbufErrors"] + 0
|
||||
}
|
||||
' /proc/meminfo
|
||||
awk '{printf "UPTIME=%d\n", $1}' /proc/uptime
|
||||
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
|
||||
}
|
||||
}
|
||||
'
|
||||
' /proc/net/snmp
|
||||
awk '/^(pgfault|pgmajfault|pgpgin|pgpgout) / {key=toupper($1); printf "%s=%s\n", key, $2}' /proc/vmstat
|
||||
echo END=1
|
||||
sleep 0.1
|
||||
done
|
||||
@@ -132,7 +121,6 @@ DISK_TEMP_CACHE_FILE = "/run/control-rpi-disk-temp"
|
||||
DISK_TEMP_CACHE_MAX_AGE_SECONDS = 30
|
||||
RPI_THROTTLE_INTERVAL_MS = SAMPLE_INTERVAL_MS
|
||||
SYNOLOGY_HEALTH_INTERVAL_MS = FAN_INTERVAL_MS
|
||||
RPI_SERVICE_INTERVAL_MS = 5000
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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=""):
|
||||
try:
|
||||
return Path(path).read_text(encoding="utf-8").strip()
|
||||
@@ -343,6 +276,69 @@ def disk_io():
|
||||
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):
|
||||
clean = mac.replace(":", "").replace("-", "")
|
||||
if len(clean) != 12:
|
||||
@@ -656,6 +652,8 @@ class TouchPanel(QMainWindow):
|
||||
self.last_net = {}
|
||||
self.last_synology = {}
|
||||
self.last_synology_net_rate = {}
|
||||
self.last_rpi_activity = {}
|
||||
self.last_synology_activity = {}
|
||||
self.synology_buffer = ""
|
||||
self.synology_values = {}
|
||||
self.synology_cpu_temp = 0.0
|
||||
@@ -686,10 +684,6 @@ class TouchPanel(QMainWindow):
|
||||
self.sample_timer.timeout.connect(self.sample)
|
||||
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.timeout.connect(self.update_fan_status)
|
||||
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.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.readyReadStandardOutput.connect(self.synology_status_ready)
|
||||
self.synology_proc.finished.connect(self.synology_status_finished)
|
||||
@@ -745,12 +733,10 @@ class TouchPanel(QMainWindow):
|
||||
self.update_fan_status()
|
||||
self.ping_pc()
|
||||
self.update_disk_temp()
|
||||
self.update_rpi_services()
|
||||
self.update_synology_status()
|
||||
self.update_synology_health()
|
||||
self.ping_synology()
|
||||
self.update_rpi_throttle()
|
||||
self.update_dashboard()
|
||||
|
||||
def _build_ui(self):
|
||||
root = QWidget()
|
||||
@@ -764,34 +750,24 @@ class TouchPanel(QMainWindow):
|
||||
|
||||
rpi_rows = [
|
||||
(("io", "SSD I/O"), ("rpi_temp", "TEMP"), ("rpi_fan", "FAN")),
|
||||
(("rpi_throttle", "THROT"), ("rpi_load", "LOAD"), ("rpi_mem", "MEM")),
|
||||
(("rpi_root", "ROOT"), ("rpi_uptime", "UPTIME"), ("local", "LOCAL")),
|
||||
(("rpi_throttle", "THROT"), ("rpi_cpu_activity", "CPU"), ("rpi_irq", "IRQ")),
|
||||
(("rpi_ctx", "CTX"), ("rpi_fork", "FORK"), ("local", "LOCAL")),
|
||||
(("wan", "UPLINK"), ("lan", "ETH1"), ("wifi5", "5G")),
|
||||
(("wifi24", "2.4G"), ("rpi_docker", "DOCKER"), ("rpi_nginx", "NGINX")),
|
||||
(("rpi_mariadb", "MARIADB"), ("rpi_control", "CONTROL"), ("rpi_fanpolicy", "FANPOL")),
|
||||
(("wifi24", "2.4G"), ("rpi_softirq", "SOFTIRQ"), ("rpi_tcp", "TCP")),
|
||||
(("rpi_udp", "UDP"), ("rpi_vm", "VM"), ("rpi_runq", "RUNQ")),
|
||||
]
|
||||
synology_rows = [
|
||||
(("synology_ping", "PING"), ("synology_temp", "TEMP"), ("synology_fan", "FAN")),
|
||||
(("synology_guard", "GUARD"), ("synology_pwm", "PWM"), ("synology_load", "LOAD")),
|
||||
(("synology_mem", "MEM"), ("synology_swap", "SWAP"), ("synology_uptime", "UPTIME")),
|
||||
(("synology_root", "ROOT"), ("synology_vol1", "VOL1"), ("synology_vol2", "VOL2")),
|
||||
(("synology_local", "LOCAL"), ("synology_wan", "UPLINK"), ("synology_ssh", "SSH")),
|
||||
(("synology_guard", "GUARD"), ("synology_cpu_activity", "CPU"), ("synology_irq", "IRQ")),
|
||||
(("synology_ctx", "CTX"), ("synology_fork", "FORK"), ("synology_softirq", "SOFTIRQ")),
|
||||
(("synology_tcp", "TCP"), ("synology_udp", "UDP"), ("synology_vm", "VM")),
|
||||
(("synology_local", "LOCAL"), ("synology_wan", "UPLINK"), ("synology_runq", "RUNQ")),
|
||||
(("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)
|
||||
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(right, 1)
|
||||
layout.addLayout(panel_layout, 1)
|
||||
@@ -886,6 +862,7 @@ class TouchPanel(QMainWindow):
|
||||
self.current_rates = rates
|
||||
self.update_wan_graph()
|
||||
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):
|
||||
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
|
||||
|
||||
def update_dashboard(self):
|
||||
self.clock.setText(time.strftime("%H:%M:%S"))
|
||||
self.render_rpi_system()
|
||||
def render_rpi_activity(self):
|
||||
self.render_activity_tiles("rpi", kernel_activity(), "last_rpi_activity")
|
||||
|
||||
def render_rpi_system(self):
|
||||
cores = max(1, os.cpu_count() or 1)
|
||||
load = load_average()
|
||||
self.rows["rpi_load"].set_state(
|
||||
f"{load:.2f}",
|
||||
read_steady=load < cores * 0.7,
|
||||
write_steady=cores * 0.7 <= load < cores * 1.2,
|
||||
error_steady=load >= cores * 1.2,
|
||||
)
|
||||
self.set_percent_tile("rpi_mem", memory_used_percent(), warn=80, critical=92)
|
||||
root_used = disk_used_percent("/")
|
||||
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)
|
||||
|
||||
def update_rpi_services(self):
|
||||
if self.rpi_services_proc.state() != QProcess.NotRunning:
|
||||
def render_activity_tiles(self, prefix, values, previous_attr):
|
||||
row_keys = [
|
||||
"cpu_activity",
|
||||
"irq",
|
||||
"ctx",
|
||||
"fork",
|
||||
"softirq",
|
||||
"tcp",
|
||||
"udp",
|
||||
"vm",
|
||||
"runq",
|
||||
]
|
||||
if not values:
|
||||
for suffix in row_keys:
|
||||
self.rows[f"{prefix}_{suffix}"].set_state("-", read_steady=False, write_steady=False, error_steady=True)
|
||||
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):
|
||||
del exit_code, exit_status
|
||||
output = bytes(self.rpi_services_proc.readAllStandardOutput()).decode("utf-8", "ignore")
|
||||
states = output.splitlines()
|
||||
for index, (key, _unit) in enumerate(RPI_SERVICE_UNITS):
|
||||
state = states[index].strip() if index < len(states) else "unknown"
|
||||
active = state == "active"
|
||||
detail = "OK" if active else state.upper()[:6]
|
||||
self.rows[key].set_state(detail, read_steady=active, write_steady=False, error_steady=not active)
|
||||
current = {key: self.int_value(value) for key, value in values.items()}
|
||||
previous = getattr(self, previous_attr)
|
||||
has_previous = bool(previous)
|
||||
|
||||
def delta(name):
|
||||
if not has_previous:
|
||||
return 0
|
||||
return max(0, current.get(name, 0) - previous.get(name, current.get(name, 0)))
|
||||
|
||||
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):
|
||||
if self.pc_ping.state() != QProcess.NotRunning:
|
||||
@@ -1085,15 +1122,15 @@ class TouchPanel(QMainWindow):
|
||||
"synology_wan",
|
||||
"synology_local",
|
||||
"synology_guard",
|
||||
"synology_pwm",
|
||||
"synology_load",
|
||||
"synology_mem",
|
||||
"synology_swap",
|
||||
"synology_uptime",
|
||||
"synology_root",
|
||||
"synology_vol1",
|
||||
"synology_vol2",
|
||||
"synology_ssh",
|
||||
"synology_cpu_activity",
|
||||
"synology_irq",
|
||||
"synology_ctx",
|
||||
"synology_fork",
|
||||
"synology_softirq",
|
||||
"synology_tcp",
|
||||
"synology_udp",
|
||||
"synology_vm",
|
||||
"synology_runq",
|
||||
]:
|
||||
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),
|
||||
"lo",
|
||||
)
|
||||
self.render_synology_system(values)
|
||||
self.rows["synology_ssh"].set_state("OK", read_steady=True, write_steady=False, error_steady=False)
|
||||
self.render_activity_tiles("synology", values, "last_synology_activity")
|
||||
|
||||
def set_synology_io_tile(self, key, reads, writes, detail):
|
||||
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)
|
||||
else:
|
||||
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):
|
||||
detail = f"{int(rpm):,} rpm" if rpm > 0 else "0 rpm"
|
||||
@@ -1307,6 +1319,13 @@ class TouchPanel(QMainWindow):
|
||||
except (TypeError, ValueError):
|
||||
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):
|
||||
if percent is None:
|
||||
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; }
|
||||
#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; }
|
||||
#clock { font-size: 18px; color: #8fb3ff; font-weight: 700; }
|
||||
#statusText { color: #93a4bd; font-size: 13px; background: transparent; }
|
||||
"""
|
||||
)
|
||||
win = TouchPanel()
|
||||
|
||||
Reference in new issue
Block a user