터치 패널 그래프 현재값 표시 정리
This commit is contained in:
1 parent
1f894bcde1
commit
49a3349a1d
2 files changed
+28
-14
No files matched your search
@@ -148,7 +148,7 @@ cd /var/www/control
|
|||||||
|
|
||||||
패널은 Control PHP/API/DB snapshot을 호출하지 않는 standalone 방식입니다. Python이 `/sys/class/net`, `/proc/diskstats`, `/sys/class/thermal`, 팬 hwmon 파일을 직접 읽습니다. 상태패널, 팬패널, 오른쪽 대시보드 카드/그래프는 50ms 간격으로 갱신합니다. 왼쪽 `DISK I/O`, `WAN`, `LAN`, `2.4G`, `5G` 표시등은 장식용 heartbeat가 아니라 직전 샘플 대비 현재 카운터 증가량이 0 초과인지에 반응합니다. 각 타일에는 글자 아래에 세 개의 작은 표시등을 두며 읽기/업로드은 초록, 쓰기/다운로드는 주황, 오류/드롭 증가는 빨강으로 표시합니다. 활동 표시등은 이벤트 감지 후 120ms 동안 유지하고, 오류 표시등은 2초 동안 유지해 너무 짧은 이벤트도 눈에 보이게 합니다. `DISK I/O`의 값은 `/dev/sda` Samsung T7을 `smartctl -d sntasmedia`로 1초마다 읽은 디스크 자체 온도입니다. GUI 사용자 `seo`는 `/etc/sudoers.d/control-touch-panel-smartctl`에서 해당 smartctl 명령만 NOPASSWD로 허용합니다. 네트워크 상태값은 정상, 드롭 누적, 꺼짐으로 표시합니다. 상태패널 최하단의 `PC` 타일은 `192.168.50.73`에 1초마다 ping을 보내 켜짐 여부를 표시하고, 꺼져 있을 때 누르면 `74:56:3C:CD:CD:79`로 WOL magic packet을 보냅니다. PC 타일의 주황은 WOL 후 최대 1분 동안 컴퓨터 응답 대기 표시, 빨강은 magic packet 전송 실패 시 5초 또는 1분 대기 후 응답 실패 시 1분 표시입니다.
|
패널은 Control PHP/API/DB snapshot을 호출하지 않는 standalone 방식입니다. Python이 `/sys/class/net`, `/proc/diskstats`, `/sys/class/thermal`, 팬 hwmon 파일을 직접 읽습니다. 상태패널, 팬패널, 오른쪽 대시보드 카드/그래프는 50ms 간격으로 갱신합니다. 왼쪽 `DISK I/O`, `WAN`, `LAN`, `2.4G`, `5G` 표시등은 장식용 heartbeat가 아니라 직전 샘플 대비 현재 카운터 증가량이 0 초과인지에 반응합니다. 각 타일에는 글자 아래에 세 개의 작은 표시등을 두며 읽기/업로드은 초록, 쓰기/다운로드는 주황, 오류/드롭 증가는 빨강으로 표시합니다. 활동 표시등은 이벤트 감지 후 120ms 동안 유지하고, 오류 표시등은 2초 동안 유지해 너무 짧은 이벤트도 눈에 보이게 합니다. `DISK I/O`의 값은 `/dev/sda` Samsung T7을 `smartctl -d sntasmedia`로 1초마다 읽은 디스크 자체 온도입니다. GUI 사용자 `seo`는 `/etc/sudoers.d/control-touch-panel-smartctl`에서 해당 smartctl 명령만 NOPASSWD로 허용합니다. 네트워크 상태값은 정상, 드롭 누적, 꺼짐으로 표시합니다. 상태패널 최하단의 `PC` 타일은 `192.168.50.73`에 1초마다 ping을 보내 켜짐 여부를 표시하고, 꺼져 있을 때 누르면 `74:56:3C:CD:CD:79`로 WOL magic packet을 보냅니다. PC 타일의 주황은 WOL 후 최대 1분 동안 컴퓨터 응답 대기 표시, 빨강은 magic packet 전송 실패 시 5초 또는 1분 대기 후 응답 실패 시 1분 표시입니다.
|
||||||
|
|
||||||
오른쪽 상단 카드는 팬 RPM, CPU 온도, PWM, 메모리 사용률을 표시합니다. 아래에는 온도/RPM 그래프와 WAN 트래픽 그래프를 표시합니다. WAN 속도와 그래프 Y축은 byte/sec 기준으로 `B/s`, `KB/s`, `MB/s`, `GB/s` 중 자동 변환합니다. 위 그래프는 좌측 Y축에 온도, 우측 Y축에 RPM 단위를 표시하고, 아래 WAN 그래프는 기존 형태를 유지합니다. 팬 auto/manual/off 버튼은 없고 PWM 슬라이더는 현재값 위치만 보여 주며 터치 제어는 받지 않습니다.
|
오른쪽 상단 카드는 팬 RPM, CPU 온도, PWM 퍼센트, 메모리 사용률을 표시합니다. 아래에는 최근 5초 기준 온도/RPM 그래프와 WAN 트래픽 그래프를 표시합니다. WAN 속도와 그래프 Y축은 byte/sec 기준으로 `B/s`, `KB/s`, `MB/s`, `GB/s` 중 자동 변환하고, 그래프 우측 상단 현재값은 고정폭으로 표시합니다. 위 그래프는 좌측 Y축에 온도, 우측 Y축에 RPM 단위를 표시하고, 아래 WAN 그래프는 기존 형태를 유지합니다. 팬 auto/manual/off 버튼은 없고 PWM 슬라이더는 현재값 위치만 보여 주며 터치 제어는 받지 않습니다.
|
||||||
|
|
||||||
GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다.
|
GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다.
|
||||||
|
|
||||||
|
|||||||
+27
-13
@@ -29,6 +29,9 @@ NET_GROUPS = {
|
|||||||
|
|
||||||
PC_IP = "192.168.50.73"
|
PC_IP = "192.168.50.73"
|
||||||
PC_MAC = "74:56:3C:CD:CD:79"
|
PC_MAC = "74:56:3C:CD:CD:79"
|
||||||
|
SAMPLE_INTERVAL_MS = 50
|
||||||
|
GRAPH_SECONDS = 5
|
||||||
|
GRAPH_POINTS = GRAPH_SECONDS * 1000 // SAMPLE_INTERVAL_MS
|
||||||
|
|
||||||
|
|
||||||
def byte_rate_unit(values):
|
def byte_rate_unit(values):
|
||||||
@@ -44,11 +47,13 @@ def byte_rate_unit(values):
|
|||||||
return 1, "B/s"
|
return 1, "B/s"
|
||||||
|
|
||||||
|
|
||||||
def format_byte_rate(value, factor=None, unit=None):
|
def format_byte_rate(value, factor=None, unit=None, fixed=False):
|
||||||
if factor is None or unit is None:
|
if factor is None or unit is None:
|
||||||
factor, unit = byte_rate_unit([value])
|
factor, unit = byte_rate_unit([value])
|
||||||
scaled = float(value) / factor
|
scaled = float(value) / factor
|
||||||
if abs(scaled) >= 100:
|
if fixed:
|
||||||
|
text = f"{scaled:6.2f}"
|
||||||
|
elif abs(scaled) >= 100:
|
||||||
text = f"{scaled:.0f}"
|
text = f"{scaled:.0f}"
|
||||||
elif abs(scaled) >= 10:
|
elif abs(scaled) >= 10:
|
||||||
text = f"{scaled:.1f}"
|
text = f"{scaled:.1f}"
|
||||||
@@ -57,6 +62,15 @@ def format_byte_rate(value, factor=None, unit=None):
|
|||||||
return f"{text} {unit}"
|
return f"{text} {unit}"
|
||||||
|
|
||||||
|
|
||||||
|
def format_wan_caption(rx_rate, tx_rate):
|
||||||
|
factor, unit = byte_rate_unit([rx_rate, tx_rate])
|
||||||
|
return f"{format_byte_rate(rx_rate, factor, unit, True)} / {format_byte_rate(tx_rate, factor, unit, True)}"
|
||||||
|
|
||||||
|
|
||||||
|
def format_temp_rpm_caption(temp, rpm):
|
||||||
|
return f"{temp:5.1f} C / {int(rpm):5d} rpm"
|
||||||
|
|
||||||
|
|
||||||
def read_text(path, default=""):
|
def read_text(path, default=""):
|
||||||
try:
|
try:
|
||||||
return Path(path).read_text(encoding="utf-8").strip()
|
return Path(path).read_text(encoding="utf-8").strip()
|
||||||
@@ -393,8 +407,8 @@ class Sparkline(QFrame):
|
|||||||
layout.addWidget(self.canvas, 1)
|
layout.addWidget(self.canvas, 1)
|
||||||
|
|
||||||
def set_values(self, values, caption, second_values=None):
|
def set_values(self, values, caption, second_values=None):
|
||||||
self.values = list(values)[-80:]
|
self.values = list(values)[-GRAPH_POINTS:]
|
||||||
self.second_values = list(second_values or [])[-80:]
|
self.second_values = list(second_values or [])[-GRAPH_POINTS:]
|
||||||
self.caption.setText(caption)
|
self.caption.setText(caption)
|
||||||
self.canvas.update()
|
self.canvas.update()
|
||||||
|
|
||||||
@@ -480,9 +494,9 @@ class TouchPanel(QMainWindow):
|
|||||||
self.latest_rpm = 0
|
self.latest_rpm = 0
|
||||||
self.latest_pwm = self.current_pwm
|
self.latest_pwm = self.current_pwm
|
||||||
self.latest_percent = 0.0
|
self.latest_percent = 0.0
|
||||||
self.temp_history = deque(maxlen=80)
|
self.temp_history = deque(maxlen=GRAPH_POINTS)
|
||||||
self.rpm_history = deque(maxlen=80)
|
self.rpm_history = deque(maxlen=GRAPH_POINTS)
|
||||||
self.net_history = {"wan": {"rx": deque(maxlen=80), "tx": deque(maxlen=80)}}
|
self.net_history = {"wan": {"rx": deque(maxlen=GRAPH_POINTS), "tx": deque(maxlen=GRAPH_POINTS)}}
|
||||||
|
|
||||||
self.setWindowTitle("Standalone Touch Dashboard")
|
self.setWindowTitle("Standalone Touch Dashboard")
|
||||||
self.setCursor(Qt.BlankCursor)
|
self.setCursor(Qt.BlankCursor)
|
||||||
@@ -491,11 +505,11 @@ class TouchPanel(QMainWindow):
|
|||||||
|
|
||||||
self.sample_timer = QTimer(self)
|
self.sample_timer = QTimer(self)
|
||||||
self.sample_timer.timeout.connect(self.sample)
|
self.sample_timer.timeout.connect(self.sample)
|
||||||
self.sample_timer.start(50)
|
self.sample_timer.start(SAMPLE_INTERVAL_MS)
|
||||||
|
|
||||||
self.dashboard_timer = QTimer(self)
|
self.dashboard_timer = QTimer(self)
|
||||||
self.dashboard_timer.timeout.connect(self.update_dashboard)
|
self.dashboard_timer.timeout.connect(self.update_dashboard)
|
||||||
self.dashboard_timer.start(50)
|
self.dashboard_timer.start(SAMPLE_INTERVAL_MS)
|
||||||
|
|
||||||
self.pc_ping = QProcess(self)
|
self.pc_ping = QProcess(self)
|
||||||
self.pc_ping.finished.connect(self.pc_ping_finished)
|
self.pc_ping.finished.connect(self.pc_ping_finished)
|
||||||
@@ -663,7 +677,7 @@ class TouchPanel(QMainWindow):
|
|||||||
|
|
||||||
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")
|
||||||
self.metrics["pwm"].set_value(f"{pwm} / {percent:.1f}%")
|
self.metrics["pwm"].set_value(f"{percent:.1f}%")
|
||||||
self.metrics["mem"].set_value(f"{mem_percent:.1f}%")
|
self.metrics["mem"].set_value(f"{mem_percent:.1f}%")
|
||||||
|
|
||||||
self.slider.blockSignals(True)
|
self.slider.blockSignals(True)
|
||||||
@@ -703,14 +717,14 @@ class TouchPanel(QMainWindow):
|
|||||||
rpm = getattr(self, "latest_rpm", read_int(self.fan["rpm"]))
|
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)
|
self.rpm_history.append(rpm)
|
||||||
self.charts["temp"].set_values(self.temp_history, "C / RPM", self.rpm_history)
|
self.charts["temp"].set_values(self.temp_history, format_temp_rpm_caption(temp, rpm), self.rpm_history)
|
||||||
|
|
||||||
wan = self.current_rates.get("wan", {"rx": 0.0, "tx": 0.0})
|
wan = self.current_rates.get("wan", {"rx": 0.0, "tx": 0.0})
|
||||||
self.net_history["wan"]["rx"].append(wan["rx"])
|
self.net_history["wan"]["rx"].append(wan["rx"])
|
||||||
self.net_history["wan"]["tx"].append(wan["tx"])
|
self.net_history["wan"]["tx"].append(wan["tx"])
|
||||||
self.charts["wan"].set_values(
|
self.charts["wan"].set_values(
|
||||||
self.net_history["wan"]["rx"],
|
self.net_history["wan"]["rx"],
|
||||||
f"{format_byte_rate(wan['rx'])}/{format_byte_rate(wan['tx'])}",
|
format_wan_caption(wan["rx"], wan["tx"]),
|
||||||
self.net_history["wan"]["tx"],
|
self.net_history["wan"]["tx"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -747,7 +761,7 @@ def main():
|
|||||||
#metricTitle { color: #8fa7c5; font-size: 13px; background: transparent; }
|
#metricTitle { color: #8fa7c5; font-size: 13px; background: transparent; }
|
||||||
#metricValue { color: #f8fafc; font-size: 24px; font-weight: 800; background: transparent; }
|
#metricValue { color: #f8fafc; font-size: 24px; font-weight: 800; background: transparent; }
|
||||||
#chartTitle { color: #dce8f8; font-size: 14px; font-weight: 700; background: transparent; }
|
#chartTitle { color: #dce8f8; font-size: 14px; font-weight: 700; background: transparent; }
|
||||||
#chartCaption { color: #93a4bd; font-size: 11px; background: transparent; }
|
#chartCaption { color: #93a4bd; font-size: 11px; background: transparent; font-family: "DejaVu Sans Mono", monospace; }
|
||||||
#clock { font-size: 18px; color: #8fb3ff; font-weight: 700; }
|
#clock { font-size: 18px; color: #8fb3ff; font-weight: 700; }
|
||||||
#statusText { color: #93a4bd; font-size: 13px; background: transparent; }
|
#statusText { color: #93a4bd; font-size: 13px; background: transparent; }
|
||||||
QSlider::groove:horizontal { height: 10px; background: #263650; border-radius: 5px; }
|
QSlider::groove:horizontal { height: 10px; background: #263650; border-radius: 5px; }
|
||||||
|
|||||||
Reference in new issue
Block a user