0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

Merge branch 'motor_csax_features_request'

This commit is contained in:
wyzula-jan
2023-09-06 14:07:35 +02:00
4 changed files with 351 additions and 138 deletions

View File

@ -6,10 +6,16 @@
<rect>
<x>0</x>
<y>0</y>
<width>1219</width>
<height>668</height>
<width>1409</width>
<height>748</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>1409</width>
<height>748</height>
</size>
</property>
<property name="windowTitle">
<string>Motor Controller</string>
</property>
@ -29,9 +35,21 @@
<height>471</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<layout class="QVBoxLayout" name="verticalLayout_6" stretch="1,1,1,0,1">
<property name="spacing">
<number>1</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item>
<widget class="QGroupBox" name="motorSelection">
<property name="minimumSize">
<size>
<width>261</width>
<height>145</height>
</size>
</property>
<property name="title">
<string>Motor Selection</string>
</property>
@ -71,6 +89,9 @@
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
@ -81,6 +102,12 @@
</item>
<item>
<widget class="QGroupBox" name="motorControl">
<property name="minimumSize">
<size>
<width>261</width>
<height>339</height>
</size>
</property>
<property name="title">
<string>Motor Relative</string>
</property>
@ -93,36 +120,48 @@
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_6">
<widget class="QCheckBox" name="checkBox_same_xy">
<property name="text">
<string>Step [X] = Step [Y]</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="step_grid">
<item row="2" column="0">
<widget class="QLabel" name="label_step_y">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Step [Y]</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Decimal</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="spinBox_precision">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="maximum">
<number>8</number>
</property>
<property name="value">
<number>2</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Step</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="spinBox_step">
<widget class="QDoubleSpinBox" name="spinBox_step_x">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
@ -140,10 +179,67 @@
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_step_x">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Step [X]</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="spinBox_step_y">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="spinBox_precision">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="maximum">
<number>8</number>
</property>
<property name="value">
<number>2</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<layout class="QGridLayout" name="direction_grid">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
@ -273,20 +369,36 @@
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>13</height>
<height>18</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="motorControl_absolute">
<property name="minimumSize">
<size>
<width>261</width>
<height>195</height>
</size>
</property>
<property name="title">
<string>Move Absolute</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="checkBox_save_with_go">
<property name="text">
<string>Save position with Go</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="1">

View File

@ -1,22 +1,23 @@
import os
from enum import Enum
from functools import partial
import numpy as np
from enum import Enum
import pyqtgraph as pg
from PyQt5 import QtGui
from PyQt5.QtCore import QThread, pyqtSlot
from PyQt5.QtCore import QThread, pyqtSlot, QPoint
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtGui import QDoubleValidator
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QApplication, QWidget, QTableWidget
from PyQt5.QtWidgets import QShortcut
from pyqtgraph.Qt import QtWidgets, uic, QtCore
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QShortcut
from bec_lib.core import MessageEndpoints, BECMessage
from bec_widgets.qt_utils import DoubleValidationDelegate
# TODO - General features
# - updating motor precision
# - put motor status (moving, stopped, etc)
# - add mouse interactions with the plot -> click to select coordinates, double click to move?
# - adjust right click actions
@ -197,10 +198,16 @@ class MotorApp(QWidget):
self.enable_motor_controls(False)
target_coordinates = (x, y)
self.motor_thread.move_to_coordinates(target_coordinates)
if self.checkBox_save_with_go.isChecked():
self.save_absolute_coordinates()
def move_motor_relative(self, motor, value: float) -> None:
def move_motor_relative(self, motor, axis: str, direction: int) -> None:
self.enable_motor_controls(False)
self.motor_thread.move_relative(motor, value)
if axis == "x":
step = direction * self.spinBox_step_x.value()
elif axis == "y":
step = direction * self.spinBox_step_y.value()
self.motor_thread.move_relative(motor, step)
def update_plot_setting(self, max_points, num_dim_points, scatter_size):
self.max_points = max_points
@ -223,17 +230,8 @@ class MotorApp(QWidget):
self.spinBox_precision.setValue(self.precision)
self.update_precision(self.precision)
def init_ui(self) -> None:
"""Setup all ui elements"""
# TODO can be separated to multiple functions
# Set default parameters
self.set_from_config()
##########################
# 2D Plot
##########################
def init_ui_plot_elements(self) -> None:
"""Initialize the plot elements"""
self.label_coorditanes = self.glw.addLabel(f"Motor position: (X, Y)", row=0, col=0)
self.plot_map = self.glw.addPlot(row=1, col=0)
self.limit_map = pg.ImageItem()
@ -255,28 +253,23 @@ class MotorApp(QWidget):
self.plot_map.addItem(self.saved_motor_map)
self.plot_map.showGrid(x=True, y=True)
##########################
# Motor General setting
##########################
def init_ui_motor_control(self) -> None:
"""Initialize the motor control elements"""
# # TODO make function to update precision
# self.precision = 2 # self.spinBox_precision.value() # Define the decimal precision
##########################
# Motor movements signals
##########################
# Connect CheckBox stateChanged signal to sync function
self.checkBox_same_xy.stateChanged.connect(
lambda: self.sync_step_sizes(self.spinBox_step_x, self.spinBox_step_y)
)
self.toolButton_right.clicked.connect(
lambda: self.move_motor_relative(self.motor_x, self.spinBox_step.value())
lambda: self.move_motor_relative(self.motor_x, "x", 1)
)
self.toolButton_left.clicked.connect(
lambda: self.move_motor_relative(self.motor_x, -self.spinBox_step.value())
)
self.toolButton_up.clicked.connect(
lambda: self.move_motor_relative(self.motor_y, self.spinBox_step.value())
lambda: self.move_motor_relative(self.motor_x, "x", -1)
)
self.toolButton_up.clicked.connect(lambda: self.move_motor_relative(self.motor_y, "y", 1))
self.toolButton_down.clicked.connect(
lambda: self.move_motor_relative(self.motor_y, -self.spinBox_step.value())
lambda: self.move_motor_relative(self.motor_y, "y", -1)
)
# Switch between key shortcuts active
@ -290,36 +283,18 @@ class MotorApp(QWidget):
)
)
# Go absolute button
self.pushButton_go_absolute.clicked.connect(self.save_absolute_coordinates)
self.pushButton_go_absolute.setShortcut("Ctrl+G")
self.pushButton_go_absolute.setToolTip("Ctrl+G")
# Set absolute coordinates
self.pushButton_set.clicked.connect(self.save_absolute_coordinates)
self.pushButton_set.setShortcut("Ctrl+D")
self.pushButton_set.setToolTip("Ctrl+D")
# Save Current coordinates
self.pushButton_save.clicked.connect(self.save_current_coordinates)
self.pushButton_save.setShortcut("Ctrl+S")
self.pushButton_save.setToolTip("Ctrl+S")
# Stop Button
self.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
self.pushButton_stop.setShortcut("Ctrl+X")
self.pushButton_stop.setToolTip("Ctrl+X")
# Enable/Disable GUI
self.motor_thread.move_finished.connect(lambda: self.enable_motor_controls(True))
# Update precision
# Precision update
self.spinBox_precision.valueChanged.connect(lambda x: self.update_precision(x))
##########################
# Motor Configs
##########################
# SpinBoxes - Motor Limits #TODO make spinboxes own limits updated, currently is [-1000, 1000]
def init_ui_motor_configs(self) -> None:
"""Limit and plot spinBoxes"""
# SpinBoxes change color to yellow before updated, limits are updated with update button
self.spinBox_x_min.valueChanged.connect(lambda: self.param_changed(self.spinBox_x_min))
@ -338,7 +313,7 @@ class MotorApp(QWidget):
lambda: self.param_changed(self.spinBox_scatter_size)
)
# Config updates
# Limit Update
self.pushButton_updateLimits.clicked.connect(
lambda: self.update_all_motor_limits(
x_limit=[self.spinBox_x_min.value(), self.spinBox_x_max.value()],
@ -346,6 +321,7 @@ class MotorApp(QWidget):
)
)
# Plot Update
self.pushButton_update_config.clicked.connect(
lambda: self.update_plot_setting(
max_points=self.spinBox_max_points.value(),
@ -354,12 +330,15 @@ class MotorApp(QWidget):
)
)
# TODO map with floats as well -> or decide system for higher precision
self.pushButton_enableGUI.clicked.connect(lambda: self.enable_motor_controls(True))
def init_ui_motor_connections(self) -> None:
# Signal from motor thread to update coordinates
self.motor_thread.coordinates_updated.connect(
lambda x, y: self.update_image_map(round(x, self.precision), round(y, self.precision))
)
# Motor connections
# Motor connections button
self.pushButton_connecMotors.clicked.connect(
lambda: self.connect_motor(
self.comboBox_motor_x.currentText(), self.comboBox_motor_y.currentText()
@ -372,7 +351,8 @@ class MotorApp(QWidget):
self.motorControl_absolute.setEnabled(False)
self.tabWidget_tables.setTabEnabled(1, False)
# Keyboard shortcuts
def init_keyboard_shortcuts(self) -> None:
"""Initialize the keyboard shortcuts"""
# Delete table entry
delete_shortcut = QShortcut(QKeySequence("Delete"), self)
@ -380,13 +360,58 @@ class MotorApp(QWidget):
delete_shortcut.activated.connect(self.delete_selected_row)
backspace_shortcut.activated.connect(self.delete_selected_row)
# Increase/decrease step
increase_shortcut = QShortcut(QKeySequence("Ctrl+A"), self)
decrease_shortcut = QShortcut(QKeySequence("Ctrl+Z"), self)
increase_shortcut.activated.connect(self.increase_step)
decrease_shortcut.activated.connect(self.decrease_step)
# Increase/decrease step size for X motor
increase_x_shortcut = QShortcut(QKeySequence("Ctrl+A"), self)
decrease_x_shortcut = QShortcut(QKeySequence("Ctrl+Z"), self)
increase_x_shortcut.activated.connect(lambda: self.change_step_size(self.spinBox_step_x, 2))
decrease_x_shortcut.activated.connect(
lambda: self.change_step_size(self.spinBox_step_x, 0.5)
)
self.pushButton_enableGUI.clicked.connect(lambda: self.enable_motor_controls(True))
# Increase/decrease step size for Y motor
increase_y_shortcut = QShortcut(QKeySequence("Alt+A"), self)
decrease_y_shortcut = QShortcut(QKeySequence("Alt+Z"), self)
increase_y_shortcut.activated.connect(lambda: self.change_step_size(self.spinBox_step_y, 2))
decrease_y_shortcut.activated.connect(
lambda: self.change_step_size(self.spinBox_step_y, 0.5)
)
# Go absolute button
self.pushButton_go_absolute.setShortcut("Ctrl+G")
self.pushButton_go_absolute.setToolTip("Ctrl+G")
# Set absolute coordinates
self.pushButton_set.setShortcut("Ctrl+D")
self.pushButton_set.setToolTip("Ctrl+D")
# Save Current coordinates
self.pushButton_save.setShortcut("Ctrl+S")
self.pushButton_save.setToolTip("Ctrl+S")
# Stop Button
self.pushButton_stop.setShortcut("Ctrl+X")
self.pushButton_stop.setToolTip("Ctrl+X")
def init_ui_table(self) -> None:
"""Initialize the table validators for x and y coordinates and table signals"""
# Validators
self.double_delegate = DoubleValidationDelegate(self.tableWidget_coordinates)
self.tableWidget_coordinates.setItemDelegateForColumn(2, self.double_delegate)
self.tableWidget_coordinates.setItemDelegateForColumn(3, self.double_delegate)
# Signals
self.tableWidget_coordinates.itemChanged.connect(self.update_saved_coordinates)
def init_ui(self) -> None:
"""Setup all ui elements"""
self.set_from_config() # Set default parameters
self.init_ui_plot_elements() # 2D Plot
self.init_ui_motor_control() # Motor Controls
self.init_ui_motor_configs() # Motor Configs
self.init_ui_motor_connections() # Motor Connections
self.init_keyboard_shortcuts() # Keyboard Shortcuts
self.init_ui_table() # Table validators for x and y coordinates
def init_motor_map(self):
# Get motor limits
@ -490,43 +515,37 @@ class MotorApp(QWidget):
current_row_count = table.rowCount()
table.setRowCount(current_row_count + 1)
# Create QDoubleValidator
validator = QDoubleValidator()
validator.setDecimals(precision) # TODO not sure if necessary
checkBox = QtWidgets.QCheckBox()
checkBox.setChecked(True)
button = QtWidgets.QPushButton("Go")
# Connect checkBox state change to toggle visibility
checkBox.stateChanged.connect(
lambda state, coord=coordinates: self.toggle_point_visibility(state, coord)
lambda state, widget=checkBox: self.toggle_point_visibility(state, widget)
)
table.setItem(current_row_count, 0, QtWidgets.QTableWidgetItem(str(tag)))
table.setCellWidget(current_row_count, 1, checkBox)
# Apply validator to x and y coordinate QTableWidgetItem
item_x = QtWidgets.QTableWidgetItem(str(f"{coordinates[0]:.{precision}f}"))
item_y = QtWidgets.QTableWidgetItem(str(f"{coordinates[1]:.{precision}f}"))
item_x.setFlags(item_x.flags() | Qt.ItemIsEditable)
item_y.setFlags(item_y.flags() | Qt.ItemIsEditable)
table.setItem(
current_row_count, 2, QtWidgets.QTableWidgetItem(str(f"{coordinates[0]:.{precision}f}"))
)
table.setItem(
current_row_count, 3, QtWidgets.QTableWidgetItem(str(f"{coordinates[1]:.{precision}f}"))
)
table.setCellWidget(current_row_count, 4, button)
# Hook signals of table
button.clicked.connect(
lambda: self.move_motor_absolute(
float(table.item(current_row_count, 2).text()),
float(table.item(current_row_count, 3).text()),
)
)
# Add point to scatter plot
# Add a True value to saved_point_visibility list when a new point is added.
self.saved_point_visibility.append(True)
# Update the scatter plot to maintain the visibility of existing points
new_pos = np.array(coordinates)
if self.saved_motor_positions.size == 0:
self.saved_motor_positions = np.array([new_pos])
else:
self.saved_motor_positions = np.vstack((self.saved_motor_positions, new_pos))
button.clicked.connect(partial(self.move_to_row_coordinates, table, current_row_count))
brushes = [
pg.mkBrush(255, 165, 0, 255) if visible else pg.mkBrush(255, 165, 0, 0)
@ -537,32 +556,104 @@ class MotorApp(QWidget):
table.resizeColumnsToContents()
def toggle_point_visibility(self, state, coord):
index = np.where((self.saved_motor_positions == coord).all(axis=1))[0][0]
self.saved_point_visibility[index] = state == Qt.Checked
def move_to_row_coordinates(self, table, row):
x = float(table.item(row, 2).text())
y = float(table.item(row, 3).text())
self.move_motor_absolute(x, y)
def toggle_point_visibility(self, state, checkBox_widget):
parent = checkBox_widget.parent()
while not isinstance(parent, QTableWidget):
parent = parent.parent()
table = parent
pos = checkBox_widget.pos()
item = table.indexAt(pos)
row_index = item.row()
# print(f"Row {row_index} visibility changed to {state == Qt.Checked}")
self.saved_point_visibility[row_index] = state == Qt.Checked
# Generate brushes based on visibility state
brushes = [
pg.mkBrush(255, 0, 0, 255) if visible else pg.mkBrush(255, 0, 0, 0)
pg.mkBrush(255, 165, 0, 255) if visible else pg.mkBrush(255, 165, 0, 0)
for visible in self.saved_point_visibility
]
# brushed_rgb = [brush.color().getRgb() for brush in brushes]
# print(f"Poinst: {self.saved_motor_positions}")
# print(f"Brushes: {brushed_rgb}")
self.saved_motor_map.setData(pos=self.saved_motor_positions, brush=brushes)
def update_saved_coordinates(self):
"""
Update the saved coordinates and replot them.
"""
rows = self.tableWidget_coordinates.rowCount()
# Initialize an empty array to hold new coordinates
new_saved_positions = np.empty((0, 2))
new_visibility = []
for row in range(rows):
x = (
float(self.tableWidget_coordinates.item(row, 2).text())
if self.tableWidget_coordinates.item(row, 2) is not None
else None
)
y = (
float(self.tableWidget_coordinates.item(row, 3).text())
if self.tableWidget_coordinates.item(row, 3) is not None
else None
)
# Only add the point if both x and y are not None
if x is not None and y is not None:
new_saved_positions = np.vstack((new_saved_positions, [x, y]))
checkbox = self.tableWidget_coordinates.cellWidget(row, 1)
new_visibility.append(checkbox.isChecked())
# Update saved positions and visibility
self.saved_motor_positions = new_saved_positions
self.saved_point_visibility = new_visibility
# Replot saved positions based on new data
brushes = [
pg.mkBrush(255, 165, 0, 255) if visible else pg.mkBrush(255, 165, 0, 0)
for visible in self.saved_point_visibility
]
self.saved_motor_map.setData(pos=self.saved_motor_positions, brush=brushes)
def delete_selected_row(self):
selected_rows = self.tableWidget_coordinates.selectionModel().selectedRows()
for row in reversed(selected_rows):
row_index = row.row()
rows_to_delete = [row.row() for row in selected_rows]
rows_to_delete.sort(reverse=True) # Sort in descending order
for row_index in rows_to_delete:
self.saved_motor_positions = np.delete(self.saved_motor_positions, row_index, axis=0)
del self.saved_point_visibility[row_index] # Update this line
del self.saved_point_visibility[row_index]
# Update the plot
brushes = [
pg.mkBrush(255, 0, 0, 255) if visible else pg.mkBrush(255, 0, 0, 0)
pg.mkBrush(255, 165, 0, 255) if visible else pg.mkBrush(255, 165, 0, 0)
for visible in self.saved_point_visibility
] # Regenerate brushes
self.saved_motor_map.setData(
pos=self.saved_motor_positions, brush=brushes
) # Update this line
]
self.saved_motor_map.setData(pos=self.saved_motor_positions, brush=brushes)
# Remove the row from the table
self.tableWidget_coordinates.removeRow(row_index)
# Update the 'Go' buttons
for row in range(self.tableWidget_coordinates.rowCount()):
button = self.tableWidget_coordinates.cellWidget(row, 4)
button.clicked.disconnect()
button.clicked.connect(
partial(self.move_to_row_coordinates, self.tableWidget_coordinates, row)
)
def save_absolute_coordinates(self):
self.generate_table_coordinate(
self.tableWidget_coordinates,
@ -585,21 +676,20 @@ class MotorApp(QWidget):
def update_precision(self, precision: int):
self.precision = precision
self.spinBox_step.setDecimals(self.precision)
self.spinBox_step_x.setDecimals(self.precision)
self.spinBox_step_y.setDecimals(self.precision)
self.spinBox_absolute_x.setDecimals(self.precision)
self.spinBox_absolute_y.setDecimals(self.precision)
def increase_step(self):
old_step = self.spinBox_step.value()
new_step = old_step * 2
def change_step_size(self, spinBox: QtWidgets.QDoubleSpinBox, factor: float) -> None:
old_step = spinBox.value()
new_step = old_step * factor
spinBox.setValue(new_step)
self.spinBox_step.setValue(new_step)
def decrease_step(self):
old_step = self.spinBox_step.value()
new_step = old_step / 2
self.spinBox_step.setValue(new_step)
def sync_step_sizes(self, spinBox1, spinBox2):
if self.checkBox_same_xy.isChecked():
value = spinBox1.value()
spinBox2.setValue(value)
@staticmethod
def param_changed(ui_element):
@ -827,7 +917,7 @@ if __name__ == "__main__":
from bec_lib import BECClient
from bec_lib.core import ServiceConfig, RedisConnector
from bec_lib.core import ServiceConfig
parser = argparse.ArgumentParser(description="Motor App")

View File

@ -1,2 +1,3 @@
from .crosshair import Crosshair
from .colors import Colors
from .validator_delegate import DoubleValidationDelegate

View File

@ -0,0 +1,10 @@
from PyQt5.QtGui import QDoubleValidator
from PyQt5.QtWidgets import QStyledItemDelegate, QLineEdit
class DoubleValidationDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = QLineEdit(parent)
validator = QDoubleValidator()
editor.setValidator(validator)
return editor