343 lines
12 KiB
Python
343 lines
12 KiB
Python
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': '<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, *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)
|