639 lines
23 KiB
Python
Executable File
639 lines
23 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import sys
|
|
import time
|
|
import socket
|
|
from collections import deque
|
|
from pathlib import Path
|
|
|
|
from PyQt5.QtCore import QPointF, QProcess, Qt, QTimer
|
|
from PyQt5.QtGui import QColor, QPainter, QPainterPath, QPen
|
|
from PyQt5.QtWidgets import (
|
|
QApplication,
|
|
QFrame,
|
|
QGridLayout,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QMainWindow,
|
|
QPushButton,
|
|
QSlider,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
|
|
NET_GROUPS = {
|
|
"wan": ["eth0"],
|
|
"lan": ["br0", "eth1"],
|
|
"wifi24": ["wlan0"],
|
|
"wifi5": ["wlan1"],
|
|
}
|
|
|
|
PC_IP = "192.168.50.73"
|
|
PC_MAC = "74:56:3C:CD:CD:79"
|
|
|
|
|
|
def read_text(path, default=""):
|
|
try:
|
|
return Path(path).read_text(encoding="utf-8").strip()
|
|
except OSError:
|
|
return default
|
|
|
|
|
|
def read_int(path, default=0):
|
|
raw = read_text(path)
|
|
return int(raw) if raw.isdigit() else default
|
|
|
|
|
|
def cpu_temp():
|
|
raw = read_int("/sys/class/thermal/thermal_zone0/temp")
|
|
return round(raw / 1000, 1) if raw > 0 else 0.0
|
|
|
|
|
|
def fan_paths():
|
|
for pwm in Path("/sys/devices/platform/cooling_fan/hwmon").glob("hwmon*/pwm1"):
|
|
base = pwm.parent
|
|
return {
|
|
"pwm": str(pwm),
|
|
"enable": str(base / "pwm1_enable"),
|
|
"rpm": str(base / "fan1_input"),
|
|
}
|
|
for pwm in Path("/sys/class/hwmon").glob("hwmon*/pwm1"):
|
|
base = pwm.parent
|
|
return {
|
|
"pwm": str(pwm),
|
|
"enable": str(base / "pwm1_enable"),
|
|
"rpm": str(base / "fan1_input"),
|
|
}
|
|
return {"pwm": "", "enable": "", "rpm": ""}
|
|
|
|
|
|
def default_iface():
|
|
for line in read_text("/proc/net/route").splitlines()[1:]:
|
|
parts = line.split()
|
|
if len(parts) >= 2 and parts[1] == "00000000":
|
|
return parts[0]
|
|
return "eth0"
|
|
|
|
|
|
def iface_stats(name):
|
|
base = Path("/sys/class/net") / name
|
|
stats = base / "statistics"
|
|
return {
|
|
"name": name,
|
|
"state": read_text(base / "operstate", "unknown"),
|
|
"carrier": read_text(base / "carrier", "0") == "1",
|
|
"rx_bytes": read_int(stats / "rx_bytes"),
|
|
"tx_bytes": read_int(stats / "tx_bytes"),
|
|
"rx_packets": read_int(stats / "rx_packets"),
|
|
"tx_packets": read_int(stats / "tx_packets"),
|
|
"rx_errors": read_int(stats / "rx_errors"),
|
|
"tx_errors": read_int(stats / "tx_errors"),
|
|
"rx_dropped": read_int(stats / "rx_dropped"),
|
|
"tx_dropped": read_int(stats / "tx_dropped"),
|
|
}
|
|
|
|
|
|
def group_stats(names):
|
|
rows = [iface_stats(name) for name in names if (Path("/sys/class/net") / name).exists()]
|
|
if not rows:
|
|
return iface_stats(names[0])
|
|
return {
|
|
"name": "+".join(row["name"] for row in rows),
|
|
"state": "up" if any(row["state"] == "up" for row in rows) else rows[0]["state"],
|
|
"carrier": any(row["carrier"] for row in rows),
|
|
"rx_bytes": sum(row["rx_bytes"] for row in rows),
|
|
"tx_bytes": sum(row["tx_bytes"] for row in rows),
|
|
"rx_packets": sum(row["rx_packets"] for row in rows),
|
|
"tx_packets": sum(row["tx_packets"] for row in rows),
|
|
"rx_errors": sum(row["rx_errors"] for row in rows),
|
|
"tx_errors": sum(row["tx_errors"] for row in rows),
|
|
"rx_dropped": sum(row["rx_dropped"] for row in rows),
|
|
"tx_dropped": sum(row["tx_dropped"] for row in rows),
|
|
}
|
|
|
|
|
|
def disk_io():
|
|
reads = 0
|
|
writes = 0
|
|
found = False
|
|
for line in read_text("/proc/diskstats").splitlines():
|
|
parts = line.split()
|
|
if len(parts) < 14:
|
|
continue
|
|
name = parts[2]
|
|
if name.startswith(("loop", "ram", "zram", "dm-")):
|
|
continue
|
|
found = True
|
|
reads += int(parts[3])
|
|
writes += int(parts[7])
|
|
return reads, writes, not found
|
|
|
|
|
|
def write_sys(path, value):
|
|
if not path:
|
|
return False
|
|
|
|
|
|
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
|
|
try:
|
|
Path(path).write_text(str(value) + "\n", encoding="utf-8")
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
class StatusDot(QWidget):
|
|
def __init__(self, active_color):
|
|
super().__init__()
|
|
self.active_color = QColor(active_color)
|
|
self.steady = False
|
|
self.pulse = 0
|
|
self.setFixedSize(15, 15)
|
|
|
|
def set_steady(self, steady=False):
|
|
self.steady = steady
|
|
self.update()
|
|
|
|
def blink(self):
|
|
self.pulse = 3
|
|
self.update()
|
|
|
|
def tick(self):
|
|
if self.pulse > 0:
|
|
self.pulse -= 1
|
|
self.update()
|
|
|
|
def paintEvent(self, event):
|
|
del event
|
|
painter = QPainter(self)
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
base = QColor("#8fa0aa")
|
|
if self.steady or self.pulse > 0:
|
|
base = self.active_color
|
|
painter.setBrush(base)
|
|
painter.setPen(Qt.NoPen)
|
|
painter.drawEllipse(2, 2, 11, 11)
|
|
painter.setBrush(QColor(255, 255, 255, 55))
|
|
painter.drawEllipse(6, 4, 4, 4)
|
|
|
|
|
|
class StatusTile(QFrame):
|
|
def __init__(self, title):
|
|
super().__init__()
|
|
self.read_dot = StatusDot("#16a34a")
|
|
self.write_dot = StatusDot("#f97316")
|
|
self.error_dot = StatusDot("#dc2626")
|
|
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)
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(6, 5, 6, 5)
|
|
layout.setSpacing(2)
|
|
texts = QVBoxLayout()
|
|
texts.setContentsMargins(0, 0, 0, 0)
|
|
texts.setSpacing(0)
|
|
texts.addWidget(self.title)
|
|
texts.addWidget(self.detail)
|
|
layout.addLayout(texts)
|
|
|
|
dots = QHBoxLayout()
|
|
dots.setContentsMargins(0, 0, 0, 0)
|
|
dots.setSpacing(5)
|
|
dots.addWidget(self.read_dot, 0, Qt.AlignLeft)
|
|
dots.addWidget(self.write_dot, 0, Qt.AlignLeft)
|
|
dots.addWidget(self.error_dot, 0, Qt.AlignLeft)
|
|
dots.addStretch(1)
|
|
layout.addLayout(dots)
|
|
self.setObjectName("statusTile")
|
|
|
|
def set_state(self, detail, error=False, read_activity=False, write_activity=False):
|
|
self.error_dot.set_steady(error)
|
|
if read_activity:
|
|
self.read_dot.blink()
|
|
if write_activity:
|
|
self.write_dot.blink()
|
|
self.detail.setText(detail)
|
|
|
|
def tick(self):
|
|
self.read_dot.tick()
|
|
self.write_dot.tick()
|
|
self.error_dot.tick()
|
|
|
|
|
|
class PcWakeButton(QFrame):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setObjectName("pcWake")
|
|
self.online = False
|
|
self.dot = StatusDot("#16a34a")
|
|
self.label = QLabel("PC")
|
|
self.label.setObjectName("pcLabel")
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(5, 4, 5, 4)
|
|
layout.setSpacing(5)
|
|
layout.addWidget(self.dot)
|
|
layout.addWidget(self.label)
|
|
layout.addStretch(1)
|
|
self.setFixedHeight(28)
|
|
|
|
def set_online(self, online):
|
|
self.online = online
|
|
self.dot.set_steady(online)
|
|
|
|
def mousePressEvent(self, event):
|
|
if event.button() == Qt.LeftButton and not self.online:
|
|
send_magic_packet(PC_MAC)
|
|
super().mousePressEvent(event)
|
|
|
|
|
|
class MetricCard(QFrame):
|
|
def __init__(self, title):
|
|
super().__init__()
|
|
self.setObjectName("metricCard")
|
|
self.title = QLabel(title)
|
|
self.title.setObjectName("metricTitle")
|
|
self.value = QLabel("-")
|
|
self.value.setObjectName("metricValue")
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(10, 8, 10, 8)
|
|
layout.setSpacing(2)
|
|
layout.addWidget(self.title)
|
|
layout.addWidget(self.value)
|
|
|
|
def set_value(self, text):
|
|
self.value.setText(text)
|
|
|
|
|
|
class Sparkline(QFrame):
|
|
def __init__(self, title, color="#38bdf8", second_color=None):
|
|
super().__init__()
|
|
self.setObjectName("chartCard")
|
|
self.color = QColor(color)
|
|
self.second_color = QColor(second_color) if second_color else None
|
|
self.values = []
|
|
self.second_values = []
|
|
self.title = QLabel(title)
|
|
self.title.setObjectName("chartTitle")
|
|
self.caption = QLabel("-")
|
|
self.caption.setObjectName("chartCaption")
|
|
self.canvas = QWidget()
|
|
self.canvas.paintEvent = self.paint_chart
|
|
self.canvas.setMinimumHeight(58)
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(10, 8, 10, 8)
|
|
layout.setSpacing(4)
|
|
top = QHBoxLayout()
|
|
top.addWidget(self.title)
|
|
top.addStretch(1)
|
|
top.addWidget(self.caption)
|
|
layout.addLayout(top)
|
|
layout.addWidget(self.canvas, 1)
|
|
|
|
def set_values(self, values, caption, second_values=None):
|
|
self.values = list(values)[-80:]
|
|
self.second_values = list(second_values or [])[-80:]
|
|
self.caption.setText(caption)
|
|
self.canvas.update()
|
|
|
|
def paint_chart(self, event):
|
|
del event
|
|
painter = QPainter(self.canvas)
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
rect = self.canvas.rect().adjusted(2, 4, -2, -4)
|
|
painter.fillRect(rect, QColor("#0f172a"))
|
|
painter.setPen(QPen(QColor("#263650"), 1))
|
|
painter.drawLine(rect.left(), rect.bottom(), rect.right(), rect.bottom())
|
|
all_values = self.values + self.second_values
|
|
if len(all_values) < 2:
|
|
return
|
|
low = min(all_values)
|
|
high = max(all_values)
|
|
if high <= low:
|
|
high = low + 1
|
|
self.draw_series(painter, rect, self.values, low, high, self.color)
|
|
if self.second_color and self.second_values:
|
|
self.draw_series(painter, rect, self.second_values, low, high, self.second_color)
|
|
|
|
@staticmethod
|
|
def draw_series(painter, rect, values, low, high, color):
|
|
path = QPainterPath()
|
|
for index, value in enumerate(values):
|
|
x = rect.left() + rect.width() * index / max(1, len(values) - 1)
|
|
y = rect.bottom() - rect.height() * ((float(value) - low) / (high - low))
|
|
point = QPointF(x, y)
|
|
path.moveTo(point) if index == 0 else path.lineTo(point)
|
|
painter.setPen(QPen(color, 2))
|
|
painter.drawPath(path)
|
|
|
|
|
|
class TouchPanel(QMainWindow):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.rows = {}
|
|
self.metrics = {}
|
|
self.charts = {}
|
|
self.mode_buttons = {}
|
|
self.fan = fan_paths()
|
|
self.wan_iface = default_iface()
|
|
NET_GROUPS["wan"] = [self.wan_iface]
|
|
self.last_net = {}
|
|
self.last_io = None
|
|
self.last_sample = None
|
|
self.last_graph = None
|
|
self.current_rates = {"wan": {"rx": 0.0, "tx": 0.0}}
|
|
self.current_pwm = read_int(self.fan["pwm"])
|
|
self.temp_history = deque(maxlen=80)
|
|
self.rpm_history = deque(maxlen=80)
|
|
self.net_history = {"wan": {"rx": deque(maxlen=80), "tx": deque(maxlen=80)}}
|
|
|
|
self.setWindowTitle("Standalone Touch Dashboard")
|
|
self.setCursor(Qt.BlankCursor)
|
|
self.setMinimumSize(800, 480)
|
|
self._build_ui()
|
|
|
|
self.sample_timer = QTimer(self)
|
|
self.sample_timer.timeout.connect(self.sample)
|
|
self.sample_timer.start(120)
|
|
|
|
self.graph_timer = QTimer(self)
|
|
self.graph_timer.timeout.connect(self.update_graphs)
|
|
self.graph_timer.start(500)
|
|
|
|
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.pulse_timer = QTimer(self)
|
|
self.pulse_timer.timeout.connect(self.tick_pulses)
|
|
self.pulse_timer.start(70)
|
|
self.sample()
|
|
self.ping_pc()
|
|
|
|
def _build_ui(self):
|
|
root = QWidget()
|
|
layout = QHBoxLayout(root)
|
|
layout.setContentsMargins(4, 8, 10, 8)
|
|
layout.setSpacing(8)
|
|
|
|
left = QFrame()
|
|
left.setObjectName("statusPanel")
|
|
left.setFixedWidth(132)
|
|
left_layout = QVBoxLayout(left)
|
|
left_layout.setContentsMargins(6, 8, 6, 8)
|
|
left_layout.setSpacing(6)
|
|
title = QLabel("상태")
|
|
title.setObjectName("sectionTitle")
|
|
left_layout.addWidget(title)
|
|
|
|
for key, label in [("io", "DISK I/O"), ("wan", "WAN"), ("lan", "LAN"), ("wifi24", "2.4G"), ("wifi5", "5G")]:
|
|
tile = StatusTile(label)
|
|
self.rows[key] = tile
|
|
left_layout.addWidget(tile)
|
|
left_layout.addStretch(1)
|
|
self.pc_button = PcWakeButton()
|
|
left_layout.addWidget(self.pc_button, 0, Qt.AlignLeft | Qt.AlignBottom)
|
|
|
|
right = QWidget()
|
|
right_layout = QVBoxLayout(right)
|
|
right_layout.setContentsMargins(0, 0, 0, 0)
|
|
right_layout.setSpacing(8)
|
|
|
|
header = QHBoxLayout()
|
|
heading = QLabel("Dashboard")
|
|
heading.setObjectName("sectionTitle")
|
|
self.clock = QLabel("--:--:--")
|
|
self.clock.setObjectName("clock")
|
|
self.status_text = QLabel("")
|
|
self.status_text.setObjectName("statusText")
|
|
header.addWidget(heading)
|
|
header.addStretch(1)
|
|
header.addWidget(self.status_text)
|
|
header.addWidget(self.clock)
|
|
right_layout.addLayout(header)
|
|
|
|
metric_grid = QGridLayout()
|
|
metric_grid.setHorizontalSpacing(8)
|
|
metric_grid.setVerticalSpacing(8)
|
|
for index, (key, title) in enumerate([("rpm", "팬 RPM"), ("temp", "CPU 온도"), ("pwm", "PWM"), ("wan", "WAN")]):
|
|
card = MetricCard(title)
|
|
self.metrics[key] = card
|
|
metric_grid.addWidget(card, 0, index)
|
|
right_layout.addLayout(metric_grid)
|
|
|
|
mode_layout = QHBoxLayout()
|
|
for mode, label in [("auto", "auto"), ("manual", "manual"), ("off", "off")]:
|
|
btn = QPushButton(label)
|
|
btn.setCheckable(True)
|
|
btn.clicked.connect(lambda checked=False, m=mode: self.set_fan(m))
|
|
self.mode_buttons[mode] = btn
|
|
mode_layout.addWidget(btn)
|
|
right_layout.addLayout(mode_layout)
|
|
|
|
slider_wrap = QHBoxLayout()
|
|
minus = QPushButton("-")
|
|
minus.setObjectName("stepButton")
|
|
minus.clicked.connect(lambda: self.step_pwm(-10))
|
|
plus = QPushButton("+")
|
|
plus.setObjectName("stepButton")
|
|
plus.clicked.connect(lambda: self.step_pwm(10))
|
|
self.slider = QSlider(Qt.Horizontal)
|
|
self.slider.setRange(0, 255)
|
|
self.slider.setValue(self.current_pwm)
|
|
self.slider.valueChanged.connect(self.slider_changed)
|
|
self.slider.sliderReleased.connect(lambda: self.set_fan("manual"))
|
|
slider_wrap.addWidget(minus)
|
|
slider_wrap.addWidget(self.slider, 1)
|
|
slider_wrap.addWidget(plus)
|
|
right_layout.addLayout(slider_wrap)
|
|
|
|
chart_grid = QGridLayout()
|
|
chart_grid.setHorizontalSpacing(8)
|
|
chart_grid.setVerticalSpacing(8)
|
|
for index, (key, chart) in enumerate([
|
|
("temp", Sparkline("온도 / RPM", "#f97316", "#38bdf8")),
|
|
("wan", Sparkline("WAN 트래픽", "#38bdf8", "#22c55e")),
|
|
]):
|
|
self.charts[key] = chart
|
|
chart_grid.addWidget(chart, index, 0)
|
|
right_layout.addLayout(chart_grid, 1)
|
|
|
|
layout.addWidget(left)
|
|
layout.addWidget(right, 1)
|
|
self.setCentralWidget(root)
|
|
|
|
def sample(self):
|
|
now = time.monotonic()
|
|
dt = max(0.001, now - self.last_sample) if self.last_sample else 0.001
|
|
self.last_sample = now
|
|
|
|
temp = cpu_temp()
|
|
rpm = read_int(self.fan["rpm"])
|
|
pwm = read_int(self.fan["pwm"])
|
|
enable = read_int(self.fan["enable"], 2)
|
|
percent = pwm / 255 * 100 if pwm > 0 else 0
|
|
|
|
reads, writes, disk_error = disk_io()
|
|
io_read_activity = self.last_io is not None and reads != self.last_io[0]
|
|
io_write_activity = self.last_io is not None and writes != self.last_io[1]
|
|
self.last_io = (reads, writes)
|
|
|
|
groups = {key: group_stats(names) for key, names in NET_GROUPS.items()}
|
|
rates = {}
|
|
activity = {}
|
|
for key, row in groups.items():
|
|
previous = self.last_net.get(key)
|
|
current = (row["rx_bytes"], row["tx_bytes"], row["rx_packets"], row["tx_packets"])
|
|
activity[key] = {
|
|
"read": previous is not None and current[1] != previous[1],
|
|
"write": previous is not None and current[0] != previous[0],
|
|
}
|
|
if previous:
|
|
rates[key] = {
|
|
"rx": max(0.0, (current[0] - previous[0]) * 8 / dt / 1_000_000),
|
|
"tx": max(0.0, (current[1] - previous[1]) * 8 / dt / 1_000_000),
|
|
}
|
|
else:
|
|
rates[key] = {"rx": 0.0, "tx": 0.0}
|
|
self.last_net[key] = current
|
|
|
|
self.current_rates = rates
|
|
self.render(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):
|
|
self.rows["io"].set_state(f"{temp:.1f} C", disk_error, io_read_activity, io_write_activity)
|
|
self.rows["wan"].set_state(self.iface_detail(groups["wan"]), self.iface_warn(groups["wan"]), activity["wan"]["read"], activity["wan"]["write"])
|
|
self.rows["lan"].set_state(self.iface_detail(groups["lan"]), self.iface_warn(groups["lan"]), activity["lan"]["read"], activity["lan"]["write"])
|
|
self.rows["wifi24"].set_state(self.iface_detail(groups["wifi24"]), self.iface_warn(groups["wifi24"]), activity["wifi24"]["read"], activity["wifi24"]["write"])
|
|
self.rows["wifi5"].set_state(self.iface_detail(groups["wifi5"]), self.iface_warn(groups["wifi5"]), activity["wifi5"]["read"], activity["wifi5"]["write"])
|
|
|
|
self.metrics["rpm"].set_value(f"{rpm:,}")
|
|
self.metrics["temp"].set_value(f"{temp:.1f} C")
|
|
self.metrics["pwm"].set_value(f"{pwm} / {percent:.1f}%")
|
|
self.metrics["wan"].set_value(f"{rates['wan']['rx']:.2f}/{rates['wan']['tx']:.2f} Mbps")
|
|
|
|
self.slider.blockSignals(True)
|
|
self.slider.setValue(pwm)
|
|
self.slider.blockSignals(False)
|
|
self.mode_buttons["off"].setChecked(enable == 0 or pwm == 0)
|
|
self.mode_buttons["manual"].setChecked(enable == 1 and pwm > 0)
|
|
self.mode_buttons["auto"].setChecked(enable not in [0, 1])
|
|
|
|
self.clock.setText(time.strftime("%H:%M:%S"))
|
|
self.latest_temp = temp
|
|
self.latest_rpm = rpm
|
|
|
|
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 update_graphs(self):
|
|
temp = getattr(self, "latest_temp", cpu_temp())
|
|
rpm = getattr(self, "latest_rpm", read_int(self.fan["rpm"]))
|
|
self.temp_history.append(temp)
|
|
self.rpm_history.append(rpm / 100)
|
|
self.charts["temp"].set_values(self.temp_history, "C / RPMx100", self.rpm_history)
|
|
|
|
wan = self.current_rates.get("wan", {"rx": 0.0, "tx": 0.0})
|
|
self.net_history["wan"]["rx"].append(wan["rx"])
|
|
self.net_history["wan"]["tx"].append(wan["tx"])
|
|
self.charts["wan"].set_values(
|
|
self.net_history["wan"]["rx"],
|
|
f"{wan['rx']:.2f}/{wan['tx']:.2f} Mbps",
|
|
self.net_history["wan"]["tx"],
|
|
)
|
|
|
|
def set_fan(self, mode):
|
|
pwm = self.slider.value()
|
|
ok = True
|
|
if mode == "off":
|
|
ok = write_sys(self.fan["enable"], 0) and write_sys(self.fan["pwm"], 0)
|
|
elif mode == "manual":
|
|
ok = write_sys(self.fan["enable"], 1) and write_sys(self.fan["pwm"], pwm)
|
|
elif mode == "auto":
|
|
ok = write_sys(self.fan["enable"], 2)
|
|
self.status_text.setText("" if ok else "팬 쓰기 권한 없음")
|
|
|
|
def step_pwm(self, delta):
|
|
self.slider.setValue(max(0, min(255, self.slider.value() + delta)))
|
|
self.set_fan("manual")
|
|
|
|
def slider_changed(self, value):
|
|
self.current_pwm = value
|
|
self.metrics["pwm"].set_value(f"{value} / {value / 255 * 100:.1f}%")
|
|
|
|
def tick_pulses(self):
|
|
for row in self.rows.values():
|
|
row.tick()
|
|
|
|
@staticmethod
|
|
def iface_detail(row):
|
|
if not row["carrier"]:
|
|
return "down"
|
|
drops = row["rx_dropped"] + row["tx_dropped"]
|
|
return f"up D{drops}"
|
|
|
|
@staticmethod
|
|
def iface_warn(row):
|
|
return row["rx_errors"] > 0 or row["tx_errors"] > 0
|
|
|
|
|
|
def main():
|
|
app = QApplication(sys.argv)
|
|
app.setOverrideCursor(Qt.BlankCursor)
|
|
app.setStyleSheet(
|
|
"""
|
|
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; }
|
|
#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; }
|
|
#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; }
|
|
#metricTitle { color: #8fa7c5; font-size: 13px; background: transparent; }
|
|
#metricValue { color: #f8fafc; font-size: 24px; font-weight: 800; background: transparent; }
|
|
#chartTitle { color: #dce8f8; font-size: 14px; font-weight: 700; background: transparent; }
|
|
#chartCaption { color: #93a4bd; font-size: 11px; background: transparent; }
|
|
#clock { font-size: 18px; color: #8fb3ff; font-weight: 700; }
|
|
#statusText { color: #93a4bd; font-size: 13px; background: transparent; }
|
|
QPushButton { min-height: 42px; border: 1px solid #33445f; border-radius: 14px; background: #1a2942; color: #dce8f8; font-size: 18px; font-weight: 700; }
|
|
QPushButton:checked { background: #16a34a; color: white; border-color: #22c55e; }
|
|
QPushButton#stepButton { min-width: 56px; font-size: 28px; }
|
|
QSlider::groove:horizontal { height: 10px; background: #263650; border-radius: 5px; }
|
|
QSlider::sub-page:horizontal { background: #22c55e; border-radius: 5px; }
|
|
QSlider::handle:horizontal { width: 28px; margin: -10px 0; border-radius: 14px; background: #f8fafc; }
|
|
"""
|
|
)
|
|
win = TouchPanel()
|
|
win.showFullScreen()
|
|
sys.exit(app.exec_())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|