""" ui/readout_panel.py Right-side numeric readout panel. Shows current value for every channel with a colored bar indicator and alarm highlighting. """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QScrollArea, QFrame, QProgressBar ) from PyQt6.QtCore import Qt, pyqtSlot from PyQt6.QtGui import QColor from devices.device_registry import DeviceRegistry class ChannelReadout(QFrame): def __init__(self, device_name: str, channel_config, parent=None): super().__init__(parent) self.ch = channel_config self.device_name = device_name self.setObjectName("channelReadout") self._alarm_active = False self._build() def _build(self): layout = QVBoxLayout(self) layout.setContentsMargins(8, 6, 8, 6) layout.setSpacing(2) top = QHBoxLayout() self.name_lbl = QLabel(f"{self.ch.name}") self.name_lbl.setObjectName("readoutName") top.addWidget(self.name_lbl) top.addStretch() self.value_lbl = QLabel("---") self.value_lbl.setObjectName("readoutValue") self.value_lbl.setStyleSheet(f"color: {self.ch.color};") top.addWidget(self.value_lbl) unit = QLabel(self.ch.unit) unit.setObjectName("readoutUnit") top.addWidget(unit) layout.addLayout(top) # Mini bar self.bar = QProgressBar() self.bar.setRange(0, 1000) self.bar.setValue(0) self.bar.setTextVisible(False) self.bar.setMaximumHeight(4) self.bar.setObjectName("readoutBar") self.bar.setStyleSheet( f"QProgressBar::chunk {{ background: {self.ch.color}; border-radius: 2px; }}" ) layout.addWidget(self.bar) def update_value(self, value: float): self.value_lbl.setText(f"{value:.3f}") rng = self.ch.max_value - self.ch.min_value if rng > 0: norm = int(((value - self.ch.min_value) / rng) * 1000) self.bar.setValue(max(0, min(1000, norm))) def set_alarm(self, active: bool): if active != self._alarm_active: self._alarm_active = active self.setProperty("alarm", active) self.style().unpolish(self) self.style().polish(self) class ReadoutPanel(QWidget): def __init__(self, registry: DeviceRegistry): super().__init__() self.registry = registry self._readouts: dict = {} # (device_id, channel_id) -> ChannelReadout self._build_ui() self.refresh() def _build_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) hdr = QLabel(" CHANNELS") hdr.setObjectName("panelHeader") layout.addWidget(hdr) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self._container = QWidget() self._inner = QVBoxLayout(self._container) self._inner.setContentsMargins(6, 6, 6, 6) self._inner.setSpacing(4) self._inner.addStretch() scroll.setWidget(self._container) layout.addWidget(scroll) def refresh(self): for w in self._readouts.values(): self._inner.removeWidget(w) w.deleteLater() self._readouts.clear() for dev in self.registry.all_instances(): for ch in dev.info.channels: if not ch.enabled: continue ro = ChannelReadout(dev.info.name, ch) self._inner.insertWidget(self._inner.count() - 1, ro) self._readouts[(dev.info.device_id, ch.channel_id)] = ro @pyqtSlot(str, str, float, float) def on_new_data(self, device_id: str, channel_id: str, timestamp: float, value: float): ro = self._readouts.get((device_id, channel_id)) if ro: ro.update_value(value) ch = ro.ch alarm = False if ch.alarm_low is not None and value < ch.alarm_low: alarm = True if ch.alarm_high is not None and value > ch.alarm_high: alarm = True ro.set_alarm(alarm)