add epics widgets, CustomROI

This commit is contained in:
2022-07-14 15:10:49 +02:00
parent 98297263bc
commit 7445a5aae6
8 changed files with 1694 additions and 0 deletions

627
CustomROI.py Normal file
View File

@@ -0,0 +1,627 @@
import logging
from PyQt5 import QtGui, QtCore
from PyQt5.QtGui import QColor, QTransform
from PyQt5.QtWidgets import QMenu, QAction, QSpinBox, QMenu
from GenericDialog import GenericDialog
from pyqtgraph import ROI, Point, RectROI, mkPen
from pyqtgraph import functions as fn
from app_config import settings
from math import *
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class Retangulo(ROI):
"""
Rectangular ROI subclass with a single scale handle at the top-right corner.
============== =============================================================
**Arguments**
pos (length-2 sequence) The position of the ROI origin.
See ROI().
size (length-2 sequence) The size of the ROI. See ROI().
centered (bool) If True, scale handles affect the ROI relative to its
center, rather than its origin.
sideScalers (bool) If True, extra scale handles are added at the top and
right edges.
\**args All extra keyword arguments are passed to ROI()
============== =============================================================
"""
def __init__(self, pos, size, centered=False, sideScalers=False, **args):
ROI.__init__(self, pos, size, **args)
if centered:
center = [0.5, 0.5]
else:
center = [0, 0]
self.addScaleRotateHandle([0, 0.5], [1, 0.5])
self.addScaleHandle([1, 1], center)
if sideScalers:
self.addScaleHandle([1, 0.5], [center[0], 0.5])
self.addScaleHandle([0.5, 1], [0.5, center[1]])
class Grid(ROI):
"""a grid"""
def __init__(
self,
x_step,
y_step,
x_offset,
y_offset,
grid_index=0,
parent=None,
gonio_xy=(0, 0),
**kargs
):
self._grid_index = grid_index
self._size = None
self._pos = None
self._shape = None
ROI.__init__(self, (500, 500), (300, 300), **kargs)
self.parent = parent
self.addScaleHandle([1, 1], [0, 0])
self.addScaleHandle([0, 0], [1, 1])
self.addScaleRotateHandle([1, 0], [0, 0])
# self.addScaleRotateHandle([1, 0.5], [0.5, 0.5])
self._steps = x_step / 1000., y_step / 1000.
self._offsets = x_offset / 1000, y_offset / 1000
self._beam_position = parent.get_beam_mark_on_camera_xy()
self.set_gonio_xy(*gonio_xy)
self.setToolTip("grid")
parent.pixelsPerMillimeter.connect(self.update_ppm)
parent.beamCameraCoordinatesChanged.connect(self.update_beam_position)
self.enable_stage_tracking()
self._ppm = parent.getPpm()
self._mm_size = 300 / self._ppm, 300 / self._ppm
self._original_ppm = self._ppm # marker only works at original zoom
# because zoom axis is not aligned
# with viewing axis
self._fx, self._fy = self.parent.getFastX(), self.parent.getFastY()
self.invertible = True
self.sigRegionChanged.connect(self.invalidate)
self.sigRegionChangeFinished.connect(self.calculate_gonio_xy)
self.scaleSnap = False
self.aspectLocked = False
self.translatable = True
self.rotateAllowed = False
self.removable = True
self.follow_stage()
# self.calculate_gonio_xy()
def enable_stage_tracking(self):
self.parent.fast_x_position.connect(self.follow_x)
self.parent.fast_y_position.connect(self.follow_y)
def disable_stage_tracking(self):
self.parent.fast_x_position.disconnect(self.follow_x)
self.parent.fast_y_position.disconnect(self.follow_y)
def follow_x(self, v):
self.follow_stage(fx=v)
def follow_y(self, v):
self.follow_stage(fy=v)
def removeClicked(self):
self.parent.pixelsPerMillimeter.disconnect(self.update_ppm)
self.parent.beamCameraCoordinatesChanged.disconnect(self.update_beam_position)
self.parent.fast_x_position.disconnect(self.follow_x)
self.parent.fast_y_position.disconnect(self.follow_y)
super(Grid, self).removeClicked()
def calculate_gonio_xy(self):
self.update_grid_pos()
state = self.getState()
angle = state["angle"]
w, h = state["size"]
w, h = 1000 * w / self._ppm, 1000 * h / self._ppm
self._mm_size = w / 1000, h / 1000
self.calculate_targets()
def update_ppm(self, ppm):
self._ppm = ppm
self.update_grid_size(ppm)
self.update_grid_pos()
self.stateChanged()
def update_grid_size(self, ppm, finish=False):
mmw, mmh = self._mm_size
w, h = mmw * ppm, mmh * ppm
self.setSize((w, h), update=False)
def follow_stage(self, fx=None, fy=None):
self._fx, self._fy = self.parent.getFastX(), self.parent.getFastY()
self.update_grid_pos()
self.stateChanged()
def set_gonio_xy(self, x, y):
self._gx, self._gy = x, y
def update_grid_pos(self):
idx = self._grid_index
ppm = self._ppm
bx, by = self.parent.get_beam_mark_on_camera_xy()
gx, gy = self._gx, self._gy # marker position in gonio coords
fx, fy = self._fx, self._fy
dx, dy = (
ppm * (fx - gx),
ppm * (fy - gy),
) # distance to beam in pixels for new ppm
x, y = bx - dx, by + dy
self.setPos(x, y, update=False)
mmw, mmh = self._mm_size
def calculate_targets(self):
state = self.getState()
angle = radians(state["angle"])
cosa = cos(angle)
sina = sin(angle)
w, h = state["size"]
xs, ys = self._steps
w, h = w / self._ppm, h / self._ppm # mm
gx, gy = self._gx, self._gy
nx = int(abs(w) / xs)
ny = int(abs(h) / ys)
points = []
dx = xs
dy = ys
for yi in range(ny):
ly = yi * dy
y = gy + ly
for xi in range(nx):
lx = xi * dx
x = gx + lx
l = sqrt((lx) ** 2 + (ly) ** 2)
try:
alpha = acos((lx) / l)
except:
alpha = 0
x2 = gx + l * cos(alpha + angle)
y2 = gy - l * sin(alpha + angle)
# print(
# "l={:8.3f} alpha={:.3f} xy={:8.3f}{:8.3f} xy2={:8.3f}{:8.3f}".format(
# l, degrees(alpha), x, y, x2, y2
# )
# )
points.append((x2, y2))
self._grid_dimensions = (nx, ny)
self._points = points
self.parent.gridUpdated.emit(self._grid_index)
def get_sorted_grid_targets(self):
pass
def get_grid_targets(self):
return self._points
def update_beam_position(self, pos):
self._beam_position = pos
def invalidate(self):
state = self.getState()
x, y = state["pos"]
w, h = state["size"]
self._mm_size = w / self._ppm, h / self._ppm
fx, fy = self.camera2gonio(x, y)
self.set_gonio_xy(fx, fy)
self._shape = None
self.prepareGeometryChange()
def boundingRect(self):
return self.shape().boundingRect()
def shape(self):
w, h = self.getState()["size"]
if self._shape is None:
radius = self.getState()["size"][1]
p = QtGui.QPainterPath()
p.addRect(0, 0, w, h)
stroker = QtGui.QPainterPathStroker()
stroker.setWidth(10)
outline = stroker.createStroke(p)
self._shape = outline
return self._shape
def paint(self, p, *args):
state = self.getState()
w, h = state["size"]
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(fn.mkPen(width=5, color=[200, 200, 100]))
p.drawRect(0, 0, w, h)
p.setPen(fn.mkPen(width=3, color=[200, 100, 100]))
ppm = self._ppm
xs, ys = self._steps
w1, h1 = w / ppm, h / ppm # mm
nx = int(abs(w1) / xs)
ny = int(abs(h1) / ys)
sw = (xs) * ppm
sh = (ys) * ppm
# print((nx,ny), (w1, h1), (sw, sh))
r = 10
a, b = state["pos"]
if nx * ny > 1000:
return
for y in range(ny):
for x in range(nx):
p.drawEllipse((x * sw) - r, (y * sh) - r, 2 * r, 2 * r)
def camera2gonio(self, x, y):
bx, by = self.parent.get_beam_mark_on_camera_xy()
ppm = self._ppm
dx, dy = (x - bx) / ppm, (y - by) / ppm
fx, fy = self._fx + dx, self._fy - dy
return fx, fy
def gonio2camera(self, x, y):
ppm = self._ppm
bx, by = self.parent.get_beam_mark_on_camera_xy()
gx, gy = self._gx, self._gy # marker position in gonio coords
fx, fy = self._fx, self._fy
dx, dy = (
ppm * (fx - gx),
ppm * (fy - gy),
) # distance to beam in pixels for new ppm
x, y = bx - dx, by + dy
return x, y
class BeamMark(ROI):
"""A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable."""
def __init__(self, pos=None, size=None, parent=None, **kargs):
if size is None:
size = [20, 20]
if pos is None:
pos = [0, 0]
self._size = size
self._pos = pos
self._shape = None
ROI.__init__(self, pos, size, **kargs)
parent.pixelsPerMillimeter.connect(self.my_update_ppm)
parent.beamCameraCoordinatesChanged.connect(lambda x, y: self.setPos((x, y)))
self._scale_handler = None
self.sigRegionChanged.connect(self.invalidate)
self.aspectLocked = False
self.translatable = False
self.rotateAllowed = False
self.invertible = False
self.removable = False
self.scaleSnap = True
self.snapSize = 1
self.sigRegionChangeFinished.connect(self.show_size)
self.flip_ud()
def show_size(self):
try:
ppm = self._ppm
except:
return
w, h = self.getState()["size"]
# convert to micrometer
nw, nh = (w * 1000) / ppm, (h * 1000) / ppm
logger.debug("persisting new beam marker size: {}".format((nw, nh)))
settings.setValue("beam/size", (nw, nh))
def toggle_handle(self):
if self._scale_handler is None:
logger.debug("adding beam marker resize handle")
self._scale_handler = self.addScaleHandle([0.5, 0.5], [0, 0])
else:
logger.debug("removing beam marker resize handle")
self.removeHandle(self._scale_handler)
self._scale_handler = None
def get_camera_beam_position(self):
return self.getState()["pos"]
def set_beam_size_marker_dialog(self):
w, h = settings.value("beam/size")
d = GenericDialog(
title="Beamsize",
message="Enter the size of the beam in microns",
inputs={
"width": ("Width (\u00B5)", int(w), QSpinBox()),
"height": ("Height (\u00B5)", int(h), QSpinBox()),
},
)
if d.exec():
results = d.results
logger.info("Updating beamsize to {}".format(results))
w, h = results["width"], results["height"]
logger.debug("types {}".format(type(w)))
settings.setValue("beam/size", (w, h))
self.set_beam_size((w, h))
def flip_ud(self):
"""flip up-down"""
t = self.transform()
m11 = t.m11()
m12 = t.m12()
m13 = t.m13()
m21 = t.m21()
m22 = t.m22()
m23 = t.m23()
m31 = t.m31()
m32 = t.m32()
m33 = t.m33()
t.setMatrix(m11, m12, m13, m21, -m22, m23, m31, m32, m33)
self.setTransform(t)
def set_beam_size(self, size=None):
if size is not None:
self._size = size
ppm = self._ppm
w, h = self._size
nw, nh = (w / 1000) * ppm, (h / 1000) * ppm
self.setSize((nw, nh))
self.setToolTip("Beamsize (W x H): {:.1f}\u00B5 x {:.1f}\u00B5".format(w, h))
def my_update_ppm(self, ppm):
self._ppm = ppm
self.set_beam_size()
def invalidate(self):
self._shape = None
self.prepareGeometryChange()
def boundingRect(self):
return self.shape().boundingRect()
def shape(self):
w, h = self.size()
if self._shape is None:
p = QtGui.QPainterPath()
p.moveTo(Point(0, -h))
p.lineTo(Point(0, h))
p.moveTo(Point(-w, 0))
p.lineTo(Point(w, 0))
#
p.moveTo(Point(-w, -h))
p.lineTo(Point(w, -h))
p.lineTo(Point(w, h))
p.lineTo(Point(-w, -h))
stroker = QtGui.QPainterPathStroker()
outline = stroker.createStroke(p)
self._shape = outline
return self._shape
def paint(self, p, *args):
w, h = self.getState()["size"]
v1, v2 = -(h * 0.8) / 2, (h * 0.8) / 2
h1, h2 = -(w * 0.8) / 2, (w * 0.8) / 2
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(fn.mkPen(width=3, color=[200, 100, 100]))
p.drawLine(Point(0, v1), Point(0, v2))
p.drawLine(Point(h1, 0), Point(h2, 0))
p.setPen(fn.mkPen(width=3, color=[200, 200, 100]))
p.drawRect(-w / 2, -h / 2, w, h)
# p.drawText(-w, -h, '{:.0f}x{:.0f}'.format(*self._size))
class CrystalCircle(ROI):
"""A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable."""
def __init__(
self,
pos=None,
size=None,
parent=None,
gonio_xy=(0, 0),
model_row_index=1,
is_fiducial=False,
ppm=False,
**kargs
):
if size is None:
size = [20, 20]
if pos is None:
pos = [0, 0]
self._size = size
self._shape = None
self.parent = parent
self._model_row_index = model_row_index
self._is_fiducial = is_fiducial
ROI.__init__(self, pos, size, **kargs)
self._beam_position = parent.get_beam_mark_on_camera_xy()
self.set_gonio_xy(*gonio_xy)
self.setToolTip(f"{'fiducial' if is_fiducial else 'crystal'} {model_row_index}")
# parent.pixelsPerMillimeter.connect(self.update_pixels_per_millimeter)
# parent.beamCameraCoordinatesChanged.connect(self.update_beam_position)
# parent.fast_x_position.connect(self.follow_x)
# parent.fast_y_position.connect(self.follow_y)
self._ppm = ppm
self._original_ppm = ppm # marker only works at original zoom
# because zoom axis is not aligned
# with viewing axis
# self.calculate_gonio_xy()
self.sigRegionChanged.connect(self.invalidate)
# self.sigRegionChangeFinished.connect(self.calculate_gonio_xy)
# self.sigHoverEvent.connect(self.my_hovering)
self.aspectLocked = False
self.translatable = True
self.rotateAllowed = False
self.invertible = False
self.deletable = False
def enable_stage_tracking(self):
self.parent.fast_x_position.connect(self.follow_x)
self.parent.fast_y_position.connect(self.follow_y)
def disable_stage_tracking(self):
self.parent.fast_x_position.disconnect(self.follow_x)
self.parent.fast_y_position.disconnect(self.follow_y)
def follow_x(self, v):
self.follow_stage(fx=v)
def follow_y(self, v):
self.follow_stage(fy=v)
def my_hovering(self, event):
print("hovering: {}".format(self.mouseHovering))
def get_model_row(self):
return self._model_row_index
def update_ppm(self, ppm):
self._ppm = ppm
self.recalc_camera_position()
def follow_stage(self, fx=None, fy=None):
self._fx, self._fy = self.parent.getFastX(), self.parent.getFastY()
self.recalc_camera_position()
def set_gonio_xy(self, x, y):
self._gx, self._gy = x, y
def disconnect_signals(self):
self.parent.pixelsPerMillimeter.disconnect(self.update_ppm)
self.parent.beamCameraCoordinatesChanged.disconnect(self.update_beam_position)
self.parent.fast_x_position.disconnect(self.follow_x)
self.parent.fast_y_position.disconnect(self.follow_y)
def reconnect_signals(self):
self.parent.pixelsPerMillimeter.connect(self.update_ppm)
self.parent.beamCameraCoordinatesChanged.connect(self.update_beam_position)
self.parent.fast_x_position.connect(self.follow_x)
self.parent.fast_y_position.connect(self.follow_y)
def removeClicked(self):
self.disconnect_signals()
super(CrystalCircle, self).removeClicked()
def recalc_camera_position(self):
idx = self._model_row_index
ppm = self._ppm
bx, by = self.parent.get_beam_mark_on_camera_xy()
gx, gy = self._gx, self._gy # marker position in gonio coords
fx, fy = self._fx, self._fy
# distance to beam in pixels for new ppm
dx, dy = (ppm * (fx - gx), ppm * (fy - gy))
x, y = bx - dx, by + dy
# logger.debug(f"recalc {idx}: cam = {(x,y)}, gonio = {(gx, gy)}")
self.setPos(x, y)
def update_beam_position(self, pos):
self._beam_position = pos
def show_size(self):
try:
ppm = self._ppm
except:
return
w, h = self.getState()["size"]
# convert to micrometer
nw, nh = (w * 1000) / ppm, (h * 1000) / ppm
def invalidate(self):
self._shape = None
self.prepareGeometryChange()
def boundingRect(self):
return self.shape().boundingRect()
def shape(self):
if self._shape is None:
radius = self.getState()["size"][1]
p = QtGui.QPainterPath()
p.moveTo(Point(0, -radius))
p.lineTo(Point(0, radius))
p.moveTo(Point(-radius, 0))
p.lineTo(Point(radius, 0))
stroker = QtGui.QPainterPathStroker()
stroker.setWidth(10)
outline = stroker.createStroke(p)
self._shape = outline
return self._shape
def paint(self, p, *args):
if abs(self._ppm - self._original_ppm) < 1.:
if self._is_fiducial:
pen = mkPen(color="r", width=2)
else:
pen = mkPen(color="y", width=2)
else:
pen = mkPen(color="#ffffaa88", width=2)
x, y = self.getState()["size"]
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(pen)
p.drawLine(Point(0, -x), Point(0, x))
p.drawLine(Point(-x, 0), Point(x, 0))
p.drawEllipse(-x, -y, 2 * x, 2 * y)
class Crosshair(ROI):
"""A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable."""
def __init__(self, pos=None, size=None, **kargs):
if size is None:
size = [10, 10]
if pos is None:
pos = [0, 0]
self._shape = None
ROI.__init__(self, pos, size, **kargs)
self.sigRegionChanged.connect(self.invalidate)
self.aspectLocked = True
def invalidate(self):
self._shape = None
self.prepareGeometryChange()
def boundingRect(self):
return self.shape().boundingRect()
def shape(self):
if self._shape is None:
radius = self.getState()["size"][1]
p = QtGui.QPainterPath()
p.moveTo(Point(0, -radius))
p.lineTo(Point(0, radius))
p.moveTo(Point(-radius, 0))
p.lineTo(Point(radius, 0))
# p = self.mapToDevice(p)
stroker = QtGui.QPainterPathStroker()
stroker.setWidth(10)
outline = stroker.createStroke(p)
# self._shape = self.mapFromDevice(outline)
self._shape = outline
return self._shape
def paint(self, p, *args):
radius = self.getState()["size"][1]
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.currentPen)
p.drawLine(Point(0, -radius), Point(0, radius))
p.drawLine(Point(-radius, 0), Point(radius, 0))

51
app_config.py Normal file
View File

@@ -0,0 +1,51 @@
import yaml
from PyQt5.QtCore import QSettings
from pathlib import Path
import logging
logger = logging.getLogger(__name__)
settings = QSettings("Paul Scherrer Institut", "SwissMX")
inst_folder = Path(__file__).absolute().parent
config_file = inst_folder / "swissmx.yaml"
configs = yaml.load(config_file.read_text())
endstation = configs["configure_for"]
appsconf = configs[endstation]
simulated = appsconf.get("simulate", False)
logger.info(f"configuring for endstation: {endstation.upper()}")
if simulated:
logger.warning("SIMULATION is ACTIVE")
css_file = inst_folder / "swissmx.css"
def font(name: str) -> str:
p = Path(__file__).absolute().parent / "fonts" / name
return str(p)
def logo(size: int = 0) -> str:
p = Path(__file__).absolute().parent / "logos" / "logo.png"
if size:
p = Path(__file__).absolute().parent / "logos" / f"tell_logo_{size}x{size}.png"
return str(p)
def option(key: str) -> bool:
try:
return settings.value(key, type=bool)
except:
logger.error(f"option {key} not known")
return False
def toggle_option(key: str):
v = settings.value(key, type=bool)
settings.setValue(key, not v)
settings.sync()

104
app_utils.py Normal file
View File

@@ -0,0 +1,104 @@
import time
import logging
import enum
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
from epics.ca import pend_event
class DeltatauMotorStatus(enum.Flag):
"""names are taken from motorx_all.ui"""
DIRECTION = enum.auto()
MOTION_IS_COMPLETE = enum.auto()
PLUS_LIMIT_SWITCH = enum.auto()
HOME_SWITCH = enum.auto()
UNUSED_1 = enum.auto()
CLOSED_LOOP_POSITION = enum.auto()
SLIP_STALL_DETECTED = enum.auto()
AT_HOME_POSITION = enum.auto()
ENCODER_IS_PRESENT = enum.auto()
PROBLEM = enum.auto()
MOVING = enum.auto()
GAIN_SUPPORT = enum.auto()
COMMUNICATION_ERROR = enum.auto()
MINUS_LIMIT_SWITCH = enum.auto()
MOTOR_HAS_BEEN_HOMED = enum.auto()
AT_SWITCH = MINUS_LIMIT_SWITCH | PLUS_LIMIT_SWITCH
class PositionsNotReached(Exception):
pass
def assert_tweaker_positions(targets, timeout=60.0):
"""
wait for each of the given motors to have specified positions within tolerance
:param targets: [
(motor, target_position, tolerance),
...
]
:return: None
:raises: PositionsNotReached
"""
num_motors = len(targets)
timeisup = timeout + time.time()
while time.time() < timeisup:
count = 0
summary = []
for i, m in enumerate(targets):
motor, target, tolerance = m
name = motor.short_name
pend_event()
cur = motor.get_position()
done = motor.is_done()
logger.debug("check {}[done={}]: {} == {}".format(name, done, cur, target))
summary.append("{}[done={}]: {} == {}".format(name, done, cur, target))
if done and tolerance >= abs(cur - target):
count += 1
pend_event(0.1)
if count == num_motors:
break
pend_event(0.1)
if count != num_motors:
raise PositionsNotReached("failed to reach target positions: {}".format("#".join(summary)))
def assert_motor_positions(targets, timeout=60.0):
"""
wait for each of the given motors to have specified positions within tolerance
:param targets: [
(motor, target_position, tolerance),
...
]
:return: None
:raises: PositionsNotReached
"""
num_motors = len(targets)
timeisup = timeout + time.time()
while time.time() < timeisup:
count = 0
summary = []
for i, m in enumerate(targets):
motor, target, tolerance = m
name = motor._prefix
pend_event()
cur = motor.get_position(readback=True)
done = motor.done_moving
logger.debug("check {}[done={}]: {} == {}".format(name, done, cur, target))
summary.append("{}[done={}]: {} == {}".format(name, done, cur, target))
if done and tolerance >= abs(cur - target):
count += 1
pend_event(0.1)
if count == num_motors:
break
pend_event()
if count != num_motors:
raise PositionsNotReached("failed to reach target positions: {}".format("#".join(summary)))

355
epics_widgets/MotorTweak.py Normal file
View 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)

172
epics_widgets/MotorTweak.ui Normal file
View File

@@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Frame</class>
<widget class="QFrame" name="Frame">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>650</width>
<height>300</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>10</height>
</size>
</property>
<property name="windowTitle">
<string>Frame</string>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>2</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="_controlbox" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="QLineEdit" name="_drive_val">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>2</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="maxLength">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="tweak_reverse">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>35</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>↶</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="tweak_forward">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>35</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>↷</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="_tweak_val">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>2</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="jog_reverse">
<property name="text">
<string>◀</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="jog_forward">
<property name="text">
<string>▶</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,334 @@
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_readback = 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):
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_motor_to_position)
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_relative(self, dist, delay=None):
cur = self._pv_readback.get()
target = cur + dist
if delay:
sleep(delay)
self._pv_drive.put(target)
def get_position(self):
return self._pv_readback.get(use_monitor=False)
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_motor_to_position(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=3.)
def emit_signals(self, **kw):
field = kw['source_field']
if field == 'VAL':
self.event_val.emit(self._pvname, kw)
elif field == 'RBV':
self.event_readback.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)

View File

51
qutilities.py Normal file
View File

@@ -0,0 +1,51 @@
from PyQt5 import QtWidgets, QtGui
from PyQt5.QtWidgets import QWidget, QSizePolicy, QVBoxLayout, QDoubleSpinBox, QMessageBox
from app_config import option, toggle_option
def toggle_warn(key):
toggle_option(key)
m = f"Option {key} => {option(key)}"
QMessageBox.warning(None, m, m)
def horiz_spacer():
spacer = QWidget()
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
return spacer
def toggle_visibility(widget, state):
if state:
widget.show()
else:
widget.hide()
def get_sep_line():
line = QtWidgets.QFrame()
line.setFixedHeight(2)
line.setFrameShadow(QtWidgets.QFrame.Sunken)
line.setFrameShape(QtWidgets.QFrame.HLine)
return line
def check_font_by_id(id):
for family in QtGui.QFontDatabase.applicationFontFamilies(id):
print(family)
def toggle_groupbox(gbox, state):
widget = gbox.layout().itemAt(0).widget()
widget.setVisible(state)
def add_item_to_toolbox(toolbox, label, widget_list=[]):
block = QWidget()
block.setAccessibleName(label)
block.setContentsMargins(0, 0, 0, 0)
block.setLayout(QVBoxLayout())
for w in widget_list:
block.layout().addWidget(w)
block.layout().addStretch()
item = toolbox.addItem(block, label)
return item