import math 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 import PV from epics.ca import pend_event #from app_utils import assert_tweaker_positions Ui_MotorTweak, QWidget = loadUiType('epics_widgets/MotorTweak.ui') logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) SPMG_STOP = 0 SPMG_PAUSE = 1 SPMG_MOVE = 2 SPMG_GO = 3 class SmaractMotorTweak(QWidget, Ui_MotorTweak): event_val = pyqtSignal(str, dict) event_rbv = 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(SmaractMotorTweak, 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): # TODO: DO NOT USE so many PVs, but reduce to the only really needed PV: s.a. class QopticZoom(object) self._pvname = motor_base+':DRIVE' self._pv_name = PV(motor_base+':NAME') self._pv_drive = PV(motor_base+':DRIVE') self._pv_readback = PV(motor_base+':MOTRBV') self._pv_tweak_r = PV(motor_base+':TWR.PROC') self._pv_tweak_f = PV(motor_base+':TWF.PROC') self._pv_tweak_val = PV(motor_base+':TWV') self._pv_status = PV(motor_base + ':STATUS') self._pv_home_f = PV(motor_base + ':FRM_FORW.PROC') self._pv_home_b = PV(motor_base + ':FRM_BACK.PROC') self._pv_is_homed = PV(motor_base + ':GET_HOMED') self._pv_llm = PV(motor_base + ':LLM') self._pv_hlm = PV(motor_base + ':HLM') self.label.setToolTip('{} => {}'.format(motor_base, self._pv_name.get())) try: self._prec = kwargs['prec'] except: self._prec = 5 try: self._units = kwargs['units'] except: self._units = 'mm' self._ignore_limits = self._pv_llm.get() == self._pv_hlm.get() # if both high/low limits are equal they are meaningless if not short_name: short_name = self._pv_name.value self.short_name = short_name self.set_motor_validator() self._drive_val.setText(self._pv_drive.get(as_string=True)) self._drive_val.returnPressed.connect(self.move_abs) 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(self._pv_tweak_val.get(as_string=True)) 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._pv_llm.get(), self._pv_hlm.get() if min == max: min = -1e6 max = 1e6 lineedit.setValidator(QDoubleValidator(min, max, 5, lineedit)) def move_rel(self, dist, delay=None): cur = self._pv_readback.get() target = cur + dist if delay: sleep(delay) self._pv_drive.put(target) def get_rbv(self): return self._pv_readback.get(use_monitor=False) def get_val(self): # return self._motor.get('VAL') try: v=self._val except AttributeError: self._val=v=self._pv_drive.get() return v def is_homed(self): pend_event() return 1 == self._pv_is_homed.get(use_monitor=False) 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 self._pv_status.get(as_string=as_string, use_monitor=False) def wait(self): pend_event(.01) while not self.is_done(): pend_event(0.2) def is_done(self): return self._pv_status.get() in (0, 3) def move_abs(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._pv_drive.put(drive) if wait: self.wait() if assert_position: assert_tweaker_positions([(self, drive, 0.05)], timeout=10.) def emit_signals(self, **kw): field = kw['source_field'] if field == 'VAL': self.event_val.emit(self._pvname, kw) elif field == 'RBV': self.event_rbv.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.short_name, precision=self._prec, units=self._units) self.label.setText(target[self._label_style].format(rbv=self._pv_readback.get())) 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)