import logging _log = logging.getLogger(__name__) import math 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 Motor from epics.ca import pend_event from app_utils import DeltatauMotorStatus, assert_motor_positions Ui_MotorTweak, QWidget = loadUiType('epics_widgets/MotorTweak.ui') SPMG_STOP = 0 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) 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(MotorTweak, 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, **kwargs): m = Motor(motor_base) m.get_position() self._ignore_limits = m.HLM == m.LLM # if both high/low limits are equal they are meaningless self.motor = m if not short_name: short_name = m.description self.short_name = short_name self._pvname = motor_base for attr in ['RTYP', 'JVEL', 'HLS', 'LLS', 'TWV', 'RBV', 'VAL', 'LVIO', 'HLM', 'LLM']: # _log.debug('connecting to field {}'.format(attr)) m.PV(attr, connect=True) self.set_motor_validator() self._drive_val.setText(m.get('VAL', as_string=True)) self._drive_val.returnPressed.connect(self.move_motor_to_position) tweak_min = kwargs.get("tweak_min", 0.0001) tweak_max = kwargs.get("tweak_max", 10.0) self._tweak_val.setText(m.get('TWV', as_string=True)) self._tweak_val.setValidator(QDoubleValidator(tweak_min, tweak_max, 4, self._tweak_val)) self._tweak_val.editingFinished.connect(lambda m=m: m.PV('TWV').put(self._tweak_val.text())) self.jog_reverse.hide() self.jog_forward.hide() # self.jog_forward.pressed.connect(lambda: self.jog('forward', True)) # self.jog_reverse.pressed.connect(lambda: self.jog('reverse', True)) # self.jog_forward.released.connect(lambda: self.jog('forward', False)) # self.jog_reverse.released.connect(lambda: self.jog('reverse', False)) self.tweak_forward.clicked.connect(lambda m: self.motor.tweak('forward')) self.tweak_reverse.clicked.connect(lambda m: self.motor.tweak('reverse')) self.bind_wheel() self.update_label_template() m.add_callback('VAL', self.set_val) m.add_callback('RBV', self.update_label) m.set_callback('HLS', self.update_label) m.set_callback('LLS', self.update_label) m.set_callback('VAL', self.emit_signals, {'source_field': 'VAL'}) m.set_callback('RBV', self.emit_signals, {'source_field': 'RBV'}) m.set_callback('HLS', self.emit_signals, {'source_field': 'HLS'}) m.set_callback('LLS', self.emit_signals, {'source_field': 'LLS'}) m.set_callback('LVIO', self.emit_signals, {'source_field': 'LVIO'}) m.set_callback('STAT', self.emit_signals, {'source_field': 'STAT'}) m.set_callback('HLM', self.set_motor_validator) def set_val(self, **kw): v = kw['char_value'] _log.debug('updating VAL = {}'.format(v)) self._drive_val.setText(v) def set_motor_validator(self, **kwargs): m = self.motor lineedit = self._drive_val min, max = m.PV('LLM').get(), m.PV('HLM').get() if min == max: min = -1e6 max = 1e6 lineedit.setValidator(QDoubleValidator(min, max, m.PV('PREC').get(), lineedit)) def move_relative(self, dist): self.motor.move(dist, ignore_limits=True, relative=True) def is_done(self): self.motor.refresh() return 1 == self.motor.done_moving def get_position(self): return self.motor.get_position(readback=True) def is_homed(self): self.motor.refresh() status = DeltatauMotorStatus(self.motor.motor_status) return bool(DeltatauMotorStatus.MOTOR_HAS_BEEN_HOMED & status) def move_motor_to_position(self, drive=None, wait=False, assert_position=False): if drive is None: drive = float(self._drive_val.text()) if assert_position: wait=True self.motor.move(drive, wait=wait, ignore_limits=True, relative=False) if assert_position: assert_motor_positions([(self.motor, drive, 0.1)], timeout=1) def emit_signals(self, **kw): ''' :param kw: info about the channel { 'access': 'read-only', 'char_value': '-0.105', 'chid': 36984304, 'count': 1, 'enum_strs': None, 'ftype': 20, 'host': 'SAR-CPPM-EXPMX1.psi.ch:5064', 'lower_alarm_limit': None, 'lower_ctrl_limit': 0.0, 'lower_disp_limit': 0.0, 'lower_warning_limit': None, 'motor_field': 'RBV', 'nelm': 1, 'precision': 3, 'pvname': 'SAR-EXPMX:MOT_ROT_Y.RBV', 'read_access': True, 'severity': 0, 'source_field': 'RBV', 'status': 0, 'timestamp': 1522314072.878331, 'type': 'time_double', 'typefull': 'time_double', 'units': b'deg', 'upper_alarm_limit': None, 'upper_ctrl_limit': 0.0, 'upper_disp_limit': 0.0, 'upper_warning_limit': None, 'value': -0.105, 'write_access': False} :return: ''' field = kw['motor_field'] src = kw['source_field'] kw['alias'] = self.short_name if field != src: return if field == 'VAL': self.event_val.emit(self._pvname, kw) elif field == 'RBV': self.event_readback.emit(kw['alias'], kw['value'], 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 update_label(self, **kwargs): m = self.motor self.label.setText(self._templates[self._label_style].format(rbv=m.readback)) self.jog_forward.setToolTip('jog forward at {:.3f} {}/s'.format(m.jog_speed, m.units)) self.jog_reverse.setToolTip('jog reverse at {:.3f} {}/s'.format(m.jog_speed, m.units)) self.tweak_forward.setToolTip('tweak forward by {:.3f} {}'.format(m.tweak_val, m.units)) self.tweak_reverse.setToolTip('tweak reverse by {:.3f} {}'.format(m.tweak_val, m.units)) def update_jog_speed(self, event): m = self.motor speed = m.jog_speed sign = math.copysign(1, event.angleDelta().y()) m.jog_speed = speed + (sign * 0.1 * speed) self.jog_forward.setToolTip('jog forward at {:.3f} {}/s'.format(m.jog_speed, m.units)) self.jog_reverse.setToolTip('jog reverse at {:.3f} {}/s'.format(m.jog_speed, m.units)) def tweak_event(self, event): m = self.motor sign = event.angleDelta().y() if sign < 0: m.tweak_reverse = 1 else: m.tweak_forward = 1 def bind_wheel(self): self.tweak_forward.wheelEvent = self.tweak_event self.tweak_reverse.wheelEvent = self.tweak_event def jog(self, direction, enable): m = self.motor if 'forw' in direction: m.jog_forward = int(enable) else: m.jog_reverse = int(enable) def contextMenuEvent(self, event): m = self.motor 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") changejogspeedAction = menu.addAction("Jog speed {:.3f} {}/s".format(m.jog_speed, m.units)) changetweakstepAction = menu.addAction("Tweak step {:.3f} {}".format(m.tweak_val, m.units)) 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: name = m.NAME prec = m.PREC msg = 'Precision for motor {}'.format(name) _log.debug('prec before %d', prec) prec, ok = QInputDialog.getInt(self, msg, msg, prec, 0, 10) _log.debug('prec after (%d) %d', ok, prec) if ok: self.motor.put('PREC', prec, wait=True) elif action == changejogspeedAction: name = m.NAME speed = m.jog_speed msg = 'Jog speed for motor {}'.format(name) speed, ok = QInputDialog.getDouble(self, msg, msg, speed, 0.01, m.slew_speed, m.precision) if ok: self.motor.put('JVEL', speed, wait=True) elif action == changetweakstepAction: name = m.NAME tv = m.tweak_val msg = 'Tweak step for motor {} at {} {}'.format(name, tv, m.units) tv, ok = QInputDialog.getDouble(self, msg, msg, tv, pow(10, -m.precision), m.slew_speed, m.precision) if ok: self.motor.put('TWV', tv, wait=True) elif action == tozeroAction: m.move(0.0, ignore_limits=True) elif action == stopmotorAction: self.motor.stop_go = SPMG_STOP if m.stop_go == SPMG_GO else SPMG_GO 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): return # FIXME disable for the time being if not self._auto_labels: return w, h = event.size().width(), event.size().height() if w < 400: self.jog_reverse.hide() self.jog_forward.hide() else: self.jog_reverse.show() self.jog_forward.show() self.update_label() def update_label_template(self): m = self.motor source = self._templates_source target = self._templates for k in source: target[k] = source[k].format( short_name=self.short_name, precision=m.PREC, units=m.units) self.label.setText(target[self._label_style].format(rbv=m.readback)) def paintEvent(self, e): qp = QPainter() qp.begin(self) qp.setRenderHint(QPainter.Antialiasing) self._draw_limits(qp) qp.end() def _draw_limits(self, qp): m = self.motor 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)