SFELPHOTON-1527: Refactor: Improve ARESvis.py for robustness and thread-safety
Addressed several issues in ARESvis.py: - Implemented thread-safe GUI updates using pyqtSignals for EPICS callbacks. - Enhanced function with null checks and value clipping. - Improved image loading in with file existence checks and logging. - Refactored to use widget dictionaries, avoiding . - Cleaned up and methods, removing dead code and using widget dictionaries. - Updated Python version check to exit on failure.
This commit is contained in:
@@ -53,8 +53,8 @@ import numpy as np
|
||||
import sys, logging, copy
|
||||
import epics
|
||||
|
||||
if sys.version_info[0] < 3 or sys.version_info[1] < 6:
|
||||
print(f"Must be using Python 3.6 or newer. Try e.g. /opt/gfa/python-3.8/latest/bin/python /sf/furka/bin/ARESvis")
|
||||
if sys.version_info < (3, 6):
|
||||
sys.exit(f"Must be using Python 3.6 or newer. Try e.g. /opt/gfa/python-3.8/latest/bin/python {sys.argv[0]}")
|
||||
|
||||
_log=logging.getLogger(__name__)
|
||||
|
||||
@@ -104,18 +104,23 @@ class logHandler(logging.StreamHandler):
|
||||
self.handleError(record)
|
||||
|
||||
def adjust_transparency(pixmap, transparency_factor):
|
||||
"ChatGPT: Adjust the transparency of a QPixmap."
|
||||
# Convert QPixmap to QImage for pixel manipulation
|
||||
image=pixmap.toImage()
|
||||
image=image.convertToFormat(QImage.Format_ARGB32)
|
||||
# Convert QImage to a NumPy array
|
||||
width=image.width()
|
||||
height=image.height()
|
||||
ptr=image.bits()
|
||||
ptr.setsize(image.byteCount())
|
||||
arr=np.frombuffer(ptr, dtype=np.uint8).reshape((height, width, 4))
|
||||
arr[:, :, 3]=(arr[:, :, 3]*transparency_factor).astype(np.uint8)
|
||||
return QPixmap.fromImage(QImage(arr.data, width, height, image.bytesPerLine(), QImage.Format_ARGB32))
|
||||
"Adjust the transparency of a QPixmap."
|
||||
image = pixmap.toImage().convertToFormat(QImage.Format_ARGB32)
|
||||
if image.isNull():
|
||||
return pixmap
|
||||
|
||||
# Convert QImage to a NumPy array for efficient manipulation
|
||||
width = image.width()
|
||||
height = image.height()
|
||||
ptr = image.bits()
|
||||
ptr.setsize(image.byteCount()) # Make the sip.voidptr object aware of its size
|
||||
arr = np.frombuffer(ptr, dtype=np.uint8).reshape((height, width, 4))
|
||||
|
||||
# Modify the alpha channel, ensuring values are within the valid 0-255 range
|
||||
arr[:, :, 3] = (arr[:, :, 3] * transparency_factor).clip(0, 255).astype(np.uint8)
|
||||
|
||||
# The numpy array modifies the QImage's buffer in place. Return a new QPixmap from the modified image.
|
||||
return QPixmap.fromImage(image)
|
||||
|
||||
class ARESdevice():
|
||||
|
||||
@@ -149,20 +154,25 @@ class ARESdevice():
|
||||
self.setGeometry(g)
|
||||
self._pic=pic=dict()
|
||||
base=os.path.join(os.path.dirname(os.path.realpath(__file__)),'pic')
|
||||
print(base)
|
||||
for k,v in (
|
||||
('ir','ARES_2Theta_InnerRing.png'),
|
||||
('or','ARES_2Theta_OuterRing.png'),
|
||||
('bl','ARES_Below.png'),
|
||||
('df','ARES_Diffractometer.png'),
|
||||
('di','ARES_Diode.png'),
|
||||
('jf','ARES_Jungfrau.png'),
|
||||
('lm','ARES_LaserMirror.png'),
|
||||
('ma','ARES_Master.png'),
|
||||
('mi','ARES_Mirrors.png'),
|
||||
('pb','ARES_Parabola.png'),
|
||||
):
|
||||
pic[k]=adjust_transparency(QPixmap(os.path.join(base,v)),.7)
|
||||
_log.debug(f"Loading pictures from: {base}")
|
||||
pic_files = {
|
||||
'ir':'ARES_2Theta_InnerRing.png',
|
||||
'or':'ARES_2Theta_OuterRing.png',
|
||||
'bl':'ARES_Below.png',
|
||||
'df':'ARES_Diffractometer.png',
|
||||
'di':'ARES_Diode.png',
|
||||
'jf':'ARES_Jungfrau.png',
|
||||
'lm':'ARES_LaserMirror.png',
|
||||
'ma':'ARES_Master.png',
|
||||
'mi':'ARES_Mirrors.png',
|
||||
'pb':'ARES_Parabola.png',
|
||||
}
|
||||
for k, v in pic_files.items():
|
||||
path = os.path.join(base, v)
|
||||
if os.path.exists(path):
|
||||
pic[k] = adjust_transparency(QPixmap(path), .7)
|
||||
else:
|
||||
_log.warning(f"Pixmap file not found: {path}")
|
||||
|
||||
def setGeometry(self,geo):
|
||||
self._geo=geo
|
||||
@@ -464,6 +474,9 @@ class ARESdevice():
|
||||
|
||||
|
||||
class WndVisualize(QWidget):
|
||||
updateSignal = pyqtSignal(str, float, str)
|
||||
connectionSignal = pyqtSignal(str, bool)
|
||||
|
||||
_pv2key={
|
||||
'SATES30-RIXS:MOT_RY.RBV' :'aArm',
|
||||
'SATES30-ARES:MOT_2TRY.RBV':'a2Th',
|
||||
@@ -480,11 +493,12 @@ class WndVisualize(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.initUI()
|
||||
self.updateSignal.connect(self.vis_update)
|
||||
self.connectionSignal.connect(self.update_connection_status)
|
||||
self.connectEPICS()
|
||||
# self.update() must be called in main thread. Else gui may block after some time
|
||||
# PV monitoring is done in a separate thread. Therefore functions as:
|
||||
# OnConnectionChange, OnValueChange MUST NOT call update, but use this Signal helper class.
|
||||
#self.updSignal.connect(lambda key,value,col: self.vis_update(key,value,col))
|
||||
|
||||
def initUI(self):
|
||||
self.setGeometry(560, 100, 1300, 800)
|
||||
@@ -496,6 +510,10 @@ class WndVisualize(QWidget):
|
||||
row=0
|
||||
lg=QGridLayout(w)
|
||||
pDev=dev._paint
|
||||
|
||||
self.labels = {}
|
||||
self.sliders = {}
|
||||
|
||||
for key, rng, tk in (
|
||||
('aArm' ,( 0,360,), 30), # angle ARES sliding seal
|
||||
('a2Th' ,( 0,360,), 30), # angle 2thetha
|
||||
@@ -512,7 +530,8 @@ class WndVisualize(QWidget):
|
||||
#('y', (-100, 100), 10),
|
||||
):
|
||||
|
||||
wLb=QLabel(f"<font>{key}</font>",objectName=key) #MOST BE with <font> as it is changed later and else creates a seg fault
|
||||
wLb=QLabel(key, objectName=key)
|
||||
self.labels[key] = wLb
|
||||
|
||||
wSl=QSlider(Qt.Horizontal,objectName=key)
|
||||
wSl.setFixedWidth(200);wSl.setMinimum(rng[0]);wSl.setMaximum(rng[1])
|
||||
@@ -529,6 +548,7 @@ class WndVisualize(QWidget):
|
||||
wSl.setValue(v)
|
||||
wSl.setTickPosition(QSlider.TicksBelow);wSl.setTickInterval(tk)
|
||||
wSl.valueChanged.connect(lambda val,key=key: self.sldChanged(key,val))
|
||||
self.sliders[key] = wSl
|
||||
|
||||
lg.addWidget(wLb, row, 0)
|
||||
lg.addWidget(wSl, row, 1);row+=1
|
||||
@@ -581,11 +601,16 @@ class WndVisualize(QWidget):
|
||||
if pvc>0: pvc-=1
|
||||
_log.info(f'PV connection {pvc}/{len(self._pvDict)}: {pvname} {conn}')
|
||||
self._pvConnected=pvc
|
||||
self.connectionSignal.emit(pvname, conn)
|
||||
|
||||
def update_connection_status(self, pvname, conn):
|
||||
key = self._pv2key.get(pvname)
|
||||
if not key:
|
||||
return
|
||||
wLb = self.labels.get(key)
|
||||
if not wLb:
|
||||
return
|
||||
|
||||
app=QApplication.instance()
|
||||
wGrp=app._wndVisualize._wdGrpDraw
|
||||
key=self._pv2key[pvname]
|
||||
wLb=wGrp.findChild(QLabel, key)
|
||||
if not conn:
|
||||
v=f"<font color='#a00000'>{key}</font>"
|
||||
wLb.setText(v)
|
||||
@@ -594,36 +619,27 @@ class WndVisualize(QWidget):
|
||||
wLb.setText(v)
|
||||
|
||||
def OnValueChange(self, pvname, value, **kw):
|
||||
#_log.info(kw) #{
|
||||
# 'pvname': 'SATES30-ARES:MOT_2TRY.RBV', 'value': 102.507, 'char_value': '102.5070', 'status': 0, 'ftype': 20,
|
||||
# 'chid': 26100744, 'host': 'localhost:5064', 'count': 1, 'access': 'read-only', 'write_access': False, 'read_access': True,
|
||||
# 'severity': 0, 'timestamp': 1724915455.46745, 'posixseconds': 1724915455.0, 'nanoseconds': 467450100, 'precision': 4,
|
||||
# 'units': 'deg', 'enum_strs': None, 'upper_disp_limit': 0.0, 'lower_disp_limit': 0.0, 'upper_alarm_limit': nan,
|
||||
#'lower_alarm_limit': nan, 'lower_warning_limit': nan, 'upper_warning_limit': nan, 'upper_ctrl_limit': 0.0,
|
||||
#'lower_ctrl_limit': 0.0, 'nelm': 1, 'type': 'time_double', 'typefull': 'time_double',
|
||||
#'cb_info': (1, <PV 'SATES30-ARES:MOT_2TRY.RBV', count=1, type=time_double, access=read-only>)
|
||||
#}
|
||||
_log.info(f"PV val:{pvname}:{value}")
|
||||
try:
|
||||
key=self._pv2key[pvname]
|
||||
except KeyError as e:
|
||||
_log.warning(f"can't handle PV: {pvname}:{value}")
|
||||
return
|
||||
#self._updSignal.emit() # self.update() would block after some time
|
||||
self.vis_update(key,value,'008000')
|
||||
self.updateSignal.emit(key,value,'008000')
|
||||
|
||||
def vis_update(self,key,value,col='000000'):
|
||||
app=QApplication.instance()
|
||||
wGrp=app._wndVisualize._wdGrpDraw
|
||||
wSl=wGrp.findChild(QSlider, key)
|
||||
wSl.blockSignals(True)
|
||||
wSl.setValue(int(value)) # move the slider without emiting a signal
|
||||
wSl.blockSignals(False)
|
||||
wSl = self.sliders.get(key)
|
||||
if wSl:
|
||||
wSl.blockSignals(True)
|
||||
wSl.setValue(int(value)) # move the slider without emiting a signal
|
||||
wSl.blockSignals(False)
|
||||
|
||||
self.sldChanged(key,value) # emit the signal
|
||||
#self.event_update.emit(pvname=pvname, value=None)
|
||||
wLb=wGrp.findChild(QLabel, key)
|
||||
v=f"<font color='#{col}'>{key}</font>"
|
||||
wLb.setText(v)
|
||||
|
||||
wLb = self.labels.get(key)
|
||||
if wLb:
|
||||
v=f"<font color='#{col}'>{key}</font>"
|
||||
wLb.setText(v)
|
||||
|
||||
def liveView(self):
|
||||
# try to live update all PVs
|
||||
@@ -651,7 +667,6 @@ class WndVisualize(QWidget):
|
||||
dev=app._dev
|
||||
p=dev._paint
|
||||
if key.startswith('scl'):
|
||||
wGrp=self._wdGrpDraw
|
||||
if key[-1]=='A':
|
||||
p['sclTrf']=(2**(val/2),p['sclTrf'][1])
|
||||
else:
|
||||
@@ -661,18 +676,12 @@ class WndVisualize(QWidget):
|
||||
else:
|
||||
p[key]=val
|
||||
g=dev._geo
|
||||
if key in ('r1','r2'):
|
||||
sclD=p['sclTrf'][1]
|
||||
g[key]=val/sclD
|
||||
elif key in ('aa', 'bb'):
|
||||
sclA=p['sclTrf'][0]
|
||||
g[key]=90-val/sclA
|
||||
else:
|
||||
g[key]=val
|
||||
wGrp=app._wndVisualize._wdGrpDraw
|
||||
wLb=wGrp.findChild(QLabel, key)
|
||||
v=f"<font color='#0000ff'>{key}</font>"
|
||||
wLb.setText(v)#print(wLb,v)
|
||||
g[key]=val
|
||||
|
||||
wLb = self.labels.get(key)
|
||||
if wLb:
|
||||
v=f"<font color='#0000ff'>{key}</font>"
|
||||
wLb.setText(v)
|
||||
|
||||
self.update()
|
||||
|
||||
@@ -736,9 +745,7 @@ class WndVisualize(QWidget):
|
||||
devP=dev._paint
|
||||
wGrp=self._wdGrpDraw
|
||||
wGrp.setTitle(dev._name)
|
||||
for wSl in wGrp.findChildren(QSlider):
|
||||
# _log.info(wSl)
|
||||
key=wSl.objectName()
|
||||
for key, wSl in self.sliders.items():
|
||||
if key.startswith('scl'):
|
||||
if key[-1]=='A':
|
||||
v=devP['sclTrf'][0]
|
||||
|
||||
Reference in New Issue
Block a user