From a4217ef1c8fcb66016df6b7e3189831c728ea8b2 Mon Sep 17 00:00:00 2001 From: Thierry Zamofing Date: Mon, 18 Jul 2022 09:11:46 +0200 Subject: [PATCH] wip --- epics_widgets/MotorTweak.py | 7 +- epics_widgets/SimMotorTweak.py | 309 +++++++++++++++++++++++++++++++++ swissmx.py | 85 +++++---- 3 files changed, 366 insertions(+), 35 deletions(-) create mode 100644 epics_widgets/SimMotorTweak.py diff --git a/epics_widgets/MotorTweak.py b/epics_widgets/MotorTweak.py index c5407b1..fe239d8 100644 --- a/epics_widgets/MotorTweak.py +++ b/epics_widgets/MotorTweak.py @@ -18,7 +18,6 @@ SPMG_PAUSE = 1 SPMG_MOVE = 2 SPMG_GO = 3 - class MotorTweak(QWidget, Ui_MotorTweak): event_val = pyqtSignal(str, dict) event_readback = pyqtSignal(str, float, dict) @@ -335,11 +334,7 @@ class MotorTweak(QWidget, Ui_MotorTweak): qp.end() def _draw_limits(self, qp): - try: - m = self.motor - except AttributeError: - _log.warning('Motor not connected') - return + m = self.motor width, height = self.size().width(), self.size().height() pad = 5 rounding = 2 diff --git a/epics_widgets/SimMotorTweak.py b/epics_widgets/SimMotorTweak.py new file mode 100644 index 0000000..112f238 --- /dev/null +++ b/epics_widgets/SimMotorTweak.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python +# *-----------------------------------------------------------------------* +# | | +# | Copyright (c) 2022 by Paul Scherrer Institute (http://www.psi.ch) | +# | Based on Zac great first implementation | +# | Author Thierry Zamofing (thierry.zamofing@psi.ch) | +# *-----------------------------------------------------------------------* + +# simulated motor tweaks ui: +# same interface as MotorTweak and SmaractMotorTweaks, but mimes just a simulated motor + +import logging +from time import sleep +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtGui import QPainter, QBrush, QColor, QPainterPath, QPen, QDoubleValidator +from PyQt5.QtWidgets import QMenu, QInputDialog, QAction +from PyQt5.uic import loadUiType +from epics.ca import pend_event + +Ui_MotorTweak, QWidget = loadUiType('epics_widgets/MotorTweak.ui') +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +class SimMotor: + def __init__(self,motor_base, short_name): + self._llm = -10 + self._hlm = 10 + self._prec = 5 + self._units = 'mm' + self._pos = 3.1415 + self._short_name=short_name + +class SimMotorTweak(QWidget, Ui_MotorTweak): + event_val = pyqtSignal(str, dict) + event_readback = pyqtSignal(str, dict) + event_soft_limit = pyqtSignal(str, dict) + event_high_hard_limit = pyqtSignal(str, dict) + event_low_hard_limit = pyqtSignal(str, dict) + event_axis_fault = pyqtSignal(str, dict) + + def __init__(self, parent=None): + super(SimMotorTweak, self).__init__(parent=parent) + self.setupUi(self) + self._wheel_tweaks = True + self._auto_labels = True + self._locked = False + self._label_style = 'basic' + self._templates_source = { + 'basic': '{short_name} {{rbv:.{precision}f}} {units}', + 'small': '{short_name} {{rbv:.{precision}f}} {units}', + '2 lines': '{short_name}
{{rbv:.{precision}f}} {units}', + 'busy': '{short_name} {{rbv:.{precision}f}} {units}' + } + self._templates = {} + + + def connect_motor(self, motor_base, short_name=None, *args, **kwargs): + self.label.setToolTip('{} => {}'.format(motor_base, motor_base)) + self.motor=SimMotor(motor_base, short_name) + + twv=0.1 + + self.set_motor_validator() + self._drive_val.setText(str(self.motor._pos)) + self._drive_val.returnPressed.connect(self.move_motor_to_position) + + self.jog_forward.hide() + self.jog_reverse.hide() + + tweak_min = kwargs.get("tweak_min", 0.0001) + tweak_max = kwargs.get("tweak_max", 20.0) + self._tweak_val.setText(str(twv)) + self._tweak_val.setValidator(QDoubleValidator(tweak_min, tweak_max, 4, self._tweak_val)) + #self._tweak_val.editingFinished.connect(lambda m=self._pv_tweak_val: m.put(self._tweak_val.text())) + #self.tweak_forward.clicked.connect(lambda m: self._pv_tweak_f.put(1)) + #self.tweak_reverse.clicked.connect(lambda m: self._pv_tweak_r.put(1)) + + self.bind_wheel() + + self.update_label_template() + + #self._pv_readback.add_callback(self.update_label) + #self._pv_status.add_callback(self.set_val_on_status_change) + + # self._pv_drive.add_callback(self.set_val) + #self._pv_drive.add_callback(self.emit_signals, source_field='VAL') + #self._pv_readback.add_callback(self.emit_signals, source_field='RBV') + #self._pv_hlm.add_callback(self.emit_signals, source_field='HLS') + #self._pv_llm.add_callback(self.emit_signals, source_field='LLS') + + # m.set_callback('HLM', self.set_motor_validator) + + + def set_motor_validator(self, **kwargs): + lineedit = self._drive_val + min, max = self.motor._llm, self.motor._hlm + if min == max: + min = -1e6 + max = 1e6 + lineedit.setValidator(QDoubleValidator(min, max, 5, lineedit)) + + + def move_relative(self, dist, delay=None): + target = self.motor._pos + dist + if delay: + sleep(delay) + + def get_position(self): + return self.motor._pos + + def is_homed(self): + return True + + def get_status(self, as_string=False): + """ + States: + 0 Stopped + 1 Stepping + 2 Scanning + 3 Holding + 4 Targeting + 5 Move Delay + 6 Calibrating + 7 Finding Ref + 8 Locked + :return: + """ + return 0 + + def wait(self): + pend_event(.01) + while not self.is_done(): + pend_event(0.2) + + def is_done(self): + return True # in (0, 3) + + def move_motor_to_position(self, drive=None, wait=False, assert_position=False): + + if assert_position: + wait=True + if drive is None: + logger.debug('{} abs target from widget'.format(self.short_name)) + drive = float(self._drive_val.text()) + logger.debug('{} abs move => {}'.format(self.short_name, drive)) + self._pos=drive + + def emit_signals(self, **kw): + field = kw['source_field'] + if field == 'VAL': + self.event_val.emit(self._pvname, kw) + elif field == 'RBV': + self.event_readback.emit(self._pvname, kw) + elif field == 'LVIO': + self.event_soft_limit.emit(self._pvname, kw) + elif field == 'HLS': + self.event_high_hard_limit.emit(self._pvname, kw) + self.event_axis_fault.emit(self._pvname, kw) + elif field == 'LVIO': + self.event_low_hard_limit.emit(self._pvname, kw) + self.event_axis_fault.emit(self._pvname, kw) + elif field == 'STAT': + self.event_axis_fault.emit(self._pvname, kw) + + + def set_val_on_status_change(self, **kw): + ''' + 0 Stopped + 1 Stepping + 2 Scanning + 3 Holding + 4 Targeting + 5 Move Delay + 6 Calibrating + 7 Finding Ref + 8 Locked + :param kw: + :return: + ''' + status = kw['char_value'] + if status in ('Holding', 'Stopped'): + v = self._pv_readback.get(as_string=True) + logger.debug('updating VAL on status change to holding/stopped = {}'.format(v)) + self._drive_val.setText(v) + + def set_val(self, **kw): + v = kw['char_value'] + logger.debug('updating VAL = {}'.format(v)) + self._drive_val.setText(v) + + def update_label(self, **kwargs): + self.label.setText(self._templates[self._label_style].format(rbv=self._pv_readback.get())) + self.tweak_forward.setToolTip('tweak forward by {:.3f} {}'.format(self._pv_tweak_val.get(), self._units)) + self.tweak_reverse.setToolTip('tweak reverse by {:.3f} {}'.format(self._pv_tweak_val.get(), self._units)) + + def tweak_event(self, event): + sign = event.angleDelta().y() + if sign < 0: + self._pv_tweak_r.put(1) + else: + self._pv_tweak_f.put(1) + + def bind_wheel(self): + self.tweak_forward.wheelEvent = self.tweak_event + self.tweak_reverse.wheelEvent = self.tweak_event + + def contextMenuEvent(self, event): + name = self._pv_name.get() + unit = self._units + prec = self._prec + + menu = QMenu(self) + menu.setTitle(self.short_name) + + lockmotor = QAction('lock motor', menu, checkable=True) + lockmotor.setChecked(self._locked) + menu.addAction(lockmotor) + + autolabelsAction = QAction('auto', menu, checkable=True) + autolabelsAction.setChecked(self._auto_labels) + menu.addAction(autolabelsAction) + + wheel_tweaks = QAction('Mouse wheel tweaks motor', menu, checkable=True) + wheel_tweaks.setChecked(self._wheel_tweaks) + menu.addAction(wheel_tweaks) + + # stopmotorAction = QAction('Stopped', menu, checkable=True) + # stopmotorAction.setChecked(SPMG_STOP == m.stop_go) + # menu.addAction(stopmotorAction) + changeprecAction = menu.addAction("Change Precision") + changetweakstepAction = menu.addAction("Tweak step {:.3f} {}".format(self._pv_tweak_val.get(), unit)) + + tozeroAction = menu.addAction("move to Zero") + + + action = menu.exec_(self.mapToGlobal(event.pos())) + if action == lockmotor: + self._locked = not self._locked + if self._locked: + self._controlbox.setDisabled(True) + else: + self._controlbox.setDisabled(False) + elif action == changeprecAction: + msg = 'Precision for motor {}'.format(name) + logger.debug('prec before %d', prec) + prec, ok = QInputDialog.getInt(self, msg, msg, prec, 0, 10) + logger.debug('prec after (%d) %d', ok, prec) + if ok: + self._prec = prec + + elif action == changetweakstepAction: + tv = self._pv_tweak_val.get() + msg = 'Tweak step for motor {} at {} {}'.format(name, tv, unit) + tv, ok = QInputDialog.getDouble(self, msg, msg, tv, pow(10, -prec), 10.0, prec) + if ok: + self._pv_tweak_val.put(tv) + + elif action == tozeroAction: + self._pv_drive.put(0.0) + + elif action == autolabelsAction: + self._auto_labels = not self._auto_labels + + elif action == wheel_tweaks: + self._wheel_tweaks = not self._wheel_tweaks + self.bind_wheel() + + self.update_label_template() + + def resizeEvent(self, event): + pass + + def update_label_template(self): + source = self._templates_source + target = self._templates + + for k in source: + target[k] = source[k].format( + short_name=self.motor._short_name, + precision=self.motor._prec, + units=self.motor._units) + self.label.setText(target[self._label_style].format(rbv=self.motor._pos)) + + def paintEvent(self, e): + qp = QPainter() + qp.begin(self) + qp.setRenderHint(QPainter.Antialiasing) + self._draw_limits(qp) + qp.end() + + def _draw_limits(self, qp): + pass + # width, height = self.size().width(), self.size().height() + # pad = 5 + # rounding = 2 + # size = 10 + # if m.HLS: + # x, y, w, h = width - size, pad, size, height - 2 * pad + # elif m.LLS: + # x, y, w, h = 0, pad, size, height - 2 * pad + # else: + # return + # color = QColor('indianred') + # qp.setBrush(QBrush(color, Qt.SolidPattern)) + # qp.setPen(QPen(color)) + # path = QPainterPath() + # path.setFillRule(Qt.WindingFill) + # path.addRoundedRect(x, y, w, h, rounding, rounding) + # qp.drawPath(path) diff --git a/swissmx.py b/swissmx.py index dba64fe..aab5467 100755 --- a/swissmx.py +++ b/swissmx.py @@ -77,7 +77,7 @@ TASK_HELICAL = "helical" TASK_EMBL = "embl" -#import PrelocatedCoordinatesModel #ZAC: orig. code +import PrelocatedCoordinatesModel #from EmblModule import EmblWidget #ZAC: orig. code #from HelicalTable import HelicalTableWidget #ZAC: orig. code #from Wigis import Spinner, Checkbox #ZAC: orig. code @@ -105,12 +105,15 @@ from CustomROI import BeamMark #from epics_widgets import zoom #ZAC: orig. code from epics_widgets.MotorTweak import MotorTweak from epics_widgets.SmaractMotorTweak import SmaractMotorTweak +from epics_widgets.SimMotorTweak import SimMotorTweak + from matplotlib import pyplot import numpy as np import pyqtgraph as pg import pyqtgraph.exporters -pg.setConfigOption("antialias", True) +# use antialias for draw lines, interpret image data as row-major instead of col-major +pg.setConfigOptions(antialias=True,imageAxisOrder='row-major') import app_utils from app_config import settings, appsconf, option, toggle_option, simulated @@ -222,6 +225,8 @@ class Main(QMainWindow, Ui_MainWindow): super(Main, self).__init__() self.setupUi(self) + app=QApplication.instance() + if os.getenv("DEVELOPMENT_VERSION"): title = "[DEVELOPMENT] SwissMX - Solid support scanning for FEL sources" else: @@ -251,13 +256,13 @@ class Main(QMainWindow, Ui_MainWindow): self.microscope_page.layout().addWidget(self.glw) self.glw.show() - self.vb = self.glw.addViewBox(enableMenu=False) + self.vb = self.glw.addViewBox(invertY=True)#,enableMenu=False) self.img = pg.ImageItem() self.img._swissmx_name = "microscope_image_item" # self.graphicsView.setCentralItem(self.vb) self.glw.scene().sigMouseMoved.connect(self.mouse_move_event) self.glw.scene().sigMouseClicked.connect(self.mouse_click_event) - self.vb.setAspectLocked() + self.vb.setAspectLocked(True) self.vb.setBackgroundColor((120, 90, 90)) self.vb.addItem(self.img) @@ -330,9 +335,9 @@ class Main(QMainWindow, Ui_MainWindow): #self.create_helical_widgets() #ZAC: orig. code self.center_piece_update(0) # start camera updater - #curzoom = qoptic_zoom.get_position(cached=False) #ZAC: orig. code - #_log.debug(f"starting app with zoom at {curzoom}") - #self.zoom_changed_cb(curzoom) + curzoom = app._zoom.get() + _log.debug(f"starting app with zoom at {curzoom}") + self.zoom_changed_cb(curzoom) self._tabs_daq_methods.currentChanged.connect(self.switch_task) self.switch_task() @@ -806,7 +811,7 @@ class Main(QMainWindow, Ui_MainWindow): exp_tab.layout().addStretch() # DAQ Methods Tabs - #self.build_daq_methods_prelocated_tab() #ZAC: orig. code + self.build_daq_methods_prelocated_tab() #ZAC: orig. code self.build_daq_methods_grid_tab() self.build_sample_selection_tab() @@ -1037,17 +1042,20 @@ class Main(QMainWindow, Ui_MainWindow): self._lb_coords.setText("") def mouse_move_event(self, pos): + app = QApplication.instance() self._mouse_pos = pos task = self.active_task() xy = self.img.mapFromScene(pos) - z = qoptic_zoom.get_position() + z = app._zoom.get() + _log.debug('mouse_pos:{} scene_pos:{} zoom:{}'.format(pos,xy,z)) + #TODO: implement mouse handling # if self._ppm_toolbox._force_ppm > 0: # ppm = self._ppm_toolbox._force_ppm # else: try: ppm = self.ppm_fitter(z) except: - ppm = 0 # do not move if we don't know ppm + ppm = 1 x, y = xy.x(), xy.y() try: bx, by = self.get_beam_mark_on_camera_xy() @@ -1055,12 +1063,13 @@ class Main(QMainWindow, Ui_MainWindow): bx, by = 500, 500 dx = (x - bx) / ppm dy = -(y - by) / ppm - - fx = self.tweakers["fast_x"].motor.get_position() - fy = self.tweakers["fast_y"].motor.get_position() - - fx += dx - fy += dy + try: + fx = self.tweakers["fast_x"].motor.get_position() + fy = self.tweakers["fast_y"].motor.get_position() + fx += dx + fy += dy + except: + fx=fy=0 self._lb_coords.setText( "\u23FA{:>}:{:>6.0f} {:<.0f}" @@ -1073,10 +1082,11 @@ class Main(QMainWindow, Ui_MainWindow): "PPM", ppm, "Distance to beam", 1000 * dx, 1000 * dy, "Stage", fx, fy, - ) + ) ) def mouse_click_event(self, event): + app=QApplication.instance() fx_motor, fy_motor, bx_motor, bz_motor, omega_motor = self.get_gonio_tweakers() _log.debug("{}".format(event)) _log.debug(" pos {}".format(event.pos())) @@ -1090,13 +1100,13 @@ class Main(QMainWindow, Ui_MainWindow): event.ignore() return pos = event.scenePos() - zoom_level = qoptic_zoom.get_position() + zoom_level = app._zoom.get() _log.debug(" zoom {}".format(zoom_level)) try: ppm = self.ppm_fitter(zoom_level) except: - ppm = 0 # do not move if we don't know ppm + ppm = 1 # 0: do not move if we don't know ppm dblclick = event.double() shft = Qt.ShiftModifier & event.modifiers() ctrl = Qt.ControlModifier & event.modifiers() @@ -1110,11 +1120,11 @@ class Main(QMainWindow, Ui_MainWindow): pixel_dist = np.linalg.norm(a - b) _log.debug("distance to beam: {:.1f} pixels".format(pixel_dist)) - omega = omega_motor.motor.get_position() + omega = omega_motor.get_position() _log.debug("omega at {:.03f} degrees".format(omega)) - fx = fx_motor.motor.get_position() - fy = fy_motor.motor.get_position() + fx = fx_motor.get_position() + fy = fy_motor.get_position() dx = (x - bx) / ppm dy = -(y - by) / ppm fx += dx @@ -1210,7 +1220,8 @@ class Main(QMainWindow, Ui_MainWindow): pass def get_beam_mark_on_camera_xy(self): - z = qoptic_zoom.get_position() + app=QApplication.instance() + z = app._zoom.get() try: bx = self.beamx_fitter(z) by = self.beamy_fitter(z) @@ -1250,6 +1261,7 @@ class Main(QMainWindow, Ui_MainWindow): self._escape_current_state = "ManualSampleExchange" def escape_goToSampleAlignment(self): + app=QApplication.instance() self._escape_current_state = "busy" steps = [ @@ -1258,7 +1270,7 @@ class Main(QMainWindow, Ui_MainWindow): ] if option(CRYOJET_MOTION_ENABLED): steps.extend([lambda: self.move_cryojet_nozzle("in")]) - steps.extend([lambda: self.move_post_tube("out"), lambda: backlight.move("in")]) + steps.extend([lambda: self.move_post_tube("out"), lambda: app._backlight.move("in")]) self.escape_run_steps(steps, "Transitioning to Sample Alignment") self._escape_current_state = "SampleAlignment" @@ -1435,6 +1447,9 @@ class Main(QMainWindow, Ui_MainWindow): dx = settings.value("collimator/dx", 1e10, type=float) dy = settings.value("collimator/dy", 1e10, type=float) + _log.info('Skip Zac''s orig. code') + return + # ZAC: orig. code if 1e9 < x_pos + y_pos + dx + dy: raise IncompleteConfiguration("COLLIMATOR configuration is incomplete!") @@ -1704,6 +1719,9 @@ class Main(QMainWindow, Ui_MainWindow): ) def move_post_tube(self, dir): + _log.info('Skip Zac''s orig. code') + return + # ZAC: orig. code try: tandem_twv = float(self._post_tandem_tweak_val.text()) except: @@ -1809,11 +1827,19 @@ class Main(QMainWindow, Ui_MainWindow): dsy.move_relative(-tandem_twv) def get_tweaker(self, pv, alias=None, label=None, mtype="epics_motor", layout=None, **kwargs): + app = QApplication.instance() + sim=app._args.sim if mtype == "epics_motor": - m = MotorTweak() + if sim&0x10: + m=SimMotorTweak() + else: + m = MotorTweak() else: - m = SmaractMotorTweak() - #m.connect_motor(pv, label, **kwargs) #ZAC: orig. code + if sim&0x20: + m=SimMotorTweak() + else: + m = SmaractMotorTweak() + m.connect_motor(pv, label, **kwargs) self.tweakers[alias] = m return m @@ -1825,7 +1851,7 @@ class Main(QMainWindow, Ui_MainWindow): else: m = SmaractMotorTweak() layout.addWidget(m) - #m.connect_motor(pv, label)#ZAC: orig. code + m.connect_motor(pv, label) self.tweakers[alias] = m def done_sliding(self): @@ -3217,6 +3243,7 @@ def main(): # set app icon app = QApplication(sys.argv) + app._args=args if args.sim&0x01: app._backlight = backlight.Backlight(None) @@ -3232,7 +3259,7 @@ def main(): app._zoom = zoom.QopticZoom() if args.sim&0x08: app._camera = camera.epics_cam(None) - app._camera.sim_gen(mode=0) + app._camera.sim_gen(mode=1) app._camera.run() else: app._camera = camera.epics_cam()