터치 패널 축 단위와 PC 깨우기 잠금 개선
This commit is contained in:
1 parent
9388122892
commit
e0b384838d
2 files changed
+72
-31
No files matched your search
@@ -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 간격으로 갱신하고, 그래프는 500ms 간격으로 샘플링합니다. 왼쪽 `DISK I/O`, `WAN`, `LAN`, `2.4G`, `5G` 표시등은 장식용 heartbeat가 아니라 실제 커널 카운터 증가에 반응합니다. 각 타일에는 글자 아래에 세 개의 작은 표시등을 두며 읽기/업로드는 초록, 쓰기/다운로드는 주황, 오류는 빨강으로 표시합니다. 활동이 없으면 초록/주황은 회색입니다. 상태패널 최하단 왼쪽의 `PC` 표시등은 `192.168.50.73`에 1초마다 ping을 보내 켜짐 여부를 표시하고, 꺼져 있을 때 누르면 `74:56:3C:CD:CD:79`로 WOL magic packet을 보냅니다.
|
패널은 Control PHP/API/DB snapshot을 호출하지 않는 standalone 방식입니다. Python이 `/sys/class/net`, `/proc/diskstats`, `/sys/class/thermal`, 팬 hwmon 파일을 직접 읽습니다. 상태패널과 팬패널은 120ms 간격으로 갱신하고, 그래프는 500ms 간격으로 샘플링합니다. 왼쪽 `DISK I/O`, `WAN`, `LAN`, `2.4G`, `5G` 표시등은 장식용 heartbeat가 아니라 실제 커널 카운터 증가에 반응합니다. 각 타일에는 글자 아래에 세 개의 작은 표시등을 두며 읽기/업로드는 초록, 쓰기/다운로드는 주황, 오류는 빨강으로 표시합니다. 활동이 없으면 초록/주황은 회색입니다. `DISK I/O`의 값은 `/dev/sda` Samsung T7을 `smartctl -d sntasmedia`로 읽은 디스크 자체 온도입니다. 네트워크 상태값은 정상, 드롭 누적, 꺼짐으로 표시합니다. 상태패널 최하단의 `PC` 타일은 `192.168.50.73`에 1초마다 ping을 보내 켜짐 여부를 표시하고, 꺼져 있을 때 누르면 `74:56:3C:CD:CD:79`로 WOL magic packet을 보냅니다. WOL은 한 번 누르면 30초 동안 추가 전송을 막습니다.
|
||||||
|
|
||||||
오른쪽에는 팬 RPM, CPU 온도, PWM, WAN 실시간 속도와 온도/RPM, WAN 트래픽 그래프를 표시합니다. 팬 버튼과 슬라이더는 Control DB를 거치지 않고 팬 hwmon sysfs에 직접 쓰기를 시도합니다. GUI 사용자의 sysfs 쓰기 권한이 없으면 대시보드는 계속 동작하되 `팬 쓰기 권한 없음`을 표시합니다. 팬 모드 표시는 hwmon `pwm1_enable` 기준이므로 Control DB가 auto여도 fan policy가 sysfs에 직접 PWM을 쓰는 순간 하드웨어 상태는 manual처럼 보일 수 있습니다.
|
오른쪽에는 팬 RPM, CPU 온도, PWM, WAN 실시간 속도와 온도/RPM, WAN 트래픽 그래프를 표시합니다. 그래프는 기존 범례/캡션을 유지하고 Y축에 단위를 함께 표시합니다. 팬 버튼과 슬라이더는 Control DB를 거치지 않고 팬 hwmon sysfs에 직접 쓰기를 시도합니다. GUI 사용자의 sysfs 쓰기 권한이 없으면 대시보드는 계속 동작하되 `팬 쓰기 권한 없음`을 표시합니다. 팬 모드 표시는 hwmon `pwm1_enable` 기준이므로 Control DB가 auto여도 fan policy가 sysfs에 직접 PWM을 쓰는 순간 하드웨어 상태는 manual처럼 보일 수 있습니다.
|
||||||
|
|
||||||
GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다.
|
GUI 자동 실행은 데스크톱 autostart가 1차 기준입니다.
|
||||||
|
|
||||||
|
|||||||
+70
-29
@@ -132,6 +132,11 @@ def disk_io():
|
|||||||
def write_sys(path, value):
|
def write_sys(path, value):
|
||||||
if not path:
|
if not path:
|
||||||
return False
|
return False
|
||||||
|
try:
|
||||||
|
Path(path).write_text(str(value) + "\n", encoding="utf-8")
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def send_magic_packet(mac):
|
def send_magic_packet(mac):
|
||||||
@@ -147,11 +152,6 @@ def send_magic_packet(mac):
|
|||||||
return True
|
return True
|
||||||
except OSError:
|
except OSError:
|
||||||
return False
|
return False
|
||||||
try:
|
|
||||||
Path(path).write_text(str(value) + "\n", encoding="utf-8")
|
|
||||||
return True
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class StatusDot(QWidget):
|
class StatusDot(QWidget):
|
||||||
@@ -236,29 +236,32 @@ class StatusTile(QFrame):
|
|||||||
self.error_dot.tick()
|
self.error_dot.tick()
|
||||||
|
|
||||||
|
|
||||||
class PcWakeButton(QFrame):
|
class PcWakeButton(StatusTile):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__("PC")
|
||||||
self.setObjectName("pcWake")
|
|
||||||
self.online = False
|
self.online = False
|
||||||
self.dot = StatusDot("#16a34a")
|
self.last_wake_at = 0.0
|
||||||
self.label = QLabel("PC")
|
|
||||||
self.label.setObjectName("pcLabel")
|
|
||||||
layout = QHBoxLayout(self)
|
|
||||||
layout.setContentsMargins(5, 4, 5, 4)
|
|
||||||
layout.setSpacing(5)
|
|
||||||
layout.addWidget(self.dot)
|
|
||||||
layout.addWidget(self.label)
|
|
||||||
layout.addStretch(1)
|
|
||||||
self.setFixedHeight(28)
|
|
||||||
|
|
||||||
def set_online(self, online):
|
def set_online(self, online):
|
||||||
self.online = online
|
self.online = online
|
||||||
self.dot.set_steady(online)
|
self.read_dot.set_steady(online)
|
||||||
|
self.error_dot.set_steady(False)
|
||||||
|
if online:
|
||||||
|
self.detail.setText("켜짐")
|
||||||
|
elif time.monotonic() - self.last_wake_at < 30:
|
||||||
|
remaining = int(30 - (time.monotonic() - self.last_wake_at))
|
||||||
|
self.detail.setText(f"대기 {remaining}s")
|
||||||
|
else:
|
||||||
|
self.detail.setText("꺼짐")
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
if event.button() == Qt.LeftButton and not self.online:
|
if event.button() == Qt.LeftButton:
|
||||||
send_magic_packet(PC_MAC)
|
self.write_dot.blink()
|
||||||
|
if not self.online and time.monotonic() - self.last_wake_at >= 30:
|
||||||
|
self.last_wake_at = time.monotonic()
|
||||||
|
self.detail.setText("깨우는 중")
|
||||||
|
if not send_magic_packet(PC_MAC):
|
||||||
|
self.error_dot.set_steady(True)
|
||||||
super().mousePressEvent(event)
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
|
||||||
@@ -281,11 +284,12 @@ class MetricCard(QFrame):
|
|||||||
|
|
||||||
|
|
||||||
class Sparkline(QFrame):
|
class Sparkline(QFrame):
|
||||||
def __init__(self, title, color="#38bdf8", second_color=None):
|
def __init__(self, title, color="#38bdf8", second_color=None, y_unit=""):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setObjectName("chartCard")
|
self.setObjectName("chartCard")
|
||||||
self.color = QColor(color)
|
self.color = QColor(color)
|
||||||
self.second_color = QColor(second_color) if second_color else None
|
self.second_color = QColor(second_color) if second_color else None
|
||||||
|
self.y_unit = y_unit
|
||||||
self.values = []
|
self.values = []
|
||||||
self.second_values = []
|
self.second_values = []
|
||||||
self.title = QLabel(title)
|
self.title = QLabel(title)
|
||||||
@@ -316,7 +320,7 @@ class Sparkline(QFrame):
|
|||||||
del event
|
del event
|
||||||
painter = QPainter(self.canvas)
|
painter = QPainter(self.canvas)
|
||||||
painter.setRenderHint(QPainter.Antialiasing)
|
painter.setRenderHint(QPainter.Antialiasing)
|
||||||
rect = self.canvas.rect().adjusted(2, 4, -2, -4)
|
rect = self.canvas.rect().adjusted(34, 4, -2, -13)
|
||||||
painter.fillRect(rect, QColor("#0f172a"))
|
painter.fillRect(rect, QColor("#0f172a"))
|
||||||
painter.setPen(QPen(QColor("#263650"), 1))
|
painter.setPen(QPen(QColor("#263650"), 1))
|
||||||
painter.drawLine(rect.left(), rect.bottom(), rect.right(), rect.bottom())
|
painter.drawLine(rect.left(), rect.bottom(), rect.right(), rect.bottom())
|
||||||
@@ -327,10 +331,22 @@ class Sparkline(QFrame):
|
|||||||
high = max(all_values)
|
high = max(all_values)
|
||||||
if high <= low:
|
if high <= low:
|
||||||
high = low + 1
|
high = low + 1
|
||||||
|
painter.setPen(QColor("#93a4bd"))
|
||||||
|
painter.drawText(1, rect.top() + 10, self.axis_text(high))
|
||||||
|
painter.drawText(1, rect.bottom(), self.axis_text(low))
|
||||||
self.draw_series(painter, rect, self.values, low, high, self.color)
|
self.draw_series(painter, rect, self.values, low, high, self.color)
|
||||||
if self.second_color and self.second_values:
|
if self.second_color and self.second_values:
|
||||||
self.draw_series(painter, rect, self.second_values, low, high, self.second_color)
|
self.draw_series(painter, rect, self.second_values, low, high, self.second_color)
|
||||||
|
|
||||||
|
def axis_text(self, value):
|
||||||
|
if abs(value) >= 100:
|
||||||
|
text = f"{value:.0f}"
|
||||||
|
elif abs(value) >= 10:
|
||||||
|
text = f"{value:.1f}"
|
||||||
|
else:
|
||||||
|
text = f"{value:.2f}"
|
||||||
|
return f"{text}{self.y_unit}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def draw_series(painter, rect, values, low, high, color):
|
def draw_series(painter, rect, values, low, high, color):
|
||||||
path = QPainterPath()
|
path = QPainterPath()
|
||||||
@@ -358,6 +374,7 @@ class TouchPanel(QMainWindow):
|
|||||||
self.last_sample = None
|
self.last_sample = None
|
||||||
self.last_graph = None
|
self.last_graph = None
|
||||||
self.current_rates = {"wan": {"rx": 0.0, "tx": 0.0}}
|
self.current_rates = {"wan": {"rx": 0.0, "tx": 0.0}}
|
||||||
|
self.disk_temp = None
|
||||||
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)
|
||||||
@@ -382,11 +399,18 @@ class TouchPanel(QMainWindow):
|
|||||||
self.pc_timer.timeout.connect(self.ping_pc)
|
self.pc_timer.timeout.connect(self.ping_pc)
|
||||||
self.pc_timer.start(1000)
|
self.pc_timer.start(1000)
|
||||||
|
|
||||||
|
self.disk_temp_proc = QProcess(self)
|
||||||
|
self.disk_temp_proc.finished.connect(self.disk_temp_finished)
|
||||||
|
self.disk_temp_timer = QTimer(self)
|
||||||
|
self.disk_temp_timer.timeout.connect(self.update_disk_temp)
|
||||||
|
self.disk_temp_timer.start(5000)
|
||||||
|
|
||||||
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)
|
||||||
self.sample()
|
self.sample()
|
||||||
self.ping_pc()
|
self.ping_pc()
|
||||||
|
self.update_disk_temp()
|
||||||
|
|
||||||
def _build_ui(self):
|
def _build_ui(self):
|
||||||
root = QWidget()
|
root = QWidget()
|
||||||
@@ -410,7 +434,7 @@ class TouchPanel(QMainWindow):
|
|||||||
left_layout.addWidget(tile)
|
left_layout.addWidget(tile)
|
||||||
left_layout.addStretch(1)
|
left_layout.addStretch(1)
|
||||||
self.pc_button = PcWakeButton()
|
self.pc_button = PcWakeButton()
|
||||||
left_layout.addWidget(self.pc_button, 0, Qt.AlignLeft | Qt.AlignBottom)
|
left_layout.addWidget(self.pc_button)
|
||||||
|
|
||||||
right = QWidget()
|
right = QWidget()
|
||||||
right_layout = QVBoxLayout(right)
|
right_layout = QVBoxLayout(right)
|
||||||
@@ -469,8 +493,8 @@ class TouchPanel(QMainWindow):
|
|||||||
chart_grid.setHorizontalSpacing(8)
|
chart_grid.setHorizontalSpacing(8)
|
||||||
chart_grid.setVerticalSpacing(8)
|
chart_grid.setVerticalSpacing(8)
|
||||||
for index, (key, chart) in enumerate([
|
for index, (key, chart) in enumerate([
|
||||||
("temp", Sparkline("온도 / RPM", "#f97316", "#38bdf8")),
|
("temp", Sparkline("온도 / RPM", "#f97316", "#38bdf8", "C")),
|
||||||
("wan", Sparkline("WAN 트래픽", "#38bdf8", "#22c55e")),
|
("wan", Sparkline("WAN 트래픽", "#38bdf8", "#22c55e", "M")),
|
||||||
]):
|
]):
|
||||||
self.charts[key] = chart
|
self.charts[key] = chart
|
||||||
chart_grid.addWidget(chart, index, 0)
|
chart_grid.addWidget(chart, index, 0)
|
||||||
@@ -519,7 +543,8 @@ class TouchPanel(QMainWindow):
|
|||||||
self.render(temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity, disk_error)
|
self.render(temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity, disk_error)
|
||||||
|
|
||||||
def render(self, temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity, disk_error):
|
def render(self, temp, rpm, pwm, percent, enable, groups, rates, activity, io_read_activity, io_write_activity, disk_error):
|
||||||
self.rows["io"].set_state(f"{temp:.1f} C", disk_error, io_read_activity, io_write_activity)
|
disk_temp_text = f"{self.disk_temp:.1f} C" if self.disk_temp is not None else "- C"
|
||||||
|
self.rows["io"].set_state(disk_temp_text, disk_error, io_read_activity, io_write_activity)
|
||||||
self.rows["wan"].set_state(self.iface_detail(groups["wan"]), self.iface_warn(groups["wan"]), activity["wan"]["read"], activity["wan"]["write"])
|
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"]["read"], activity["lan"]["write"])
|
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"]["read"], activity["wifi24"]["write"])
|
self.rows["wifi24"].set_state(self.iface_detail(groups["wifi24"]), self.iface_warn(groups["wifi24"]), activity["wifi24"]["read"], activity["wifi24"]["write"])
|
||||||
@@ -550,6 +575,22 @@ class TouchPanel(QMainWindow):
|
|||||||
del exit_status
|
del exit_status
|
||||||
self.pc_button.set_online(exit_code == 0)
|
self.pc_button.set_online(exit_code == 0)
|
||||||
|
|
||||||
|
def update_disk_temp(self):
|
||||||
|
if self.disk_temp_proc.state() != QProcess.NotRunning:
|
||||||
|
return
|
||||||
|
self.disk_temp_proc.start("/usr/sbin/smartctl", ["-A", "-d", "sntasmedia", "/dev/sda"])
|
||||||
|
|
||||||
|
def disk_temp_finished(self, exit_code, exit_status):
|
||||||
|
del exit_code, exit_status
|
||||||
|
output = bytes(self.disk_temp_proc.readAllStandardOutput()).decode("utf-8", "ignore")
|
||||||
|
for line in output.splitlines():
|
||||||
|
if line.strip().startswith("Temperature:"):
|
||||||
|
parts = line.split()
|
||||||
|
for part in parts:
|
||||||
|
if part.isdigit():
|
||||||
|
self.disk_temp = float(part)
|
||||||
|
return
|
||||||
|
|
||||||
def update_graphs(self):
|
def update_graphs(self):
|
||||||
temp = getattr(self, "latest_temp", cpu_temp())
|
temp = getattr(self, "latest_temp", cpu_temp())
|
||||||
rpm = getattr(self, "latest_rpm", read_int(self.fan["rpm"]))
|
rpm = getattr(self, "latest_rpm", read_int(self.fan["rpm"]))
|
||||||
@@ -592,9 +633,9 @@ class TouchPanel(QMainWindow):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def iface_detail(row):
|
def iface_detail(row):
|
||||||
if not row["carrier"]:
|
if not row["carrier"]:
|
||||||
return "down"
|
return "꺼짐"
|
||||||
drops = row["rx_dropped"] + row["tx_dropped"]
|
drops = row["rx_dropped"] + row["tx_dropped"]
|
||||||
return f"up D{drops}"
|
return "정상" if drops <= 0 else f"드롭 {drops}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def iface_warn(row):
|
def iface_warn(row):
|
||||||
|
|||||||
Reference in new issue
Block a user