From 80976e5b637f80b660a4597897b230b0e453e229 Mon Sep 17 00:00:00 2001 From: Thierry Zamofing Date: Mon, 17 Nov 2025 14:12:07 +0100 Subject: [PATCH] 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. --- ARESvis/ARESvis.py | 149 ++++++++++++++++++++++++--------------------- 1 file changed, 78 insertions(+), 71 deletions(-) diff --git a/ARESvis/ARESvis.py b/ARESvis/ARESvis.py index c3c5e73..a6a1670 100755 --- a/ARESvis/ARESvis.py +++ b/ARESvis/ARESvis.py @@ -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"{key}",objectName=key) #MOST BE with 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"{key}" 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, ) - #} _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"{key}" - wLb.setText(v) + + wLb = self.labels.get(key) + if wLb: + v=f"{key}" + 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"{key}" - wLb.setText(v)#print(wLb,v) + g[key]=val + + wLb = self.labels.get(key) + if wLb: + v=f"{key}" + 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]