Files
John Henry BealeandClaude Opus 5.5 2d9bbdac9d replace runtime print statements with logging
- all print calls outside the __main__ test blocks are now log calls:
  info for run information (CTA configuration summary on one line, run
  number, CTA start pid), warning/error for aborts and failures, debug for
  dumps (object/transform info, autofocus measurements, logger tree)
- psi_device: remove the import-time logging.basicConfig(level=DEBUG) and
  print('past logging'); the standalone test configures logging itself
- startup import timings are logged at info level
- ctadaq added to the -l log level list

Prints from PBTools/PBSwissMX, slic and the JFJ client are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:56:49 +02:00

468 lines
15 KiB
Python

import logging
_log = logging.getLogger(__name__)
import math, os.path
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, QToolTip
from PyQt5.uic import loadUiType
from epics import Motor
from app_utils import assert_tweaker_positions
Ui_MotorTweak, QWidget = loadUiType(os.path.join(os.path.dirname(__file__),'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_rbv = 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)
ui_set_drive_val = pyqtSignal(str)
ui_update_label = pyqtSignal()
ui_update_validator = pyqtSignal()
def __init__(self, parent=None):
super(MotorTweak, self).__init__(parent=parent)
self.setupUi(self)
self.ui_set_drive_val.connect(self._drive_val.setText, Qt.QueuedConnection)
self.ui_update_label.connect(self._update_label_ui, Qt.QueuedConnection)
self.ui_update_validator.connect(self._set_motor_validator_ui, Qt.QueuedConnection)
self._wheel_tweaks = True
self._auto_labels = True
self._locked = False
self._max_step = None
self._label_style = 'basic'
self._templates_source = {
'basic': '<b>{label}</b> <font color="#080">{{rbv:.{precision}f}} {units}</font>',
'small': '<small>{label} <font size="small" color="#080">{{rbv:.{precision}f}} {units}</font><small>',
'2 lines': '<b>{label}</b><br><font size="small" color="#080">{{rbv:.{precision}f}} {units}</font>',
'busy': '<b>{label}</b> <font color="#080">{{rbv:.{precision}f}} {units}</font>'
}
self._templates = {}
def connect_motor(self, rec_name, **kwargs):
# TODO: DO NOT USE Motor base class, but reduce to the only really needed PV: s.a. class QopticZoom(object)
# TODO: have a own motor Class as class SimMotor:
label=kwargs['label']
self._label=label
m = Motor(rec_name)
m.get_position()
self._ignore_limits = m.HLM == m.LLM # if both high/low limits are equal they are meaningless
self._motor = m
self._rec_name = rec_name
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_abs)
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 x: self.tweak('forward'))
self.tweak_reverse.clicked.connect(lambda x: self.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):
try: #pv-monitor-func
v = kw['char_value']
if isinstance(v, bytes):
v = v.decode('utf-8', errors='ignore')
_log.debug('updating VAL = {}'.format(v))
self._val = float(v)
self.ui_set_drive_val.emit(str(v))
except Exception as e:
_log.critical(f'{e}')
def set_motor_validator(self, **kwargs):
self.ui_update_validator.emit()
def _set_motor_validator_ui(self):
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 set_max_step(self, max_step):
'''limit the total user move to max_step from the current position (None: no limit)'''
self._max_step=max_step
self._ref_pos=None if max_step is None else self.get_rbv()
def _target_allowed(self, target):
max_step=self._max_step
if max_step is None or abs(target-self._ref_pos)<=max_step+1e-9:
return True
msg=f'{self._label}: total move {abs(target-self._ref_pos):.4g} exceeds the limit of {max_step:g} during data collection'
_log.warning(msg)
QToolTip.showText(self.mapToGlobal(self.rect().center()), msg, self)
return False
def tweak(self, direction):
m=self._motor
sign=1 if 'forw' in direction else -1
if self._max_step is None or self._target_allowed(m.get('VAL')+sign*m.tweak_val):
m.tweak(direction)
def move_rel(self, dist):
m=self._motor
if self._max_step is not None and not self._target_allowed(m.get_position(readback=True)+dist):
return
rbv=m.get_position(readback=True)
try:
self._val=rbv+dist
except AttributeError:
pass
m.move(self._val, ignore_limits=self._ignore_limits, relative=False)
#old code move relative to VAL not relative to RBV
#try:
# self._val+=dist
#except AttributeError:
# pass
#self._motor.move(dist, ignore_limits=True, relative=True)
def stop(self):
self._motor.stop()
def is_done(self):
m=self._motor
m.refresh()
return 1 == m.done_moving
def get_rbv(self):
#return self._motor.get('RBV')
m=self._motor
return m.get_position(readback=True)
def get_val(self):
#return self._motor.get('VAL')
try:
v=self._val
except AttributeError:
m=self._motor
self._val=v=m.get('VAL')
return v
def move_abs(self, drive=None, wait=False, assert_position=False):
if drive is None:
drive = float(self._drive_val.text())
if assert_position:
wait=True
if not self._target_allowed(drive):
return
try:
self._val=drive
except AttributeError:
pass
m=self._motor
m.move(drive, wait=wait, ignore_limits=self._ignore_limits, relative=False)
if assert_position:
assert_tweaker_positions([(self, 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:
'''
try: #pv-monitor-func
field = kw['motor_field']
src = kw['source_field']
kw['alias'] = self._label
if field != src:
return
if field == 'VAL':
self.event_val.emit(self._rec_name, kw)
elif field == 'RBV':
self.event_rbv.emit(kw['alias'], kw['value'], kw)
elif field == 'LVIO':
self.event_soft_limit.emit(self._rec_name, kw)
elif field == 'HLS':
self.event_high_hard_limit.emit(self._rec_name, kw)
self.event_axis_fault.emit(self._rec_name, kw)
elif field == 'LVIO':
self.event_low_hard_limit.emit(self._rec_name, kw)
self.event_axis_fault.emit(self._rec_name, kw)
elif field == 'STAT':
self.event_axis_fault.emit(self._rec_name, kw)
except Exception as e:
_log.critical(f'{e}')
def update_label(self, **kwargs):
try: #pv-monitor-func
self.ui_update_label.emit()
except Exception as e:
_log.critical(f'{e}')
def _update_label_ui(self):
try:
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))
except Exception as e:
_log.critical(f'{e}')
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 self._max_step is not None and not self._target_allowed(m.get('VAL')+(m.tweak_val if sign>=0 else -m.tweak_val)):
return
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._label)
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:
m.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:
m.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:
m.put('TWV', tv, wait=True)
elif action == tozeroAction:
self.move_abs(0.0)
elif action == stopmotorAction:
m.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(
label=self._label,
precision=m.PREC,
units=m.units)
try:
self.label.setText(target[self._label_style].format(rbv=m.readback))
except ValueError as e:
_log.error(f'{e}: label_style={self._label_style} readback={m.readback} motor={m}')
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)
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(module)s:%(lineno)d:%(funcName)s:%(message)s ')
_log.debug('start')
def cb_rbv(*args,**kwargs):
_log.debug(f'RBV{args} {kwargs}')
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel
app=QApplication(sys.argv)
mot_widget=MotorTweak()
#mot_widget.event_val.connect(cb_rbv)
mot_widget.event_rbv.connect(cb_rbv)
pfx='SAR-EXPMX'
mot_widget.connect_motor(f"{pfx}:MOT_FY", alias="fast_y", label="fast Y")
mot_widget.setGeometry(50, 50, 520, 100)
mot_widget.setWindowTitle("PyQt5 Example")
mot_widget.show()
sys.exit(app.exec_())