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:
2025-11-17 14:12:07 +01:00
parent 0136dfa466
commit 80976e5b63

View File

@@ -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]