diff --git a/touch_panel/control_touch_bridge.php b/touch_panel/control_touch_bridge.php index af8c86b..d3cfe3b 100644 --- a/touch_panel/control_touch_bridge.php +++ b/touch_panel/control_touch_bridge.php @@ -4,6 +4,68 @@ declare(strict_types=1); define('CONTROL_API_LIBRARY', true); require_once __DIR__ . '/../public/api.php'; +function touch_panel_snapshot(array $snapshot): array +{ + $network = is_array($snapshot['network'] ?? null) ? $snapshot['network'] : []; + $wifi = is_array($snapshot['wifi'] ?? null) ? $snapshot['wifi'] : []; + $system = is_array($snapshot['system'] ?? null) ? $snapshot['system'] : []; + $fan = is_array($snapshot['fan'] ?? null) ? $snapshot['fan'] : []; + $touch = is_array($snapshot['touch_display'] ?? null) ? $snapshot['touch_display'] : []; + + $interfaces = []; + foreach (($network['interfaces'] ?? []) as $row) { + if (!is_array($row) || !isset($row['name'])) { + continue; + } + $interfaces[] = [ + 'name' => $row['name'], + 'state' => $row['state'] ?? null, + 'carrier' => $row['carrier'] ?? null, + 'rx_errors' => $row['rx_errors'] ?? 0, + 'tx_errors' => $row['tx_errors'] ?? 0, + 'rx_dropped' => $row['rx_dropped'] ?? 0, + 'tx_dropped' => $row['tx_dropped'] ?? 0, + ]; + } + + $clients = []; + foreach (($wifi['clients'] ?? []) as $client) { + if (!is_array($client)) { + continue; + } + $clients[] = [ + 'band' => $client['band'] ?? null, + ]; + } + + return [ + 'ok' => (bool)($snapshot['ok'] ?? true), + 'generated_at' => $snapshot['generated_at'] ?? null, + 'fan' => [ + 'mode' => $fan['mode'] ?? 'auto', + 'pwm' => $fan['pwm'] ?? 0, + 'percent' => $fan['percent'] ?? 0, + 'rpm' => $fan['rpm'] ?? 0, + 'control_temp_c' => $fan['control_temp_c'] ?? null, + 'policy_ok' => $fan['policy_ok'] ?? true, + ], + 'system' => [ + 'hostname' => $system['hostname'] ?? null, + 'temp_c' => $system['temp_c'] ?? null, + ], + 'network' => [ + 'default_interface' => $network['default_interface'] ?? null, + 'interfaces' => $interfaces, + ], + 'wifi' => [ + 'clients' => $clients, + ], + 'touch_display' => [ + 'connected' => $touch['connected'] ?? false, + ], + ]; +} + function touch_panel_json(array $payload, int $code = 0): never { fwrite(STDOUT, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL); @@ -16,7 +78,7 @@ try { if ($command === 'status') { touch_panel_json([ 'ok' => true, - 'data' => collect_snapshot(false), + 'data' => touch_panel_snapshot(collect_snapshot(false)), ]); } @@ -41,7 +103,7 @@ try { touch_panel_json([ 'ok' => true, - 'data' => collect_snapshot(false), + 'data' => touch_panel_snapshot(collect_snapshot(false)), ]); } diff --git a/touch_panel/panel.py b/touch_panel/panel.py index a3a335c..870399d 100755 --- a/touch_panel/panel.py +++ b/touch_panel/panel.py @@ -9,7 +9,6 @@ from PyQt5.QtGui import QColor, QFont, QPainter from PyQt5.QtWidgets import ( QApplication, QFrame, - QGridLayout, QHBoxLayout, QLabel, QMainWindow, @@ -49,7 +48,7 @@ class StatusDot(QWidget): self.active = False self.warning = False self.pulse = 0 - self.setFixedSize(34, 34) + self.setFixedSize(22, 22) def set_state(self, active, warning=False): self.active = active @@ -73,9 +72,9 @@ class StatusDot(QWidget): base = QColor("#86efac") if self.pulse % 2 == 0 else QColor("#16a34a") painter.setBrush(base) painter.setPen(Qt.NoPen) - painter.drawEllipse(4, 4, 26, 26) + painter.drawEllipse(3, 3, 16, 16) painter.setBrush(QColor(255, 255, 255, 55)) - painter.drawEllipse(10, 8, 8, 8) + painter.drawEllipse(8, 6, 5, 5) class StatusTile(QFrame): @@ -88,16 +87,20 @@ class StatusTile(QFrame): self.detail.setObjectName("statusDetail") self.detail.setWordWrap(True) - self.detail.setAlignment(Qt.AlignCenter) + self.detail.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + self.setFixedHeight(58) - layout = QVBoxLayout(self) - layout.setContentsMargins(8, 7, 8, 8) - layout.setSpacing(4) - layout.addWidget(self.title, 0, Qt.AlignLeft) - layout.addStretch(1) - layout.addWidget(self.dot, 0, Qt.AlignCenter) - layout.addStretch(1) - layout.addWidget(self.detail) + layout = QHBoxLayout(self) + layout.setContentsMargins(7, 6, 7, 6) + layout.setSpacing(6) + layout.addWidget(self.dot, 0, Qt.AlignLeft | Qt.AlignVCenter) + + text_layout = QVBoxLayout() + text_layout.setContentsMargins(0, 0, 0, 0) + text_layout.setSpacing(1) + text_layout.addWidget(self.title) + text_layout.addWidget(self.detail) + layout.addLayout(text_layout, 1) self.setObjectName("statusTile") def update_state(self, active, detail, warning=False): @@ -140,35 +143,31 @@ class TouchPanel(QMainWindow): def _build_ui(self): root = QWidget() - grid = QGridLayout(root) - grid.setContentsMargins(16, 14, 16, 14) - grid.setHorizontalSpacing(14) - grid.setVerticalSpacing(10) + layout = QHBoxLayout(root) + layout.setContentsMargins(4, 8, 10, 8) + layout.setSpacing(8) left = QFrame() - left.setObjectName("panel") + left.setObjectName("statusPanel") + left.setFixedWidth(132) left_layout = QVBoxLayout(left) - left_layout.setContentsMargins(14, 14, 14, 14) - left_layout.setSpacing(8) + left_layout.setContentsMargins(6, 8, 6, 8) + left_layout.setSpacing(6) title = QLabel("상태") title.setObjectName("sectionTitle") left_layout.addWidget(title) - status_grid = QGridLayout() - status_grid.setHorizontalSpacing(8) - status_grid.setVerticalSpacing(8) - for index, (key, label) in enumerate([ + for key, label in [ ("io", "IO"), ("wan", "WAN"), ("lan", "LAN"), ("wifi24", "2.4G"), ("wifi5", "5G"), - ]): + ]: row = StatusTile(label) self.rows[key] = row - status_grid.addWidget(row, index // 2, index % 2) - left_layout.addLayout(status_grid, 1) + left_layout.addWidget(row) left_layout.addStretch(1) right = QFrame() @@ -228,10 +227,8 @@ class TouchPanel(QMainWindow): right_layout.addWidget(self.notice) right_layout.addStretch(1) - grid.addWidget(left, 0, 0) - grid.addWidget(right, 0, 1) - grid.setColumnStretch(0, 3) - grid.setColumnStretch(1, 4) + layout.addWidget(left) + layout.addWidget(right, 1) self.setCentralWidget(root) def tick_pulses(self): @@ -295,12 +292,12 @@ class TouchPanel(QMainWindow): w5 = interfaces.get("wlan1") or {} io_active = bool(touch.get("connected")) and bool(fan.get("policy_ok", True)) - io_detail = f"{system.get('hostname', '-')} / {temp:.1f} C" + io_detail = f"{temp:.1f} C" self.rows["io"].update_state(io_active, io_detail, temp >= 75) self.rows["wan"].update_state(self.iface_up(wan), self.iface_detail(wan), self.iface_warn(wan)) - self.rows["lan"].update_state(self.iface_up(lan), f"{len(lan_clients)}대 / {self.iface_detail(lan)}", self.iface_warn(lan)) - self.rows["wifi24"].update_state(self.iface_up(w24), f"{len(wifi24)}대 / {self.iface_detail(w24)}", self.iface_warn(w24)) - self.rows["wifi5"].update_state(self.iface_up(w5), f"{len(wifi5)}대 / {self.iface_detail(w5)}", self.iface_warn(w5)) + self.rows["lan"].update_state(self.iface_up(lan), f"{len(lan_clients)}대 {self.iface_detail(lan)}", self.iface_warn(lan)) + self.rows["wifi24"].update_state(self.iface_up(w24), f"{len(wifi24)}대 {self.iface_detail(w24)}", self.iface_warn(w24)) + self.rows["wifi5"].update_state(self.iface_up(w5), f"{len(wifi5)}대 {self.iface_detail(w5)}", self.iface_warn(w5)) self.current_mode = mode self.current_pwm = pwm @@ -328,7 +325,7 @@ class TouchPanel(QMainWindow): return "없음" rx_drop = int(row.get("rx_dropped") or 0) tx_drop = int(row.get("tx_dropped") or 0) - return f"{row.get('name', '-')} {row.get('state', '-')} / drop {rx_drop + tx_drop}" + return f"{row.get('state', '-')} D{rx_drop + tx_drop}" def main(): @@ -336,12 +333,13 @@ def main(): app.setStyleSheet( """ QWidget { background: #0b1220; color: #e5edf7; font-family: "Noto Sans CJK KR", "Noto Sans KR", sans-serif; } - #panel { background: #111a2e; border: 1px solid #27364f; border-radius: 18px; } - #sectionTitle { font-size: 24px; font-weight: 700; } + #panel { background: #111a2e; border: 1px solid #27364f; border-radius: 16px; } + #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: 15px; font-weight: 700; color: #111827; background: transparent; } - #statusDetail { font-size: 11px; color: #475569; background: transparent; } - #bigValue { font-size: 56px; font-weight: 800; color: #f8fafc; } + #statusTitle { font-size: 13px; font-weight: 700; color: #111827; background: transparent; } + #statusDetail { font-size: 10px; color: #475569; background: transparent; } + #bigValue { font-size: 62px; font-weight: 800; color: #f8fafc; } #subValue { font-size: 22px; color: #b8c5d8; } #clock { font-size: 18px; color: #8fb3ff; font-weight: 700; } #notice { padding: 10px; background: #17233a; border: 1px solid #2b3c59; border-radius: 12px; color: #a8b8d0; }