터치 패널 상태 표시등 실시간 분리

This commit is contained in:
seo committed 2026-07-30 12:57:49 +09:00
1 parent 66d305bc18
commit e89f1da109
2 files changed
+65 -47

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 파일을 직접 읽어 120ms 간격으로 상태를 갱신합니다. 왼쪽 `IO`, `WAN`, `LAN`, `2.4G`, `5G` 원은 장식용 heartbeat가 아니라 실제 커널 카운터 증가에 반응합니다. `IO`는 디스크 read/write 카운터가 증가할 때, 네트워크는 RX/TX byte 또는 packet 카운터가 증가할 때 짧게 점멸합니다. 패널은 Control PHP/API/DB snapshot을 호출하지 않는 standalone 방식입니다. Python이 `/sys/class/net`, `/proc/diskstats`, `/sys/class/thermal`, 팬 hwmon 파일을 직접 읽습니다. 상태패널과 팬패널은 120ms 간격으로 갱신하고, 그래프는 500ms 간격으로 샘플링합니다. 왼쪽 `IO`, `WAN`, `LAN`, `2.4G`, `5G` 표시등은 장식용 heartbeat가 아니라 실제 커널 카운터 증가에 반응합니다. 각 타일에는 두 개의 작은 표시등을 두며 읽기/업로드는 초록, 쓰기/다운로드는 주황으로 점멸합니다. 둘 다 활동이 없으면 둘 다 회색입니다.
오른쪽에는 팬 RPM, CPU 온도, PWM, WAN 실시간 속도와 온도/RPM, WAN/LAN/WiFi 트래픽 그래프를 표시합니다. 팬 버튼과 슬라이더는 Control DB를 거치지 않고 팬 hwmon sysfs에 직접 쓰기를 시도합니다. GUI 사용자의 sysfs 쓰기 권한이 없으면 대시보드는 계속 동작하되 `팬 쓰기 권한 없음`을 표시합니다. 오른쪽에는 팬 RPM, CPU 온도, PWM, WAN 실시간 속도와 온도/RPM, WAN 트래픽 그래프를 표시합니다. 팬 버튼과 슬라이더는 Control DB를 거치지 않고 팬 hwmon sysfs에 직접 쓰기를 시도합니다. GUI 사용자의 sysfs 쓰기 권한이 없으면 대시보드는 계속 동작하되 `팬 쓰기 권한 없음`을 표시합니다. 팬 모드 표시는 hwmon `pwm1_enable` 기준이므로 Control DB가 auto여도 fan policy가 sysfs에 직접 PWM을 쓰는 순간 하드웨어 상태는 manual처럼 보일 수 있습니다.
GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다. GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다.
+63 -45
View File
@@ -134,12 +134,12 @@ def write_sys(path, value):
class StatusDot(QWidget): class StatusDot(QWidget):
def __init__(self): def __init__(self, active_color):
super().__init__() super().__init__()
self.active_color = QColor(active_color)
self.warning = False self.warning = False
self.pulse = 0 self.pulse = 0
self.phase = False self.setFixedSize(15, 15)
self.setFixedSize(22, 22)
def set_warning(self, warning=False): def set_warning(self, warning=False):
self.warning = warning self.warning = warning
@@ -147,7 +147,6 @@ class StatusDot(QWidget):
def blink(self): def blink(self):
self.pulse = 3 self.pulse = 3
self.phase = not self.phase
self.update() self.update()
def tick(self): def tick(self):
@@ -160,21 +159,20 @@ class StatusDot(QWidget):
painter = QPainter(self) painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing) painter.setRenderHint(QPainter.Antialiasing)
base = QColor("#8fa0aa") base = QColor("#8fa0aa")
if self.warning:
base = QColor("#f59e0b")
if self.pulse > 0: if self.pulse > 0:
base = QColor("#86efac") if self.phase else QColor("#16a34a") base = self.active_color
painter.setBrush(base) painter.setBrush(base)
painter.setPen(Qt.NoPen) painter.setPen(QPen(QColor("#ef4444"), 1) if self.warning else Qt.NoPen)
painter.drawEllipse(3, 3, 16, 16) painter.drawEllipse(2, 2, 11, 11)
painter.setBrush(QColor(255, 255, 255, 55)) painter.setBrush(QColor(255, 255, 255, 55))
painter.drawEllipse(8, 6, 5, 5) painter.drawEllipse(6, 4, 4, 4)
class StatusTile(QFrame): class StatusTile(QFrame):
def __init__(self, title): def __init__(self, title):
super().__init__() super().__init__()
self.dot = StatusDot() self.read_dot = StatusDot("#16a34a")
self.write_dot = StatusDot("#f97316")
self.title = QLabel(title) self.title = QLabel(title)
self.title.setObjectName("statusTitle") self.title.setObjectName("statusTitle")
self.detail = QLabel("-") self.detail = QLabel("-")
@@ -183,9 +181,15 @@ class StatusTile(QFrame):
self.setFixedHeight(58) self.setFixedHeight(58)
layout = QHBoxLayout(self) layout = QHBoxLayout(self)
layout.setContentsMargins(7, 6, 7, 6) layout.setContentsMargins(6, 6, 6, 6)
layout.setSpacing(6) layout.setSpacing(4)
layout.addWidget(self.dot, 0, Qt.AlignLeft | Qt.AlignVCenter)
dots = QVBoxLayout()
dots.setContentsMargins(0, 0, 0, 0)
dots.setSpacing(4)
dots.addWidget(self.read_dot, 0, Qt.AlignCenter)
dots.addWidget(self.write_dot, 0, Qt.AlignCenter)
layout.addLayout(dots)
texts = QVBoxLayout() texts = QVBoxLayout()
texts.setContentsMargins(0, 0, 0, 0) texts.setContentsMargins(0, 0, 0, 0)
@@ -195,14 +199,18 @@ class StatusTile(QFrame):
layout.addLayout(texts, 1) layout.addLayout(texts, 1)
self.setObjectName("statusTile") self.setObjectName("statusTile")
def set_state(self, detail, warning=False, activity=False): def set_state(self, detail, warning=False, read_activity=False, write_activity=False):
self.dot.set_warning(warning) self.read_dot.set_warning(warning)
if activity: self.write_dot.set_warning(warning)
self.dot.blink() if read_activity:
self.read_dot.blink()
if write_activity:
self.write_dot.blink()
self.detail.setText(detail) self.detail.setText(detail)
def tick(self): def tick(self):
self.dot.tick() self.read_dot.tick()
self.write_dot.tick()
class MetricCard(QFrame): class MetricCard(QFrame):
@@ -299,10 +307,12 @@ class TouchPanel(QMainWindow):
self.last_net = {} self.last_net = {}
self.last_io = None self.last_io = None
self.last_sample = 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.current_pwm = read_int(self.fan["pwm"])
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 = {key: {"rx": deque(maxlen=80), "tx": deque(maxlen=80)} for key in ["wan", "lan", "wifi"]} self.net_history = {"wan": {"rx": deque(maxlen=80), "tx": deque(maxlen=80)}}
self.setWindowTitle("Standalone Touch Dashboard") self.setWindowTitle("Standalone Touch Dashboard")
self.setCursor(Qt.BlankCursor) self.setCursor(Qt.BlankCursor)
@@ -313,6 +323,10 @@ class TouchPanel(QMainWindow):
self.sample_timer.timeout.connect(self.sample) self.sample_timer.timeout.connect(self.sample)
self.sample_timer.start(120) self.sample_timer.start(120)
self.graph_timer = QTimer(self)
self.graph_timer.timeout.connect(self.update_graphs)
self.graph_timer.start(500)
self.pulse_timer = QTimer(self) self.pulse_timer = QTimer(self)
self.pulse_timer.timeout.connect(self.tick_pulses) self.pulse_timer.timeout.connect(self.tick_pulses)
self.pulse_timer.start(70) self.pulse_timer.start(70)
@@ -399,11 +413,9 @@ class TouchPanel(QMainWindow):
for index, (key, chart) in enumerate([ for index, (key, chart) in enumerate([
("temp", Sparkline("온도 / RPM", "#f97316", "#38bdf8")), ("temp", Sparkline("온도 / RPM", "#f97316", "#38bdf8")),
("wan", Sparkline("WAN 트래픽", "#38bdf8", "#22c55e")), ("wan", Sparkline("WAN 트래픽", "#38bdf8", "#22c55e")),
("lan", Sparkline("LAN 트래픽", "#38bdf8", "#22c55e")),
("wifi", Sparkline("WiFi 트래픽", "#38bdf8", "#22c55e")),
]): ]):
self.charts[key] = chart self.charts[key] = chart
chart_grid.addWidget(chart, index // 2, index % 2) chart_grid.addWidget(chart, index, 0)
right_layout.addLayout(chart_grid, 1) right_layout.addLayout(chart_grid, 1)
layout.addWidget(left) layout.addWidget(left)
@@ -422,7 +434,8 @@ class TouchPanel(QMainWindow):
percent = pwm / 255 * 100 if pwm > 0 else 0 percent = pwm / 255 * 100 if pwm > 0 else 0
reads, writes = disk_io() reads, writes = disk_io()
io_activity = self.last_io is not None and (reads, writes) != self.last_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) self.last_io = (reads, writes)
groups = {key: group_stats(names) for key, names in NET_GROUPS.items()} groups = {key: group_stats(names) for key, names in NET_GROUPS.items()}
@@ -431,7 +444,10 @@ class TouchPanel(QMainWindow):
for key, row in groups.items(): for key, row in groups.items():
previous = self.last_net.get(key) previous = self.last_net.get(key)
current = (row["rx_bytes"], row["tx_bytes"], row["rx_packets"], row["tx_packets"]) current = (row["rx_bytes"], row["tx_bytes"], row["rx_packets"], row["tx_packets"])
activity[key] = previous is not None and current != previous activity[key] = {
"read": previous is not None and current[1] != previous[1],
"write": previous is not None and current[0] != previous[0],
}
if previous: if previous:
rates[key] = { rates[key] = {
"rx": max(0.0, (current[0] - previous[0]) * 8 / dt / 1_000_000), "rx": max(0.0, (current[0] - previous[0]) * 8 / dt / 1_000_000),
@@ -441,14 +457,15 @@ class TouchPanel(QMainWindow):
rates[key] = {"rx": 0.0, "tx": 0.0} rates[key] = {"rx": 0.0, "tx": 0.0}
self.last_net[key] = current self.last_net[key] = current
self.render(temp, rpm, pwm, percent, enable, groups, rates, activity, io_activity) self.current_rates = rates
self.render(temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity)
def render(self, temp, rpm, pwm, percent, enable, groups, rates, activity, io_activity): def render(self, temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity):
self.rows["io"].set_state(f"{temp:.1f} C", temp >= 75, io_activity) self.rows["io"].set_state(f"{temp:.1f} C", temp >= 75, io_read_activity, io_write_activity)
self.rows["wan"].set_state(self.iface_detail(groups["wan"]), self.iface_warn(groups["wan"]), activity["wan"]) 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"]) 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"]) 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"]) 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["rpm"].set_value(f"{rpm:,}")
self.metrics["temp"].set_value(f"{temp:.1f} C") self.metrics["temp"].set_value(f"{temp:.1f} C")
@@ -463,23 +480,24 @@ class TouchPanel(QMainWindow):
self.mode_buttons["auto"].setChecked(enable not in [0, 1]) self.mode_buttons["auto"].setChecked(enable not in [0, 1])
self.clock.setText(time.strftime("%H:%M:%S")) self.clock.setText(time.strftime("%H:%M:%S"))
self.latest_temp = temp
self.latest_rpm = rpm
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.temp_history.append(temp)
self.rpm_history.append(rpm / 100) self.rpm_history.append(rpm / 100)
self.charts["temp"].set_values(self.temp_history, "C / RPMx100", self.rpm_history) self.charts["temp"].set_values(self.temp_history, "C / RPMx100", self.rpm_history)
for key, chart_key in [("wan", "wan"), ("lan", "lan")]: wan = self.current_rates.get("wan", {"rx": 0.0, "tx": 0.0})
self.net_history[key]["rx"].append(rates[key]["rx"]) self.net_history["wan"]["rx"].append(wan["rx"])
self.net_history[key]["tx"].append(rates[key]["tx"]) self.net_history["wan"]["tx"].append(wan["tx"])
self.charts[chart_key].set_values( self.charts["wan"].set_values(
self.net_history[key]["rx"], self.net_history["wan"]["rx"],
f"{rates[key]['rx']:.2f}/{rates[key]['tx']:.2f} Mbps", f"{wan['rx']:.2f}/{wan['tx']:.2f} Mbps",
self.net_history[key]["tx"], self.net_history["wan"]["tx"],
) )
wifi_rx = rates["wifi24"]["rx"] + rates["wifi5"]["rx"]
wifi_tx = rates["wifi24"]["tx"] + rates["wifi5"]["tx"]
self.net_history["wifi"]["rx"].append(wifi_rx)
self.net_history["wifi"]["tx"].append(wifi_tx)
self.charts["wifi"].set_values(self.net_history["wifi"]["rx"], f"{wifi_rx:.2f}/{wifi_tx:.2f} Mbps", self.net_history["wifi"]["tx"])
def set_fan(self, mode): def set_fan(self, mode):
pwm = self.slider.value() pwm = self.slider.value()