diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py
index bb37544a..88739868 100644
--- a/src/aare/gui/main_window.py
+++ b/src/aare/gui/main_window.py
@@ -23,6 +23,7 @@ from aare.gui.models.gui_state_manager import UIStateManager
from aare.gui.panels.LogPanel import LogDock
from aare.gui.panels.beamline_controls import BeamlineControls
+from aare.gui.panels.beamline_state_panel import BeamlineStatePanel
from aare.gui.panels.data_collection_settings import DataCollectionSettings
from aare.gui.panels.developer_help_dialog import DeveloperHelpDialog
from aare.gui.panels.beamline_recovery_panel import BeamlineRecoveryDialog
@@ -166,17 +167,31 @@ class MainWindow(QMainWindow):
collection_controls_scroll = NoWheelScrollArea(top_widget)
+ self.left_column = QWidget(parent=top_widget)
+ self.left_column_layout = QVBoxLayout(self.left_column)
+ self.left_column_layout.setContentsMargins(0, 0, 0, 0)
+ self.left_column_layout.setSpacing(8)
+
self.data_collection = DataCollectionSettings(s=geom,
- parent=top_widget,
+ parent=self.left_column,
raster_mgr=self.raster,
diffraction=diffraction)
+ self.beamline_state_panel = BeamlineStatePanel(parent=self.left_column)
+
+ self.left_column_layout.addWidget(self.data_collection)
+ self.left_column_layout.addWidget(self.beamline_state_panel)
+ self.left_column_layout.addStretch()
+
top_widget_layout.addWidget(collection_controls_scroll)
- collection_controls_scroll.setWidget(self.data_collection)
+ collection_controls_scroll.setWidget(self.left_column)
collection_controls_scroll.setHorizontalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOff
)
- collection_controls_scroll.setFixedWidth(self.data_collection.set_width + 10)
+ collection_controls_scroll.setWidgetResizable(True)
+ collection_controls_scroll.setFixedWidth(
+ max(self.data_collection.set_width, self.beamline_state_panel.set_width) + 10
+ )
self.video_tab = QTabWidget(parent=top_widget)
@@ -527,6 +542,11 @@ class MainWindow(QMainWindow):
self.status_bar.close_shutter.connect(self.daq.close_shutter)
self.status_bar.open_shutter.connect(self.daq.open_shutter)
+ self.beamline_state_panel.dewar_exchange.connect(self.daq.dewar_exchange)
+ self.beamline_state_panel.sample_exchange.connect(self.daq.sample_exchange)
+ self.beamline_state_panel.sample_alignment.connect(self.daq.sample_alignment)
+ self.beamline_state_panel.beam_location.connect(self.daq.beam_location)
+
self.rotation.file_ready.connect(self.viewer.load_image)
self.raster.image_selected.connect(self.viewer.load_image)
@@ -562,6 +582,7 @@ class MainWindow(QMainWindow):
self.daq.update.connect(self.sample_logic.update_daq_status)
self.daq.update.connect(self.manual_sample_panel.update_daq_status)
self.daq.update.connect(self.fluor_panel.update_daq_status)
+ self.daq.update.connect(self.beamline_state_panel.update_daq_status)
self.daq.update.connect(self.update_daq_status)
self.daq.sample_missing.connect(self.show_sample_missing_dialog)
diff --git a/src/aare/gui/panels/beamline_state_panel.py b/src/aare/gui/panels/beamline_state_panel.py
index 1928f630..7da5e1ef 100644
--- a/src/aare/gui/panels/beamline_state_panel.py
+++ b/src/aare/gui/panels/beamline_state_panel.py
@@ -1,20 +1,540 @@
-from PySide6.QtWidgets import QWidget, QGridLayout, QPushButton
+from collections import deque
+from dataclasses import dataclass
-from aare.gui.widgets.title_label import TitleLabel
+from PySide6.QtCore import QPoint, QRect, Qt, Signal, Slot, QSettings
+from PySide6.QtGui import QColor, QPainter, QPen
+from PySide6.QtWidgets import QFrame, QLabel, QPushButton
+
+from aare.common.models import BeamlineStateEnum, DAQStatusModel
-class BeamlineStatePanel(QWidget):
+@dataclass(frozen=True)
+class StationSpec:
+ state: BeamlineStateEnum
+ label: str
+ x: int
+ y: int
+ clickable: bool = False
+ tooltip: str = ""
+
+
+class HoverableLabel(QLabel):
+ hovered = Signal(object)
+ unhovered = Signal()
+
+ def enterEvent(self, event) -> None:
+ self.hovered.emit(getattr(self, "_beamline_state", None))
+ super().enterEvent(event)
+
+ def leaveEvent(self, event) -> None:
+ self.unhovered.emit()
+ super().leaveEvent(event)
+
+
+class HoverableButton(QPushButton):
+ hovered = Signal(object)
+ unhovered = Signal()
+
+ def enterEvent(self, event) -> None:
+ self.hovered.emit(getattr(self, "_beamline_state", None))
+ super().enterEvent(event)
+
+ def leaveEvent(self, event) -> None:
+ self.unhovered.emit()
+ super().leaveEvent(event)
+
+
+class BeamlineStatePanel(QFrame):
+ sample_exchange = Signal()
+ sample_alignment = Signal()
+ dewar_exchange = Signal()
+ beam_location = Signal()
+ data_collection = Signal()
+ xtal_snapshot = Signal()
+ xray_fluorescence = Signal()
+
+ set_width = 400
+ map_height = 560
+ station_radius = 8
+
def __init__(self, parent=None):
super().__init__(parent)
- grid_layout = QGridLayout(self)
+ self.setFrameShape(QFrame.Shape.StyledPanel)
+ self.setFrameShadow(QFrame.Shadow.Raised)
+ self.setFixedWidth(self.set_width)
+ self.setMinimumHeight(self.map_height)
- grid_layout.addWidget(TitleLabel("Beamline state", self), 0, 0)
+ self._settings = QSettings("PSI", "AareGUI")
- self.sample_exchange = QPushButton("Sample exchange", parent=self)
- grid_layout.addWidget(self.sample_exchange, 1, 0)
+ self._current_state: BeamlineStateEnum | None = None
+ self._hovered_state: BeamlineStateEnum | None = None
+ self._pending_target_state: BeamlineStateEnum | None = None
+ self._last_stable_state: BeamlineStateEnum | None = None
+ self._view_mode: str = self._settings.value("beamline_state_panel/view_mode", "metro", type=str)
- self.sample_alignment = QPushButton("Sample alignment", parent=self)
- grid_layout.addWidget(self.sample_alignment, 2, 0)
+ self._line_color = QColor(111, 129, 160)
+ self._line_current = QColor(0, 126, 229)
+ self._line_hover = QColor(244, 196, 48)
- self.dewar_exchange = QPushButton("Load/unload samples", parent=self)
- grid_layout.addWidget(self.dewar_exchange, 3, 0)
+ self._station_current = QColor(0, 126, 229)
+ self._station_current_ring = QColor(120, 195, 255)
+ self._station_hover = QColor(244, 196, 48)
+ self._label_current_bg = "rgba(0, 126, 229, 0.12)"
+ self._label_hover_bg = "rgba(244, 196, 48, 0.22)"
+
+ self._group_colors: dict[BeamlineStateEnum, QColor] = {
+ BeamlineStateEnum.DewarTransfer: QColor(128, 90, 213), # Purple
+ BeamlineStateEnum.SampleExchange: QColor(237, 137, 54), # Orange
+ BeamlineStateEnum.RobotSampleExchange: QColor(237, 137, 54), # Orange
+ BeamlineStateEnum.SampleAlignment: QColor(72, 187, 120), # Green
+ BeamlineStateEnum.BeamLocation: QColor(72, 187, 120), # Green
+ BeamlineStateEnum.DataCollection: QColor(236, 72, 153), # Pink
+ BeamlineStateEnum.XtalSnapshot: QColor(236, 72, 153), # Pink
+ BeamlineStateEnum.XrayFluorescence: QColor(236, 72, 153), # Pink
+ }
+
+ self._group_label_colors: dict[BeamlineStateEnum, str] = {
+ state: color.name() for state, color in self._group_colors.items()
+ }
+
+ self._group_label_backgrounds: dict[BeamlineStateEnum, str] = {
+ BeamlineStateEnum.DewarTransfer: "rgba(128, 90, 213, 0.14)",
+ BeamlineStateEnum.SampleExchange: "rgba(237, 137, 54, 0.16)",
+ BeamlineStateEnum.RobotSampleExchange: "rgba(237, 137, 54, 0.16)",
+ BeamlineStateEnum.SampleAlignment: "rgba(72, 187, 120, 0.16)",
+ BeamlineStateEnum.BeamLocation: "rgba(72, 187, 120, 0.16)",
+ BeamlineStateEnum.DataCollection: "rgba(236, 72, 153, 0.14)",
+ BeamlineStateEnum.XtalSnapshot: "rgba(236, 72, 153, 0.14)",
+ BeamlineStateEnum.XrayFluorescence: "rgba(236, 72, 153, 0.14)",
+ }
+
+ self._metro_stations = [
+ StationSpec(BeamlineStateEnum.DewarTransfer, "Dewar transfer", 96, 126, True, "Dewar transfer mode"),
+ StationSpec(BeamlineStateEnum.SampleExchange, "Manual sample exchange", 96, 186, True, "Manual sample exchange mode"),
+ StationSpec(BeamlineStateEnum.RobotSampleExchange, "Robot sample exchange", 96, 246, False, "Robot-assisted sample exchange"),
+ StationSpec(BeamlineStateEnum.SampleAlignment, "Sample alignment", 96, 316, True, "Sample centring and alignment mode"),
+ StationSpec(BeamlineStateEnum.BeamLocation, "Beam location", 250, 316, True, "Beam location mode"),
+ StationSpec(BeamlineStateEnum.DataCollection, "Data collection", 96, 396, True, "Measurement / collection mode"),
+ StationSpec(BeamlineStateEnum.XtalSnapshot, "Crystal snapshot", 250, 396, True, "Crystal snapshot mode"),
+ StationSpec(BeamlineStateEnum.XrayFluorescence, "XRF", 96, 476, True, "X-ray fluorescence mode"),
+ ]
+
+ self._line_stations = [
+ StationSpec(BeamlineStateEnum.DewarTransfer, "Dewar transfer", 54, 170, True, "Dewar transfer mode"),
+ StationSpec(BeamlineStateEnum.SampleExchange, "Manual sample exchange", 54, 220, True, "Manual sample exchange mode"),
+ StationSpec(BeamlineStateEnum.RobotSampleExchange, "Robot sample exchange", 54, 270, False, "Robot-assisted sample exchange"),
+ StationSpec(BeamlineStateEnum.SampleAlignment, "Sample alignment", 54, 320, True, "Sample centring and alignment mode"),
+ StationSpec(BeamlineStateEnum.BeamLocation, "Beam location", 54, 370, True, "Beam location mode"),
+ StationSpec(BeamlineStateEnum.DataCollection, "Data collection", 54, 420, True, "Measurement / collection mode"),
+ StationSpec(BeamlineStateEnum.XtalSnapshot, "Crystal snapshot", 54, 470, True, "Crystal snapshot mode"),
+ StationSpec(BeamlineStateEnum.XrayFluorescence, "XRF", 54, 520, True, "X-ray fluorescence mode"),
+ ]
+
+ self._segments = [
+ (BeamlineStateEnum.DewarTransfer, BeamlineStateEnum.SampleExchange),
+ (BeamlineStateEnum.SampleExchange, BeamlineStateEnum.RobotSampleExchange),
+ (BeamlineStateEnum.RobotSampleExchange, BeamlineStateEnum.SampleAlignment),
+ (BeamlineStateEnum.SampleAlignment, BeamlineStateEnum.BeamLocation),
+ (BeamlineStateEnum.SampleAlignment, BeamlineStateEnum.DataCollection),
+ (BeamlineStateEnum.DataCollection, BeamlineStateEnum.XtalSnapshot),
+ (BeamlineStateEnum.DataCollection, BeamlineStateEnum.XrayFluorescence),
+ ]
+
+ self._graph = self._build_graph(self._segments)
+ self._station_widgets: dict[BeamlineStateEnum, QLabel | QPushButton] = {}
+
+ self.title = QLabel(self)
+ self.title.setText("
Beamline state
")
+ self.title.setStyleSheet("background-color: #4B0082; color: #ffffff;")
+ self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.title.setFixedHeight(50)
+ self.title.setGeometry(0, 0, self.set_width, 50)
+
+ self.current_label = QLabel("Current: —", self)
+ self.current_label.setStyleSheet("""
+ QLabel {
+ color: rgb(30, 41, 59);
+ font-size: 18px;
+ font-weight: 700;
+ padding-left: 4px;
+ background: transparent;
+ }
+ """)
+ self.current_label.move(14, 58)
+ self.current_label.adjustSize()
+
+ self.view_toggle = QPushButton(self)
+ self.view_toggle.setCheckable(False)
+ self.view_toggle.setStyleSheet("""
+ QPushButton {
+ background-color: rgb(240, 244, 252);
+ color: rgb(55, 67, 87);
+ border: 1px solid rgb(160, 175, 200);
+ border-radius: 8px;
+ padding: 4px 10px;
+ font-size: 12px;
+ font-weight: 600;
+ }
+ QPushButton:hover {
+ background-color: rgb(228, 236, 248);
+ }
+ """)
+ self.view_toggle.clicked.connect(self._toggle_view_mode)
+ self._update_toggle_text()
+
+ self._build_station_widgets()
+ self._position_station_widgets()
+
+ @staticmethod
+ def _canon_segment(a: BeamlineStateEnum, b: BeamlineStateEnum) -> tuple[BeamlineStateEnum, BeamlineStateEnum]:
+ return tuple(sorted((a, b), key=lambda state: state.value))
+
+ def _build_graph(
+ self,
+ segments: list[tuple[BeamlineStateEnum, BeamlineStateEnum]],
+ ) -> dict[BeamlineStateEnum, set[BeamlineStateEnum]]:
+ graph: dict[BeamlineStateEnum, set[BeamlineStateEnum]] = {}
+ for a, b in segments:
+ graph.setdefault(a, set()).add(b)
+ graph.setdefault(b, set()).add(a)
+ return graph
+
+ def _stations(self) -> list[StationSpec]:
+ return self._metro_stations if self._view_mode == "metro" else self._line_stations
+
+ def _station_map(self) -> dict[BeamlineStateEnum, StationSpec]:
+ return {station.state: station for station in self._stations()}
+
+ def _path_segments_between(
+ self,
+ start: BeamlineStateEnum | None,
+ end: BeamlineStateEnum | None,
+ ) -> set[tuple[BeamlineStateEnum, BeamlineStateEnum]]:
+ if start is None or end is None:
+ return set()
+
+ if start == BeamlineStateEnum.Moving or end == BeamlineStateEnum.Moving:
+ return set()
+
+ if start == end:
+ return set()
+
+ queue = deque([start])
+ previous: dict[BeamlineStateEnum, BeamlineStateEnum | None] = {start: None}
+
+ while queue:
+ node = queue.popleft()
+ if node == end:
+ break
+
+ for neighbour in self._graph.get(node, set()):
+ if neighbour in previous:
+ continue
+ previous[neighbour] = node
+ queue.append(neighbour)
+
+ if end not in previous:
+ return set()
+
+ path_segments: set[tuple[BeamlineStateEnum, BeamlineStateEnum]] = set()
+ cursor = end
+ while previous[cursor] is not None:
+ parent = previous[cursor]
+ path_segments.add(self._canon_segment(cursor, parent))
+ cursor = parent
+
+ return path_segments
+
+ def _active_hover_route(self) -> set[tuple[BeamlineStateEnum, BeamlineStateEnum]]:
+ if self._hovered_state is not None:
+ route_source = self._last_stable_state if self._current_state == BeamlineStateEnum.Moving else self._current_state
+ return self._path_segments_between(route_source, self._hovered_state)
+
+ if self._current_state == BeamlineStateEnum.Moving and self._pending_target_state is not None:
+ return self._path_segments_between(self._last_stable_state, self._pending_target_state)
+
+ return set()
+
+ def _build_station_widgets(self) -> None:
+ for station in self._metro_stations:
+ if station.clickable:
+ widget: QLabel | QPushButton = HoverableButton(station.label, self)
+ widget.setFlat(True)
+ widget.setCursor(Qt.CursorShape.PointingHandCursor)
+ widget.clicked.connect(lambda _checked=False, state=station.state: self._emit_for_state(state))
+ else:
+ widget = HoverableLabel(station.label, self)
+
+ widget._beamline_state = station.state
+ widget.hovered.connect(self._set_hovered_state)
+ widget.unhovered.connect(self._clear_hovered_state)
+ widget.setToolTip(station.tooltip or station.label)
+ self._station_widgets[station.state] = widget
+
+ self._apply_station_highlight()
+
+ def _position_station_widgets(self) -> None:
+ station_map = self._station_map()
+
+ for state, widget in self._station_widgets.items():
+ station = station_map[state]
+ widget.setText(station.label)
+ widget.adjustSize()
+
+ label_x = station.x + 18
+ label_y = station.y - 12
+
+ if self._view_mode == "metro":
+ if state == BeamlineStateEnum.SampleAlignment:
+ label_x = station.x + 24
+ label_y = station.y + 8
+ elif state == BeamlineStateEnum.DataCollection:
+ label_x = station.x + 24
+ label_y = station.y + 8
+ elif state == BeamlineStateEnum.BeamLocation:
+ label_y = station.y - 18
+ elif state == BeamlineStateEnum.XtalSnapshot:
+ label_y = station.y - 14
+ elif state == BeamlineStateEnum.XrayFluorescence:
+ label_y = station.y - 14
+ else:
+ label_x = station.x + 20
+ label_y = station.y - 12
+
+ widget.move(label_x, label_y)
+ widget.show()
+
+ self._apply_station_highlight()
+
+ def _update_toggle_text(self) -> None:
+ self.view_toggle.setText("View: Metro" if self._view_mode == "metro" else "View: Line")
+ self.view_toggle.adjustSize()
+ self.view_toggle.move(self.set_width - 118, 58)
+
+ @Slot()
+ def _toggle_view_mode(self) -> None:
+ self._view_mode = "line" if self._view_mode == "metro" else "metro"
+ self._settings.setValue("beamline_state_panel/view_mode", self._view_mode)
+ self._update_toggle_text()
+ self._position_station_widgets()
+ self.update()
+
+ def _emit_for_state(self, state: BeamlineStateEnum) -> None:
+ self._pending_target_state = state
+ self._hovered_state = None
+ self.update()
+
+ if state == BeamlineStateEnum.SampleExchange:
+ self.sample_exchange.emit()
+ elif state == BeamlineStateEnum.SampleAlignment:
+ self.sample_alignment.emit()
+ elif state == BeamlineStateEnum.DewarTransfer:
+ self.dewar_exchange.emit()
+ elif state == BeamlineStateEnum.BeamLocation:
+ self.beam_location.emit()
+ elif state == BeamlineStateEnum.DataCollection:
+ self.data_collection.emit()
+ elif state == BeamlineStateEnum.XtalSnapshot:
+ self.xtal_snapshot.emit()
+ elif state == BeamlineStateEnum.XrayFluorescence:
+ self.xray_fluorescence.emit()
+
+ @Slot(object)
+ def _set_hovered_state(self, state: BeamlineStateEnum | None) -> None:
+ self._hovered_state = state
+ self._apply_station_highlight()
+
+ @Slot()
+ def _clear_hovered_state(self) -> None:
+ self._hovered_state = None
+ self._apply_station_highlight()
+
+ def _apply_station_highlight(self) -> None:
+ for station in self._stations():
+ widget = self._station_widgets[station.state]
+ is_current = station.state == self._current_state
+ is_hovered = station.state == self._hovered_state
+ is_pending = station.state == self._pending_target_state and self._current_state == BeamlineStateEnum.Moving
+
+ label_color = self._group_label_colors.get(station.state, "rgb(55, 67, 87)")
+ label_bg = self._group_label_backgrounds.get(station.state, "transparent")
+
+ if isinstance(widget, QPushButton):
+ if is_current:
+ widget.setStyleSheet(f"""
+ QPushButton {{
+ border: none;
+ border-radius: 10px;
+ background: {self._label_current_bg};
+ color: rgb(0, 92, 170);
+ font-size: 13px;
+ font-weight: 700;
+ text-align: left;
+ padding: 2px 6px 2px 8px;
+ }}
+ QPushButton:hover {{
+ color: rgb(0, 92, 170);
+ }}
+ """)
+ elif is_hovered or is_pending:
+ widget.setStyleSheet(f"""
+ QPushButton {{
+ border: none;
+ border-radius: 10px;
+ background: {self._label_hover_bg};
+ color: rgb(115, 88, 0);
+ font-size: 13px;
+ font-weight: 700;
+ text-align: left;
+ padding: 2px 6px 2px 8px;
+ }}
+ QPushButton:hover {{
+ color: rgb(115, 88, 0);
+ }}
+ """)
+ else:
+ widget.setStyleSheet(f"""
+ QPushButton {{
+ border: none;
+ border-radius: 10px;
+ background: {label_bg};
+ color: {label_color};
+ font-size: 13px;
+ font-weight: 600;
+ text-align: left;
+ padding: 2px 6px 2px 8px;
+ }}
+ QPushButton:hover {{
+ color: {label_color};
+ }}
+ """)
+ else:
+ if is_current:
+ widget.setStyleSheet(f"""
+ QLabel {{
+ border-radius: 10px;
+ background: {self._label_current_bg};
+ color: rgb(0, 92, 170);
+ font-size: 13px;
+ font-weight: 700;
+ padding: 2px 6px 2px 8px;
+ }}
+ """)
+ elif is_hovered or is_pending:
+ widget.setStyleSheet(f"""
+ QLabel {{
+ border-radius: 10px;
+ background: {self._label_hover_bg};
+ color: rgb(115, 88, 0);
+ font-size: 13px;
+ font-weight: 700;
+ padding: 2px 6px 2px 8px;
+ }}
+ """)
+ else:
+ widget.setStyleSheet(f"""
+ QLabel {{
+ border-radius: 10px;
+ background: {label_bg};
+ color: {label_color};
+ font-size: 13px;
+ font-weight: 600;
+ padding: 2px 6px 2px 8px;
+ }}
+ """)
+
+ widget.adjustSize()
+
+ self.update()
+
+ def _station_center(self, state: BeamlineStateEnum) -> QPoint:
+ station = self._station_map()[state]
+ return QPoint(station.x, station.y)
+
+ def _segment_color(self, a: BeamlineStateEnum, b: BeamlineStateEnum) -> QColor:
+ segment = self._canon_segment(a, b)
+
+ active_hover_route = self._active_hover_route()
+ if segment in active_hover_route:
+ return self._line_hover
+
+ current_path = self._path_segments_between(BeamlineStateEnum.DewarTransfer, self._last_stable_state or self._current_state)
+ if segment in current_path:
+ return self._line_current
+
+ return self._line_color
+
+ def _draw_segment(self, painter: QPainter, start: QPoint, end: QPoint, color: QColor) -> None:
+ pen = QPen(color, 4)
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
+ painter.setPen(pen)
+ painter.drawLine(start, end)
+
+ def _station_base_color(self, state: BeamlineStateEnum) -> QColor:
+ return self._group_colors.get(state, QColor(180, 190, 210))
+
+ def _draw_station(self, painter: QPainter, station: StationSpec) -> None:
+ center = self._station_center(station.state)
+ rect = QRect(
+ center.x() - self.station_radius,
+ center.y() - self.station_radius,
+ self.station_radius * 2,
+ self.station_radius * 2,
+ )
+
+ if station.state == self._hovered_state or (
+ self._current_state == BeamlineStateEnum.Moving and station.state == self._pending_target_state
+ ):
+ painter.setPen(QPen(self._station_hover, 3))
+ painter.setBrush(self._station_hover)
+ elif station.state == self._current_state:
+ painter.setPen(QPen(self._station_current_ring, 4))
+ painter.setBrush(Qt.BrushStyle.NoBrush)
+ painter.drawEllipse(
+ QRect(
+ center.x() - self.station_radius - 4,
+ center.y() - self.station_radius - 4,
+ (self.station_radius + 4) * 2,
+ (self.station_radius + 4) * 2,
+ )
+ )
+ painter.setPen(QPen(self._station_current, 2))
+ painter.setBrush(self._station_current)
+ else:
+ base_color = self._station_base_color(station.state)
+ painter.setPen(QPen(base_color.darker(125), 2))
+ painter.setBrush(base_color)
+
+ painter.drawEllipse(rect)
+
+ def paintEvent(self, event) -> None:
+ super().paintEvent(event)
+
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
+
+ for start_state, end_state in self._segments:
+ start = self._station_center(start_state)
+ end = self._station_center(end_state)
+ color = self._segment_color(start_state, end_state)
+ self._draw_segment(painter, start, end, color)
+
+ for station in self._stations():
+ self._draw_station(painter, station)
+
+ @Slot(DAQStatusModel)
+ def update_daq_status(self, status: DAQStatusModel) -> None:
+ self.set_current_state(status.state)
+
+ def set_current_state(self, state: BeamlineStateEnum | None) -> None:
+ self._current_state = state
+
+ if state is not None and state != BeamlineStateEnum.Moving:
+ self._last_stable_state = state
+ if self._pending_target_state == state:
+ self._pending_target_state = None
+
+ label = state.display_name() if state is not None else "—"
+ self.current_label.setText(f"Current: {label}")
+ self.current_label.adjustSize()
+ self._apply_station_highlight()
\ No newline at end of file