add epics widgets, CustomROI
This commit is contained in:
355
epics_widgets/MotorTweak.py
Normal file
355
epics_widgets/MotorTweak.py
Normal file
@@ -0,0 +1,355 @@
|
||||
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 Motor
|
||||
from epics.ca import pend_event
|
||||
|
||||
from app_utils import DeltatauMotorStatus, assert_motor_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 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': '<b>{short_name}</b> <font color="#080">{{rbv:.{precision}f}} {units}</font>',
|
||||
'small': '<small>{short_name} <font size="small" color="#080">{{rbv:.{precision}f}} {units}</font><small>',
|
||||
'2 lines': '<b>{short_name}</b><br><font size="small" color="#080">{{rbv:.{precision}f}} {units}</font>',
|
||||
'busy': '<b>{short_name}</b> <font color="#080">{{rbv:.{precision}f}} {units}</font>'
|
||||
}
|
||||
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']:
|
||||
# logger.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']
|
||||
logger.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)
|
||||
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.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)
|
||||
Reference in New Issue
Block a user