터치 패널 Qt 프로토타입 추가
This commit is contained in:
1 parent
892e37e72d
commit
44ed804121
4 files changed
+458
No files matched your search
@@ -68,7 +68,10 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
|||||||
- `apply_policy.php`: CLI/cron 팬 정책 적용
|
- `apply_policy.php`: CLI/cron 팬 정책 적용
|
||||||
- `bin/control_ws.php`: WebSocket 서버. 핵심 PHP 파일 변경 감지 시 종료되고 systemd가 재시작해 새 코드를 로드
|
- `bin/control_ws.php`: WebSocket 서버. 핵심 PHP 파일 변경 감지 시 종료되고 systemd가 재시작해 새 코드를 로드
|
||||||
- `bin/wifi_observe.php`: 5G WiFi 연결 시간 보정을 위한 시스템단 관측 CLI
|
- `bin/wifi_observe.php`: 5G WiFi 연결 시간 보정을 위한 시스템단 관측 CLI
|
||||||
|
- `touch_panel/panel.py`: 라즈베리파이 정품 터치 디스플레이용 Qt 관리자 패널 프로토타입
|
||||||
|
- `touch_panel/control_touch_bridge.php`: Qt 패널이 기존 Control PHP 함수를 호출하는 로컬 JSON 브리지
|
||||||
- `systemd/control-wifi-observe.*`: WiFi 관측 CLI를 주기적으로 실행하는 systemd unit 예시
|
- `systemd/control-wifi-observe.*`: WiFi 관측 CLI를 주기적으로 실행하는 systemd unit 예시
|
||||||
|
- `systemd/control-touch-panel.service`: GUI 세션에서 Qt 관리자 패널을 띄우는 systemd unit 예시
|
||||||
|
|
||||||
`asset-version.php`는 `public`의 PHP/JS/CSS/manifest/icon 파일과 `config`의 PHP 파일을 읽어 크기, 수정 시각, sha256 hash를 묶은 전체 버전과 파일별 버전을 만듭니다. `index.php`는 `app.js`, `asset-reload.js`, `wakelock.js`, manifest, icon 경로에 실제 파일 수정 시각과 hash 기반 `v` 값을 붙여 정적 파일 캐시가 오래 남지 않게 합니다. `asset-reload.js`는 화면 복귀, 포커스 복귀, 온라인 복귀, 모바일 터치/클릭 복귀, 30초 간격으로 이 값을 다시 확인하고, 전체 버전 또는 현재 로드된 script/link의 파일별 `v` 값이 다르면 브라우저 Cache Storage를 비우고 service worker 등록을 해제한 뒤 현재 화면을 다시 엽니다. 현재 URL의 `assetReload` 토큰이 최신 파일 묶음 hash와 다르면 첫 진입도 토큰을 붙인 URL로 다시 열어 낡은 JS/CSS가 먼저 실행되는 상황을 줄입니다. 삼성 브라우저 등에서 Web Storage나 `Promise.finally()` 호환성이 부족해도 동작하도록 쿠키 fallback과 timeout 기반 캐시 삭제를 사용합니다. `index.php`는 `no-store` 헤더를 내려 문서 자체가 브라우저 캐시에 오래 남지 않게 합니다.
|
`asset-version.php`는 `public`의 PHP/JS/CSS/manifest/icon 파일과 `config`의 PHP 파일을 읽어 크기, 수정 시각, sha256 hash를 묶은 전체 버전과 파일별 버전을 만듭니다. `index.php`는 `app.js`, `asset-reload.js`, `wakelock.js`, manifest, icon 경로에 실제 파일 수정 시각과 hash 기반 `v` 값을 붙여 정적 파일 캐시가 오래 남지 않게 합니다. `asset-reload.js`는 화면 복귀, 포커스 복귀, 온라인 복귀, 모바일 터치/클릭 복귀, 30초 간격으로 이 값을 다시 확인하고, 전체 버전 또는 현재 로드된 script/link의 파일별 `v` 값이 다르면 브라우저 Cache Storage를 비우고 service worker 등록을 해제한 뒤 현재 화면을 다시 엽니다. 현재 URL의 `assetReload` 토큰이 최신 파일 묶음 hash와 다르면 첫 진입도 토큰을 붙인 URL로 다시 열어 낡은 JS/CSS가 먼저 실행되는 상황을 줄입니다. 삼성 브라우저 등에서 Web Storage나 `Promise.finally()` 호환성이 부족해도 동작하도록 쿠키 fallback과 timeout 기반 캐시 삭제를 사용합니다. `index.php`는 `no-store` 헤더를 내려 문서 자체가 브라우저 캐시에 오래 남지 않게 합니다.
|
||||||
|
|
||||||
@@ -124,6 +127,26 @@ API 응답의 `battery.enabled`는 false, `battery.removed`는 true로 내려가
|
|||||||
|
|
||||||
설정 모달이 열려 있는 동안 1초 상태 갱신은 폼 DOM을 다시 만들지 않습니다. 입력 중인 값은 저장, 기본값 초기화, 닫기 전까지 유지되며, 백그라운드 snapshot은 내부 설정 payload만 갱신합니다. 단순 boolean 설정은 `켜기`/`끄기` 세그먼트로 표시합니다.
|
설정 모달이 열려 있는 동안 1초 상태 갱신은 폼 DOM을 다시 만들지 않습니다. 입력 중인 값은 저장, 기본값 초기화, 닫기 전까지 유지되며, 백그라운드 snapshot은 내부 설정 payload만 갱신합니다. 단순 boolean 설정은 `켜기`/`끄기` 세그먼트로 표시합니다.
|
||||||
|
|
||||||
|
## 터치 패널 프로토타입
|
||||||
|
|
||||||
|
라즈베리파이 정품 7인치 DSI 터치 디스플레이에 띄울 로컬 Qt 관리자 패널 초안을 둡니다. 현재 라즈베리파이는 `multi-user.target` 콘솔 부팅이며 PyQt5는 설치되어 있습니다. GUI로 원복하려면 X/데스크톱 패키지를 다시 설치한 뒤 graphical target으로 바꾸고 재부팅해야 합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo raspi-config
|
||||||
|
sudo systemctl set-default graphical.target
|
||||||
|
```
|
||||||
|
|
||||||
|
패키지가 제거된 상태면 Raspberry Pi OS 데스크톱 패키지와 X 서버를 먼저 복구해야 합니다. 운영 패키지 설치는 사용자가 확인한 뒤 진행합니다.
|
||||||
|
|
||||||
|
프로토타입 수동 실행은 GUI 세션에서 다음처럼 합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/control
|
||||||
|
/usr/bin/python3 touch_panel/panel.py
|
||||||
|
```
|
||||||
|
|
||||||
|
패널은 `touch_panel/control_touch_bridge.php`를 통해 기존 `collect_snapshot()`, `set_control_state()`를 재사용합니다. 왼쪽에는 `IO`, `WAN`, `LAN`, `2.4G`, `5G` 상태 원을 표시하고 active 상태면 초록 원이 갱신 때 짧게 깜빡입니다. 오른쪽에는 팬 RPM, 판단 온도, PWM, `auto`/`manual`/`off`, PWM 증감 버튼을 둡니다. 팬 버튼은 우선 Control DB의 팬 상태를 저장하고, 실제 적용은 기존 Control 팬 정책 루프가 이어받는 구조입니다.
|
||||||
|
|
||||||
## 갱신 주기
|
## 갱신 주기
|
||||||
|
|
||||||
- WebSocket 상태 갱신: 1초마다
|
- WebSocket 상태 갱신: 1초마다
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Control Qt touch panel
|
||||||
|
After=graphical.target mariadb.service
|
||||||
|
Wants=graphical.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=seo
|
||||||
|
WorkingDirectory=/var/www/control
|
||||||
|
Environment=DISPLAY=:0
|
||||||
|
Environment=XAUTHORITY=/home/seo/.Xauthority
|
||||||
|
ExecStart=/usr/bin/python3 /var/www/control/touch_panel/panel.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=graphical.target
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
define('CONTROL_API_LIBRARY', true);
|
||||||
|
require_once __DIR__ . '/../public/api.php';
|
||||||
|
|
||||||
|
function touch_panel_json(array $payload, int $code = 0): never
|
||||||
|
{
|
||||||
|
fwrite(STDOUT, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||||
|
exit($code);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$command = (string)($argv[1] ?? 'status');
|
||||||
|
|
||||||
|
if ($command === 'status') {
|
||||||
|
touch_panel_json([
|
||||||
|
'ok' => true,
|
||||||
|
'data' => collect_snapshot(false),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($command === 'fan') {
|
||||||
|
$mode = (string)($argv[2] ?? 'auto');
|
||||||
|
$pwm = max(0, min(255, (int)($argv[3] ?? 120)));
|
||||||
|
if (!in_array($mode, ['auto', 'manual', 'off'], true)) {
|
||||||
|
touch_panel_json([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => 'bad_mode',
|
||||||
|
], 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
set_control_state($mode, $pwm);
|
||||||
|
add_fan_action(
|
||||||
|
'touch_panel_fan_set',
|
||||||
|
$mode,
|
||||||
|
$pwm,
|
||||||
|
'qt touch panel fan control',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
touch_panel_json([
|
||||||
|
'ok' => true,
|
||||||
|
'data' => collect_snapshot(false),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
touch_panel_json([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => 'unknown_command',
|
||||||
|
], 2);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
touch_panel_json([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => 'exception',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
], 1);
|
||||||
|
}
|
||||||
Executable
+360
@@ -0,0 +1,360 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
|
from PyQt5.QtGui import QColor, QFont, QPainter
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QFrame,
|
||||||
|
QGridLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QMainWindow,
|
||||||
|
QPushButton,
|
||||||
|
QSlider,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
BRIDGE = ROOT / "touch_panel" / "control_touch_bridge.php"
|
||||||
|
PHP = "/usr/bin/php"
|
||||||
|
|
||||||
|
|
||||||
|
def run_bridge(*args):
|
||||||
|
proc = subprocess.run(
|
||||||
|
[PHP, str(BRIDGE), *map(str, args)],
|
||||||
|
cwd=str(ROOT),
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
timeout=8,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise RuntimeError((proc.stderr or proc.stdout or "bridge failed").strip())
|
||||||
|
payload = json.loads(proc.stdout)
|
||||||
|
if not payload.get("ok"):
|
||||||
|
raise RuntimeError(payload.get("message") or payload.get("error") or "bridge error")
|
||||||
|
return payload["data"]
|
||||||
|
|
||||||
|
|
||||||
|
class StatusDot(QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.active = False
|
||||||
|
self.warning = False
|
||||||
|
self.pulse = 0
|
||||||
|
self.setFixedSize(34, 34)
|
||||||
|
|
||||||
|
def set_state(self, active, warning=False):
|
||||||
|
changed = self.active != active or self.warning != warning
|
||||||
|
self.active = active
|
||||||
|
self.warning = warning
|
||||||
|
if active and changed:
|
||||||
|
self.pulse = 6
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def tick(self):
|
||||||
|
if self.active:
|
||||||
|
self.pulse = 6 if self.pulse <= 0 else self.pulse - 1
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
del event
|
||||||
|
painter = QPainter(self)
|
||||||
|
painter.setRenderHint(QPainter.Antialiasing)
|
||||||
|
base = QColor("#22c55e") if self.active else QColor("#64748b")
|
||||||
|
if self.warning:
|
||||||
|
base = QColor("#f59e0b")
|
||||||
|
if self.pulse > 0 and self.active and not self.warning:
|
||||||
|
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.setBrush(QColor(255, 255, 255, 55))
|
||||||
|
painter.drawEllipse(10, 8, 8, 8)
|
||||||
|
|
||||||
|
|
||||||
|
class StatusRow(QFrame):
|
||||||
|
def __init__(self, title):
|
||||||
|
super().__init__()
|
||||||
|
self.dot = StatusDot()
|
||||||
|
self.title = QLabel(title)
|
||||||
|
self.title.setObjectName("statusTitle")
|
||||||
|
self.detail = QLabel("-")
|
||||||
|
self.detail.setObjectName("statusDetail")
|
||||||
|
self.detail.setWordWrap(True)
|
||||||
|
|
||||||
|
texts = QVBoxLayout()
|
||||||
|
texts.setContentsMargins(0, 0, 0, 0)
|
||||||
|
texts.setSpacing(2)
|
||||||
|
texts.addWidget(self.title)
|
||||||
|
texts.addWidget(self.detail)
|
||||||
|
|
||||||
|
layout = QHBoxLayout(self)
|
||||||
|
layout.setContentsMargins(12, 9, 12, 9)
|
||||||
|
layout.setSpacing(10)
|
||||||
|
layout.addWidget(self.dot)
|
||||||
|
layout.addLayout(texts, 1)
|
||||||
|
self.setObjectName("statusRow")
|
||||||
|
|
||||||
|
def update_state(self, active, detail, warning=False):
|
||||||
|
self.dot.set_state(active, warning)
|
||||||
|
self.detail.setText(detail)
|
||||||
|
|
||||||
|
def tick(self):
|
||||||
|
self.dot.tick()
|
||||||
|
|
||||||
|
|
||||||
|
class FanButton(QPushButton):
|
||||||
|
def __init__(self, text):
|
||||||
|
super().__init__(text)
|
||||||
|
self.setCheckable(True)
|
||||||
|
self.setMinimumHeight(44)
|
||||||
|
|
||||||
|
|
||||||
|
class TouchPanel(QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.snapshot = {}
|
||||||
|
self.rows = {}
|
||||||
|
self.mode_buttons = {}
|
||||||
|
self.current_mode = "auto"
|
||||||
|
self.current_pwm = 120
|
||||||
|
self.in_flight = False
|
||||||
|
|
||||||
|
self.setWindowTitle("Control Touch Panel")
|
||||||
|
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.pulse_timer = QTimer(self)
|
||||||
|
self.pulse_timer.timeout.connect(self.tick_pulses)
|
||||||
|
self.pulse_timer.start(180)
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
root = QWidget()
|
||||||
|
grid = QGridLayout(root)
|
||||||
|
grid.setContentsMargins(16, 14, 16, 14)
|
||||||
|
grid.setHorizontalSpacing(14)
|
||||||
|
grid.setVerticalSpacing(10)
|
||||||
|
|
||||||
|
left = QFrame()
|
||||||
|
left.setObjectName("panel")
|
||||||
|
left_layout = QVBoxLayout(left)
|
||||||
|
left_layout.setContentsMargins(14, 14, 14, 14)
|
||||||
|
left_layout.setSpacing(10)
|
||||||
|
|
||||||
|
title = QLabel("상태")
|
||||||
|
title.setObjectName("sectionTitle")
|
||||||
|
left_layout.addWidget(title)
|
||||||
|
for key, label in [
|
||||||
|
("io", "IO"),
|
||||||
|
("wan", "WAN"),
|
||||||
|
("lan", "LAN"),
|
||||||
|
("wifi24", "2.4G"),
|
||||||
|
("wifi5", "5G"),
|
||||||
|
]:
|
||||||
|
row = StatusRow(label)
|
||||||
|
self.rows[key] = row
|
||||||
|
left_layout.addWidget(row)
|
||||||
|
left_layout.addStretch(1)
|
||||||
|
|
||||||
|
right = QFrame()
|
||||||
|
right.setObjectName("panel")
|
||||||
|
right_layout = QVBoxLayout(right)
|
||||||
|
right_layout.setContentsMargins(16, 14, 16, 14)
|
||||||
|
right_layout.setSpacing(12)
|
||||||
|
|
||||||
|
header = QHBoxLayout()
|
||||||
|
fan_title = QLabel("팬")
|
||||||
|
fan_title.setObjectName("sectionTitle")
|
||||||
|
self.clock = QLabel("--:--:--")
|
||||||
|
self.clock.setObjectName("clock")
|
||||||
|
header.addWidget(fan_title)
|
||||||
|
header.addStretch(1)
|
||||||
|
header.addWidget(self.clock)
|
||||||
|
right_layout.addLayout(header)
|
||||||
|
|
||||||
|
self.fan_rpm = QLabel("- RPM")
|
||||||
|
self.fan_rpm.setObjectName("bigValue")
|
||||||
|
self.fan_temp = QLabel("- C")
|
||||||
|
self.fan_temp.setObjectName("subValue")
|
||||||
|
self.fan_pwm = QLabel("- PWM")
|
||||||
|
self.fan_pwm.setObjectName("subValue")
|
||||||
|
right_layout.addWidget(self.fan_rpm)
|
||||||
|
right_layout.addWidget(self.fan_temp)
|
||||||
|
right_layout.addWidget(self.fan_pwm)
|
||||||
|
|
||||||
|
mode_layout = QHBoxLayout()
|
||||||
|
for mode in ["auto", "manual", "off"]:
|
||||||
|
btn = FanButton(mode)
|
||||||
|
btn.clicked.connect(lambda checked=False, m=mode: self.set_fan(m, self.current_pwm))
|
||||||
|
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(120)
|
||||||
|
self.slider.sliderReleased.connect(lambda: self.set_fan("manual", self.slider.value()))
|
||||||
|
self.slider.valueChanged.connect(self.slider_changed)
|
||||||
|
slider_wrap.addWidget(minus)
|
||||||
|
slider_wrap.addWidget(self.slider, 1)
|
||||||
|
slider_wrap.addWidget(plus)
|
||||||
|
right_layout.addLayout(slider_wrap)
|
||||||
|
|
||||||
|
self.notice = QLabel("프로토타입")
|
||||||
|
self.notice.setObjectName("notice")
|
||||||
|
self.notice.setWordWrap(True)
|
||||||
|
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)
|
||||||
|
self.setCentralWidget(root)
|
||||||
|
|
||||||
|
def tick_pulses(self):
|
||||||
|
for row in self.rows.values():
|
||||||
|
row.tick()
|
||||||
|
|
||||||
|
def slider_changed(self, value):
|
||||||
|
self.current_pwm = value
|
||||||
|
self.fan_pwm.setText(f"{value} PWM / {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
|
||||||
|
|
||||||
|
def step_pwm(self, delta):
|
||||||
|
value = max(0, min(255, self.slider.value() + delta))
|
||||||
|
self.slider.setValue(value)
|
||||||
|
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)
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
data = self.snapshot
|
||||||
|
fan = data.get("fan", {})
|
||||||
|
system = data.get("system", {})
|
||||||
|
network = data.get("network", {})
|
||||||
|
wifi = data.get("wifi", {})
|
||||||
|
touch = data.get("touch_display", {})
|
||||||
|
|
||||||
|
interfaces = {row.get("name"): row for row in network.get("interfaces", [])}
|
||||||
|
clients = wifi.get("clients", [])
|
||||||
|
wifi24 = [c for c in clients if c.get("band") == "2.4G"]
|
||||||
|
wifi5 = [c for c in clients if c.get("band") == "5G"]
|
||||||
|
lan_clients = [c for c in clients if c.get("band") == "LAN"]
|
||||||
|
|
||||||
|
temp = float(system.get("temp_c") or 0)
|
||||||
|
rpm = int(fan.get("rpm") or 0)
|
||||||
|
pwm = int(fan.get("pwm") or 0)
|
||||||
|
mode = str(fan.get("mode") or "auto")
|
||||||
|
wan = interfaces.get(network.get("default_interface") or "eth0", {})
|
||||||
|
lan = interfaces.get("br0") or interfaces.get("eth1") or {}
|
||||||
|
w24 = interfaces.get("wlan0") or {}
|
||||||
|
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"
|
||||||
|
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.current_mode = mode
|
||||||
|
self.current_pwm = pwm
|
||||||
|
self.fan_rpm.setText(f"{rpm:,} RPM")
|
||||||
|
self.fan_temp.setText(f"판단 온도 {float(fan.get('control_temp_c') or temp):.1f} C")
|
||||||
|
self.fan_pwm.setText(f"{pwm} PWM / {float(fan.get('percent') or 0):.1f}%")
|
||||||
|
self.slider.blockSignals(True)
|
||||||
|
self.slider.setValue(pwm)
|
||||||
|
self.slider.blockSignals(False)
|
||||||
|
for key, btn in self.mode_buttons.items():
|
||||||
|
btn.setChecked(key == mode)
|
||||||
|
self.clock.setText(str(data.get("generated_at", "--:--:--")).split(" ")[-1])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def iface_up(row):
|
||||||
|
return row.get("state") == "up" and row.get("carrier") == "up"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def iface_warn(row):
|
||||||
|
return int(row.get("rx_errors") or 0) > 0 or int(row.get("tx_errors") or 0) > 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def iface_detail(row):
|
||||||
|
if not row:
|
||||||
|
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}"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
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; }
|
||||||
|
#statusRow { background: #18243a; border: 1px solid #263650; border-radius: 16px; }
|
||||||
|
#statusTitle { font-size: 18px; font-weight: 700; color: #f8fafc; }
|
||||||
|
#statusDetail { font-size: 13px; color: #9fb0c8; }
|
||||||
|
#bigValue { font-size: 56px; 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; }
|
||||||
|
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()
|
||||||
Reference in new issue
Block a user