터치 패널에 시놀로지 입출력 상태패널 추가
This commit is contained in:
1 parent
f9f2d2c79d
commit
8386e2c8e7
2 files changed
+249
-20
No files matched your search
+248
-19
@@ -21,17 +21,80 @@ from PyQt5.QtWidgets import (
|
||||
|
||||
|
||||
NET_GROUPS = {
|
||||
"local": ["lo"],
|
||||
"wan": ["eth0"],
|
||||
"lan": ["br0", "eth1"],
|
||||
"lan": ["eth1"],
|
||||
"wifi24": ["wlan0"],
|
||||
"wifi5": ["wlan1"],
|
||||
}
|
||||
|
||||
PC_IP = "192.168.50.73"
|
||||
PC_MAC = "74:56:3C:CD:CD:79"
|
||||
SYNOLOGY_SSH = [
|
||||
"/usr/bin/ssh",
|
||||
"-i",
|
||||
"/home/seo/.ssh/id_ed25519",
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"IdentitiesOnly=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=5",
|
||||
"-p",
|
||||
"1026",
|
||||
"seo@chaegeon.com",
|
||||
]
|
||||
SYNOLOGY_STATUS_SCRIPT = r"""
|
||||
while :; do
|
||||
wan="$(awk '$2=="00000000"{print $1; exit}' /proc/net/route)"
|
||||
[ -n "$wan" ] || wan=ovs_eth0
|
||||
echo "WAN_IFACE=$wan"
|
||||
awk -v wan="$wan" '
|
||||
NR > 2 {
|
||||
split($0, row, ":")
|
||||
iface = row[1]
|
||||
gsub(/^[ \t]+|[ \t]+$/, "", iface)
|
||||
data = row[2]
|
||||
gsub(/^[ \t]+/, "", data)
|
||||
split(data, f, /[ \t]+/)
|
||||
}
|
||||
iface == wan {printf "WAN_RX=%s\nWAN_TX=%s\nWAN_RX_ERR=%s\nWAN_TX_ERR=%s\nWAN_RX_DROP=%s\nWAN_TX_DROP=%s\n", f[1], f[9], f[3], f[11], f[4], f[12]}
|
||||
iface == "lo" {printf "LOCAL_RX=%s\nLOCAL_TX=%s\nLOCAL_RX_ERR=%s\nLOCAL_TX_ERR=%s\nLOCAL_RX_DROP=%s\nLOCAL_TX_DROP=%s\n", f[1], f[9], f[3], f[11], f[4], f[12]}
|
||||
' /proc/net/dev
|
||||
awk '
|
||||
$3 == "sata1" {printf "SATA1_READS=%s\nSATA1_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}
|
||||
' /proc/diskstats
|
||||
echo END=1
|
||||
sleep 0.05
|
||||
done
|
||||
"""
|
||||
SYNOLOGY_DISK_TEMP_SCRIPT = r"""
|
||||
for disk in /dev/sata1 /dev/sata2 /dev/sata4; do
|
||||
temp="-"
|
||||
if [ -b "$disk" ]; then
|
||||
temp="$(
|
||||
/usr/bin/timeout 3 sudo -n /usr/bin/smartctl -A "$disk" 2>/dev/null |
|
||||
awk '/Temperature_Celsius|Airflow_Temperature_Cel/ {print $10; exit}'
|
||||
)"
|
||||
case "$temp" in
|
||||
''|*[!0-9]*) temp="-" ;;
|
||||
esac
|
||||
fi
|
||||
printf '%s=%s\n' "${disk##/dev/}" "$temp"
|
||||
done |
|
||||
awk -F= '
|
||||
$1 == "sata1" {printf "SATA1_TEMP=%s\n", $2 ? $2 : "-"}
|
||||
$1 == "sata2" {printf "SATA2_TEMP=%s\n", $2 ? $2 : "-"}
|
||||
$1 == "sata4" {printf "SATA4_TEMP=%s\n", $2 ? $2 : "-"}
|
||||
'
|
||||
"""
|
||||
SAMPLE_INTERVAL_MS = 50
|
||||
GRAPH_SECONDS = 30
|
||||
GRAPH_POINTS = GRAPH_SECONDS * 1000 // SAMPLE_INTERVAL_MS
|
||||
SYNOLOGY_RESTART_INTERVAL_MS = 1000
|
||||
SYNOLOGY_DISK_TEMP_INTERVAL_MS = 10000
|
||||
|
||||
|
||||
def byte_rate_unit(values):
|
||||
@@ -246,16 +309,19 @@ class StatusTile(QFrame):
|
||||
self.last_read_at = 0.0
|
||||
self.last_write_at = 0.0
|
||||
self.last_error_at = 0.0
|
||||
self.read_steady = None
|
||||
self.write_steady = None
|
||||
self.error_steady = None
|
||||
self.title = QLabel(title)
|
||||
self.title.setObjectName("statusTitle")
|
||||
self.detail = QLabel("-")
|
||||
self.detail.setObjectName("statusDetail")
|
||||
self.detail.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
self.setFixedHeight(58)
|
||||
self.setFixedHeight(48)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(6, 5, 6, 5)
|
||||
layout.setSpacing(2)
|
||||
layout.setContentsMargins(6, 4, 6, 4)
|
||||
layout.setSpacing(1)
|
||||
texts = QVBoxLayout()
|
||||
texts.setContentsMargins(0, 0, 0, 0)
|
||||
texts.setSpacing(0)
|
||||
@@ -265,7 +331,7 @@ class StatusTile(QFrame):
|
||||
|
||||
dots = QHBoxLayout()
|
||||
dots.setContentsMargins(0, 0, 0, 0)
|
||||
dots.setSpacing(5)
|
||||
dots.setSpacing(4)
|
||||
dots.addWidget(self.read_dot, 0, Qt.AlignLeft)
|
||||
dots.addWidget(self.write_dot, 0, Qt.AlignLeft)
|
||||
dots.addWidget(self.error_dot, 0, Qt.AlignLeft)
|
||||
@@ -273,8 +339,20 @@ class StatusTile(QFrame):
|
||||
layout.addLayout(dots)
|
||||
self.setObjectName("statusTile")
|
||||
|
||||
def set_state(self, detail, error=False, read_activity=False, write_activity=False):
|
||||
def set_state(
|
||||
self,
|
||||
detail,
|
||||
error=False,
|
||||
read_activity=False,
|
||||
write_activity=False,
|
||||
read_steady=None,
|
||||
write_steady=None,
|
||||
error_steady=None,
|
||||
):
|
||||
now = time.monotonic()
|
||||
self.read_steady = read_steady
|
||||
self.write_steady = write_steady
|
||||
self.error_steady = error_steady
|
||||
if read_activity:
|
||||
self.last_read_at = now
|
||||
if write_activity:
|
||||
@@ -288,9 +366,12 @@ class StatusTile(QFrame):
|
||||
self.update_dots(time.monotonic())
|
||||
|
||||
def update_dots(self, now):
|
||||
self.read_dot.set_steady(now - self.last_read_at <= self.ACTIVITY_HOLD_SECONDS)
|
||||
self.write_dot.set_steady(now - self.last_write_at <= self.ACTIVITY_HOLD_SECONDS)
|
||||
self.error_dot.set_steady(now - self.last_error_at <= self.ERROR_HOLD_SECONDS)
|
||||
read_on = self.read_steady if self.read_steady is not None else now - self.last_read_at <= self.ACTIVITY_HOLD_SECONDS
|
||||
write_on = self.write_steady if self.write_steady is not None else now - self.last_write_at <= self.ACTIVITY_HOLD_SECONDS
|
||||
error_on = self.error_steady if self.error_steady is not None else now - self.last_error_at <= self.ERROR_HOLD_SECONDS
|
||||
self.read_dot.set_steady(read_on)
|
||||
self.write_dot.set_steady(write_on)
|
||||
self.error_dot.set_steady(error_on)
|
||||
|
||||
|
||||
class PcWakeButton(StatusTile):
|
||||
@@ -484,6 +565,10 @@ class TouchPanel(QMainWindow):
|
||||
self.wan_iface = default_iface()
|
||||
NET_GROUPS["wan"] = [self.wan_iface]
|
||||
self.last_net = {}
|
||||
self.last_synology = {}
|
||||
self.synology_buffer = ""
|
||||
self.synology_values = {}
|
||||
self.synology_disk_temps = {}
|
||||
self.last_io = None
|
||||
self.last_sample = None
|
||||
self.last_graph = None
|
||||
@@ -523,12 +608,27 @@ class TouchPanel(QMainWindow):
|
||||
self.disk_temp_timer.timeout.connect(self.update_disk_temp)
|
||||
self.disk_temp_timer.start(1000)
|
||||
|
||||
self.synology_proc = QProcess(self)
|
||||
self.synology_proc.readyReadStandardOutput.connect(self.synology_status_ready)
|
||||
self.synology_proc.finished.connect(self.synology_status_finished)
|
||||
self.synology_timer = QTimer(self)
|
||||
self.synology_timer.timeout.connect(self.update_synology_status)
|
||||
self.synology_timer.start(SYNOLOGY_RESTART_INTERVAL_MS)
|
||||
|
||||
self.synology_disk_temp_proc = QProcess(self)
|
||||
self.synology_disk_temp_proc.finished.connect(self.synology_disk_temp_finished)
|
||||
self.synology_disk_temp_timer = QTimer(self)
|
||||
self.synology_disk_temp_timer.timeout.connect(self.update_synology_disk_temp)
|
||||
self.synology_disk_temp_timer.start(SYNOLOGY_DISK_TEMP_INTERVAL_MS)
|
||||
|
||||
self.pulse_timer = QTimer(self)
|
||||
self.pulse_timer.timeout.connect(self.tick_pulses)
|
||||
self.pulse_timer.start(25)
|
||||
self.sample()
|
||||
self.ping_pc()
|
||||
self.update_disk_temp()
|
||||
self.update_synology_status()
|
||||
self.update_synology_disk_temp()
|
||||
self.update_dashboard()
|
||||
|
||||
def _build_ui(self):
|
||||
@@ -539,7 +639,7 @@ class TouchPanel(QMainWindow):
|
||||
|
||||
left = QFrame()
|
||||
left.setObjectName("statusPanel")
|
||||
left.setFixedWidth(132)
|
||||
left.setFixedWidth(266)
|
||||
left_layout = QVBoxLayout(left)
|
||||
left_layout.setContentsMargins(6, 8, 6, 8)
|
||||
left_layout.setSpacing(6)
|
||||
@@ -547,10 +647,41 @@ class TouchPanel(QMainWindow):
|
||||
title.setObjectName("sectionTitle")
|
||||
left_layout.addWidget(title)
|
||||
|
||||
for key, label in [("io", "DISK I/O"), ("wan", "WAN"), ("lan", "LAN"), ("wifi24", "2.4G"), ("wifi5", "5G")]:
|
||||
status_grid = QGridLayout()
|
||||
status_grid.setContentsMargins(0, 0, 0, 0)
|
||||
status_grid.setHorizontalSpacing(6)
|
||||
status_grid.setVerticalSpacing(6)
|
||||
pi_title = QLabel("RASPBERRY")
|
||||
pi_title.setObjectName("statusGroupTitle")
|
||||
synology_title = QLabel("SYNOLOGY")
|
||||
synology_title.setObjectName("statusGroupTitle")
|
||||
status_grid.addWidget(pi_title, 0, 0)
|
||||
status_grid.addWidget(synology_title, 0, 1)
|
||||
|
||||
status_rows = [
|
||||
(("io", "DISK I/O"), ("synology_sata1", "SATA1")),
|
||||
(("local", "LOCAL"), ("synology_sata2", "SATA2")),
|
||||
(("wan", "UPLINK"), ("synology_sata4", "SATA4")),
|
||||
(("lan", "ETH1"), ("synology_local", "LOCAL")),
|
||||
(("wifi5", "5G"), ("synology_wan", "UPLINK")),
|
||||
(("wifi24", "2.4G"), None),
|
||||
]
|
||||
for index, (pi_row, synology_row) in enumerate(status_rows, 1):
|
||||
key, label = pi_row
|
||||
tile = StatusTile(label)
|
||||
self.rows[key] = tile
|
||||
left_layout.addWidget(tile)
|
||||
status_grid.addWidget(tile, index, 0)
|
||||
if synology_row is None:
|
||||
spacer = QWidget()
|
||||
spacer.setFixedHeight(48)
|
||||
spacer.setObjectName("statusEmpty")
|
||||
status_grid.addWidget(spacer, index, 1)
|
||||
else:
|
||||
key, label = synology_row
|
||||
tile = StatusTile(label)
|
||||
self.rows[key] = tile
|
||||
status_grid.addWidget(tile, index, 1)
|
||||
left_layout.addLayout(status_grid)
|
||||
left_layout.addStretch(1)
|
||||
self.pc_button = PcWakeButton()
|
||||
left_layout.addWidget(self.pc_button)
|
||||
@@ -657,10 +788,11 @@ class TouchPanel(QMainWindow):
|
||||
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"])
|
||||
self.rows["lan"].set_state(self.iface_detail(groups["lan"]), activity["lan"]["error"], activity["lan"]["read"], activity["lan"]["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["local"].set_state(self.iface_detail(groups["local"], "lo"), activity["local"]["error"], activity["local"]["read"], activity["local"]["write"])
|
||||
self.rows["wan"].set_state(self.iface_detail(groups["wan"], self.wan_iface), activity["wan"]["error"], activity["wan"]["read"], activity["wan"]["write"])
|
||||
self.rows["lan"].set_state(self.iface_detail(groups["lan"], "eth1"), activity["lan"]["error"], activity["lan"]["read"], activity["lan"]["write"])
|
||||
self.rows["wifi24"].set_state(self.iface_detail(groups["wifi24"], "wlan0"), activity["wifi24"]["error"], activity["wifi24"]["read"], activity["wifi24"]["write"])
|
||||
self.rows["wifi5"].set_state(self.iface_detail(groups["wifi5"], "wlan1"), activity["wifi5"]["error"], activity["wifi5"]["read"], activity["wifi5"]["write"])
|
||||
|
||||
self.latest_temp = temp
|
||||
self.latest_rpm = rpm
|
||||
@@ -712,6 +844,102 @@ class TouchPanel(QMainWindow):
|
||||
self.disk_temp = float(part)
|
||||
return
|
||||
|
||||
def update_synology_status(self):
|
||||
if self.synology_proc.state() != QProcess.NotRunning:
|
||||
return
|
||||
self.synology_buffer = ""
|
||||
self.synology_values = {}
|
||||
self.synology_proc.start(SYNOLOGY_SSH[0], SYNOLOGY_SSH[1:] + [SYNOLOGY_STATUS_SCRIPT])
|
||||
|
||||
def update_synology_disk_temp(self):
|
||||
if self.synology_disk_temp_proc.state() != QProcess.NotRunning:
|
||||
return
|
||||
self.synology_disk_temp_proc.start(SYNOLOGY_SSH[0], SYNOLOGY_SSH[1:] + [SYNOLOGY_DISK_TEMP_SCRIPT])
|
||||
|
||||
def synology_disk_temp_finished(self, exit_code, exit_status):
|
||||
del exit_status
|
||||
if exit_code != 0:
|
||||
return
|
||||
output = bytes(self.synology_disk_temp_proc.readAllStandardOutput()).decode("utf-8", "ignore").strip()
|
||||
temps = {}
|
||||
for line in output.splitlines():
|
||||
if "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
temps[key.strip()] = value.strip()
|
||||
self.synology_disk_temps = temps
|
||||
|
||||
def synology_status_ready(self):
|
||||
output = bytes(self.synology_proc.readAllStandardOutput()).decode("utf-8", "ignore")
|
||||
self.synology_buffer += output
|
||||
while "\n" in self.synology_buffer:
|
||||
line, self.synology_buffer = self.synology_buffer.split("\n", 1)
|
||||
if line == "END=1":
|
||||
self.render_synology_status(self.synology_values)
|
||||
self.synology_values = {}
|
||||
continue
|
||||
if "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
self.synology_values[key.strip()] = value.strip()
|
||||
|
||||
def synology_status_finished(self, exit_code, exit_status):
|
||||
del exit_code, exit_status
|
||||
self.set_synology_offline()
|
||||
|
||||
def set_synology_offline(self):
|
||||
for key in ["synology_sata1", "synology_sata2", "synology_sata4", "synology_wan", "synology_local"]:
|
||||
self.rows[key].set_state("-", read_steady=False, write_steady=False, error_steady=True)
|
||||
|
||||
def render_synology_status(self, values):
|
||||
if not values:
|
||||
self.set_synology_offline()
|
||||
return
|
||||
for disk in ["SATA1", "SATA2", "SATA4"]:
|
||||
self.set_synology_io_tile(
|
||||
f"synology_{disk.lower()}",
|
||||
int(values.get(f"{disk}_READS", "0") or 0),
|
||||
int(values.get(f"{disk}_WRITES", "0") or 0),
|
||||
self.synology_disk_temp_detail(disk),
|
||||
)
|
||||
self.set_synology_net_tile(
|
||||
"synology_wan",
|
||||
int(values.get("WAN_RX", "0") or 0),
|
||||
int(values.get("WAN_TX", "0") or 0),
|
||||
int(values.get("WAN_RX_ERR", "0") or 0) + int(values.get("WAN_TX_ERR", "0") or 0),
|
||||
int(values.get("WAN_RX_DROP", "0") or 0) + int(values.get("WAN_TX_DROP", "0") or 0),
|
||||
values.get("WAN_IFACE", "wan"),
|
||||
)
|
||||
self.set_synology_net_tile(
|
||||
"synology_local",
|
||||
int(values.get("LOCAL_RX", "0") or 0),
|
||||
int(values.get("LOCAL_TX", "0") or 0),
|
||||
int(values.get("LOCAL_RX_ERR", "0") or 0) + int(values.get("LOCAL_TX_ERR", "0") or 0),
|
||||
int(values.get("LOCAL_RX_DROP", "0") or 0) + int(values.get("LOCAL_TX_DROP", "0") or 0),
|
||||
"lo",
|
||||
)
|
||||
|
||||
def set_synology_io_tile(self, key, reads, writes, detail):
|
||||
previous = self.last_synology.get(key)
|
||||
self.last_synology[key] = (reads, writes, 0, 0)
|
||||
read_activity = previous is not None and reads > previous[0]
|
||||
write_activity = previous is not None and writes > previous[1]
|
||||
self.rows[key].set_state(detail, read_activity=read_activity, write_activity=write_activity, error_steady=False)
|
||||
|
||||
def synology_disk_temp_detail(self, disk):
|
||||
value = self.synology_disk_temps.get(f"{disk}_TEMP", "")
|
||||
if not value:
|
||||
return "- C"
|
||||
return f"{value} C"
|
||||
|
||||
def set_synology_net_tile(self, key, rx, tx, errors, drops, detail):
|
||||
previous = self.last_synology.get(key)
|
||||
self.last_synology[key] = (rx, tx, errors, drops)
|
||||
read_activity = previous is not None and tx > previous[1]
|
||||
write_activity = previous is not None and rx > previous[0]
|
||||
error_activity = previous is not None and (errors > previous[2] or drops > previous[3])
|
||||
self.rows[key].set_state(detail, error_activity, read_activity, write_activity)
|
||||
|
||||
def update_graphs(self):
|
||||
temp = getattr(self, "latest_temp", cpu_temp())
|
||||
rpm = getattr(self, "latest_rpm", read_int(self.fan["rpm"]))
|
||||
@@ -733,11 +961,11 @@ class TouchPanel(QMainWindow):
|
||||
row.tick()
|
||||
|
||||
@staticmethod
|
||||
def iface_detail(row):
|
||||
def iface_detail(row, normal_label="정상"):
|
||||
if not row["carrier"]:
|
||||
return "꺼짐"
|
||||
drops = row["rx_dropped"] + row["tx_dropped"]
|
||||
return "정상" if drops <= 0 else f"드롭 {drops}"
|
||||
return normal_label if drops <= 0 else f"드롭 {drops}"
|
||||
|
||||
@staticmethod
|
||||
def iface_warn(row):
|
||||
@@ -752,9 +980,10 @@ def main():
|
||||
QWidget { background: #0b1220; color: #e5edf7; font-family: "Noto Sans CJK KR", "Noto Sans CJK KR Regular", "Noto Sans KR", sans-serif; }
|
||||
#statusPanel { background: #101827; border-right: 1px solid #263650; }
|
||||
#sectionTitle { font-size: 20px; font-weight: 700; }
|
||||
#statusGroupTitle { color: #8fa7c5; font-size: 10px; font-weight: 700; background: transparent; }
|
||||
#statusTile { background: #d7d9dc; border: 2px groove #f4f4f4; border-radius: 2px; }
|
||||
#statusTitle { font-size: 13px; font-weight: 700; color: #111827; background: transparent; }
|
||||
#statusDetail { font-size: 10px; color: #475569; background: transparent; }
|
||||
#statusDetail { font-size: 9px; color: #475569; background: transparent; }
|
||||
#pcWake { background: transparent; border: 0; }
|
||||
#pcLabel { color: #dce8f8; font-size: 13px; font-weight: 700; background: transparent; }
|
||||
#metricCard, #chartCard { background: #111a2e; border: 1px solid #27364f; border-radius: 12px; }
|
||||
|
||||
Reference in new issue
Block a user