add epics widgets, CustomROI
This commit is contained in:
627
CustomROI.py
Normal file
627
CustomROI.py
Normal 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
51
app_config.py
Normal 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
104
app_utils.py
Normal 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
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)
|
||||||
172
epics_widgets/MotorTweak.ui
Normal file
172
epics_widgets/MotorTweak.ui
Normal 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>
|
||||||
334
epics_widgets/SmaractMotorTweak.py
Normal file
334
epics_widgets/SmaractMotorTweak.py
Normal 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)
|
||||||
0
epics_widgets/__init__.py
Normal file
0
epics_widgets/__init__.py
Normal file
51
qutilities.py
Normal file
51
qutilities.py
Normal 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
|
||||||
Reference in New Issue
Block a user