mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 11:41:49 +02:00
feat: motor_control.py MotorCoordinateTable added basic version to store coordinates and show them in motor_map.py
This commit is contained in:
@ -0,0 +1,6 @@
|
|||||||
|
from .motor_movement import (
|
||||||
|
MotorControlMap,
|
||||||
|
MotorControlPanel,
|
||||||
|
MotorControlPanelRelative,
|
||||||
|
MotorControlPanelAbsolute,
|
||||||
|
)
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
from .motor_control_compilations import (
|
||||||
|
MotorControlMap,
|
||||||
|
MotorControlPanel,
|
||||||
|
MotorControlPanelRelative,
|
||||||
|
MotorControlPanelAbsolute,
|
||||||
|
)
|
||||||
|
@ -9,4 +9,5 @@ from .motor_control import (
|
|||||||
MotorControlAbsolute,
|
MotorControlAbsolute,
|
||||||
MotorControlSelection,
|
MotorControlSelection,
|
||||||
MotorThread,
|
MotorThread,
|
||||||
|
MotorCoordinateTable,
|
||||||
)
|
)
|
||||||
|
@ -3,4 +3,5 @@ from .motor_control import (
|
|||||||
MotorControlAbsolute,
|
MotorControlAbsolute,
|
||||||
MotorControlSelection,
|
MotorControlSelection,
|
||||||
MotorThread,
|
MotorThread,
|
||||||
|
MotorCoordinateTable,
|
||||||
)
|
)
|
||||||
|
@ -3,6 +3,8 @@ import os
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
import qdarktheme
|
import qdarktheme
|
||||||
|
from PyQt6.QtGui import QDoubleValidator
|
||||||
|
from PyQt6.QtWidgets import QPushButton, QTableWidgetItem, QCheckBox, QLineEdit
|
||||||
|
|
||||||
from qtpy import uic
|
from qtpy import uic
|
||||||
from qtpy.QtCore import QThread, Slot as pyqtSlot
|
from qtpy.QtCore import QThread, Slot as pyqtSlot
|
||||||
@ -10,10 +12,13 @@ from qtpy.QtCore import Signal as pyqtSignal, Qt
|
|||||||
from qtpy.QtGui import QKeySequence
|
from qtpy.QtGui import QKeySequence
|
||||||
from qtpy.QtWidgets import (
|
from qtpy.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
|
QHeaderView,
|
||||||
QWidget,
|
QWidget,
|
||||||
QSpinBox,
|
QSpinBox,
|
||||||
QDoubleSpinBox,
|
QDoubleSpinBox,
|
||||||
QShortcut,
|
QShortcut,
|
||||||
|
QVBoxLayout,
|
||||||
|
QTableWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from qtpy.QtWidgets import QApplication, QMessageBox
|
from qtpy.QtWidgets import QApplication, QMessageBox
|
||||||
@ -205,7 +210,6 @@ class MotorControlAbsolute(MotorControlWidget):
|
|||||||
|
|
||||||
# Enable/Disable GUI
|
# Enable/Disable GUI
|
||||||
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
|
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
|
||||||
# self.motor_thread.move_finished.connect(lambda: self._enable_motor_controls(True))
|
|
||||||
|
|
||||||
# Error messages
|
# Error messages
|
||||||
self.motor_thread.motor_error.connect(
|
self.motor_thread.motor_error.connect(
|
||||||
@ -508,6 +512,237 @@ class MotorControlRelative(MotorControlWidget):
|
|||||||
self.motor_thread.move_relative(motor, step)
|
self.motor_thread.move_relative(motor, step)
|
||||||
|
|
||||||
|
|
||||||
|
class MotorCoordinateTable(MotorControlWidget):
|
||||||
|
plot_coordinates_signal = pyqtSignal(list)
|
||||||
|
|
||||||
|
def _load_ui(self):
|
||||||
|
"""Load the UI for the coordinate table."""
|
||||||
|
current_path = os.path.dirname(__file__)
|
||||||
|
uic.loadUi(os.path.join(current_path, "motor_control_table.ui"), self)
|
||||||
|
|
||||||
|
def _init_ui(self):
|
||||||
|
"""Initialize the UI"""
|
||||||
|
# Setup table behaviour
|
||||||
|
self._setup_table()
|
||||||
|
self.table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||||
|
|
||||||
|
# for tag columns default tag
|
||||||
|
self.tag_counter = 1
|
||||||
|
|
||||||
|
# Connect signals and slots
|
||||||
|
self.checkBox_resize_auto.stateChanged.connect(self.resize_table_auto)
|
||||||
|
|
||||||
|
# Keyboard shortcuts for deleting a row
|
||||||
|
self.delete_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self.table)
|
||||||
|
self.delete_shortcut.activated.connect(self.delete_selected_row)
|
||||||
|
self.backspace_shortcut = QShortcut(QKeySequence(Qt.Key_Backspace), self.table)
|
||||||
|
self.backspace_shortcut.activated.connect(self.delete_selected_row)
|
||||||
|
|
||||||
|
@pyqtSlot(dict)
|
||||||
|
def on_config_update(self, config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Update config dict
|
||||||
|
Args:
|
||||||
|
config(dict): New config dict
|
||||||
|
"""
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
# Get motor names
|
||||||
|
self.motor_x, self.motor_y = (
|
||||||
|
self.config["motor_control"]["motor_x"],
|
||||||
|
self.config["motor_control"]["motor_y"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Decimal precision of the table coordinates
|
||||||
|
self.precision = self.config["motor_control"].get("precision", 3)
|
||||||
|
|
||||||
|
# Mode switch default option
|
||||||
|
self.mode = self.config["motor_control"].get("mode", "Individual")
|
||||||
|
|
||||||
|
# Set combobox to default mode
|
||||||
|
self.mode_combobox.setCurrentText(self.mode)
|
||||||
|
|
||||||
|
self._init_ui()
|
||||||
|
|
||||||
|
def _setup_table(self):
|
||||||
|
"""Setup the table with appropriate headers and configurations."""
|
||||||
|
mode = self.mode_combobox.currentText()
|
||||||
|
|
||||||
|
if mode == "Individual":
|
||||||
|
self._setup_individual_mode()
|
||||||
|
elif mode == "Start/Stop":
|
||||||
|
self._setup_start_stop_mode()
|
||||||
|
self.start_stop_counter = 0
|
||||||
|
|
||||||
|
def _setup_individual_mode(self):
|
||||||
|
self.table.setColumnCount(5)
|
||||||
|
self.table.setHorizontalHeaderLabels(["Show", "Move", "Tag", "X", "Y"])
|
||||||
|
self.table.horizontalHeader().setStretchLastSection(True)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||||
|
self.table.verticalHeader().setVisible(False)
|
||||||
|
|
||||||
|
def _setup_start_stop_mode(self):
|
||||||
|
self.table.setColumnCount(8)
|
||||||
|
self.table.setHorizontalHeaderLabels(
|
||||||
|
[
|
||||||
|
"Show",
|
||||||
|
"Move [start]",
|
||||||
|
"Move [end]" "Tag",
|
||||||
|
"X [start]",
|
||||||
|
"Y [start]",
|
||||||
|
"X [end]",
|
||||||
|
"Y [end]",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.table.horizontalHeader().setStretchLastSection(True)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||||
|
self.table.verticalHeader().setVisible(False)
|
||||||
|
|
||||||
|
@pyqtSlot(tuple)
|
||||||
|
def add_coordinate(self, coordinates: tuple):
|
||||||
|
"""
|
||||||
|
Add a coordinate to the table.
|
||||||
|
Args:
|
||||||
|
coordinates(tuple): Coordinates (x,y) to add to the table.
|
||||||
|
"""
|
||||||
|
tag = f"Pos {self.tag_counter}"
|
||||||
|
self.tag_counter += 1
|
||||||
|
x, y = coordinates
|
||||||
|
self._add_row(tag, x, y)
|
||||||
|
|
||||||
|
def _add_row(self, tag, x, y):
|
||||||
|
"""Internal method to add a row to the table."""
|
||||||
|
row_count = self.table.rowCount()
|
||||||
|
self.table.insertRow(row_count)
|
||||||
|
|
||||||
|
# Checkbox for toggling visibility
|
||||||
|
show_checkbox = QCheckBox()
|
||||||
|
show_checkbox.setChecked(True)
|
||||||
|
show_checkbox.stateChanged.connect(self.emit_plot_coordinates)
|
||||||
|
self.table.setCellWidget(row_count, 0, show_checkbox)
|
||||||
|
|
||||||
|
# TODO add mode switch recognision
|
||||||
|
# Move button
|
||||||
|
move_button = QPushButton("Move")
|
||||||
|
move_button.clicked.connect(self.handle_move_button_click)
|
||||||
|
self.table.setCellWidget(row_count, 1, move_button)
|
||||||
|
|
||||||
|
# Tag
|
||||||
|
self.table.setItem(row_count, 2, QTableWidgetItem(tag))
|
||||||
|
|
||||||
|
# Adding validator
|
||||||
|
validator = QDoubleValidator()
|
||||||
|
validator.setDecimals(self.precision)
|
||||||
|
|
||||||
|
# X as QLineEdit with validator
|
||||||
|
x_edit = QLineEdit(str(f"{x:.{self.precision}f}"))
|
||||||
|
x_edit.setValidator(validator)
|
||||||
|
self.table.setCellWidget(row_count, 3, x_edit)
|
||||||
|
x_edit.textChanged.connect(self.emit_plot_coordinates)
|
||||||
|
|
||||||
|
# Y as QLineEdit with validator
|
||||||
|
y_edit = QLineEdit(str(f"{y:.{self.precision}f}"))
|
||||||
|
y_edit.setValidator(validator)
|
||||||
|
self.table.setCellWidget(row_count, 4, y_edit)
|
||||||
|
y_edit.textChanged.connect(self.emit_plot_coordinates)
|
||||||
|
|
||||||
|
# Emit the coordinates to be plotted
|
||||||
|
self.emit_plot_coordinates()
|
||||||
|
|
||||||
|
# Connect item edit to emit coordinates
|
||||||
|
self.table.itemChanged.connect(self.emit_plot_coordinates)
|
||||||
|
|
||||||
|
# Auto table resize
|
||||||
|
self.resize_table_auto()
|
||||||
|
|
||||||
|
# Align table center
|
||||||
|
self._align_table_center()
|
||||||
|
|
||||||
|
def handle_move_button_click(self):
|
||||||
|
"""Handle the click event of the move button."""
|
||||||
|
button = self.sender()
|
||||||
|
row = self.table.indexAt(button.pos()).row()
|
||||||
|
|
||||||
|
x = self.get_coordinate(row, 3)
|
||||||
|
y = self.get_coordinate(row, 4)
|
||||||
|
self.move_motor(x, y)
|
||||||
|
|
||||||
|
# Emit updated coordinates to update the map
|
||||||
|
self.emit_plot_coordinates()
|
||||||
|
|
||||||
|
def emit_plot_coordinates(self):
|
||||||
|
"""Emit the coordinates to be plotted."""
|
||||||
|
coordinates = []
|
||||||
|
for row in range(self.table.rowCount()):
|
||||||
|
show = self.table.cellWidget(row, 0).isChecked()
|
||||||
|
x = self.get_coordinate(row, 3)
|
||||||
|
y = self.get_coordinate(row, 4)
|
||||||
|
|
||||||
|
coordinates.append((x, y, show)) # (x, y, show_flag)
|
||||||
|
self.plot_coordinates_signal.emit(coordinates)
|
||||||
|
|
||||||
|
def get_coordinate(self, row: int, column: int) -> float:
|
||||||
|
"""
|
||||||
|
Helper function to get the coordinate from the table QLineEdit cells.
|
||||||
|
Args:
|
||||||
|
row(int): Row of the table.
|
||||||
|
column(int): Column of the table.
|
||||||
|
Returns:
|
||||||
|
float: Value of the coordinate.
|
||||||
|
"""
|
||||||
|
edit = self.table.cellWidget(row, column)
|
||||||
|
if edit.text() is not None and edit.text() != "":
|
||||||
|
value = float(edit.text()) if edit else None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def delete_selected_row(self):
|
||||||
|
"""Delete the selected row from the table."""
|
||||||
|
selected_rows = self.table.selectionModel().selectedRows()
|
||||||
|
for row in selected_rows:
|
||||||
|
self.table.removeRow(row.row())
|
||||||
|
self.emit_plot_coordinates()
|
||||||
|
|
||||||
|
def resize_table_auto(self):
|
||||||
|
"""Resize the table to fit the contents."""
|
||||||
|
if self.checkBox_resize_auto.isChecked():
|
||||||
|
self.table.resizeColumnsToContents()
|
||||||
|
|
||||||
|
def _align_table_center(self) -> None:
|
||||||
|
for row in range(self.table.rowCount()):
|
||||||
|
for col in range(self.table.columnCount()):
|
||||||
|
item = self.table.item(row, col)
|
||||||
|
if item:
|
||||||
|
item.setTextAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
|
def move_motor(self, x, y):
|
||||||
|
"""Move the motor to the specified coordinates."""
|
||||||
|
self.motor_thread.move_absolute(self.motor_x, self.motor_y, (x, y))
|
||||||
|
|
||||||
|
@pyqtSlot(str, str)
|
||||||
|
def change_motors(self, motor_x: str, motor_y: str):
|
||||||
|
"""
|
||||||
|
Change the active motors and update config.
|
||||||
|
Can be connected to the selected_motors_signal from MotorControlSelection.
|
||||||
|
Args:
|
||||||
|
motor_x(str): New motor X to be controlled.
|
||||||
|
motor_y(str): New motor Y to be controlled.
|
||||||
|
"""
|
||||||
|
self.motor_x = motor_x
|
||||||
|
self.motor_y = motor_y
|
||||||
|
self.config["motor_control"]["motor_x"] = motor_x
|
||||||
|
self.config["motor_control"]["motor_y"] = motor_y
|
||||||
|
|
||||||
|
@pyqtSlot(int)
|
||||||
|
def set_precision(self, precision: int) -> None:
|
||||||
|
"""
|
||||||
|
Set the precision of the coordinates.
|
||||||
|
Args:
|
||||||
|
precision(int): Precision of the coordinates.
|
||||||
|
"""
|
||||||
|
self.precision = precision
|
||||||
|
self.config["motor_control"]["precision"] = precision
|
||||||
|
|
||||||
|
|
||||||
class MotorControlErrors:
|
class MotorControlErrors:
|
||||||
"""Class for displaying formatted error messages."""
|
"""Class for displaying formatted error messages."""
|
||||||
|
|
||||||
|
85
bec_widgets/widgets/motor_control/motor_control_table.ui
Normal file
85
bec_widgets/widgets/motor_control/motor_control_table.ui
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>676</width>
|
||||||
|
<height>667</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Motor Coordinates Table</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_4">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="checkBox_resize_auto">
|
||||||
|
<property name="text">
|
||||||
|
<string>Resize Auto</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pushButton_editColumns">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Edit Custom Column</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Entries Mode:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="mode_combobox">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Individual</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Start/Stop</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QTableWidget" name="table"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
@ -513,6 +513,26 @@ class MotorMap(pg.GraphicsLayoutWidget):
|
|||||||
self.curves_data[plot_name]["highlight_V"].setPos(current_x)
|
self.curves_data[plot_name]["highlight_V"].setPos(current_x)
|
||||||
self.curves_data[plot_name]["highlight_H"].setPos(current_y)
|
self.curves_data[plot_name]["highlight_H"].setPos(current_y)
|
||||||
|
|
||||||
|
@pyqtSlot(list)
|
||||||
|
def plot_saved_coordinates(self, coordinates):
|
||||||
|
"""Plot the saved coordinates on the map."""
|
||||||
|
for plot_name in self.plots:
|
||||||
|
plot = self.plots[plot_name]
|
||||||
|
|
||||||
|
# Clear previous saved points
|
||||||
|
if "saved_points" in self.curves_data[plot_name]:
|
||||||
|
plot.removeItem(self.curves_data[plot_name]["saved_points"])
|
||||||
|
|
||||||
|
# Filter coordinates to be shown
|
||||||
|
visible_coords = [coord[:2] for coord in coordinates if coord[2]]
|
||||||
|
|
||||||
|
if visible_coords:
|
||||||
|
saved_points = pg.ScatterPlotItem(
|
||||||
|
pos=np.array(visible_coords), brush=pg.mkBrush(0, 255, 0, 255)
|
||||||
|
)
|
||||||
|
plot.addItem(saved_points)
|
||||||
|
self.curves_data[plot_name]["saved_points"] = saved_points
|
||||||
|
|
||||||
@pyqtSlot(dict)
|
@pyqtSlot(dict)
|
||||||
def on_device_readback(self, msg: dict):
|
def on_device_readback(self, msg: dict):
|
||||||
"""
|
"""
|
||||||
|
Reference in New Issue
Block a user