1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-03-08 01:37:52 +01:00

Merge branch 'motor_controle'

This commit is contained in:
wyzula-jan
2023-08-28 14:07:17 +02:00
4 changed files with 1443 additions and 2 deletions

View File

@@ -0,0 +1,768 @@
<?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>1129</width>
<height>550</height>
</rect>
</property>
<property name="windowTitle">
<string>Motor Controller</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="8,5,8">
<item>
<widget class="GraphicsLayoutWidget" name="glw">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="Controls">
<property name="minimumSize">
<size>
<width>221</width>
<height>471</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QGroupBox" name="motorSelection">
<property name="title">
<string>Motor Selection</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="2" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Motor Y</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_motor_x"/>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_motor_y"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Motor X</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QPushButton" name="pushButton_connecMotors">
<property name="text">
<string>Connect Motors</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>18</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="motorControl">
<property name="title">
<string>Motor Relative</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QCheckBox" name="checkBox_enableArrows">
<property name="text">
<string>Move with arrow keys</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item row="1" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
<widget class="QToolButton" name="toolButton_up">
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::UpArrow</enum>
</property>
</widget>
</item>
<item row="2" column="4">
<spacer name="horizontalSpacer_2">
<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 row="3" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
<widget class="QToolButton" name="toolButton_down">
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::DownArrow</enum>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QToolButton" name="toolButton_left">
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::LeftArrow</enum>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="3">
<widget class="QToolButton" name="toolButton_right">
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::RightArrow</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<spacer name="horizontalSpacer">
<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 row="4" column="2">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="2">
<widget class="QDoubleSpinBox" name="spinBox_step">
<property name="minimum">
<double>0.100000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>13</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="motorControl_absolute">
<property name="title">
<string>Move Absolute</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="1">
<widget class="QLabel" name="label_6">
<property name="text">
<string>X</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Y</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="pushButton_go_absolute">
<property name="text">
<string>Go</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="spinBox_absolute_x">
<property name="minimum">
<double>-500.000000000000000</double>
</property>
<property name="maximum">
<double>500.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QDoubleSpinBox" name="spinBox_absolute_y">
<property name="minimum">
<double>-500.000000000000000</double>
</property>
<property name="maximum">
<double>500.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="pushButton_stop">
<property name="text">
<string>Stop Movement</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget_tables">
<property name="enabled">
<bool>true</bool>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_coordinates">
<attribute name="title">
<string>Coordinates</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTableWidget" name="tableWidget_coordinates">
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<column>
<property name="text">
<string>Tag</string>
</property>
</column>
<column>
<property name="text">
<string>Show</string>
</property>
</column>
<column>
<property name="text">
<string>X</string>
</property>
</column>
<column>
<property name="text">
<string>Y</string>
</property>
</column>
<column>
<property name="text">
<string>Move</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_settings">
<attribute name="title">
<string>Settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QGroupBox" name="motorLimits">
<property name="title">
<string>Motor Limits</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="3" column="1">
<widget class="QSpinBox" name="spinBox_y_min">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<number>-1000</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="value">
<number>-100</number>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="pushButton_updateLimits">
<property name="text">
<string>Update</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QSpinBox" name="spinBox_x_max">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<number>-1000</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_Y_max">
<property name="text">
<string>+ Y</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spinBox_y_max">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<number>-1000</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QSpinBox" name="spinBox_x_min">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<number>-1000</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="value">
<number>-100</number>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="label_Y_min">
<property name="text">
<string>- Y</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_X_min">
<property name="text">
<string>- X</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="label_X_max">
<property name="text">
<string>+ X</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Motor Config</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="3" column="1">
<widget class="QSpinBox" name="spinBox_3">
<property name="enabled">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QSpinBox" name="spinBox_6">
<property name="enabled">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_9">
<property name="text">
<string>X</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Speed</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spinBox_speed_x">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="maximum">
<number>1000</number>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="spinBox_update_frequency_x">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="maximum">
<number>500</number>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QSpinBox" name="spinBox_speed_y">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="maximum">
<number>1000</number>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Y</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>Max Points</string>
</property>
</widget>
</item>
<item row="6" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox_num_dim_points">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="singleStep">
<number>10</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Update Frequency</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QDoubleSpinBox" name="doubleSpinBox_2">
<property name="enabled">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_3">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Tolerance</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QSpinBox" name="spinBox_update_frequency_y">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="maximum">
<number>500</number>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QPushButton" name="pushButton_update_config">
<property name="text">
<string>Update</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Decimal Precision</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>N dim</string>
</property>
</widget>
</item>
<item row="5" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox_max_points">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<number>100</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
<property name="value">
<number>5000</number>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Scatter Size</string>
</property>
</widget>
</item>
<item row="7" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox_scatter_size">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>15</number>
</property>
<property name="value">
<number>5</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_queue">
<attribute name="title">
<string>Queue</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPushButton" name="pushButton_5">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Reset Queue</string>
</property>
</widget>
</item>
<item>
<widget class="QTableWidget" name="tableWidget_2">
<property name="enabled">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>queueID</string>
</property>
</column>
<column>
<property name="text">
<string>scanID</string>
</property>
</column>
<column>
<property name="text">
<string>is_scan</string>
</property>
</column>
<column>
<property name="text">
<string>type</string>
</property>
</column>
<column>
<property name="text">
<string>scan_number</string>
</property>
</column>
<column>
<property name="text">
<string>IQ status</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>GraphicsLayoutWidget</class>
<extends>QGraphicsView</extends>
<header>pyqtgraph.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,674 @@
import os
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 pyqtSignal, Qt
from PyQt5.QtWidgets import QApplication, QWidget
from pyqtgraph.Qt import QtWidgets, uic
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QShortcut
from bec_lib.core import MessageEndpoints, BECMessage
# TODO - General features
# - setting motor speed and frequency
# - setting motor acceleration
# - updating motor precision
# - put motor status (moving, stopped, etc)
# - add spinBox for motor scatter size
# - add mouse interactions with the plot -> click to select coordinates, double click to move?
# - adjust right click actions
# - implement logic to check if motor actually has limits
class MotorApp(QWidget):
coordinates_updated = pyqtSignal(float, float)
def __init__(self):
super().__init__()
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "motor_controller.ui"), self)
# Motor Control Thread
self.motor_thread = MotorControl()
self.motor_x, self.motor_y = None, None
self.limit_x, self.limit_y = None, None
# Coordinates tracking
self.motor_positions = np.array([])
self.max_points = 5000 # Maximum number of points to keep
self.num_dim_points = 15 # Number of points to dim gradually
self.scatter_size = 5
# QThread for motor movement + signals
self.motor_thread.motors_loaded.connect(self.get_available_motors)
self.motor_thread.motors_selected.connect(self.get_selected_motors)
self.motor_thread.limits_retrieved.connect(self.update_limits)
self.motor_thread.speed_retrieved.connect(self.update_speed)
self.motor_thread.update_frequency_retrieved.connect(self.update_update_frequency)
# UI
self.init_ui()
self.tag_N = 1 # position label for saved coordinates
# Get all motors available
self.motor_thread.retrieve_all_motors()
def connect_motor(self, motor_x_name: str, motor_y_name: str):
self.motor_thread.connect_motors(motor_x_name, motor_y_name)
self.motor_thread.retrieve_motor_limits(self.motor_x, self.motor_y)
self.motor_thread.retrieve_motor_speed(self.motor_x, self.motor_y)
self.motor_thread.retrieve_motor_update_frequency(self.motor_x, self.motor_y)
self.init_motor_map()
self.motorControl.setEnabled(True)
self.motorControl_absolute.setEnabled(True)
self.tabWidget_tables.setTabEnabled(1, True)
self.generate_table_coordinate(
self.tableWidget_coordinates,
self.motor_thread.retrieve_coordinates(),
tag=f"{motor_x_name},{motor_y_name}",
precision=0,
)
@pyqtSlot(object, object)
def get_selected_motors(self, motor_x, motor_y):
self.motor_x, self.motor_y = motor_x, motor_y
@pyqtSlot(list, list)
def get_available_motors(self, motors_x, motors_y):
self.comboBox_motor_x.addItems(motors_x)
self.comboBox_motor_y.addItems(motors_y)
@pyqtSlot(list, list)
def update_limits(self, x_limits: list, y_limits: list) -> None:
self.limit_x = x_limits
self.limit_y = y_limits
self.spinBox_x_min.setValue(self.limit_x[0])
self.spinBox_x_max.setValue(self.limit_x[1])
self.spinBox_y_min.setValue(self.limit_y[0])
self.spinBox_y_max.setValue(self.limit_y[1])
for spinBox in (
self.spinBox_x_min,
self.spinBox_x_max,
self.spinBox_y_min,
self.spinBox_y_max,
):
spinBox.setStyleSheet("")
# TODO - names can be get from MotorController
self.label_Y_max.setText(f"+ ({self.motor_y.name})")
self.label_Y_min.setText(f"- ({self.motor_y.name})")
self.label_X_max.setText(f"+ ({self.motor_x.name})")
self.label_X_min.setText(f"- ({self.motor_x.name})")
self.init_motor_map() # reinitialize the map with the new limits
@pyqtSlot(int, int)
def update_speed(self, speed_x, speed_y):
self.spinBox_speed_x.setValue(speed_x)
self.spinBox_speed_y.setValue(speed_y)
for spinBox in (self.spinBox_speed_x, self.spinBox_speed_y):
spinBox.setStyleSheet("")
@pyqtSlot(int, int)
def update_update_frequency(self, update_frequency_x, update_frequency_y):
self.spinBox_update_frequency_x.setValue(update_frequency_x)
self.spinBox_update_frequency_y.setValue(update_frequency_y)
for spinBox in (self.spinBox_update_frequency_x, self.spinBox_update_frequency_y):
spinBox.setStyleSheet("")
@pyqtSlot()
def enable_motor_control(self):
self.motorControl.setEnabled(True)
def enable_motor_controls(self, disable: bool) -> None:
self.motorControl.setEnabled(disable)
self.motorSelection.setEnabled(disable)
# Disable or enable all controls within the motorControl_absolute group box
for widget in self.motorControl_absolute.findChildren(QtWidgets.QWidget):
widget.setEnabled(disable)
# Enable the pushButton_stop if the motor is moving
self.pushButton_stop.setEnabled(not disable)
def move_motor_absolute(self, x: float, y: float) -> None:
self.enable_motor_controls(False)
target_coordinates = (x, y)
self.motor_thread.move_to_coordinates(target_coordinates)
def move_motor_relative(self, motor, value: float) -> None:
self.enable_motor_controls(False)
self.motor_thread.move_relative(motor, value)
def update_plot_setting(self, max_points, num_dim_points, scatter_size):
self.max_points = max_points
self.num_dim_points = num_dim_points
self.scatter_size = scatter_size
for spinBox in (
self.spinBox_max_points,
self.spinBox_num_dim_points,
self.spinBox_scatter_size,
):
spinBox.setStyleSheet("")
def init_ui(self) -> None:
"""Setup all ui elements"""
# TODO can be separated to multiple functions
##########################
# 2D Plot
##########################
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()
self.plot_map.addItem(self.limit_map)
self.motor_map = pg.ScatterPlotItem(
size=self.scatter_size, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 255)
)
self.plot_map.addItem(self.motor_map)
self.plot_map.showGrid(x=True, y=True)
##########################
# Motor General setting
##########################
# TODO make function to update precision
self.precision = 2 # self.spinBox_precision.value() # Define the decimal precision
##########################
# Motor movements signals
##########################
self.toolButton_right.clicked.connect(
lambda: self.move_motor_relative(self.motor_x, self.spinBox_step.value())
)
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())
)
self.toolButton_down.clicked.connect(
lambda: self.move_motor_relative(self.motor_y, -self.spinBox_step.value())
)
# Switch between key shortcuts active
self.checkBox_enableArrows.stateChanged.connect(self.update_arrow_key_shortcuts)
self.update_arrow_key_shortcuts()
# Move to absolute coordinates
self.pushButton_go_absolute.clicked.connect(
lambda: self.move_motor_absolute(
self.spinBox_absolute_x.value(), self.spinBox_absolute_y.value()
)
)
self.pushButton_go_absolute.clicked.connect(self.save_absolute_coordinates)
self.pushButton_go_absolute.setShortcut("Ctrl+G")
self.pushButton_go_absolute.setToolTip("Ctrl+G")
self.motor_thread.move_finished.connect(lambda: self.enable_motor_controls(True))
# Stop Button
self.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
##########################
# Motor Configs
##########################
# SpinBoxes - Motor Limits #TODO make spinboxes own limits updated, currently is [-1000, 1000]
# 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))
self.spinBox_x_max.valueChanged.connect(lambda: self.param_changed(self.spinBox_x_max))
self.spinBox_y_min.valueChanged.connect(lambda: self.param_changed(self.spinBox_y_min))
self.spinBox_y_max.valueChanged.connect(lambda: self.param_changed(self.spinBox_y_max))
# SpinBoxes - Motor Speed
self.spinBox_speed_x.valueChanged.connect(lambda: self.param_changed(self.spinBox_speed_x))
self.spinBox_speed_y.valueChanged.connect(lambda: self.param_changed(self.spinBox_speed_y))
# SpinBoxes - Motor Update Frequency
self.spinBox_update_frequency_x.valueChanged.connect(
lambda: self.param_changed(self.spinBox_update_frequency_x)
)
self.spinBox_update_frequency_y.valueChanged.connect(
lambda: self.param_changed(self.spinBox_update_frequency_y)
)
# SpinBoxes - Max Points and N Dim Points
self.spinBox_max_points.valueChanged.connect(
lambda: self.param_changed(self.spinBox_max_points)
)
self.spinBox_num_dim_points.valueChanged.connect(
lambda: self.param_changed(self.spinBox_num_dim_points)
)
self.spinBox_scatter_size.valueChanged.connect(
lambda: self.param_changed(self.spinBox_scatter_size)
)
# Config updates
self.pushButton_updateLimits.clicked.connect(
lambda: self.update_all_motor_limits(
x_limit=[self.spinBox_x_min.value(), self.spinBox_x_max.value()],
y_limit=[self.spinBox_y_min.value(), self.spinBox_y_max.value()],
)
)
self.pushButton_update_config.clicked.connect(
lambda: self.update_plot_setting(
max_points=self.spinBox_max_points.value(),
num_dim_points=self.spinBox_num_dim_points.value(),
scatter_size=self.spinBox_scatter_size.value(),
)
)
self.pushButton_update_config.clicked.connect(
lambda: self.update_all_config(
speed=[self.spinBox_speed_x.value(), self.spinBox_speed_y.value()],
update_frequency=[
self.spinBox_update_frequency_x.value(),
self.spinBox_update_frequency_y.value(),
],
)
)
# TODO map with floats as well -> or decide system for higher precision
self.motor_thread.coordinates_updated.connect(
lambda x, y: self.update_image_map(round(x, self.precision), round(y, self.precision))
)
# Motor connections
self.pushButton_connecMotors.clicked.connect(
lambda: self.connect_motor(
self.comboBox_motor_x.currentText(), self.comboBox_motor_y.currentText()
)
)
# Check if there are any motors connected
if self.motor_x or self.motor_y is None:
self.motorControl.setEnabled(False)
self.motorControl_absolute.setEnabled(False)
self.tabWidget_tables.setTabEnabled(1, False)
# Keyboard shortcuts
delete_shortcut = QShortcut(QKeySequence("Delete"), self)
backspace_shortcut = QShortcut(QKeySequence("Backspace"), self)
delete_shortcut.activated.connect(self.delete_selected_row)
backspace_shortcut.activated.connect(self.delete_selected_row)
def init_motor_map(self):
# Get motor limits
limit_x_min, limit_x_max = self.motor_thread.get_motor_limits(self.motor_x)
limit_y_min, limit_y_max = self.motor_thread.get_motor_limits(self.motor_y)
self.offset_x = limit_x_min
self.offset_y = limit_y_min
# Define the size of the image map based on the motor's limits
map_width = limit_x_max - limit_x_min + 1
map_height = limit_y_max - limit_y_min + 1
# Create an empty image map
self.background_value = 15
self.limit_map_data = np.full(
(map_width, map_height), self.background_value, dtype=np.float32
)
self.limit_map.setImage(self.limit_map_data)
# Set the initial position on the map
init_pos = self.motor_thread.retrieve_coordinates()
self.motor_positions = np.array([init_pos])
self.brushes = [pg.mkBrush(255, 255, 255, 255)]
self.motor_map.setData(pos=self.motor_positions, brush=self.brushes)
# Translate and scale the image item to match the motor coordinates
self.tr = QtGui.QTransform()
self.tr.translate(limit_x_min, limit_y_min)
self.limit_map.setTransform(self.tr)
def update_image_map(self, x, y):
# Update label
self.label_coorditanes.setText(f"Motor position: ({x}, {y})")
# Add new point with full brightness
new_pos = np.array([x, y])
self.motor_positions = np.vstack((self.motor_positions, new_pos))
# If the number of points exceeds max_points, delete the oldest points
if len(self.motor_positions) > self.max_points:
self.motor_positions = self.motor_positions[-self.max_points :]
# Determine brushes based on position in the array
self.brushes = [pg.mkBrush(50, 50, 50, 255)] * len(self.motor_positions)
# Calculate the decrement step based on self.num_dim_points
decrement_step = (255 - 50) / self.num_dim_points
for i in range(1, min(self.num_dim_points + 1, len(self.motor_positions) + 1)):
brightness = max(50, 255 - decrement_step * (i - 1))
self.brushes[-i] = pg.mkBrush(brightness, brightness, brightness, 255)
self.brushes[-1] = pg.mkBrush(255, 255, 255, 255) # Newest point is always full brightness
self.motor_map.setData(pos=self.motor_positions, brush=self.brushes, size=self.scatter_size)
def update_all_motor_limits(self, x_limit: list = None, y_limit: list = None) -> None:
self.motor_thread.update_all_motor_limits(x_limit=x_limit, y_limit=y_limit)
def update_all_config(self, speed: list = None, update_frequency: list = None) -> None:
# TODO now only speed and update frequency
self.motor_thread.update_all_config(speed=speed, update_frequency=update_frequency)
def update_arrow_key_shortcuts(self):
if self.checkBox_enableArrows.isChecked():
# Set the arrow key shortcuts for motor movement
self.toolButton_right.setShortcut(Qt.Key_Right)
self.toolButton_left.setShortcut(Qt.Key_Left)
self.toolButton_up.setShortcut(Qt.Key_Up)
self.toolButton_down.setShortcut(Qt.Key_Down)
else:
# Clear the shortcuts
self.toolButton_right.setShortcut("")
self.toolButton_left.setShortcut("")
self.toolButton_up.setShortcut("")
self.toolButton_down.setShortcut("")
def generate_table_coordinate(
self, table: QtWidgets.QTableWidget, coordinates: tuple, tag: str = None, precision: int = 0
) -> None:
current_row_count = table.rowCount()
table.setRowCount(current_row_count + 1)
checkBox = QtWidgets.QCheckBox()
checkBox.setChecked(True)
button = QtWidgets.QPushButton("Go")
table.setItem(current_row_count, 0, QtWidgets.QTableWidgetItem(str(tag)))
table.setCellWidget(current_row_count, 1, checkBox)
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()),
)
)
table.resizeColumnsToContents()
def delete_selected_row(self):
selected_rows = self.tableWidget_coordinates.selectionModel().selectedRows()
# If you allow multiple selections, you may want to loop through all selected rows
for row in reversed(selected_rows): # Reverse to delete from the end
self.tableWidget_coordinates.removeRow(row.row())
def save_absolute_coordinates(self):
self.generate_table_coordinate(
self.tableWidget_coordinates,
(self.spinBox_absolute_x.value(), self.spinBox_absolute_y.value()),
tag=f"Pos {self.tag_N}",
precision=0,
)
self.tag_N += 1
@staticmethod
def param_changed(ui_element):
ui_element.setStyleSheet("background-color: #FFA700;")
class MotorActions(Enum):
MOVE_TO_COORDINATES = "move_to_coordinates"
MOVE_RELATIVE = "move_relative"
class MotorControl(QThread):
coordinates_updated = pyqtSignal(float, float) # Signal to emit current coordinates
limits_retrieved = pyqtSignal(list, list) # Signal to emit current limits
speed_retrieved = pyqtSignal(int, int) # Signal to emit current speed
update_frequency_retrieved = pyqtSignal(int, int) # Signal to emit current update frequency
move_finished = pyqtSignal() # Signal to emit when the move is finished
motors_loaded = pyqtSignal(list, list) # Signal to emit when the motors are loaded
motors_selected = pyqtSignal(object, object) # Signal to emit when the motors are selected
# progress_updated = pyqtSignal(int) #TODO Signal to emit progress percentage
def __init__(
self,
parent=None,
):
super().__init__(parent)
self.action = None
self._initialize_motor()
def motor_by_string(self, motor_x_name: str, motor_y_name: str) -> tuple:
motor_x_index = self.all_motors_names.index(motor_x_name)
motor_y_index = self.all_motors_names.index(motor_y_name)
motor_x = self.all_motors[motor_x_index]
motor_y = self.all_motors[motor_y_index]
return motor_x, motor_y
def connect_motors(self, motor_x_name: str, motor_y_name: str) -> None:
self.motor_x, self.motor_y = self.motor_by_string(motor_x_name, motor_y_name)
(self.current_x, self.current_y) = self.get_coordinates()
if self.motors_consumer is not None:
self.motors_consumer.shutdown()
self.motors_consumer = client.connector.consumer(
topics=[
MessageEndpoints.device_readback(self.motor_x.name),
MessageEndpoints.device_readback(self.motor_y.name),
],
cb=self._device_status_callback_motors,
parent=self,
)
self.motors_consumer.start()
self.motors_selected.emit(self.motor_x, self.motor_y)
def get_all_motors(self) -> list:
all_motors = client.device_manager.devices.acquisition_group("motor")
return all_motors
def get_all_motors_names(self) -> list:
all_motors = client.device_manager.devices.acquisition_group("motor")
all_motors_names = [motor.name for motor in all_motors]
return all_motors_names
def retrieve_all_motors(self):
self.all_motors = self.get_all_motors()
self.all_motors_names = self.get_all_motors_names()
self.motors_loaded.emit(self.all_motors_names, self.all_motors_names)
return self.all_motors, self.all_motors_names
def get_coordinates(self) -> tuple:
"""Get current motor position"""
x = self.motor_x.read(cached=True)["value"]
y = self.motor_y.read(cached=True)["value"]
return x, y
def retrieve_coordinates(self) -> tuple:
"""Get current motor position for export to main app"""
return self.current_x, self.current_y
def get_motor_limits(self, motor) -> list:
"""Get the limits of a motor"""
return motor.limits
def get_motor_config(self, motor) -> dict:
"""Get the configuration of a motor""" # TODO at this moment just for speed and update_frequency
return motor.get_device_config()
def update_all_config(self, speed: list = None, update_frequency: list = None) -> None:
# TODO now only speed and update frequency
if speed is not None:
self.motor_x.set_device_config({"speed": speed[0]})
self.motor_y.set_device_config({"speed": speed[1]})
if update_frequency is not None:
self.motor_x.set_device_config({"update_frequency": update_frequency[0]})
self.motor_y.set_device_config({"update_frequency": update_frequency[1]})
self.retrieve_motor_speed(self.motor_x, self.motor_y)
self.retrieve_motor_update_frequency(self.motor_x, self.motor_y)
def retrieve_motor_speed(
self, motor_x, motor_y
) -> None: # TODO can be migrated to some general config function
"""Get the speed of a motor"""
speed_x = motor_x.get_device_config()["speed"]
speed_y = motor_y.get_device_config()["speed"]
self.speed_retrieved.emit(int(speed_x), int(speed_y))
def retrieve_motor_update_frequency(
self, motor_x, motor_y
) -> None: # TODO can be migrated to some general config function
"""Get the speed of a motor"""
update_frequency_x = motor_x.get_device_config()["update_frequency"]
update_frequency_y = motor_y.get_device_config()["update_frequency"]
self.update_frequency_retrieved.emit(int(update_frequency_x), int(update_frequency_y))
def retrieve_motor_limits(self, motor_x, motor_y):
limit_x = self.get_motor_limits(motor_x)
limit_y = self.get_motor_limits(motor_y)
self.limits_retrieved.emit(limit_x, limit_y)
def update_motor_limits(self, motor, low_limit=None, high_limit=None) -> None:
current_low_limit, current_high_limit = self.get_motor_limits(motor)
# Check if the low limit has changed and is not None
if low_limit is not None and low_limit != current_low_limit:
motor.low_limit = low_limit
# Check if the high limit has changed and is not None
if high_limit is not None and high_limit != current_high_limit:
motor.high_limit = high_limit
def update_all_motor_limits(self, x_limit: list = None, y_limit: list = None) -> None:
current_position = self.get_coordinates()
if x_limit is not None:
if current_position[0] < x_limit[0] or current_position[0] > x_limit[1]:
raise ValueError("Current motor position is outside the new limits (X)")
else:
self.update_motor_limits(self.motor_x, low_limit=x_limit[0], high_limit=x_limit[1])
if y_limit is not None:
if current_position[1] < y_limit[0] or current_position[1] > y_limit[1]:
raise ValueError("Current motor position is outside the new limits (Y)")
else:
self.update_motor_limits(self.motor_y, low_limit=y_limit[0], high_limit=y_limit[1])
self.retrieve_motor_limits(self.motor_x, self.motor_y)
def move_to_coordinates(self, target_coordinates: tuple):
self.action = MotorActions.MOVE_TO_COORDINATES
self.target_coordinates = target_coordinates
self.start()
def move_relative(self, motor, value: float):
self.action = MotorActions.MOVE_RELATIVE
self.motor = motor
self.value = value
self.start()
def run(self):
if self.action == MotorActions.MOVE_TO_COORDINATES:
self._move_motor_coordinate()
elif self.action == MotorActions.MOVE_RELATIVE:
self._move_motor_relative(self.motor, self.value)
def set_target_coordinates(self, target_coordinates: tuple) -> None:
self.target_coordinates = target_coordinates
def _initialize_motor(self) -> None:
self.motor_x, self.motor_y = None, None
self.current_x, self.current_y = None, None
self.motors_consumer = None
# Get all available motors in the client
self.all_motors = self.get_all_motors()
self.all_motors_names = self.get_all_motors_names()
self.retrieve_all_motors() # send motor list to GUI
self.target_coordinates = None
def _move_motor_coordinate(self) -> None:
"""Move the motor to the specified coordinates"""
status = scans.mv(
self.motor_x,
self.target_coordinates[0],
self.motor_y,
self.target_coordinates[1],
relative=False,
)
status.wait()
self.move_finished.emit()
def _move_motor_relative(self, motor, value: float) -> None:
status = scans.mv(motor, value, relative=True)
status.wait()
self.move_finished.emit()
def stop_movement(self):
queue.request_scan_abortion()
queue.request_queue_reset()
@staticmethod
def _device_status_callback_motors(msg, *, parent, **_kwargs) -> None:
deviceMSG = BECMessage.DeviceMessage.loads(msg.value)
if parent.motor_x.name in deviceMSG.content["signals"]:
parent.current_x = deviceMSG.content["signals"][parent.motor_x.name]["value"]
elif parent.motor_y.name in deviceMSG.content["signals"]:
parent.current_y = deviceMSG.content["signals"][parent.motor_y.name]["value"]
parent.coordinates_updated.emit(parent.current_x, parent.current_y)
if __name__ == "__main__":
from bec_lib import BECClient
# from bec_lib.core import ServiceConfig,RedisConnector
client = BECClient()
# client.initialize(config=ServiceConfig(config_path="test_config.yaml"))
# Client initialization - by dispatcher
# client = bec_dispatcher.client
client.start()
dev = client.device_manager.devices
scans = client.scans
queue = client.queue
app = QApplication([])
window = MotorApp()
window.show()
app.exec_()

View File

@@ -12,12 +12,11 @@ from PyQt5.QtWidgets import (
)
from pyqtgraph import mkPen
from pyqtgraph.Qt import QtCore
from crosshair import Crosshair
from bec_widgets.qt_utils import Crosshair
class ExampleApp(QWidget):
def __init__(self):
"""Example application for using the Crosshair class"""
super().__init__()
# Layout

View File