From a2f7aa58f9639b76b4243d4d5f5bf0efd7b98054 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Thu, 17 Aug 2023 16:05:10 +0200 Subject: [PATCH 01/29] refactor: folder organization changed --- bec_widgets/examples/__init__.py | 0 .../{qt_utils/utils_example.py => examples/crosshair_example.py} | 0 bec_widgets/widgets/__init__.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 bec_widgets/examples/__init__.py rename bec_widgets/{qt_utils/utils_example.py => examples/crosshair_example.py} (100%) create mode 100644 bec_widgets/widgets/__init__.py diff --git a/bec_widgets/examples/__init__.py b/bec_widgets/examples/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bec_widgets/qt_utils/utils_example.py b/bec_widgets/examples/crosshair_example.py similarity index 100% rename from bec_widgets/qt_utils/utils_example.py rename to bec_widgets/examples/crosshair_example.py diff --git a/bec_widgets/widgets/__init__.py b/bec_widgets/widgets/__init__.py new file mode 100644 index 00000000..e69de29b From 947ba9f8b730e96082cb51ff6894734a0e119ca1 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Fri, 18 Aug 2023 10:38:52 +0200 Subject: [PATCH 02/29] feat: motor_example.py created, motor samx and samy can be moved by buttons --- bec_widgets/examples/__init__.py | 0 .../motor_movement/motor_controller.ui | 160 ++++++++++++++++++ .../examples/motor_movement/motor_example.py | 68 ++++++++ .../{ => plotting}/crosshair_example.py | 3 +- 4 files changed, 230 insertions(+), 1 deletion(-) delete mode 100644 bec_widgets/examples/__init__.py create mode 100644 bec_widgets/examples/motor_movement/motor_controller.ui create mode 100644 bec_widgets/examples/motor_movement/motor_example.py rename bec_widgets/examples/{ => plotting}/crosshair_example.py (99%) diff --git a/bec_widgets/examples/__init__.py b/bec_widgets/examples/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui new file mode 100644 index 00000000..ebb64c7b --- /dev/null +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -0,0 +1,160 @@ + + + Form + + + + 0 + 0 + 800 + 266 + + + + Motor Controller + + + + + + false + + + Motor Limits + + + + + + + samy + + + + + + + + + + Update + + + + + + + + + + - samy + + + + + + + + + + + + + - samx + + + + + + + - samx + + + + + + + + + + false + + + + + + + Motor Controls + + + + + + 5 + + + + + + + ... + + + Qt::LeftArrow + + + + + + + ... + + + Qt::UpArrow + + + + + + + ... + + + Qt::DownArrow + + + + + + + ... + + + Qt::RightArrow + + + + + + + + + + Motor Status + + + Qt::AlignCenter + + + + + + + + GraphicsLayoutWidget + QGraphicsView +
pyqtgraph.h
+
+
+ + +
diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py new file mode 100644 index 00000000..2c0d5106 --- /dev/null +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -0,0 +1,68 @@ +import os + +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtWidgets import QApplication, QWidget +from pyqtgraph.Qt import QtCore, QtWidgets, uic +import pyqtgraph as pg + + +class MotorApp(QWidget): + motor_position = pyqtSignal(float) # TODO hook to motor position update + + def __init__(self): + super().__init__() + current_path = os.path.dirname(__file__) + uic.loadUi(os.path.join(current_path, "motor_controller.ui"), self) + + # UI + + self.init_ui() + + def init_ui(self): + """Setup all ui elements""" + ########################## + # 2D Plot + ########################## + + self.label_coorditanes = self.glw.addLabel("Current Motor Coordinates", row=0, col=0) + self.plot_map = self.glw.addPlot(row=1, col=0) + self.image_map = pg.ImageItem() + self.plot_map.addItem(self.image_map) + + ########################## + # Signals + ########################## + # Buttons - Motor Movement + self.toolButton_right.clicked.connect( + lambda: self.move_motor(dev.samx, self.spinBox_step.value()) + ) + self.toolButton_left.clicked.connect( + lambda: self.move_motor(dev.samx, -self.spinBox_step.value()) + ) + self.toolButton_up.clicked.connect( + lambda: self.move_motor(dev.samy, self.spinBox_step.value()) + ) + self.toolButton_down.clicked.connect( + lambda: self.move_motor(dev.samy, -self.spinBox_step.value()) + ) + + # self.toolButton_left.clicked.connect(lambda: print(self.client)) + + def move_motor(self, motor, value: float) -> None: + scans.mv(motor, value, relative=True) + self.motor_position.emit(motor.position) + + +if __name__ == "__main__": + from bec_widgets.bec_dispatcher import bec_dispatcher + + # Client initialization + client = bec_dispatcher.client + client.start() + dev = client.device_manager.devices + scans = client.scans + + app = QApplication([]) + window = MotorApp() + window.show() + app.exec_() diff --git a/bec_widgets/examples/crosshair_example.py b/bec_widgets/examples/plotting/crosshair_example.py similarity index 99% rename from bec_widgets/examples/crosshair_example.py rename to bec_widgets/examples/plotting/crosshair_example.py index f15ed110..5aed65aa 100644 --- a/bec_widgets/examples/crosshair_example.py +++ b/bec_widgets/examples/plotting/crosshair_example.py @@ -12,7 +12,8 @@ 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): From e6952a6d13c84487fd6ab08056f1f5b46d594b8a Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Fri, 18 Aug 2023 11:30:31 +0200 Subject: [PATCH 03/29] feat: map of motor position --- .../motor_movement/motor_controller.ui | 254 +++++++++--------- .../examples/motor_movement/motor_example.py | 49 +++- 2 files changed, 177 insertions(+), 126 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index ebb64c7b..ba2dc0da 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -13,139 +13,143 @@ Motor Controller - - - - - false - - - Motor Limits - - - - - - + samy - - - - - - - - - - Update - - - - - - - - - - - samy - - - - - - - - - - - - - - samx - - - - - - - - samx - - - - - - - - - - false - - - - - - - Motor Controls - - - - - - 5 - - - - - - - ... - - - Qt::LeftArrow - - - - - - - ... - - - Qt::UpArrow - - - - - - - ... - - - Qt::DownArrow - - - - - - - ... - - - Qt::RightArrow - - - - - - - + + - Motor Status + Motor Position Qt::AlignCenter + + + + + + false + + + + + + + Motor Controls + + + + + + 5 + + + + + + + ... + + + Qt::LeftArrow + + + + + + + ... + + + Qt::UpArrow + + + + + + + ... + + + Qt::DownArrow + + + + + + + ... + + + Qt::RightArrow + + + + + + + + + + false + + + Motor Limits + + + + + + - samy + + + + + + + + samy + + + + + + + - samx + + + + + + + + + + + + + + + + - samx + + + + + + + + + + Update + + + + + + + + diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index 2c0d5106..27eaaf73 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -1,5 +1,6 @@ import os +import numpy as np from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QApplication, QWidget from pyqtgraph.Qt import QtCore, QtWidgets, uic @@ -8,6 +9,7 @@ import pyqtgraph as pg class MotorApp(QWidget): motor_position = pyqtSignal(float) # TODO hook to motor position update + motor_update = pyqtSignal() def __init__(self): super().__init__() @@ -29,6 +31,10 @@ class MotorApp(QWidget): self.image_map = pg.ImageItem() self.plot_map.addItem(self.image_map) + self.image_map_data = np.zeros((100, 100), dtype=np.float32) + x, y = self.get_xy() # Assuming get_xy returns the motor position + self.update_image_map(x, y) + self.image_map.setImage(self.image_map_data.T) # Set the image ########################## # Signals ########################## @@ -46,11 +52,52 @@ class MotorApp(QWidget): lambda: self.move_motor(dev.samy, -self.spinBox_step.value()) ) + # Map + self.motor_position.connect(lambda x: self.update_image_map(*self.get_xy())) + + # make keybindings for motor movement + self.toolButton_right.setShortcut("Right") + self.toolButton_left.setShortcut("Left") + self.toolButton_up.setShortcut("Up") + self.toolButton_down.setShortcut("Down") + # self.toolButton_left.clicked.connect(lambda: print(self.client)) + self.motor_position.connect(lambda x: print(f"motor position updated: {x}")) + self.motor_update.connect( + lambda: self.label_status.setText( + f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})" + ) + ) + def move_motor(self, motor, value: float) -> None: scans.mv(motor, value, relative=True) - self.motor_position.emit(motor.position) + motor_position = motor.position() + + self.motor_position.emit(motor_position) + self.motor_update.emit() + + def get_xy(self): # TODO decide if useful + """Get current motor position""" + x = dev.samx.position() + y = dev.samy.position() + return x, y + + def update_image_map(self, x, y): + """Update the image map with the new motor position""" + + # Reduce the brightness of all pixels by a fixed fraction (e.g., 5%) + self.image_map_data *= 0.8 + + # Modify this mapping as needed + pixel_x = int(x) + pixel_y = int(y) + + # Set the bright pixel at the new position + self.image_map_data[pixel_x, pixel_y] = 255 + + # Update the display + self.image_map.setImage(self.image_map_data) if __name__ == "__main__": From 512e698e26d9eef05b4f430475ccc268b68ad632 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Fri, 18 Aug 2023 17:06:37 +0200 Subject: [PATCH 04/29] feat: setting map according to motor limits --- .../motor_movement/motor_controller.ui | 200 +++++++++++++++--- .../examples/motor_movement/motor_example.py | 143 +++++++++++-- 2 files changed, 297 insertions(+), 46 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index ba2dc0da..13cc96c5 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -6,33 +6,74 @@ 0 0 - 800 - 266 + 1289 + 596 Motor Controller - + - - - Motor Position - - - Qt::AlignCenter - - - - - + + + + + Motor Position + + + Qt::AlignCenter + + + + + + + + + Go + + + + + + + Save + + + + + - false + true + + + + + + + + General + + + + + + Decimal precision + + + + + + + + + @@ -92,7 +133,7 @@ - false + true Motor Limits @@ -120,13 +161,34 @@ - + + + -1000 + + + 1000 + + - + + + -1000 + + + 1000 + + - + + + -1000 + + + 1000 + + @@ -136,12 +198,12 @@ - - - - - - Update + + + -1000 + + + 1000 @@ -150,6 +212,94 @@ + + + + 0 + + + + Saved + + + + + + Go to selected + + + + + + + + Show + + + + + X + + + + + Y + + + + + + + + + Queue + + + + + + Reset Queue + + + + + + + + queueID + + + + + scanID + + + + + is_scan + + + + + type + + + + + scan_number + + + + + IQ status + + + + + + + + diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index 27eaaf73..491c50c5 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -1,6 +1,7 @@ import os import numpy as np +from PyQt5 import QtGui from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QApplication, QWidget from pyqtgraph.Qt import QtCore, QtWidgets, uic @@ -17,7 +18,14 @@ class MotorApp(QWidget): uic.loadUi(os.path.join(current_path, "motor_controller.ui"), self) # UI + self.start_x, self.start_y = self.get_xy() + self.limit_x, self.limit_y = self.get_motor_limits(dev.samx), self.get_motor_limits( + dev.samy + ) + self.label_status.setText( + f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})" + ) self.init_ui() def init_ui(self): @@ -31,13 +39,17 @@ class MotorApp(QWidget): self.image_map = pg.ImageItem() self.plot_map.addItem(self.image_map) - self.image_map_data = np.zeros((100, 100), dtype=np.float32) - x, y = self.get_xy() # Assuming get_xy returns the motor position - self.update_image_map(x, y) - self.image_map.setImage(self.image_map_data.T) # Set the image + # self.image_map_data = np.zeros((100, 100), dtype=np.float32) + # x, y = self.get_xy() # Assuming get_xy returns the motor position + # self.update_image_map(x, y) + # self.image_map.setImage(self.image_map_data.T) # Set the image + + # self.init_motor_map() + ########################## # Signals ########################## + # Buttons - Motor Movement self.toolButton_right.clicked.connect( lambda: self.move_motor(dev.samx, self.spinBox_step.value()) @@ -52,6 +64,32 @@ class MotorApp(QWidget): lambda: self.move_motor(dev.samy, -self.spinBox_step.value()) ) + # SpinBoxes - Motor Limits #TODO make spinboxes own limits updated, currently is [-1000, 1000] + # manually set limits before readout #TODO will be removed when spinboxes have own limits + # dev.samx.low_limit = -100 + # dev.samx.high_limit = 100 + # dev.samy.low_limit = -100 + # dev.samy.high_limit = 100 + + # display initial 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]) + # # change limits of motors for all motors and spinboxes + self.spinBox_x_min.valueChanged.connect( + lambda x: self.update_motor_limits(dev.samx, low_limit=x) + ) + self.spinBox_x_max.valueChanged.connect( + lambda x: self.update_motor_limits(dev.samx, high_limit=x) + ) + self.spinBox_y_min.valueChanged.connect( + lambda x: self.update_motor_limits(dev.samy, low_limit=x) + ) + self.spinBox_y_max.valueChanged.connect( + lambda x: self.update_motor_limits(dev.samy, high_limit=x) + ) + # Map self.motor_position.connect(lambda x: self.update_image_map(*self.get_xy())) @@ -70,8 +108,87 @@ class MotorApp(QWidget): ) ) + self.init_motor_map() + + def init_motor_map(self): + # Get motor limits + limit_x_min, limit_x_max = self.get_motor_limits(dev.samx) + limit_y_min, limit_y_max = self.get_motor_limits(dev.samy) + + 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.image_map_data = np.full( + (map_width, map_height), self.background_value, dtype=np.float32 + ) + + # Set the initial position on the map + x, y = self.get_xy() + self.prev_x, self.prev_y = x, y + self.update_image_map(x, y) + + # 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.image_map.setTransform(self.tr) + + self.image_map.dataTransform() + + def update_image_map(self, x, y): + """Update the image map with the new motor position""" + + # Define the dimming factor + dimming_factor = 0.80 + + # Apply the dimming factor only to pixels above the background value + self.image_map_data[self.image_map_data > self.background_value] *= dimming_factor + + # Mapping of motor coordinates to pixel coordinates + pixel_x = int(x - self.offset_x) + pixel_y = int(y - self.offset_y) + + # Set the bright pixel at the new position + self.image_map_data[pixel_x, pixel_y] = 255 + + # Update the display + self.image_map.updateImage(self.image_map_data, levels=(0, 255)) + + def get_motor_limits(self, motor): + """Get the limits of a motor""" + high_limit = motor.high_limit + low_limit = motor.low_limit + + return low_limit, high_limit + + def update_motor_limits(self, motor, low_limit=None, high_limit=None): + # Get current limits + current_low_limit, current_high_limit = motor.limits[0], motor.limits[1] + + # 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 move_motor(self, motor, value: float) -> None: - scans.mv(motor, value, relative=True) + try: + status = scans.mv(motor, value, relative=True) + + while status.status not in ["COMPLETED", "STOPPED"]: + print(status.status) + + # return status + except Exception as e: + print(e) + motor_position = motor.position() self.motor_position.emit(motor_position) @@ -83,22 +200,6 @@ class MotorApp(QWidget): y = dev.samy.position() return x, y - def update_image_map(self, x, y): - """Update the image map with the new motor position""" - - # Reduce the brightness of all pixels by a fixed fraction (e.g., 5%) - self.image_map_data *= 0.8 - - # Modify this mapping as needed - pixel_x = int(x) - pixel_y = int(y) - - # Set the bright pixel at the new position - self.image_map_data[pixel_x, pixel_y] = 255 - - # Update the display - self.image_map.setImage(self.image_map_data) - if __name__ == "__main__": from bec_widgets.bec_dispatcher import bec_dispatcher From cac45626fc9a315f9012b110760a92e27e5ed226 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:02:41 +0200 Subject: [PATCH 05/29] feat: switch for keyboard shortcuts for motor movement --- .../motor_movement/motor_controller.ui | 182 ++++++++++-------- .../examples/motor_movement/motor_example.py | 56 +++--- 2 files changed, 136 insertions(+), 102 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index 13cc96c5..049078aa 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -79,53 +79,64 @@ Motor Controls - - - - - 5 + + + + + Move with arrow keys - - - - ... - - - Qt::LeftArrow - - - - - - - ... - - - Qt::UpArrow - - - - - - - ... - - - Qt::DownArrow - - - - - - - ... - - - Qt::RightArrow - - + + + + + + ... + + + Qt::UpArrow + + + + + + + ... + + + Qt::LeftArrow + + + + + + + 5 + + + + + + + ... + + + Qt::RightArrow + + + + + + + ... + + + Qt::DownArrow + + + + @@ -139,28 +150,7 @@ Motor Limits - - - - - samy - - - - - - - + samy - - - - - - - - samx - - - - + -1000 @@ -168,9 +158,12 @@ 1000 + + -100 + - + -1000 @@ -178,9 +171,19 @@ 1000 + + 100 + - + + + + - samy + + + + -1000 @@ -188,16 +191,33 @@ 1000 + + 100 + - + + + + + samy + + + + - samx - + + + + - samx + + + + -1000 @@ -205,6 +225,16 @@ 1000 + + -100 + + + + + + + Update + @@ -222,13 +252,6 @@ Saved - - - - Go to selected - - - @@ -246,6 +269,11 @@ Y + + + Move + + diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index 491c50c5..f442653f 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -2,8 +2,9 @@ import os import numpy as np from PyQt5 import QtGui -from PyQt5.QtCore import pyqtSignal -from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtCore import pyqtSignal, Qt +from PyQt5.QtGui import QKeySequence +from PyQt5.QtWidgets import QApplication, QWidget, QShortcut from pyqtgraph.Qt import QtCore, QtWidgets, uic import pyqtgraph as pg @@ -64,6 +65,9 @@ class MotorApp(QWidget): lambda: self.move_motor(dev.samy, -self.spinBox_step.value()) ) + self.checkBox_enableArrows.stateChanged.connect(self.update_arrow_key_shortcuts) + self.update_arrow_key_shortcuts() + # SpinBoxes - Motor Limits #TODO make spinboxes own limits updated, currently is [-1000, 1000] # manually set limits before readout #TODO will be removed when spinboxes have own limits # dev.samx.low_limit = -100 @@ -76,31 +80,10 @@ class MotorApp(QWidget): 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]) - # # change limits of motors for all motors and spinboxes - self.spinBox_x_min.valueChanged.connect( - lambda x: self.update_motor_limits(dev.samx, low_limit=x) - ) - self.spinBox_x_max.valueChanged.connect( - lambda x: self.update_motor_limits(dev.samx, high_limit=x) - ) - self.spinBox_y_min.valueChanged.connect( - lambda x: self.update_motor_limits(dev.samy, low_limit=x) - ) - self.spinBox_y_max.valueChanged.connect( - lambda x: self.update_motor_limits(dev.samy, high_limit=x) - ) # Map self.motor_position.connect(lambda x: self.update_image_map(*self.get_xy())) - # make keybindings for motor movement - self.toolButton_right.setShortcut("Right") - self.toolButton_left.setShortcut("Left") - self.toolButton_up.setShortcut("Up") - self.toolButton_down.setShortcut("Down") - - # self.toolButton_left.clicked.connect(lambda: print(self.client)) - self.motor_position.connect(lambda x: print(f"motor position updated: {x}")) self.motor_update.connect( lambda: self.label_status.setText( @@ -166,7 +149,9 @@ class MotorApp(QWidget): return low_limit, high_limit - def update_motor_limits(self, motor, low_limit=None, high_limit=None): + def update_motor_limits( + self, motor, low_limit=None, high_limit=None + ): # TODO limits cannot be smaller that the current location of motor # Get current limits current_low_limit, current_high_limit = motor.limits[0], motor.limits[1] @@ -200,11 +185,32 @@ class MotorApp(QWidget): y = dev.samy.position() return x, y + 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("") + if __name__ == "__main__": from bec_widgets.bec_dispatcher import bec_dispatcher - # Client initialization + # from bec_lib import BECClient + # from bec_lib.core import ServiceConfig,RedisConnector + + # client = BECClient() + # client.initialize(config=ServiceConfig(config_path="test_config.yaml")) + # client.initialize(config=ServiceConfig(config_path="test_config.yaml")) + + # Client initialization - by dispatcher client = bec_dispatcher.client client.start() dev = client.device_manager.devices From 2d1665c76b8174d9fffa3442afa98fe1ea6ac207 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:18:01 +0200 Subject: [PATCH 06/29] feat: motor limits can be changed by spinBoxes --- .../motor_movement/motor_controller.ui | 7 +++ .../examples/motor_movement/motor_example.py | 43 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index 049078aa..ffdfaa19 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -138,6 +138,13 @@ + + + + Stop Movement + + + diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index f442653f..56afd090 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -24,6 +24,8 @@ class MotorApp(QWidget): dev.samy ) + print(f"Init limits: samx:{self.limit_x}, samy:{self.limit_y}") + self.label_status.setText( f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})" ) @@ -35,7 +37,9 @@ class MotorApp(QWidget): # 2D Plot ########################## - self.label_coorditanes = self.glw.addLabel("Current Motor Coordinates", row=0, col=0) + self.label_coorditanes = self.glw.addLabel( + f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})", row=0, col=0 + ) self.plot_map = self.glw.addPlot(row=1, col=0) self.image_map = pg.ImageItem() self.plot_map.addItem(self.image_map) @@ -81,6 +85,19 @@ class MotorApp(QWidget): self.spinBox_y_min.setValue(self.limit_y[0]) self.spinBox_y_max.setValue(self.limit_y[1]) + # 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)) + + 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()], + ) + ) + # Map self.motor_position.connect(lambda x: self.update_image_map(*self.get_xy())) @@ -163,6 +180,27 @@ class MotorApp(QWidget): if high_limit is not None and high_limit != current_high_limit: motor.high_limit = high_limit + # self.init_motor_map() # reinitialize the map with the new limits + + def update_all_motor_limits(self, x_limit: list = None, y_limit: list = None) -> None: + # check if current motor position if within the new limits + + current_position = self.get_xy() + + 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(dev.samx, 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(dev.samy, low_limit=y_limit[0], high_limit=y_limit[1]) + + self.init_motor_map() # reinitialize the map with the new limits + def move_motor(self, motor, value: float) -> None: try: status = scans.mv(motor, value, relative=True) @@ -185,6 +223,9 @@ class MotorApp(QWidget): y = dev.samy.position() return x, y + def param_changed(self, ui_element): + ui_element.setStyleSheet("background-color: #FFA700;") + def update_arrow_key_shortcuts(self): if self.checkBox_enableArrows.isChecked(): # Set the arrow key shortcuts for motor movement From cbe27e46cfb6282c71844641e1ed6059e8fa96bf Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:55:49 +0200 Subject: [PATCH 07/29] feat: motor move to absolute (X,Y) coordinates --- .../motor_movement/motor_controller.ui | 37 +++++++++++++++++++ .../examples/motor_movement/motor_example.py | 26 +++++++++++++ 2 files changed, 63 insertions(+) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index ffdfaa19..84979648 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -138,6 +138,43 @@ + + + + + + X + + + Qt::AlignCenter + + + + + + + Y + + + Qt::AlignCenter + + + + + + + Go + + + + + + + + + + + diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index 56afd090..64d0e93c 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -12,6 +12,7 @@ import pyqtgraph as pg class MotorApp(QWidget): motor_position = pyqtSignal(float) # TODO hook to motor position update motor_update = pyqtSignal() + motor_position_xy = pyqtSignal(list) def __init__(self): super().__init__() @@ -72,6 +73,13 @@ class MotorApp(QWidget): 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_coordinates( + [self.spinBox_absolute_x.value(), self.spinBox_absolute_y.value()] + ) + ) + # SpinBoxes - Motor Limits #TODO make spinboxes own limits updated, currently is [-1000, 1000] # manually set limits before readout #TODO will be removed when spinboxes have own limits # dev.samx.low_limit = -100 @@ -100,6 +108,7 @@ class MotorApp(QWidget): # Map self.motor_position.connect(lambda x: self.update_image_map(*self.get_xy())) + self.motor_update.connect(lambda: self.update_image_map(*self.get_xy())) self.motor_position.connect(lambda x: print(f"motor position updated: {x}")) self.motor_update.connect( @@ -207,6 +216,8 @@ class MotorApp(QWidget): while status.status not in ["COMPLETED", "STOPPED"]: print(status.status) + motor_position = motor.position() + self.motor_position.emit(motor_position) # return status except Exception as e: @@ -217,6 +228,21 @@ class MotorApp(QWidget): self.motor_position.emit(motor_position) self.motor_update.emit() + def move_motor_coordinates(self, coor: list) -> None: + try: + status = scans.mv(dev.samx, coor[0], dev.samy, coor[1], relative=False) + + while status.status not in ["COMPLETED", "STOPPED"]: + print(status.status) + + except Exception as e: + print(e) + + # motor_position = self.get_xy() + # self.motor_position_xy.emit(motor_position) + + self.motor_update.emit() + def get_xy(self): # TODO decide if useful """Get current motor position""" x = dev.samx.position() From 92388c3cab7e024978aaa2906afbd698015dec66 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Mon, 21 Aug 2023 13:48:48 +0200 Subject: [PATCH 08/29] feat: table with coordinates getting initial coordinates of motor --- .../motor_movement/motor_controller.ui | 9 ++- .../examples/motor_movement/motor_example.py | 55 +++++++++++++++++-- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index 84979648..8d5a6102 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -293,11 +293,16 @@ - Saved + Coordinates - + + + + Tag + + Show diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index 64d0e93c..e0c3f696 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -1,12 +1,11 @@ import os import numpy as np +import pyqtgraph as pg from PyQt5 import QtGui from PyQt5.QtCore import pyqtSignal, Qt -from PyQt5.QtGui import QKeySequence -from PyQt5.QtWidgets import QApplication, QWidget, QShortcut -from pyqtgraph.Qt import QtCore, QtWidgets, uic -import pyqtgraph as pg +from PyQt5.QtWidgets import QApplication, QWidget +from pyqtgraph.Qt import QtWidgets, uic class MotorApp(QWidget): @@ -117,6 +116,11 @@ class MotorApp(QWidget): ) ) + # Coordinates table + self.generate_table_coordinate( + self.tableWidget_coordinates, self.get_xy(), tag="Initial", precision=0 + ) + self.init_motor_map() def init_motor_map(self): @@ -208,6 +212,14 @@ class MotorApp(QWidget): else: self.update_motor_limits(dev.samy, low_limit=y_limit[0], high_limit=y_limit[1]) + for spinBox in ( + self.spinBox_x_min, + self.spinBox_x_max, + self.spinBox_y_min, + self.spinBox_y_max, + ): + spinBox.setStyleSheet("") + self.init_motor_map() # reinitialize the map with the new limits def move_motor(self, motor, value: float) -> None: @@ -266,6 +278,41 @@ class MotorApp(QWidget): self.toolButton_up.setShortcut("") self.toolButton_down.setShortcut("") + def generate_table_coordinate( + self, table: QtWidgets.QTableWidget, coordinates: list, tag: str = None, precision: int = 0 + ) -> None: + current_row_count = table.rowCount() + + table.setRowCount(current_row_count + 1) + + # table.insertRow(current_row_count) + + 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_coordinates( + [ + float(table.item(current_row_count, 2).text()), + float(table.item(current_row_count, 3).text()), + ] + ) + ) + + table.resizeColumnsToContents() + if __name__ == "__main__": from bec_widgets.bec_dispatcher import bec_dispatcher From af2fcffd5f4efa49e3cd728e4d481665584af941 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Tue, 22 Aug 2023 18:16:44 +0200 Subject: [PATCH 09/29] refactor: motor movement as a QThread --- .../motor_movement/motor_controller.ui | 16 +- .../examples/motor_movement/motor_example.py | 244 +++++++++++++++--- 2 files changed, 222 insertions(+), 38 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index 8d5a6102..47521271 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -60,17 +60,27 @@ General - - + + Decimal precision - + + + + + Speed + + + + + + diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index e0c3f696..c7b49a6c 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -3,15 +3,19 @@ import os import numpy as np import pyqtgraph as pg from PyQt5 import QtGui +from PyQt5.QtCore import QThread from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtWidgets import QApplication, QWidget from pyqtgraph.Qt import QtWidgets, uic +from bec_lib.core import MessageEndpoints, BECMessage + class MotorApp(QWidget): motor_position = pyqtSignal(float) # TODO hook to motor position update motor_update = pyqtSignal() motor_position_xy = pyqtSignal(list) + coordinates_updated = pyqtSignal(float, float) def __init__(self): super().__init__() @@ -20,6 +24,7 @@ class MotorApp(QWidget): # UI self.start_x, self.start_y = self.get_xy() + self.current_x, self.current_y = self.get_xy() self.limit_x, self.limit_y = self.get_motor_limits(dev.samx), self.get_motor_limits( dev.samy ) @@ -29,8 +34,39 @@ class MotorApp(QWidget): self.label_status.setText( f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})" ) + + # self.samx_consumer = client.connector.consumer(topics=[MessageEndpoints.device_readback('samx'), + # MessageEndpoints.device_readback('samy')], + # cb=self._device_status_callback,parent=self) + # self.samx_consumer.start() + + # self.samx_consumer = client.connector.consumer( + # topics=[MessageEndpoints.device_readback("samx")], + # cb=self._device_status_callback_samx, + # parent=self, + # ) + # + # self.samy_consumer = client.connector.consumer( + # topics=[MessageEndpoints.device_readback("samy")], + # cb=self._device_status_callback_samy, + # parent=self, + # ) + # self.samx_consumer.start() + # self.samy_consumer.start() + + self.motor_thread = MotorMovement() + self.init_ui() + def on_move_to_coordinates_button_clicked(self): + x = self.spinBox_absolute_x.value() + y = self.spinBox_absolute_y.value() + target_coordinates = (x, y) + self.motor_thread.move_to_coordinates(target_coordinates) + + def on_move_relative_button_clicked(self, motor, value: float): + self.motor_thread.move_relative(motor, value) + def init_ui(self): """Setup all ui elements""" ########################## @@ -56,17 +92,30 @@ class MotorApp(QWidget): ########################## # Buttons - Motor Movement + # self.toolButton_right.clicked.connect( + # lambda: self.move_motor(dev.samx, self.spinBox_step.value()) + # ) + # self.toolButton_left.clicked.connect( + # lambda: self.move_motor(dev.samx, -self.spinBox_step.value()) + # ) + # self.toolButton_up.clicked.connect( + # lambda: self.move_motor(dev.samy, self.spinBox_step.value()) + # ) + # self.toolButton_down.clicked.connect( + # lambda: self.move_motor(dev.samy, -self.spinBox_step.value()) + # ) + self.toolButton_right.clicked.connect( - lambda: self.move_motor(dev.samx, self.spinBox_step.value()) + lambda: self.on_move_relative_button_clicked(dev.samx, self.spinBox_step.value()) ) self.toolButton_left.clicked.connect( - lambda: self.move_motor(dev.samx, -self.spinBox_step.value()) + lambda: self.on_move_relative_button_clicked(dev.samx, -self.spinBox_step.value()) ) self.toolButton_up.clicked.connect( - lambda: self.move_motor(dev.samy, self.spinBox_step.value()) + lambda: self.on_move_relative_button_clicked(dev.samy, self.spinBox_step.value()) ) self.toolButton_down.clicked.connect( - lambda: self.move_motor(dev.samy, -self.spinBox_step.value()) + lambda: self.on_move_relative_button_clicked(dev.samy, -self.spinBox_step.value()) ) self.checkBox_enableArrows.stateChanged.connect(self.update_arrow_key_shortcuts) @@ -80,11 +129,6 @@ class MotorApp(QWidget): ) # SpinBoxes - Motor Limits #TODO make spinboxes own limits updated, currently is [-1000, 1000] - # manually set limits before readout #TODO will be removed when spinboxes have own limits - # dev.samx.low_limit = -100 - # dev.samx.high_limit = 100 - # dev.samy.low_limit = -100 - # dev.samy.high_limit = 100 # display initial limits self.spinBox_x_min.setValue(self.limit_x[0]) @@ -110,10 +154,14 @@ class MotorApp(QWidget): self.motor_update.connect(lambda: self.update_image_map(*self.get_xy())) self.motor_position.connect(lambda x: print(f"motor position updated: {x}")) - self.motor_update.connect( - lambda: self.label_status.setText( - f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})" - ) + # self.motor_thread.coordinates_updated.connect( + # lambda: self.label_status.setText( + # f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})" + # ) + # ) + + self.motor_thread.coordinates_updated.connect( + lambda x, y: self.update_image_map(int(x), int(y)) ) # Coordinates table @@ -157,10 +205,10 @@ class MotorApp(QWidget): """Update the image map with the new motor position""" # Define the dimming factor - dimming_factor = 0.80 + dimming_factor = 0.95 # Apply the dimming factor only to pixels above the background value - self.image_map_data[self.image_map_data > self.background_value] *= dimming_factor + self.image_map_data[self.image_map_data > 50] *= dimming_factor # Mapping of motor coordinates to pixel coordinates pixel_x = int(x - self.offset_x) @@ -223,29 +271,37 @@ class MotorApp(QWidget): self.init_motor_map() # reinitialize the map with the new limits def move_motor(self, motor, value: float) -> None: - try: - status = scans.mv(motor, value, relative=True) + self.current_x, self.current_y = self.get_xy() + scans.mv(motor, value, relative=True) - while status.status not in ["COMPLETED", "STOPPED"]: - print(status.status) - motor_position = motor.position() - self.motor_position.emit(motor_position) + # def move_motor(self, motor, value: float) -> None: + # # self.current_x, self.current_y = self.get_xy() + # scans.mv(motor, value, relative=True) + # try: + # status = scans.mv(motor, value, relative=True) + # + # # while status.status not in ["COMPLETED", "STOPPED"]: + # # print(status.status) + # # print(f"motor read{motor.read(cached=True)}") + # # motor_position = motor.position() + # # self.motor_position.emit(motor_position) + # + # # return status + # except Exception as e: + # print(e) - # return status - except Exception as e: - print(e) + # motor_position = motor.position() - motor_position = motor.position() - - self.motor_position.emit(motor_position) - self.motor_update.emit() + # self.motor_position.emit(motor_position) + # self.motor_update.emit() def move_motor_coordinates(self, coor: list) -> None: + scans.mv(dev.samx, coor[0], dev.samy, coor[1], relative=False) try: status = scans.mv(dev.samx, coor[0], dev.samy, coor[1], relative=False) - while status.status not in ["COMPLETED", "STOPPED"]: - print(status.status) + # while status.status not in ["COMPLETED", "STOPPED"]: + # print(status.status) except Exception as e: print(e) @@ -313,19 +369,137 @@ class MotorApp(QWidget): table.resizeColumnsToContents() + # @staticmethod + # def _device_status_callback(msg, *, parent, **_kwargs) -> None: + # deviceMSG = BECMessage.DeviceMessage.loads(msg.value) + # # current_coordinates = (deviceMSG.content['signals']['samx']['value'],deviceMSG.content['signals']['samy']['value']) + # # print(f'current coordinates: {current_coordinates}') + # # parent.scan_storage.update_with_scan_status(scan) + # print(deviceMSG) + + # print(deviceMSG.metadata) + # parent.motor_update.emit(deviceMSG) + + # @staticmethod + # def _device_status_callback_samx(msg, *, parent, **_kwargs) -> None: + # deviceMSG = BECMessage.DeviceMessage.loads(msg.value) + # x = deviceMSG.content["signals"]["samx"]["value"] + # print(f"x: {x}") + # # Assuming you have a way to retrieve or store the current samy value + # y = parent.current_y + # parent.coordinates_updated.emit(x, y) + # + # @staticmethod + # def _device_status_callback_samy(msg, *, parent, **_kwargs) -> None: + # deviceMSG = BECMessage.DeviceMessage.loads(msg.value) + # y = deviceMSG.content["signals"]["samy"]["value"] + # print(f"y: {y}") + # # # Assuming you have a way to retrieve or store the current samx value + # x = parent.current_x + # parent.coordinates_updated.emit(x, y) + + +class MotorMovement(QThread): + coordinates_updated = pyqtSignal(float, float) # Signal to emit current coordinates + + # progress_updated = pyqtSignal(int) # Signal to emit progress percentage + + def __init__(self, parent=None): + super().__init__(parent) + # self.initial_coordinates = None + self.target_coordinates = None + self.running = False + + # Initialize current coordinates with the provided initial coordinates + self.current_x, self.current_y = None, None + + # Create consumer for samx and samy + self.samx_consumer = client.connector.consumer( + topics=[MessageEndpoints.device_readback("samx")], + cb=self._device_status_callback_samx, + parent=self, + ) + + self.samy_consumer = client.connector.consumer( + topics=[MessageEndpoints.device_readback("samy")], + cb=self._device_status_callback_samy, + parent=self, + ) + + self.samx_consumer.start() + self.samy_consumer.start() + + self.get_xy() + + def move_to_coordinates(self, target_coordinates: tuple): + self.action = "move_to_coordinates" + self.target_coordinates = target_coordinates + self.start() + + def move_relative(self, motor, value: float): + self.action = "move_relative" + self.motor = motor + self.value = value + self.start() + + def run(self): + if self.action == "move_to_coordinates": + self.move_motor_coordinate() + elif self.action == "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 get_xy(self) -> tuple: + """Get current motor position""" + self.current_x = dev.samx.position() + self.current_y = dev.samy.position() + + def move_motor_coordinate(self): + """Move the motor to the specified coordinates""" + self.get_xy() + scans.mv( + dev.samx, + self.target_coordinates[0], + dev.samy, + self.target_coordinates[1], + relative=False, + ) + + def move_motor_relative(self, motor, value: float) -> None: + self.get_xy() + scans.mv(motor, value, relative=True) + + @staticmethod + def _device_status_callback_samx(msg, *, parent, **_kwargs) -> None: + deviceMSG = BECMessage.DeviceMessage.loads(msg.value) + parent.current_x = deviceMSG.content["signals"]["samx"]["value"] + print(f"samx moving: {parent.current_x,parent.current_y}") + # y = parent.current_y + parent.coordinates_updated.emit(parent.current_x, parent.current_y) + + @staticmethod + def _device_status_callback_samy(msg, *, parent, **_kwargs) -> None: + deviceMSG = BECMessage.DeviceMessage.loads(msg.value) + parent.current_y = deviceMSG.content["signals"]["samy"][ + "value" + ] # TODO can be move to parent instance + print(f"samy moving: {parent.current_x,parent.current_y}") + # x = parent.current_y + parent.coordinates_updated.emit(parent.current_x, parent.current_y) + if __name__ == "__main__": - from bec_widgets.bec_dispatcher import bec_dispatcher + from bec_lib import BECClient - # from bec_lib import BECClient # from bec_lib.core import ServiceConfig,RedisConnector - # client = BECClient() - # client.initialize(config=ServiceConfig(config_path="test_config.yaml")) + client = BECClient() # client.initialize(config=ServiceConfig(config_path="test_config.yaml")) # Client initialization - by dispatcher - client = bec_dispatcher.client + # client = bec_dispatcher.client client.start() dev = client.device_manager.devices scans = client.scans From 11aa15fefda7433e885cc8586f93c97af83b0c48 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Tue, 22 Aug 2023 18:55:55 +0200 Subject: [PATCH 10/29] fix: motor movement absolute fixed - movement by thread --- .../examples/motor_movement/motor_example.py | 144 +++--------------- 1 file changed, 17 insertions(+), 127 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index c7b49a6c..00e265bb 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -25,6 +25,7 @@ class MotorApp(QWidget): # UI self.start_x, self.start_y = self.get_xy() self.current_x, self.current_y = self.get_xy() + self.limit_x, self.limit_y = self.get_motor_limits(dev.samx), self.get_motor_limits( dev.samy ) @@ -35,40 +36,22 @@ class MotorApp(QWidget): f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})" ) - # self.samx_consumer = client.connector.consumer(topics=[MessageEndpoints.device_readback('samx'), - # MessageEndpoints.device_readback('samy')], - # cb=self._device_status_callback,parent=self) - # self.samx_consumer.start() - - # self.samx_consumer = client.connector.consumer( - # topics=[MessageEndpoints.device_readback("samx")], - # cb=self._device_status_callback_samx, - # parent=self, - # ) - # - # self.samy_consumer = client.connector.consumer( - # topics=[MessageEndpoints.device_readback("samy")], - # cb=self._device_status_callback_samy, - # parent=self, - # ) - # self.samx_consumer.start() - # self.samy_consumer.start() - self.motor_thread = MotorMovement() self.init_ui() - def on_move_to_coordinates_button_clicked(self): - x = self.spinBox_absolute_x.value() - y = self.spinBox_absolute_y.value() + def move_motor_absolute(self, x, y): + # x = self.spinBox_absolute_x.value() + # y = self.spinBox_absolute_y.value() target_coordinates = (x, y) self.motor_thread.move_to_coordinates(target_coordinates) - def on_move_relative_button_clicked(self, motor, value: float): + def move_motor_relative(self, motor, value: float): self.motor_thread.move_relative(motor, value) def init_ui(self): """Setup all ui elements""" + ########################## # 2D Plot ########################## @@ -80,42 +63,21 @@ class MotorApp(QWidget): self.image_map = pg.ImageItem() self.plot_map.addItem(self.image_map) - # self.image_map_data = np.zeros((100, 100), dtype=np.float32) - # x, y = self.get_xy() # Assuming get_xy returns the motor position - # self.update_image_map(x, y) - # self.image_map.setImage(self.image_map_data.T) # Set the image - - # self.init_motor_map() - ########################## # Signals ########################## - # Buttons - Motor Movement - # self.toolButton_right.clicked.connect( - # lambda: self.move_motor(dev.samx, self.spinBox_step.value()) - # ) - # self.toolButton_left.clicked.connect( - # lambda: self.move_motor(dev.samx, -self.spinBox_step.value()) - # ) - # self.toolButton_up.clicked.connect( - # lambda: self.move_motor(dev.samy, self.spinBox_step.value()) - # ) - # self.toolButton_down.clicked.connect( - # lambda: self.move_motor(dev.samy, -self.spinBox_step.value()) - # ) - self.toolButton_right.clicked.connect( - lambda: self.on_move_relative_button_clicked(dev.samx, self.spinBox_step.value()) + lambda: self.move_motor_relative(dev.samx, self.spinBox_step.value()) ) self.toolButton_left.clicked.connect( - lambda: self.on_move_relative_button_clicked(dev.samx, -self.spinBox_step.value()) + lambda: self.move_motor_relative(dev.samx, -self.spinBox_step.value()) ) self.toolButton_up.clicked.connect( - lambda: self.on_move_relative_button_clicked(dev.samy, self.spinBox_step.value()) + lambda: self.move_motor_relative(dev.samy, self.spinBox_step.value()) ) self.toolButton_down.clicked.connect( - lambda: self.on_move_relative_button_clicked(dev.samy, -self.spinBox_step.value()) + lambda: self.move_motor_relative(dev.samy, -self.spinBox_step.value()) ) self.checkBox_enableArrows.stateChanged.connect(self.update_arrow_key_shortcuts) @@ -123,8 +85,8 @@ class MotorApp(QWidget): # Move to absolute coordinates self.pushButton_go_absolute.clicked.connect( - lambda: self.move_motor_coordinates( - [self.spinBox_absolute_x.value(), self.spinBox_absolute_y.value()] + lambda: self.move_motor_absolute( + self.spinBox_absolute_x.value(), self.spinBox_absolute_y.value() ) ) @@ -270,54 +232,14 @@ class MotorApp(QWidget): self.init_motor_map() # reinitialize the map with the new limits - def move_motor(self, motor, value: float) -> None: - self.current_x, self.current_y = self.get_xy() - scans.mv(motor, value, relative=True) - - # def move_motor(self, motor, value: float) -> None: - # # self.current_x, self.current_y = self.get_xy() - # scans.mv(motor, value, relative=True) - # try: - # status = scans.mv(motor, value, relative=True) - # - # # while status.status not in ["COMPLETED", "STOPPED"]: - # # print(status.status) - # # print(f"motor read{motor.read(cached=True)}") - # # motor_position = motor.position() - # # self.motor_position.emit(motor_position) - # - # # return status - # except Exception as e: - # print(e) - - # motor_position = motor.position() - - # self.motor_position.emit(motor_position) - # self.motor_update.emit() - - def move_motor_coordinates(self, coor: list) -> None: - scans.mv(dev.samx, coor[0], dev.samy, coor[1], relative=False) - try: - status = scans.mv(dev.samx, coor[0], dev.samy, coor[1], relative=False) - - # while status.status not in ["COMPLETED", "STOPPED"]: - # print(status.status) - - except Exception as e: - print(e) - - # motor_position = self.get_xy() - # self.motor_position_xy.emit(motor_position) - - self.motor_update.emit() - def get_xy(self): # TODO decide if useful """Get current motor position""" x = dev.samx.position() y = dev.samy.position() return x, y - def param_changed(self, ui_element): + @staticmethod + def param_changed(ui_element): ui_element.setStyleSheet("background-color: #FFA700;") def update_arrow_key_shortcuts(self): @@ -359,45 +281,13 @@ class MotorApp(QWidget): # hook signals of table button.clicked.connect( - lambda: self.move_motor_coordinates( - [ - float(table.item(current_row_count, 2).text()), - float(table.item(current_row_count, 3).text()), - ] + lambda: self.move_motor_absolute( + float(table.item(current_row_count, 2).text()), + float(table.item(current_row_count, 3).text()), ) ) - table.resizeColumnsToContents() - # @staticmethod - # def _device_status_callback(msg, *, parent, **_kwargs) -> None: - # deviceMSG = BECMessage.DeviceMessage.loads(msg.value) - # # current_coordinates = (deviceMSG.content['signals']['samx']['value'],deviceMSG.content['signals']['samy']['value']) - # # print(f'current coordinates: {current_coordinates}') - # # parent.scan_storage.update_with_scan_status(scan) - # print(deviceMSG) - - # print(deviceMSG.metadata) - # parent.motor_update.emit(deviceMSG) - - # @staticmethod - # def _device_status_callback_samx(msg, *, parent, **_kwargs) -> None: - # deviceMSG = BECMessage.DeviceMessage.loads(msg.value) - # x = deviceMSG.content["signals"]["samx"]["value"] - # print(f"x: {x}") - # # Assuming you have a way to retrieve or store the current samy value - # y = parent.current_y - # parent.coordinates_updated.emit(x, y) - # - # @staticmethod - # def _device_status_callback_samy(msg, *, parent, **_kwargs) -> None: - # deviceMSG = BECMessage.DeviceMessage.loads(msg.value) - # y = deviceMSG.content["signals"]["samy"]["value"] - # print(f"y: {y}") - # # # Assuming you have a way to retrieve or store the current samx value - # x = parent.current_x - # parent.coordinates_updated.emit(x, y) - class MotorMovement(QThread): coordinates_updated = pyqtSignal(float, float) # Signal to emit current coordinates From 349c06bcac6c61c96681a00dd846741898dedf30 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Wed, 23 Aug 2023 12:57:48 +0200 Subject: [PATCH 11/29] refactor: getting motor limits and coordinates moved to MotorControl(QThread) --- .../motor_movement/motor_controller.ui | 2 +- .../examples/motor_movement/motor_example.py | 220 +++++++++--------- 2 files changed, 109 insertions(+), 113 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index 47521271..9bda1f41 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -260,7 +260,7 @@ - - samx + + samx diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index 00e265bb..3e034194 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -3,7 +3,7 @@ import os import numpy as np import pyqtgraph as pg from PyQt5 import QtGui -from PyQt5.QtCore import QThread +from PyQt5.QtCore import QThread, pyqtSlot from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtWidgets import QApplication, QWidget from pyqtgraph.Qt import QtWidgets, uic @@ -12,9 +12,6 @@ from bec_lib.core import MessageEndpoints, BECMessage class MotorApp(QWidget): - motor_position = pyqtSignal(float) # TODO hook to motor position update - motor_update = pyqtSignal() - motor_position_xy = pyqtSignal(list) coordinates_updated = pyqtSignal(float, float) def __init__(self): @@ -22,34 +19,51 @@ class MotorApp(QWidget): current_path = os.path.dirname(__file__) uic.loadUi(os.path.join(current_path, "motor_controller.ui"), self) - # UI - self.start_x, self.start_y = self.get_xy() - self.current_x, self.current_y = self.get_xy() + self.limit_x, self.limit_y = None, None - self.limit_x, self.limit_y = self.get_motor_limits(dev.samx), self.get_motor_limits( - dev.samy - ) + # QThread for motor movement + signals + self.motor_thread = MotorControl() + self.motor_thread.limits_retrieved.connect(self.update_limits) + + # UI + self.init_ui() + + # Initialize current coordinates with the provided initial coordinates + self.motor_thread.retrieve_motor_limits(dev.samx, dev.samy) print(f"Init limits: samx:{self.limit_x}, samy:{self.limit_y}") self.label_status.setText( f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})" - ) + ) # TODO has to be change to get positions from thread not directly from device - self.motor_thread = MotorMovement() + @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]) - self.init_ui() + for spinBox in ( + self.spinBox_x_min, + self.spinBox_x_max, + self.spinBox_y_min, + self.spinBox_y_max, + ): + spinBox.setStyleSheet("") - def move_motor_absolute(self, x, y): - # x = self.spinBox_absolute_x.value() - # y = self.spinBox_absolute_y.value() + self.init_motor_map() # reinitialize the map with the new limits + + def move_motor_absolute(self, x: float, y: float) -> None: target_coordinates = (x, y) self.motor_thread.move_to_coordinates(target_coordinates) - def move_motor_relative(self, motor, value: float): + def move_motor_relative(self, motor, value: float) -> None: self.motor_thread.move_relative(motor, value) - def init_ui(self): + def init_ui(self) -> None: """Setup all ui elements""" ########################## @@ -80,6 +94,7 @@ class MotorApp(QWidget): lambda: self.move_motor_relative(dev.samy, -self.spinBox_step.value()) ) + # Switch between key shortcuts active self.checkBox_enableArrows.stateChanged.connect(self.update_arrow_key_shortcuts) self.update_arrow_key_shortcuts() @@ -92,12 +107,6 @@ class MotorApp(QWidget): # SpinBoxes - Motor Limits #TODO make spinboxes own limits updated, currently is [-1000, 1000] - # display initial 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]) - # 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)) @@ -111,28 +120,19 @@ class MotorApp(QWidget): ) ) - # Map - self.motor_position.connect(lambda x: self.update_image_map(*self.get_xy())) - self.motor_update.connect(lambda: self.update_image_map(*self.get_xy())) - - self.motor_position.connect(lambda x: print(f"motor position updated: {x}")) - # self.motor_thread.coordinates_updated.connect( - # lambda: self.label_status.setText( - # f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})" - # ) - # ) - + # 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(int(x), int(y)) ) # Coordinates table self.generate_table_coordinate( - self.tableWidget_coordinates, self.get_xy(), tag="Initial", precision=0 + self.tableWidget_coordinates, + self.motor_thread.retrieve_coordinates(), + tag="Initial", + precision=0, ) - self.init_motor_map() - def init_motor_map(self): # Get motor limits limit_x_min, limit_x_max = self.get_motor_limits(dev.samx) @@ -152,8 +152,7 @@ class MotorApp(QWidget): ) # Set the initial position on the map - x, y = self.get_xy() - self.prev_x, self.prev_y = x, y + x, y = self.motor_thread.retrieve_coordinates() self.update_image_map(x, y) # Translate and scale the image item to match the motor coordinates @@ -189,58 +188,10 @@ class MotorApp(QWidget): return low_limit, high_limit - def update_motor_limits( - self, motor, low_limit=None, high_limit=None - ): # TODO limits cannot be smaller that the current location of motor - # Get current limits - current_low_limit, current_high_limit = motor.limits[0], motor.limits[1] - - # 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 - - # self.init_motor_map() # reinitialize the map with the new limits - - def update_all_motor_limits(self, x_limit: list = None, y_limit: list = None) -> None: - # check if current motor position if within the new limits - - current_position = self.get_xy() - - 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(dev.samx, 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(dev.samy, low_limit=y_limit[0], high_limit=y_limit[1]) - - for spinBox in ( - self.spinBox_x_min, - self.spinBox_x_max, - self.spinBox_y_min, - self.spinBox_y_max, - ): - spinBox.setStyleSheet("") - - self.init_motor_map() # reinitialize the map with the new limits - - def get_xy(self): # TODO decide if useful - """Get current motor position""" - x = dev.samx.position() - y = dev.samy.position() - return x, y - - @staticmethod - def param_changed(ui_element): - ui_element.setStyleSheet("background-color: #FFA700;") + def update_all_motor_limits( + self, x_limit: list = None, y_limit: list = None + ) -> None: # TODO will be moved to thread + self.motor_thread.update_all_motor_limits(x_limit=x_limit, y_limit=y_limit) def update_arrow_key_shortcuts(self): if self.checkBox_enableArrows.isChecked(): @@ -257,7 +208,7 @@ class MotorApp(QWidget): self.toolButton_down.setShortcut("") def generate_table_coordinate( - self, table: QtWidgets.QTableWidget, coordinates: list, tag: str = None, precision: int = 0 + self, table: QtWidgets.QTableWidget, coordinates: tuple, tag: str = None, precision: int = 0 ) -> None: current_row_count = table.rowCount() @@ -288,20 +239,26 @@ class MotorApp(QWidget): ) table.resizeColumnsToContents() + @staticmethod + def param_changed(ui_element): + ui_element.setStyleSheet("background-color: #FFA700;") -class MotorMovement(QThread): + +class MotorControl(QThread): coordinates_updated = pyqtSignal(float, float) # Signal to emit current coordinates - - # progress_updated = pyqtSignal(int) # Signal to emit progress percentage + limits_retrieved = pyqtSignal(list, list) # Signal to emit current limits (samx, samy) + # progress_updated = pyqtSignal(int) #TODO Signal to emit progress percentage def __init__(self, parent=None): super().__init__(parent) - # self.initial_coordinates = None self.target_coordinates = None self.running = False # Initialize current coordinates with the provided initial coordinates - self.current_x, self.current_y = None, None + ( + self.current_x, + self.current_y, + ) = self.get_coordinates() # TODO maybe not needed, coordinates always stored in redis # Create consumer for samx and samy self.samx_consumer = client.connector.consumer( @@ -319,7 +276,56 @@ class MotorMovement(QThread): self.samx_consumer.start() self.samy_consumer.start() - self.get_xy() + def get_coordinates(self) -> tuple: + # TODO decide if the get_coordinates and retrieve coordinates makes sense + # TODO or move also to signal/slot as in the case of limits + # TODO or move just to redis CB function + """Get current motor position""" + x = dev.samx.position() + y = dev.samy.position() + 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 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: + # TODO can be generalized to any motor not just samx samy + 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(dev.samx, 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(dev.samy, low_limit=y_limit[0], high_limit=y_limit[1]) + + self.retrieve_motor_limits(dev.samx, dev.samy) def move_to_coordinates(self, target_coordinates: tuple): self.action = "move_to_coordinates" @@ -341,14 +347,8 @@ class MotorMovement(QThread): def set_target_coordinates(self, target_coordinates: tuple) -> None: self.target_coordinates = target_coordinates - def get_xy(self) -> tuple: - """Get current motor position""" - self.current_x = dev.samx.position() - self.current_y = dev.samy.position() - - def move_motor_coordinate(self): + def move_motor_coordinate(self) -> None: """Move the motor to the specified coordinates""" - self.get_xy() scans.mv( dev.samx, self.target_coordinates[0], @@ -358,25 +358,21 @@ class MotorMovement(QThread): ) def move_motor_relative(self, motor, value: float) -> None: - self.get_xy() scans.mv(motor, value, relative=True) + # TODO decide if one or multiple callback functions @staticmethod def _device_status_callback_samx(msg, *, parent, **_kwargs) -> None: deviceMSG = BECMessage.DeviceMessage.loads(msg.value) parent.current_x = deviceMSG.content["signals"]["samx"]["value"] print(f"samx moving: {parent.current_x,parent.current_y}") - # y = parent.current_y parent.coordinates_updated.emit(parent.current_x, parent.current_y) @staticmethod def _device_status_callback_samy(msg, *, parent, **_kwargs) -> None: deviceMSG = BECMessage.DeviceMessage.loads(msg.value) - parent.current_y = deviceMSG.content["signals"]["samy"][ - "value" - ] # TODO can be move to parent instance + parent.current_y = deviceMSG.content["signals"]["samy"]["value"] print(f"samy moving: {parent.current_x,parent.current_y}") - # x = parent.current_y parent.coordinates_updated.emit(parent.current_x, parent.current_y) From 95ead7117e59e0979aec51b85b49537ab728cad4 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Wed, 23 Aug 2023 13:04:23 +0200 Subject: [PATCH 12/29] fix: init_motor_map receive motor position from motor_thread --- .../examples/motor_movement/motor_example.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index 3e034194..a946e8bd 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -135,8 +135,8 @@ class MotorApp(QWidget): def init_motor_map(self): # Get motor limits - limit_x_min, limit_x_max = self.get_motor_limits(dev.samx) - limit_y_min, limit_y_max = self.get_motor_limits(dev.samy) + limit_x_min, limit_x_max = self.motor_thread.get_motor_limits(dev.samx) + limit_y_min, limit_y_max = self.motor_thread.get_motor_limits(dev.samy) self.offset_x = limit_x_min self.offset_y = limit_y_min @@ -181,12 +181,12 @@ class MotorApp(QWidget): # Update the display self.image_map.updateImage(self.image_map_data, levels=(0, 255)) - def get_motor_limits(self, motor): - """Get the limits of a motor""" - high_limit = motor.high_limit - low_limit = motor.low_limit - - return low_limit, high_limit + # def get_motor_limits(self, motor): + # """Get the limits of a motor""" + # high_limit = motor.high_limit + # low_limit = motor.low_limit + # + # return low_limit, high_limit def update_all_motor_limits( self, x_limit: list = None, y_limit: list = None From 8be98c9bb6af941a69c593c62d5c52339d2262bc Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Wed, 23 Aug 2023 13:17:35 +0200 Subject: [PATCH 13/29] feat: going to absolute coordinates saves coordinate in the table for later use with tag --- .../examples/motor_movement/motor_example.py | 19 ++++++++++++------- .../examples/plotting/crosshair_example.py | 2 -- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index a946e8bd..a8dcc571 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -27,6 +27,7 @@ class MotorApp(QWidget): # UI self.init_ui() + self.tag_N = 1 # position label for saved coordinates # Initialize current coordinates with the provided initial coordinates self.motor_thread.retrieve_motor_limits(dev.samx, dev.samy) @@ -104,6 +105,7 @@ class MotorApp(QWidget): self.spinBox_absolute_x.value(), self.spinBox_absolute_y.value() ) ) + self.pushButton_go_absolute.clicked.connect(self.save_absolute_coordinates) # SpinBoxes - Motor Limits #TODO make spinboxes own limits updated, currently is [-1000, 1000] @@ -181,13 +183,6 @@ class MotorApp(QWidget): # Update the display self.image_map.updateImage(self.image_map_data, levels=(0, 255)) - # def get_motor_limits(self, motor): - # """Get the limits of a motor""" - # high_limit = motor.high_limit - # low_limit = motor.low_limit - # - # return low_limit, high_limit - def update_all_motor_limits( self, x_limit: list = None, y_limit: list = None ) -> None: # TODO will be moved to thread @@ -239,6 +234,16 @@ class MotorApp(QWidget): ) table.resizeColumnsToContents() + 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;") diff --git a/bec_widgets/examples/plotting/crosshair_example.py b/bec_widgets/examples/plotting/crosshair_example.py index 5aed65aa..f573ad69 100644 --- a/bec_widgets/examples/plotting/crosshair_example.py +++ b/bec_widgets/examples/plotting/crosshair_example.py @@ -15,10 +15,8 @@ from pyqtgraph.Qt import QtCore from bec_widgets.qt_utils import Crosshair - class ExampleApp(QWidget): def __init__(self): - """Example application for using the Crosshair class""" super().__init__() # Layout From 3f6d5c66411459703c402f7449e8b1abae9a2b08 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Wed, 23 Aug 2023 16:48:23 +0200 Subject: [PATCH 14/29] feat: motor coordinates are now scatter instead of image --- .../examples/motor_movement/motor_example.py | 111 ++++++++++++++---- 1 file changed, 86 insertions(+), 25 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index a8dcc571..e9e2670f 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -20,6 +20,8 @@ class MotorApp(QWidget): uic.loadUi(os.path.join(current_path, "motor_controller.ui"), self) self.limit_x, self.limit_y = None, None + # Coordinates tracking + self.motor_positions = [] # QThread for motor movement + signals self.motor_thread = MotorControl() @@ -38,6 +40,13 @@ class MotorApp(QWidget): f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})" ) # TODO has to be change to get positions from thread not directly from device + # self.background_map_scale = 10 + # self.visited_coordinates = {} + # self.precision = 2 # Define the decimal precision + + # Initialize the image map + self.init_motor_map() + @pyqtSlot(list, list) def update_limits(self, x_limits: list, y_limits: list) -> None: self.limit_x = x_limits @@ -75,8 +84,12 @@ class MotorApp(QWidget): f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})", row=0, col=0 ) self.plot_map = self.glw.addPlot(row=1, col=0) - self.image_map = pg.ImageItem() - self.plot_map.addItem(self.image_map) + self.limit_map = pg.ImageItem() + self.plot_map.addItem(self.limit_map) + self.motor_map = pg.ScatterPlotItem( + size=2, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 255) + ) + self.plot_map.addItem(self.motor_map) ########################## # Signals @@ -124,7 +137,7 @@ class MotorApp(QWidget): # 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(int(x), int(y)) + lambda x, y: self.update_image_map(round(x, 2), round(y, 2)) ) # Coordinates table @@ -149,39 +162,87 @@ class MotorApp(QWidget): # Create an empty image map self.background_value = 15 - self.image_map_data = np.full( + 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() + init_brush = pg.mkBrush(255, 255, 255, 255) + self.motor_positions.append({"pos": init_pos, "brush": init_brush}) - # Set the initial position on the map - x, y = self.motor_thread.retrieve_coordinates() - self.update_image_map(x, y) + self.motor_map.setData( + pos=[point["pos"] for point in self.motor_positions], + brush=[point["brush"] for point in self.motor_positions], + ) + # self.update_image_map(x, y) + # # 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.image_map.setTransform(self.tr) - - self.image_map.dataTransform() + self.limit_map.setTransform(self.tr) + # + # self.image_map.dataTransform() def update_image_map(self, x, y): - """Update the image map with the new motor position""" + # Dim previous points + # for point in self.motor_positions.values(): + # point["brush"] = pg.mkBrush( + # 255, 255, 255, max(50, point["brush"].color().alpha() * 0.95) + # ) - # Define the dimming factor - dimming_factor = 0.95 + # Add new point with full brightness + new_pos = (x, y) + new_brush = pg.mkBrush(255, 255, 255, 255) + self.motor_positions.append({"pos": new_pos, "brush": new_brush}) + self.motor_map.setData( + pos=[point["pos"] for point in self.motor_positions], + brush=[point["brush"] for point in self.motor_positions], + ) - # Apply the dimming factor only to pixels above the background value - self.image_map_data[self.image_map_data > 50] *= dimming_factor + # new_point = {"pos": key, "brush": pg.mkBrush(255, 255, 255, 255)} + # self.motor_positions[key] = new_point + # + # Update ScatterPlotItem + # positions = np.array([point["pos"] for point in self.motor_positions.values()]) + # brushes = [point["brush"] for point in self.motor_positions.values()] + # self.scatter_plot_item.setData(pos=positions, brush=brushes) - # Mapping of motor coordinates to pixel coordinates - pixel_x = int(x - self.offset_x) - pixel_y = int(y - self.offset_y) + # # Dim previous points + # for point in self.motor_positions.values(): + # point["brush"] = pg.mkBrush( + # 255, 255, 255, max(50, point["brush"].color().alpha() * 0.95) + # ) + # + # # Add new point with full brightness + # key = (x, y) + # new_point = {"pos": key, "brush": pg.mkBrush(255, 255, 255, 255)} + # self.motor_positions[key] = new_point + # + # # Update ScatterPlotItem + # positions = [point["pos"] for point in self.motor_positions.values()] + # brushes = [point["brush"] for point in self.motor_positions.values()] + # self.motor_map.setData(positions[0], positions[1], brush=brushes) - # Set the bright pixel at the new position - self.image_map_data[pixel_x, pixel_y] = 255 - - # Update the display - self.image_map.updateImage(self.image_map_data, levels=(0, 255)) + # def update_image_map(self, x, y): + # """Update the image map with the new motor position""" + # + # # Define the dimming factor + # dimming_factor = 0.95 + # + # # Apply the dimming factor only to pixels above the background value + # self.image_map_data[self.image_map_data > 50] *= dimming_factor + # + # # Mapping of motor coordinates to pixel coordinates + # pixel_x = int(x - self.offset_x) + # pixel_y = int(y - self.offset_y) + # + # # Set the bright pixel at the new position + # self.image_map_data[pixel_x, pixel_y] = 255 + # + # # Update the display + # self.image_map.updateImage(self.image_map_data, levels=(0, 255)) def update_all_motor_limits( self, x_limit: list = None, y_limit: list = None @@ -370,14 +431,14 @@ class MotorControl(QThread): def _device_status_callback_samx(msg, *, parent, **_kwargs) -> None: deviceMSG = BECMessage.DeviceMessage.loads(msg.value) parent.current_x = deviceMSG.content["signals"]["samx"]["value"] - print(f"samx moving: {parent.current_x,parent.current_y}") + # print(f"samx moving: {parent.current_x,parent.current_y}") parent.coordinates_updated.emit(parent.current_x, parent.current_y) @staticmethod def _device_status_callback_samy(msg, *, parent, **_kwargs) -> None: deviceMSG = BECMessage.DeviceMessage.loads(msg.value) parent.current_y = deviceMSG.content["signals"]["samy"]["value"] - print(f"samy moving: {parent.current_x,parent.current_y}") + # print(f"samy moving: {parent.current_x,parent.current_y}") parent.coordinates_updated.emit(parent.current_x, parent.current_y) From ed842931971fbf87ed2f3e366eb822531ef5aacc Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Wed, 23 Aug 2023 17:21:44 +0200 Subject: [PATCH 15/29] feat: controls are disabled while motor is moving and enabled when motor movement is finished --- .../examples/motor_movement/motor_example.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index e9e2670f..fa4c38b8 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -66,11 +66,25 @@ class MotorApp(QWidget): self.init_motor_map() # reinitialize the map with the new limits + @pyqtSlot() + def enable_motor_control(self): + self.motorControl.setEnabled(True) + + def enable_motor_controls(self, disable: bool) -> None: + # Disable or enable all controls within the motorControl group box + for widget in self.motorControl.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 init_ui(self) -> None: @@ -120,6 +134,8 @@ class MotorApp(QWidget): ) self.pushButton_go_absolute.clicked.connect(self.save_absolute_coordinates) + self.motor_thread.move_finished.connect(lambda: self.enable_motor_controls(True)) + # 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 @@ -313,6 +329,7 @@ class MotorApp(QWidget): class MotorControl(QThread): coordinates_updated = pyqtSignal(float, float) # Signal to emit current coordinates limits_retrieved = pyqtSignal(list, list) # Signal to emit current limits (samx, samy) + move_finished = pyqtSignal() # Signal to emit when the move is finished # progress_updated = pyqtSignal(int) #TODO Signal to emit progress percentage def __init__(self, parent=None): @@ -433,6 +450,8 @@ class MotorControl(QThread): parent.current_x = deviceMSG.content["signals"]["samx"]["value"] # print(f"samx moving: {parent.current_x,parent.current_y}") parent.coordinates_updated.emit(parent.current_x, parent.current_y) + if deviceMSG.content["signals"]["samx_motor_is_moving"]["value"] == 0: + parent.move_finished.emit() @staticmethod def _device_status_callback_samy(msg, *, parent, **_kwargs) -> None: @@ -440,6 +459,8 @@ class MotorControl(QThread): parent.current_y = deviceMSG.content["signals"]["samy"]["value"] # print(f"samy moving: {parent.current_x,parent.current_y}") parent.coordinates_updated.emit(parent.current_x, parent.current_y) + if deviceMSG.content["signals"]["samy_motor_is_moving"]["value"] == 0: + parent.move_finished.emit() if __name__ == "__main__": From 187c748e87264448d5026d9fa2f15b5fc9a55949 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Thu, 24 Aug 2023 17:22:21 +0200 Subject: [PATCH 16/29] feat: stop movement function, one callback function for 2 motors, move_finished is emitted in move_motor function not in callback --- .../motor_movement/motor_controller.ui | 10 -- .../examples/motor_movement/motor_example.py | 126 ++++++------------ 2 files changed, 41 insertions(+), 95 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index 9bda1f41..cded821d 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -16,16 +16,6 @@ - - - - Motor Position - - - Qt::AlignCenter - - - diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index fa4c38b8..a8c2252f 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -36,10 +36,6 @@ class MotorApp(QWidget): print(f"Init limits: samx:{self.limit_x}, samy:{self.limit_y}") - self.label_status.setText( - f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})" - ) # TODO has to be change to get positions from thread not directly from device - # self.background_map_scale = 10 # self.visited_coordinates = {} # self.precision = 2 # Define the decimal precision @@ -106,7 +102,7 @@ class MotorApp(QWidget): self.plot_map.addItem(self.motor_map) ########################## - # Signals + # Motor movements signals ########################## self.toolButton_right.clicked.connect( @@ -133,9 +129,15 @@ class MotorApp(QWidget): ) ) self.pushButton_go_absolute.clicked.connect(self.save_absolute_coordinates) - 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 limits signals + ########################## + # 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 @@ -192,21 +194,19 @@ class MotorApp(QWidget): brush=[point["brush"] for point in self.motor_positions], ) - # self.update_image_map(x, y) - # # 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) - # - # self.image_map.dataTransform() def update_image_map(self, x, y): + # Update label + self.label_coorditanes.setText(f"Motor position: ({x:.2f}, {y:.2f})") + # Dim previous points - # for point in self.motor_positions.values(): - # point["brush"] = pg.mkBrush( - # 255, 255, 255, max(50, point["brush"].color().alpha() * 0.95) - # ) + last_brush = self.motor_positions[-1]["brush"].color().getRgb() + dimmer_brush = tuple(max(int(0.8 * c), 50) for c in last_brush[:3]) + (255,) + self.motor_positions[-1]["brush"] = pg.mkBrush(dimmer_brush) # Add new point with full brightness new_pos = (x, y) @@ -217,49 +217,6 @@ class MotorApp(QWidget): brush=[point["brush"] for point in self.motor_positions], ) - # new_point = {"pos": key, "brush": pg.mkBrush(255, 255, 255, 255)} - # self.motor_positions[key] = new_point - # - # Update ScatterPlotItem - # positions = np.array([point["pos"] for point in self.motor_positions.values()]) - # brushes = [point["brush"] for point in self.motor_positions.values()] - # self.scatter_plot_item.setData(pos=positions, brush=brushes) - - # # Dim previous points - # for point in self.motor_positions.values(): - # point["brush"] = pg.mkBrush( - # 255, 255, 255, max(50, point["brush"].color().alpha() * 0.95) - # ) - # - # # Add new point with full brightness - # key = (x, y) - # new_point = {"pos": key, "brush": pg.mkBrush(255, 255, 255, 255)} - # self.motor_positions[key] = new_point - # - # # Update ScatterPlotItem - # positions = [point["pos"] for point in self.motor_positions.values()] - # brushes = [point["brush"] for point in self.motor_positions.values()] - # self.motor_map.setData(positions[0], positions[1], brush=brushes) - - # def update_image_map(self, x, y): - # """Update the image map with the new motor position""" - # - # # Define the dimming factor - # dimming_factor = 0.95 - # - # # Apply the dimming factor only to pixels above the background value - # self.image_map_data[self.image_map_data > 50] *= dimming_factor - # - # # Mapping of motor coordinates to pixel coordinates - # pixel_x = int(x - self.offset_x) - # pixel_y = int(y - self.offset_y) - # - # # Set the bright pixel at the new position - # self.image_map_data[pixel_x, pixel_y] = 255 - # - # # Update the display - # self.image_map.updateImage(self.image_map_data, levels=(0, 255)) - def update_all_motor_limits( self, x_limit: list = None, y_limit: list = None ) -> None: # TODO will be moved to thread @@ -343,27 +300,25 @@ class MotorControl(QThread): self.current_y, ) = self.get_coordinates() # TODO maybe not needed, coordinates always stored in redis - # Create consumer for samx and samy - self.samx_consumer = client.connector.consumer( - topics=[MessageEndpoints.device_readback("samx")], - cb=self._device_status_callback_samx, + self.active_devices = ["samx", "samy"] + + self.motors_consumer = client.connector.consumer( + topics=[ + MessageEndpoints.device_readback(self.active_devices[0]), + MessageEndpoints.device_readback(self.active_devices[1]), + ], + cb=self._device_status_callback_motors, parent=self, ) - self.samy_consumer = client.connector.consumer( - topics=[MessageEndpoints.device_readback("samy")], - cb=self._device_status_callback_samy, - parent=self, - ) - - self.samx_consumer.start() - self.samy_consumer.start() + self.motors_consumer.start() def get_coordinates(self) -> tuple: # TODO decide if the get_coordinates and retrieve coordinates makes sense # TODO or move also to signal/slot as in the case of limits # TODO or move just to redis CB function """Get current motor position""" + dev.samx.read(cached=True) # TODO to get last message ['value'] -> last position x = dev.samx.position() y = dev.samy.position() return x, y @@ -432,7 +387,7 @@ class MotorControl(QThread): def move_motor_coordinate(self) -> None: """Move the motor to the specified coordinates""" - scans.mv( + status = scans.mv( dev.samx, self.target_coordinates[0], dev.samy, @@ -440,27 +395,27 @@ class MotorControl(QThread): relative=False, ) + status.wait() + self.move_finished.emit() + def move_motor_relative(self, motor, value: float) -> None: - scans.mv(motor, value, relative=True) + status = scans.mv(motor, value, relative=True) - # TODO decide if one or multiple callback functions - @staticmethod - def _device_status_callback_samx(msg, *, parent, **_kwargs) -> None: - deviceMSG = BECMessage.DeviceMessage.loads(msg.value) - parent.current_x = deviceMSG.content["signals"]["samx"]["value"] - # print(f"samx moving: {parent.current_x,parent.current_y}") - parent.coordinates_updated.emit(parent.current_x, parent.current_y) - if deviceMSG.content["signals"]["samx_motor_is_moving"]["value"] == 0: - parent.move_finished.emit() + status.wait() + self.move_finished.emit() + + def stop_movement(self): + queue.request_scan_abortion() + queue.request_queue_reset() @staticmethod - def _device_status_callback_samy(msg, *, parent, **_kwargs) -> None: + def _device_status_callback_motors(msg, *, parent, **_kwargs) -> None: deviceMSG = BECMessage.DeviceMessage.loads(msg.value) - parent.current_y = deviceMSG.content["signals"]["samy"]["value"] - # print(f"samy moving: {parent.current_x,parent.current_y}") + if parent.active_devices[0] in deviceMSG.content["signals"]: + parent.current_x = deviceMSG.content["signals"][parent.active_devices[0]]["value"] + elif parent.active_devices[1] in deviceMSG.content["signals"]: + parent.current_y = deviceMSG.content["signals"][parent.active_devices[1]]["value"] parent.coordinates_updated.emit(parent.current_x, parent.current_y) - if deviceMSG.content["signals"]["samy_motor_is_moving"]["value"] == 0: - parent.move_finished.emit() if __name__ == "__main__": @@ -476,6 +431,7 @@ if __name__ == "__main__": client.start() dev = client.device_manager.devices scans = client.scans + queue = client.queue app = QApplication([]) window = MotorApp() From 3e408b304ba6b755fc472ea0d39d457b93b55be9 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:18:12 +0200 Subject: [PATCH 17/29] refactor: migrate to use just np.array for tracking position, only latest N points are being dimmed. --- .../examples/motor_movement/motor_example.py | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index a8c2252f..bf6c2b3d 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -20,8 +20,9 @@ class MotorApp(QWidget): uic.loadUi(os.path.join(current_path, "motor_controller.ui"), self) self.limit_x, self.limit_y = None, None + # Coordinates tracking - self.motor_positions = [] + self.motor_positions = np.array([]) # QThread for motor movement + signals self.motor_thread = MotorControl() @@ -184,15 +185,13 @@ class MotorApp(QWidget): (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() - init_brush = pg.mkBrush(255, 255, 255, 255) - self.motor_positions.append({"pos": init_pos, "brush": init_brush}) - self.motor_map.setData( - pos=[point["pos"] for point in self.motor_positions], - brush=[point["brush"] for point in self.motor_positions], - ) + # 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() @@ -203,19 +202,18 @@ class MotorApp(QWidget): # Update label self.label_coorditanes.setText(f"Motor position: ({x:.2f}, {y:.2f})") - # Dim previous points - last_brush = self.motor_positions[-1]["brush"].color().getRgb() - dimmer_brush = tuple(max(int(0.8 * c), 50) for c in last_brush[:3]) + (255,) - self.motor_positions[-1]["brush"] = pg.mkBrush(dimmer_brush) + # Determine brushes based on position in the array + self.brushes = [pg.mkBrush(50, 50, 50, 255)] * len(self.motor_positions) + for i in range(1, min(6, len(self.motor_positions) + 1)): + brightness = max(50, 255 - 20 * (i - 1)) + self.brushes[-i] = pg.mkBrush(brightness, brightness, brightness, 255) # Add new point with full brightness new_pos = (x, y) - new_brush = pg.mkBrush(255, 255, 255, 255) - self.motor_positions.append({"pos": new_pos, "brush": new_brush}) - self.motor_map.setData( - pos=[point["pos"] for point in self.motor_positions], - brush=[point["brush"] for point in self.motor_positions], - ) + self.motor_positions = np.vstack((self.motor_positions, new_pos)) + self.brushes.append(pg.mkBrush(255, 255, 255, 255)) + + self.motor_map.setData(pos=self.motor_positions, brush=self.brushes) def update_all_motor_limits( self, x_limit: list = None, y_limit: list = None From 9eae697df8a2f3961454db9ed397353f110c0e67 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:22:50 +0200 Subject: [PATCH 18/29] feat: ability to choose how many points should be dimmed before reaching the threshold + total number of point which should be stored. --- .../examples/motor_movement/motor_example.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index bf6c2b3d..a6ca112a 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -23,6 +23,9 @@ class MotorApp(QWidget): # Coordinates tracking self.motor_positions = np.array([]) + self.max_points = 50 # Maximum number of points to keep + self.num_dim_points = 15 # Number of points to dim gradually + self.precision = 2 # Define the decimal precision # QThread for motor movement + signals self.motor_thread = MotorControl() @@ -39,7 +42,6 @@ class MotorApp(QWidget): # self.background_map_scale = 10 # self.visited_coordinates = {} - # self.precision = 2 # Define the decimal precision # Initialize the image map self.init_motor_map() @@ -202,16 +204,21 @@ class MotorApp(QWidget): # Update label self.label_coorditanes.setText(f"Motor position: ({x:.2f}, {y:.2f})") + # 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) - for i in range(1, min(6, len(self.motor_positions) + 1)): + for i in range(1, min(self.num_dim_points + 1, len(self.motor_positions) + 1)): brightness = max(50, 255 - 20 * (i - 1)) self.brushes[-i] = pg.mkBrush(brightness, brightness, brightness, 255) - # Add new point with full brightness - new_pos = (x, y) - self.motor_positions = np.vstack((self.motor_positions, new_pos)) - self.brushes.append(pg.mkBrush(255, 255, 255, 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) From 3c0e5955d40a67935b8fb064d5c52fd3f29bd1a1 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:35:52 +0200 Subject: [PATCH 19/29] feat: keyboard shortcut to go to coordinates --- bec_widgets/line_plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bec_widgets/line_plot.py b/bec_widgets/line_plot.py index 53485989..ca2371b5 100644 --- a/bec_widgets/line_plot.py +++ b/bec_widgets/line_plot.py @@ -26,7 +26,7 @@ class BasicPlot(QtWidgets.QWidget): update_signal = pyqtSignal() roi_signal = pyqtSignal(tuple) - def __init__(self, name="", y_value_list=["gauss_bpm"]) -> None: + def __init__(self, name="", y_value_list=["samx", "samy"]) -> None: """ Basic plot widget for displaying scan data. @@ -387,7 +387,7 @@ if __name__ == "__main__": "--signals", help="specify recorded signals", nargs="+", - default=["gauss_bpm"], + default=["samx", "samy"], ) # default = ["gauss_bpm", "bpm4i", "bpm5i", "bpm6i", "xert"], # dispatcher = bec_dispatcher From 0226188079f1dac4eece6b1a6fa330620f1504bc Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Fri, 25 Aug 2023 11:36:49 +0200 Subject: [PATCH 20/29] feat: new GUI --- .../motor_movement/motor_controller.ui | 733 +++++++++++------- .../examples/motor_movement/motor_example.py | 53 +- 2 files changed, 498 insertions(+), 288 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index cded821d..5d0d8143 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -6,288 +6,268 @@ 0 0 - 1289 - 596 + 1115 + 506 Motor Controller - + - - - - - - - Go - - - - - - - Save - - - - - - - - - true - - - - + + + true + + - - - - - General - - - - - - Decimal precision - - - - - - - - - - Speed - - - - - - - - - - - - - Motor Controls - - - - - - Move with arrow keys - - - - - - - - - ... - - - Qt::UpArrow - - - - - - - ... - - - Qt::LeftArrow - - - - - - - 5 - - - - - - - ... - - - Qt::RightArrow - - - - - - - ... - - - Qt::DownArrow - - - - - - - - - - - X - - - Qt::AlignCenter - - - - - - - Y - - - Qt::AlignCenter - - - - - - - Go - - - - - - - - - - - - - - - Stop Movement - - - - - - - - - - true - - - Motor Limits - - - - - - -1000 - - - 1000 - - - -100 - - - - - - - -1000 - - - 1000 - - - 100 - - - - - - - - samy - - - - - - - -1000 - - - 1000 - - - 100 - - - - - - - + samy - - - - - - - + samx - - - - - - - - samx - - - - - - - -1000 - - - 1000 - - - -100 - - - - - - - Update - - - - - - - + + + + 221 + 471 + + + + + + + Motor Selection + + + + + + Motor Y + + + + + + + Motor X + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 18 + + + + + + + + Motor Relative + + + + + + Move with arrow keys + + + + + + + QLayout::SetDefaultConstraint + + + + + ... + + + Qt::UpArrow + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 5 + + + + + + + ... + + + Qt::DownArrow + + + + + + + ... + + + Qt::LeftArrow + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + ... + + + Qt::RightArrow + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 13 + + + + + + + + Move Absolute + + + + + + + + X + + + Qt::AlignCenter + + + + + + + Y + + + Qt::AlignCenter + + + + + + + Go + + + + + + + + + + + + + + + Stop Movement + + + + + + + + - + + + true + 0 @@ -327,6 +307,209 @@ + + + Settings + + + + + + Motor Limits + + + + + + Qt::AlignCenter + + + -1000 + + + 1000 + + + -100 + + + + + + + Update + + + + + + + -1000 + + + 1000 + + + 100 + + + + + + + + Y + + + Qt::AlignCenter + + + + + + + Qt::AlignCenter + + + -1000 + + + 1000 + + + 100 + + + + + + + -1000 + + + 1000 + + + -100 + + + + + + + - Y + + + Qt::AlignCenter + + + + + + + - X + + + Qt::AlignCenter + + + + + + + + X + + + Qt::AlignCenter + + + + + + + + + + Motor config + + + + + + + + + + + + Tolerance + + + + + + + Update Frequency + + + + + + + + + + Decimal Precision + + + + + + + + + + X + + + + + + + Speed + + + + + + + Y + + + + + + + X = Y + + + + + + + + + + + + + + + + + + + + Queue @@ -334,6 +517,9 @@ + + false + Reset Queue @@ -341,6 +527,9 @@ + + false + queueID diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index a6ca112a..5d32d97d 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -11,6 +11,18 @@ from pyqtgraph.Qt import QtWidgets, uic from bec_lib.core import MessageEndpoints, BECMessage +# TODO - General features +# - setting motor speed and frequency +# - setting motor acceleration +# - updating motor precision +# - put motor selection dropdown or listwidget +# - put motor status (moving, stopped, etc) +# - remove all hardcoded references to samx and samy +# - add spinBox for motor scatter size +# - add mouse interactions with the plot -> click to select coordinates, double click to move? +# - adjust right click actions + + class MotorApp(QWidget): coordinates_updated = pyqtSignal(float, float) @@ -23,9 +35,8 @@ class MotorApp(QWidget): # Coordinates tracking self.motor_positions = np.array([]) - self.max_points = 50 # Maximum number of points to keep + self.max_points = 5000 # Maximum number of points to keep self.num_dim_points = 15 # Number of points to dim gradually - self.precision = 2 # Define the decimal precision # QThread for motor movement + signals self.motor_thread = MotorControl() @@ -40,9 +51,6 @@ class MotorApp(QWidget): print(f"Init limits: samx:{self.limit_x}, samy:{self.limit_y}") - # self.background_map_scale = 10 - # self.visited_coordinates = {} - # Initialize the image map self.init_motor_map() @@ -103,6 +111,14 @@ class MotorApp(QWidget): size=2, 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 @@ -132,6 +148,9 @@ class MotorApp(QWidget): ) ) 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 @@ -158,7 +177,7 @@ class MotorApp(QWidget): # 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, 2), round(y, 2)) + lambda x, y: self.update_image_map(round(x, self.precision), round(y, self.precision)) ) # Coordinates table @@ -294,19 +313,23 @@ class MotorControl(QThread): move_finished = pyqtSignal() # Signal to emit when the move is finished # progress_updated = pyqtSignal(int) #TODO Signal to emit progress percentage - def __init__(self, parent=None): + def __init__( + self, + active_devices=["samx", "samy"], + parent=None, + ): super().__init__(parent) self.target_coordinates = None self.running = False + self.active_devices = active_devices + # Initialize current coordinates with the provided initial coordinates ( self.current_x, self.current_y, ) = self.get_coordinates() # TODO maybe not needed, coordinates always stored in redis - self.active_devices = ["samx", "samy"] - self.motors_consumer = client.connector.consumer( topics=[ MessageEndpoints.device_readback(self.active_devices[0]), @@ -319,13 +342,9 @@ class MotorControl(QThread): self.motors_consumer.start() def get_coordinates(self) -> tuple: - # TODO decide if the get_coordinates and retrieve coordinates makes sense - # TODO or move also to signal/slot as in the case of limits - # TODO or move just to redis CB function """Get current motor position""" - dev.samx.read(cached=True) # TODO to get last message ['value'] -> last position - x = dev.samx.position() - y = dev.samy.position() + x = dev.samx.read(cached=True)["value"] # TODO remove hardcoded samx and samy + y = dev.samx.read(cached=True)["value"] return x, y def retrieve_coordinates(self) -> tuple: @@ -360,7 +379,9 @@ class MotorControl(QThread): 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(dev.samx, low_limit=x_limit[0], high_limit=x_limit[1]) + self.update_motor_limits( + dev.samx, low_limit=x_limit[0], high_limit=x_limit[1] + ) # TODO Remove hardcoded samx and samy if y_limit is not None: if current_position[1] < y_limit[0] or current_position[1] > y_limit[1]: From cab32be0092185870b5a12398103475342c8b1fd Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Fri, 25 Aug 2023 16:07:05 +0200 Subject: [PATCH 21/29] feat: motor selection --- .../motor_movement/motor_controller.ui | 29 ++-- .../examples/motor_movement/motor_example.py | 161 ++++++++++++++---- 2 files changed, 144 insertions(+), 46 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index 5d0d8143..fbb122ff 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -6,8 +6,8 @@ 0 0 - 1115 - 506 + 1120 + 547 @@ -43,6 +43,12 @@ + + + + + + @@ -50,11 +56,12 @@ - - - - - + + + + Connect Motors + + @@ -208,7 +215,7 @@ - + Move Absolute @@ -271,7 +278,7 @@ 0 - + Coordinates @@ -307,7 +314,7 @@ - + Settings @@ -510,7 +517,7 @@ - + Queue diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index 5d32d97d..cba06a52 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -31,6 +31,10 @@ class MotorApp(QWidget): 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 # dev.samx, dev.samy self.limit_x, self.limit_y = None, None # Coordinates tracking @@ -39,21 +43,44 @@ class MotorApp(QWidget): self.num_dim_points = 15 # Number of points to dim gradually # QThread for motor movement + signals - self.motor_thread = MotorControl() + 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) # UI self.init_ui() self.tag_N = 1 # position label for saved coordinates - # Initialize current coordinates with the provided initial coordinates - self.motor_thread.retrieve_motor_limits(dev.samx, dev.samy) + # Get all motors available + self.motor_thread.retrieve_all_motors() - print(f"Init limits: samx:{self.limit_x}, samy:{self.limit_y}") + # Initialize current coordinates with the provided initial coordinates + # self.motor_thread.retrieve_motor_limits(self.motor_x, self.motor_y) #TODO move after connection of motors + + # print(f"Init limits: samx:{self.limit_x}, samy:{self.limit_y}") # TODO get to motor connect # Initialize the image map + # self.init_motor_map() + + 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.init_motor_map() + self.motorControl.setEnabled(True) + self.motorControl_absolute.setEnabled(True) + self.tabWidget_tables.setTabEnabled(1, True) + + @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) + print(f"got motors {motors_x} and {motors_y}") + @pyqtSlot(list, list) def update_limits(self, x_limits: list, y_limits: list) -> None: self.limit_x = x_limits @@ -101,9 +128,12 @@ class MotorApp(QWidget): # 2D Plot ########################## + # self.label_coorditanes = self.glw.addLabel( + # f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})", row=0, col=0 + # ) #TODO remove hardcoded samx and samy self.label_coorditanes = self.glw.addLabel( - f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})", row=0, col=0 - ) + f"Motor position: (X, Y)", row=0, col=0 + ) # TODO remove hardcoded samx and samy self.plot_map = self.glw.addPlot(row=1, col=0) self.limit_map = pg.ImageItem() self.plot_map.addItem(self.limit_map) @@ -125,16 +155,16 @@ class MotorApp(QWidget): ########################## self.toolButton_right.clicked.connect( - lambda: self.move_motor_relative(dev.samx, self.spinBox_step.value()) + lambda: self.move_motor_relative(self.motor_x, self.spinBox_step.value()) ) self.toolButton_left.clicked.connect( - lambda: self.move_motor_relative(dev.samx, -self.spinBox_step.value()) + lambda: self.move_motor_relative(self.motor_x, -self.spinBox_step.value()) ) self.toolButton_up.clicked.connect( - lambda: self.move_motor_relative(dev.samy, self.spinBox_step.value()) + lambda: self.move_motor_relative(self.motor_y, self.spinBox_step.value()) ) self.toolButton_down.clicked.connect( - lambda: self.move_motor_relative(dev.samy, -self.spinBox_step.value()) + lambda: self.move_motor_relative(self.motor_y, -self.spinBox_step.value()) ) # Switch between key shortcuts active @@ -188,10 +218,23 @@ class MotorApp(QWidget): precision=0, ) + # 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) + def init_motor_map(self): # Get motor limits - limit_x_min, limit_x_max = self.motor_thread.get_motor_limits(dev.samx) - limit_y_min, limit_y_max = self.motor_thread.get_motor_limits(dev.samy) + 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 @@ -241,9 +284,7 @@ class MotorApp(QWidget): self.motor_map.setData(pos=self.motor_positions, brush=self.brushes) - def update_all_motor_limits( - self, x_limit: list = None, y_limit: list = None - ) -> None: # TODO will be moved to thread + 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_arrow_key_shortcuts(self): @@ -311,6 +352,8 @@ class MotorControl(QThread): coordinates_updated = pyqtSignal(float, float) # Signal to emit current coordinates limits_retrieved = pyqtSignal(list, list) # Signal to emit current limits (samx, samy) 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__( @@ -319,21 +362,50 @@ class MotorControl(QThread): parent=None, ): super().__init__(parent) + + 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.connect_motors("samx", "samy") + self.target_coordinates = None - self.running = False - self.active_devices = active_devices + self.motor_x_debug = dev.samx + # self.all_motors = self.get_all_motors() + # self.all_motors_names = self.get_all_motors_names() - # Initialize current coordinates with the provided initial coordinates - ( - self.current_x, - self.current_y, - ) = self.get_coordinates() # TODO maybe not needed, coordinates always stored in redis + # find index of samx in self.all_motors_names + # self.motor_x_index = self.all_motors_names.index(self.active_devices[0]) + + print(f"Motor debug: {self.motor_x_debug}") + + 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.active_devices[0]), - MessageEndpoints.device_readback(self.active_devices[1]), + MessageEndpoints.device_readback(self.motor_x.name), + MessageEndpoints.device_readback(self.motor_y.name), ], cb=self._device_status_callback_motors, parent=self, @@ -341,10 +413,29 @@ class MotorControl(QThread): 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) + print("motors sent to GUI") + + return self.all_motors, self.all_motors_names + def get_coordinates(self) -> tuple: """Get current motor position""" - x = dev.samx.read(cached=True)["value"] # TODO remove hardcoded samx and samy - y = dev.samx.read(cached=True)["value"] + x = self.motor_x.read(cached=True)["value"] # TODO remove hardcoded samx and samy + y = self.motor_y.read(cached=True)["value"] return x, y def retrieve_coordinates(self) -> tuple: @@ -380,16 +471,16 @@ class MotorControl(QThread): raise ValueError("Current motor position is outside the new limits (X)") else: self.update_motor_limits( - dev.samx, low_limit=x_limit[0], high_limit=x_limit[1] + self.motor_x, low_limit=x_limit[0], high_limit=x_limit[1] ) # TODO Remove hardcoded samx and samy 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(dev.samy, low_limit=y_limit[0], high_limit=y_limit[1]) + self.update_motor_limits(self.motor_y, low_limit=y_limit[0], high_limit=y_limit[1]) - self.retrieve_motor_limits(dev.samx, dev.samy) + self.retrieve_motor_limits(self.motor_x, self.motor_y) def move_to_coordinates(self, target_coordinates: tuple): self.action = "move_to_coordinates" @@ -414,9 +505,9 @@ class MotorControl(QThread): def move_motor_coordinate(self) -> None: """Move the motor to the specified coordinates""" status = scans.mv( - dev.samx, + self.motor_x, self.target_coordinates[0], - dev.samy, + self.motor_y, self.target_coordinates[1], relative=False, ) @@ -437,10 +528,10 @@ class MotorControl(QThread): @staticmethod def _device_status_callback_motors(msg, *, parent, **_kwargs) -> None: deviceMSG = BECMessage.DeviceMessage.loads(msg.value) - if parent.active_devices[0] in deviceMSG.content["signals"]: - parent.current_x = deviceMSG.content["signals"][parent.active_devices[0]]["value"] - elif parent.active_devices[1] in deviceMSG.content["signals"]: - parent.current_y = deviceMSG.content["signals"][parent.active_devices[1]]["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) From c7e35d7da69853343aa7eee53c8ad988eb490d93 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Fri, 25 Aug 2023 16:17:58 +0200 Subject: [PATCH 22/29] fix: motor selection is disabled while motor is moving --- bec_widgets/examples/motor_movement/motor_example.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index cba06a52..fe6522ea 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -15,9 +15,7 @@ from bec_lib.core import MessageEndpoints, BECMessage # - setting motor speed and frequency # - setting motor acceleration # - updating motor precision -# - put motor selection dropdown or listwidget # - put motor status (moving, stopped, etc) -# - remove all hardcoded references to samx and samy # - add spinBox for motor scatter size # - add mouse interactions with the plot -> click to select coordinates, double click to move? # - adjust right click actions @@ -34,7 +32,7 @@ class MotorApp(QWidget): # Motor Control Thread self.motor_thread = MotorControl() - self.motor_x, self.motor_y = None, None # dev.samx, dev.samy + self.motor_x, self.motor_y = None, None self.limit_x, self.limit_y = None, None # Coordinates tracking @@ -105,8 +103,11 @@ class MotorApp(QWidget): self.motorControl.setEnabled(True) def enable_motor_controls(self, disable: bool) -> None: - # Disable or enable all controls within the motorControl group box - for widget in self.motorControl.findChildren(QtWidgets.QWidget): + 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 @@ -358,7 +359,6 @@ class MotorControl(QThread): def __init__( self, - active_devices=["samx", "samy"], parent=None, ): super().__init__(parent) From 64708bc1b2e6a4256da9123d0215fc87e0afa455 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Fri, 25 Aug 2023 16:20:05 +0200 Subject: [PATCH 23/29] fix: line_plot.py default changed back to "gauss_bpm" --- bec_widgets/line_plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bec_widgets/line_plot.py b/bec_widgets/line_plot.py index ca2371b5..53485989 100644 --- a/bec_widgets/line_plot.py +++ b/bec_widgets/line_plot.py @@ -26,7 +26,7 @@ class BasicPlot(QtWidgets.QWidget): update_signal = pyqtSignal() roi_signal = pyqtSignal(tuple) - def __init__(self, name="", y_value_list=["samx", "samy"]) -> None: + def __init__(self, name="", y_value_list=["gauss_bpm"]) -> None: """ Basic plot widget for displaying scan data. @@ -387,7 +387,7 @@ if __name__ == "__main__": "--signals", help="specify recorded signals", nargs="+", - default=["samx", "samy"], + default=["gauss_bpm"], ) # default = ["gauss_bpm", "bpm4i", "bpm5i", "bpm6i", "xert"], # dispatcher = bec_dispatcher From 7575c91c9949a4c6b0c92efa24757509b476c086 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Fri, 25 Aug 2023 16:50:18 +0200 Subject: [PATCH 24/29] refactor: introduced MotorActions enum to replace hardcoded strings + project cleanup --- .../examples/motor_movement/motor_example.py | 77 +++++++------------ 1 file changed, 29 insertions(+), 48 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index fe6522ea..6d6cdf9f 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -1,6 +1,7 @@ import os import numpy as np +from enum import Enum import pyqtgraph as pg from PyQt5 import QtGui from PyQt5.QtCore import QThread, pyqtSlot @@ -19,6 +20,7 @@ from bec_lib.core import MessageEndpoints, BECMessage # - 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): @@ -52,14 +54,6 @@ class MotorApp(QWidget): # Get all motors available self.motor_thread.retrieve_all_motors() - # Initialize current coordinates with the provided initial coordinates - # self.motor_thread.retrieve_motor_limits(self.motor_x, self.motor_y) #TODO move after connection of motors - - # print(f"Init limits: samx:{self.limit_x}, samy:{self.limit_y}") # TODO get to motor connect - - # Initialize the image map - # self.init_motor_map() - 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) @@ -69,6 +63,13 @@ class MotorApp(QWidget): 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 @@ -129,12 +130,7 @@ class MotorApp(QWidget): # 2D Plot ########################## - # self.label_coorditanes = self.glw.addLabel( - # f"Motor position: ({dev.samx.position():.2f}, {dev.samy.position():.2f})", row=0, col=0 - # ) #TODO remove hardcoded samx and samy - self.label_coorditanes = self.glw.addLabel( - f"Motor position: (X, Y)", row=0, col=0 - ) # TODO remove hardcoded samx and samy + 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) @@ -211,14 +207,6 @@ class MotorApp(QWidget): lambda x, y: self.update_image_map(round(x, self.precision), round(y, self.precision)) ) - # Coordinates table - self.generate_table_coordinate( - self.tableWidget_coordinates, - self.motor_thread.retrieve_coordinates(), - tag="Initial", - precision=0, - ) - # Motor connections self.pushButton_connecMotors.clicked.connect( lambda: self.connect_motor( @@ -265,7 +253,7 @@ class MotorApp(QWidget): def update_image_map(self, x, y): # Update label - self.label_coorditanes.setText(f"Motor position: ({x:.2f}, {y:.2f})") + self.label_coorditanes.setText(f"Motor position: ({x}, {y})") # Add new point with full brightness new_pos = np.array([x, y]) @@ -349,9 +337,14 @@ class MotorApp(QWidget): 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 (samx, samy) + limits_retrieved = pyqtSignal(list, list) # Signal to emit current limits 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 @@ -363,6 +356,8 @@ class MotorControl(QThread): ): super().__init__(parent) + self.action = None + self.motor_x, self.motor_y = None, None self.current_x, self.current_y = None, None @@ -373,19 +368,8 @@ class MotorControl(QThread): self.all_motors_names = self.get_all_motors_names() self.retrieve_all_motors() # send motor list to GUI - self.connect_motors("samx", "samy") - self.target_coordinates = None - self.motor_x_debug = dev.samx - # self.all_motors = self.get_all_motors() - # self.all_motors_names = self.get_all_motors_names() - - # find index of samx in self.all_motors_names - # self.motor_x_index = self.all_motors_names.index(self.active_devices[0]) - - print(f"Motor debug: {self.motor_x_debug}") - 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) @@ -434,7 +418,7 @@ class MotorControl(QThread): def get_coordinates(self) -> tuple: """Get current motor position""" - x = self.motor_x.read(cached=True)["value"] # TODO remove hardcoded samx and samy + x = self.motor_x.read(cached=True)["value"] y = self.motor_y.read(cached=True)["value"] return x, y @@ -463,16 +447,13 @@ class MotorControl(QThread): motor.high_limit = high_limit def update_all_motor_limits(self, x_limit: list = None, y_limit: list = None) -> None: - # TODO can be generalized to any motor not just samx samy 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] - ) # TODO Remove hardcoded samx and samy + 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]: @@ -483,26 +464,26 @@ class MotorControl(QThread): self.retrieve_motor_limits(self.motor_x, self.motor_y) def move_to_coordinates(self, target_coordinates: tuple): - self.action = "move_to_coordinates" + self.action = MotorActions.MOVE_TO_COORDINATES self.target_coordinates = target_coordinates self.start() def move_relative(self, motor, value: float): - self.action = "move_relative" + self.action = MotorActions.MOVE_RELATIVE self.motor = motor self.value = value self.start() def run(self): - if self.action == "move_to_coordinates": - self.move_motor_coordinate() - elif self.action == "move_relative": - self.move_motor_relative(self.motor, self.value) + 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 move_motor_coordinate(self) -> None: + def _move_motor_coordinate(self) -> None: """Move the motor to the specified coordinates""" status = scans.mv( self.motor_x, @@ -515,7 +496,7 @@ class MotorControl(QThread): status.wait() self.move_finished.emit() - def move_motor_relative(self, motor, value: float) -> None: + def _move_motor_relative(self, motor, value: float) -> None: status = scans.mv(motor, value, relative=True) status.wait() From 5dd0af6894a5d97457d60ef18b098e40856e4875 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:22:27 +0200 Subject: [PATCH 25/29] feat: delete coordinate table row by DELETE or BACKSPACE key --- .../motor_movement/motor_controller.ui | 6 +++ .../examples/motor_movement/motor_example.py | 44 ++++++++++++++----- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index fbb122ff..2043a83a 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -285,6 +285,12 @@ + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + Tag diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index 6d6cdf9f..a1afc2b1 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -9,6 +9,9 @@ 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 @@ -220,6 +223,12 @@ class MotorApp(QWidget): 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) @@ -322,6 +331,15 @@ class MotorApp(QWidget): ) 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()) + + print(f"deleted {selected_rows}") + def save_absolute_coordinates(self): self.generate_table_coordinate( self.tableWidget_coordinates, @@ -357,18 +375,7 @@ class MotorControl(QThread): super().__init__(parent) self.action = 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 + 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) @@ -483,6 +490,19 @@ class MotorControl(QThread): 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( From ce9816480b82373895b602d1a1bca7d1d9725f01 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Fri, 25 Aug 2023 18:06:33 +0200 Subject: [PATCH 26/29] feat: speed and frequency is retrieved from devices --- .../motor_movement/motor_controller.ui | 168 +++++++++++++----- .../examples/motor_movement/motor_example.py | 60 ++++++- 2 files changed, 177 insertions(+), 51 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index 2043a83a..4b32a4ae 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -6,14 +6,14 @@ 0 0 - 1120 - 547 + 1129 + 542 Motor Controller - + @@ -120,13 +120,6 @@ - - - - 5 - - - @@ -196,6 +189,9 @@ + + + @@ -250,10 +246,10 @@ - + - + @@ -276,7 +272,7 @@ true - 0 + 1 @@ -326,7 +322,7 @@ - + Motor Limits @@ -356,6 +352,9 @@ + + Qt::AlignCenter + -1000 @@ -395,6 +394,9 @@ + + Qt::AlignCenter + -1000 @@ -442,19 +444,16 @@ - Motor config + Motor Config - - - - - - - - - - Tolerance + + + + Qt::AlignCenter + + + 500 @@ -465,23 +464,23 @@ - - - - - + + + + false + - Decimal Precision + Tolerance - - - - - + + - X + Y + + + Qt::AlignCenter @@ -492,31 +491,102 @@ - - - - Y + + + + false + + + Qt::AlignCenter - - + + + + false + - X = Y + Decimal Precision + + + + + + + false + + + Qt::AlignCenter - + + + Qt::AlignCenter + + + 1000 + + - - + + + + X + + + Qt::AlignCenter + + - + + + false + + + Qt::AlignCenter + + - + + + false + + + Qt::AlignCenter + + + + + + + Qt::AlignCenter + + + 1000 + + + + + + + Qt::AlignCenter + + + 500 + + + + + + + Update + + diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index a1afc2b1..85afab16 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -49,6 +49,8 @@ class MotorApp(QWidget): 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() @@ -60,6 +62,8 @@ class MotorApp(QWidget): 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) @@ -102,6 +106,20 @@ class MotorApp(QWidget): 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) @@ -229,6 +247,8 @@ class MotorApp(QWidget): delete_shortcut.activated.connect(self.delete_selected_row) backspace_shortcut.activated.connect(self.delete_selected_row) + # Get speed and update frequency + def init_motor_map(self): # Get motor limits limit_x_min, limit_x_max = self.motor_thread.get_motor_limits(self.motor_x) @@ -306,8 +326,6 @@ class MotorApp(QWidget): table.setRowCount(current_row_count + 1) - # table.insertRow(current_row_count) - checkBox = QtWidgets.QCheckBox() checkBox.setChecked(True) button = QtWidgets.QPushButton("Go") @@ -363,6 +381,8 @@ class MotorActions(Enum): 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 @@ -437,6 +457,42 @@ class MotorControl(QThread): """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 get_motor_speed(self, motor) -> float: + """Get the speed of a motor""" + return motor.speed + + def set_motor_speed(self, motor, speed: float) -> None: + """Set the speed of a motor""" + motor.speed = speed + + def get_motor_update_frequency(self, motor) -> float: + """Get the update frequency of a motor""" + return motor.update_frequency + + def set_motor_update_frequency(self, motor, update_frequency: float) -> None: + """Set the update frequency of a motor""" + motor.update_frequency = update_frequency + + 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) From f391a2fd004f1dc8187cfe12d60f856427ae01ec Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Fri, 25 Aug 2023 18:14:08 +0200 Subject: [PATCH 27/29] feat: speed and frequency can be updated from GUI --- .../motor_movement/motor_controller.ui | 2 +- .../examples/motor_movement/motor_example.py | 53 +++++++++++++------ 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index 4b32a4ae..325ecbc0 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -582,7 +582,7 @@ - + Update diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index 85afab16..2f8129bd 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -146,6 +146,7 @@ class MotorApp(QWidget): def init_ui(self) -> None: """Setup all ui elements""" + # TODO can be separated to multiple functions ########################## # 2D Plot @@ -216,6 +217,19 @@ class MotorApp(QWidget): 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) + ) + + # Confog updates self.pushButton_updateLimits.clicked.connect( lambda: self.update_all_motor_limits( x_limit=[self.spinBox_x_min.value(), self.spinBox_x_max.value()], @@ -223,6 +237,16 @@ class MotorApp(QWidget): ) ) + 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)) @@ -247,8 +271,6 @@ class MotorApp(QWidget): delete_shortcut.activated.connect(self.delete_selected_row) backspace_shortcut.activated.connect(self.delete_selected_row) - # Get speed and update frequency - def init_motor_map(self): # Get motor limits limit_x_min, limit_x_max = self.motor_thread.get_motor_limits(self.motor_x) @@ -305,6 +327,10 @@ class MotorApp(QWidget): 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 @@ -461,21 +487,18 @@ class MotorControl(QThread): """Get the configuration of a motor""" # TODO at this moment just for speed and update_frequency return motor.get_device_config() - def get_motor_speed(self, motor) -> float: - """Get the speed of a motor""" - return motor.speed + 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]}) - def set_motor_speed(self, motor, speed: float) -> None: - """Set the speed of a motor""" - motor.speed = speed + 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]}) - def get_motor_update_frequency(self, motor) -> float: - """Get the update frequency of a motor""" - return motor.update_frequency - - def set_motor_update_frequency(self, motor, update_frequency: float) -> None: - """Set the update frequency of a motor""" - motor.update_frequency = update_frequency + 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 From e0b52fcedca46d913d1677b45f9815eccd92e8f7 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Mon, 28 Aug 2023 10:08:18 +0200 Subject: [PATCH 28/29] feat: total number of points, scatter size and number of point to dim after last position can be changed from GUI --- .../motor_movement/motor_controller.ui | 293 ++++++++++++------ .../examples/motor_movement/motor_example.py | 46 ++- 2 files changed, 240 insertions(+), 99 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_controller.ui b/bec_widgets/examples/motor_movement/motor_controller.ui index 325ecbc0..66c42c82 100644 --- a/bec_widgets/examples/motor_movement/motor_controller.ui +++ b/bec_widgets/examples/motor_movement/motor_controller.ui @@ -7,13 +7,13 @@ 0 0 1129 - 542 + 550 Motor Controller - + @@ -190,7 +190,17 @@ - + + + 0.100000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + @@ -246,10 +256,30 @@ - + + + -500.000000000000000 + + + 500.000000000000000 + + + 0.100000000000000 + + - + + + -500.000000000000000 + + + 500.000000000000000 + + + 0.100000000000000 + + @@ -272,7 +302,7 @@ true - 1 + 0 @@ -447,50 +477,6 @@ Motor Config - - - - Qt::AlignCenter - - - 500 - - - - - - - Update Frequency - - - - - - - false - - - Tolerance - - - - - - - Y - - - Qt::AlignCenter - - - - - - - Speed - - - @@ -501,46 +487,6 @@ - - - - false - - - Decimal Precision - - - - - - - false - - - Qt::AlignCenter - - - - - - - Qt::AlignCenter - - - 1000 - - - - - - - X - - - Qt::AlignCenter - - - @@ -551,16 +497,23 @@ - - - - false + + + + X Qt::AlignCenter + + + + Speed + + + @@ -581,6 +534,99 @@ + + + + Qt::AlignCenter + + + 1000 + + + + + + + Y + + + Qt::AlignCenter + + + + + + + Max Points + + + + + + + Qt::AlignCenter + + + 10 + + + 1000 + + + 10 + + + 100 + + + + + + + Update Frequency + + + + + + + false + + + Qt::AlignCenter + + + + + + + false + + + Tolerance + + + + + + + false + + + Qt::AlignCenter + + + + + + + Qt::AlignCenter + + + 500 + + + @@ -588,6 +634,65 @@ + + + + false + + + Decimal Precision + + + + + + + N dim + + + + + + + Qt::AlignCenter + + + 100 + + + 10000 + + + 100 + + + 5000 + + + + + + + Scatter Size + + + + + + + Qt::AlignCenter + + + 1 + + + 15 + + + 5 + + + diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index 2f8129bd..fe2a8bb6 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -44,6 +44,7 @@ class MotorApp(QWidget): 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) @@ -144,6 +145,18 @@ class MotorApp(QWidget): 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 @@ -157,7 +170,7 @@ class MotorApp(QWidget): self.limit_map = pg.ImageItem() self.plot_map.addItem(self.limit_map) self.motor_map = pg.ScatterPlotItem( - size=2, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 255) + 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) @@ -206,7 +219,7 @@ class MotorApp(QWidget): self.pushButton_stop.clicked.connect(self.motor_thread.stop_movement) ########################## - # Motor limits signals + # Motor Configs ########################## # SpinBoxes - Motor Limits #TODO make spinboxes own limits updated, currently is [-1000, 1000] @@ -229,7 +242,18 @@ class MotorApp(QWidget): lambda: self.param_changed(self.spinBox_update_frequency_y) ) - # Confog updates + # 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()], @@ -237,6 +261,14 @@ class MotorApp(QWidget): ) ) + 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()], @@ -316,13 +348,17 @@ class MotorApp(QWidget): # 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 - 20 * (i - 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) + 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) From 413e4356cfde6e2432682332e470eb69427ad397 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Mon, 28 Aug 2023 10:12:32 +0200 Subject: [PATCH 29/29] feat: labels of current motors are shown in motors limits --- bec_widgets/examples/motor_movement/motor_example.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bec_widgets/examples/motor_movement/motor_example.py b/bec_widgets/examples/motor_movement/motor_example.py index fe2a8bb6..f926918f 100644 --- a/bec_widgets/examples/motor_movement/motor_example.py +++ b/bec_widgets/examples/motor_movement/motor_example.py @@ -86,7 +86,6 @@ class MotorApp(QWidget): def get_available_motors(self, motors_x, motors_y): self.comboBox_motor_x.addItems(motors_x) self.comboBox_motor_y.addItems(motors_y) - print(f"got motors {motors_x} and {motors_y}") @pyqtSlot(list, list) def update_limits(self, x_limits: list, y_limits: list) -> None: @@ -105,6 +104,12 @@ class MotorApp(QWidget): ): 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) @@ -418,8 +423,6 @@ class MotorApp(QWidget): for row in reversed(selected_rows): # Reverse to delete from the end self.tableWidget_coordinates.removeRow(row.row()) - print(f"deleted {selected_rows}") - def save_absolute_coordinates(self): self.generate_table_coordinate( self.tableWidget_coordinates, @@ -501,7 +504,6 @@ class MotorControl(QThread): 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) - print("motors sent to GUI") return self.all_motors, self.all_motors_names