From ed766212014ce5193e4b9b9ff08230b62ebce33e Mon Sep 17 00:00:00 2001 From: seo Date: Thu, 30 Jul 2026 04:50:34 +0900 Subject: [PATCH] =?UTF-8?q?=ED=84=B0=EC=B9=98=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EB=B0=98=EC=9D=91=EC=84=B1=20=EA=B0=9C=EC=84=A0=EA=B3=BC=20?= =?UTF-8?q?=EC=BB=A4=EC=84=9C=20=EC=88=A8=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- touch_panel/panel.py | 122 +++++++++++++++++++++++++++++-------------- 1 file changed, 84 insertions(+), 38 deletions(-) diff --git a/touch_panel/panel.py b/touch_panel/panel.py index 061b119..771a120 100755 --- a/touch_panel/panel.py +++ b/touch_panel/panel.py @@ -4,8 +4,8 @@ import subprocess import sys from pathlib import Path -from PyQt5.QtCore import QPointF, Qt, QTimer -from PyQt5.QtGui import QColor, QFont, QPainter, QPainterPath, QPen +from PyQt5.QtCore import QObject, QPointF, Qt, QThread, QTimer, pyqtSignal +from PyQt5.QtGui import QColor, QPainter, QPainterPath, QPen from PyQt5.QtWidgets import ( QApplication, QFrame, @@ -51,15 +51,20 @@ class StatusDot(QWidget): self.pulse = 0 self.setFixedSize(22, 22) - def set_state(self, active, warning=False): - self.active = active + def set_warning(self, warning=False): self.warning = warning - self.pulse = 4 if active else 0 + self.update() + + def start_pulse(self): + self.active = True + self.pulse = 4 self.update() def tick(self): if self.pulse > 0: self.pulse -= 1 + if self.pulse <= 0: + self.active = False self.update() def paintEvent(self, event): @@ -79,8 +84,9 @@ class StatusDot(QWidget): class StatusTile(QFrame): - def __init__(self, title): + def __init__(self, title, pulse_delay_ms=0): super().__init__() + self.pulse_delay_ms = pulse_delay_ms self.dot = StatusDot() self.title = QLabel(title) self.title.setObjectName("statusTitle") @@ -105,7 +111,9 @@ class StatusTile(QFrame): self.setObjectName("statusTile") def update_state(self, active, detail, warning=False): - self.dot.set_state(active, warning) + self.dot.set_warning(warning) + if active: + QTimer.singleShot(self.pulse_delay_ms, self.dot.start_pulse) self.detail.setText(detail) def tick(self): @@ -119,6 +127,22 @@ class FanButton(QPushButton): self.setMinimumHeight(44) +class BridgeWorker(QObject): + finished = pyqtSignal(str, object) + failed = pyqtSignal(str, str) + + def __init__(self, action, args): + super().__init__() + self.action = action + self.args = args + + def run(self): + try: + self.finished.emit(self.action, run_bridge(*self.args)) + except Exception as exc: + self.failed.emit(self.action, str(exc)) + + class MetricCard(QFrame): def __init__(self, title): super().__init__() @@ -218,17 +242,20 @@ class TouchPanel(QMainWindow): self.metrics = {} self.charts = {} self.last_counters = {} + self.bridge_busy = False + self.worker_thread = None + self.worker = None self.current_mode = "auto" self.current_pwm = 120 - self.in_flight = False self.setWindowTitle("Control Touch Panel") + self.setCursor(Qt.BlankCursor) self.setMinimumSize(800, 480) self._build_ui() self.refresh_timer = QTimer(self) self.refresh_timer.timeout.connect(self.refresh) - self.refresh_timer.start(1200) + self.refresh_timer.start(1500) self.pulse_timer = QTimer(self) self.pulse_timer.timeout.connect(self.tick_pulses) @@ -252,14 +279,14 @@ class TouchPanel(QMainWindow): title.setObjectName("sectionTitle") left_layout.addWidget(title) - for key, label in [ + for index, (key, label) in enumerate([ ("io", "IO"), ("wan", "WAN"), ("lan", "LAN"), ("wifi24", "2.4G"), ("wifi5", "5G"), - ]: - row = StatusTile(label) + ]): + row = StatusTile(label, index * 70) self.rows[key] = row left_layout.addWidget(row) left_layout.addStretch(1) @@ -274,8 +301,11 @@ class TouchPanel(QMainWindow): fan_title.setObjectName("sectionTitle") self.clock = QLabel("--:--:--") self.clock.setObjectName("clock") + self.status_text = QLabel("") + self.status_text.setObjectName("statusText") header.addWidget(fan_title) header.addStretch(1) + header.addWidget(self.status_text) header.addWidget(self.clock) right_layout.addLayout(header) @@ -331,10 +361,6 @@ class TouchPanel(QMainWindow): chart_grid.addWidget(chart, index // 2, index % 2) right_layout.addLayout(chart_grid, 1) - self.notice = QLabel("프로토타입") - self.notice.setObjectName("notice") - self.notice.setWordWrap(True) - right_layout.addWidget(self.notice) right_layout.addStretch(1) layout.addWidget(left) @@ -347,21 +373,12 @@ class TouchPanel(QMainWindow): def slider_changed(self, value): self.current_pwm = value - self.fan_pwm.setText(f"{value} PWM / {value / 255 * 100:.1f}%") + if "pwm" in self.metrics: + self.metrics["pwm"].set_value(f"{value} / {value / 255 * 100:.1f}%") def set_fan(self, mode, pwm): - if self.in_flight: - return - self.in_flight = True - self.notice.setText("팬 변경 중") - try: - self.snapshot = run_bridge("fan", mode, pwm) - self.notice.setText("팬 상태를 바꿨어") - self.render() - except Exception as exc: - self.notice.setText(str(exc)) - finally: - self.in_flight = False + if self.start_bridge("fan", "fan", mode, pwm): + self.status_text.setText("팬 변경 중") def step_pwm(self, delta): value = max(0, min(255, self.slider.value() + delta)) @@ -369,14 +386,42 @@ class TouchPanel(QMainWindow): self.set_fan("manual", value) def refresh(self): - if self.in_flight: - return - try: - self.snapshot = run_bridge("status") - self.render() - except Exception as exc: - self.notice.setText(str(exc)) - self.rows["io"].update_state(False, "control snapshot 실패", True) + self.start_bridge("status", "status") + + def start_bridge(self, action, *args): + if self.bridge_busy: + return False + self.bridge_busy = True + self.worker_thread = QThread(self) + self.worker = BridgeWorker(action, args) + self.worker.moveToThread(self.worker_thread) + self.worker_thread.started.connect(self.worker.run) + self.worker.finished.connect(self.bridge_done) + self.worker.failed.connect(self.bridge_failed) + self.worker.finished.connect(self.worker_thread.quit) + self.worker.failed.connect(self.worker_thread.quit) + self.worker.finished.connect(self.worker.deleteLater) + self.worker.failed.connect(self.worker.deleteLater) + self.worker_thread.finished.connect(self.worker_thread.deleteLater) + self.worker_thread.finished.connect(self.bridge_finished) + self.worker_thread.start() + return True + + def bridge_done(self, action, data): + self.snapshot = data + self.status_text.setText("팬 변경 완료" if action == "fan" else "") + self.render() + + def bridge_failed(self, action, message): + del action + self.status_text.setText("통신 오류") + self.rows["io"].update_state(False, "snapshot 실패", True) + print(message, file=sys.stderr) + + def bridge_finished(self): + self.bridge_busy = False + self.worker_thread = None + self.worker = None def render(self): data = self.snapshot @@ -500,6 +545,7 @@ class TouchPanel(QMainWindow): 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; } @@ -515,7 +561,7 @@ def main(): #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; } - #notice { padding: 10px; background: #17233a; border: 1px solid #2b3c59; border-radius: 12px; color: #a8b8d0; } + #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; }