From 1887952f23ce2187af1360757a71c03b5116fc27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Sandstr=C3=B6m?= Date: Wed, 16 Aug 2023 15:04:06 +0200 Subject: [PATCH] WIP --- tools/ecmcArrayStat.py | 537 +++++++++++++++++++++ tools/ecmcGuiMain.py | 187 ++++++++ tools/ecmcMotionMainPyQtGraph.py | 33 +- tools/ecmcOneMotorGUI.py | 782 +++++++++++++++++++++++++++++++ tools/ecmcRTCanvas.py | 177 +++++++ tools/ecmcTrend.py | 199 ++++++++ tools/ecmcTrendPv.py | 70 +++ 7 files changed, 1978 insertions(+), 7 deletions(-) create mode 100644 tools/ecmcArrayStat.py create mode 100644 tools/ecmcGuiMain.py create mode 100644 tools/ecmcOneMotorGUI.py create mode 100644 tools/ecmcRTCanvas.py create mode 100644 tools/ecmcTrend.py create mode 100644 tools/ecmcTrendPv.py diff --git a/tools/ecmcArrayStat.py b/tools/ecmcArrayStat.py new file mode 100644 index 0000000..6cfae71 --- /dev/null +++ b/tools/ecmcArrayStat.py @@ -0,0 +1,537 @@ +#!/usr/bin/python3.6 +import sys +import epics +import numpy as np + +from PyQt5 import QtCore,QtWidgets, QtGui +from PyQt5.QtWidgets import * +from PyQt5.QtCore import * +from PyQt5.QtCore import QObject +from PyQt5.QtGui import * + +import random +import ecmcTrend +#import ecmcGraphWrapper as graphWrap +#import matplotlib as mpl +#mpl.use('Qt5Agg') +#import matplotlib.pyplot as plt +#import matplotlib.lines +from datetime import datetime +import threading + + +PARSE_ERROR_ELEMENT_COUNT_OUT_OF_RANGE = 1000 +ELEMENT_COUNT = 30 +TIMESTAMP_INDEX = 30 +TREND_DEFAULT_INDEX = 2 + +ECMC_COMMAND = { + 'MOVE_VEL': 1, + 'MOVE_REL' :2, + 'MOVE_ABS' :3, + 'MOVE_HOME' :10, +} + +DATASOURCE = { + 'AX_ID': 0, + 'POS_SET' :1, + 'POS_ACT' :2, + 'POS_ERR' :3, + 'POS_TARG' :4, + 'POS_ERR_TARG' :5, + 'POS_RAW' :6, + 'CNTRL_OUT' :7, + 'VEL_SET' :8, + 'VEL_ACT' :9, + 'VEL_FF_RAW' :10, + 'VEL_RAW' :11, + 'CYCLE_COUNTER' :12, + 'ERROR' :13, + 'COMMAND' :14, + 'CMD_DATA' :15, + 'SEQ_STATE' :16, + 'ILOCK' :17, + 'ILOCK_LAST_ACTIVE' :18, + 'TRAJ_SOURCE' :19, + 'ENC_SOURCE' :20, + 'ENABLE' :21, + 'ENABLED' :22, + 'EXECUTE' :23, + 'BUSY' :24, + 'AT_TAGEY' :25, + 'HOMED' :26, + 'LOW_LIM' :27, + 'HIGH_LIM' :28, + 'HOME_SENSOR' :29 + } + +DESCRIPTION = [ + 'axId', + 'posSet', + 'posAct', + 'posErr', + 'posTarg', + 'posErrTarg', + 'posRaw', + 'cntrlOut', + 'velSet', + 'velAct', + 'velFFraw', + 'velRaw', + 'cycleCounter', + 'error', + 'command', + 'cmdData', + 'seqState', + 'ilock', + 'ilockLastActive', + 'trajSource', + 'encSource', + 'enable', + 'enabled', + 'execute', + 'busy', + 'atTarget', + 'homed', + 'lowLim', + 'highLim', + 'homeSensor', +] +STYLES={ + 'ArrayStat': ''' + QTableView{ + background-color: white; + foreground-color: black; + font: bold; + width: 430px; + min-width: 430px; + max-width: 430px; + font-size:10pt; + height:750px; + min-height:750px; + max-height:750px; + } + ''' +} + +# You need to setup a signal slot mechanism, to +# send data to your GUI in a thread-safe way. +# Believe me, if you don't do this right, things +# go very very wrong.. +class comTable(QObject): + data_signal = pyqtSignal(str,float) + +''' End Class ''' +# You need to setup a signal slot mechanism, to +# send data to your GUI in a thread-safe way. +# Believe me, if you don't do this right, things +# go very very wrong.. +class comTrend(QObject): + data_signal = pyqtSignal(float) + +''' End Class ''' + +class ecmcArrayStat(QtWidgets.QTableView): + def __init__(self,parent=None): + super(ecmcArrayStat, self).__init__(parent) + self.background=None + self.startToPlot=False; + self.axId=0 + self.posSet=0 + self.posAct=0 + self.posErr=0 + self.posTarg=0 + self.posErrTarg=0 + self.posRaw=0 + self.cntrlOut=0 + self.velSet=0 + self.velAct=0 + self.velFFraw=0 + self.velRaw=0 + self.cycleCounter=0 + self.error=0 + self.errorCode=0 + self.strTime=0 + self.command=0 + self.cmdData=0 + self.seqState=0 + self.ilock=0 + self.ilockLastActive=0 + self.trajSourc=0 + self.encSource=0 + self.enable=0 + self.enabled=0 + self.execute=0 + self.busy=0 + self.atTarget=0 + self.homed=0 + self.lowLim=0 + self.highLim=0 + self.homeSensor=0 + self.trendValue = 0 + self.trendDataIndex = TREND_DEFAULT_INDEX # Pos-Act + self.dataList=[] + self.dataSourceConvFuncPoint = { + 0 :self.defaultStrFunc, + 1 :self.defaultStrFunc, + 2 :self.defaultStrFunc, + 3 :self.defaultStrFunc, + 4 :self.defaultStrFunc, + 5 :self.defaultStrFunc, + 6 :self.defaultStrFunc, + 7 :self.defaultStrFunc, + 8 :self.defaultStrFunc, + 9 :self.defaultStrFunc, + 10 :self.defaultStrFunc, + 11 :self.defaultStrFunc, + 12 :self.defaultStrFunc, + 13 :self.errorFunc, + 14 :self.commandStrFunc, + 15 :self.cmdDataStrFunc, + 16 :self.defaultStrFunc, + 17 :self.iLockFunc, + 18 :self.iLockFunc, + 19 :self.sourceFunc, + 20 :self.sourceFunc, + 21 :self.defaultStrFunc, + 22 :self.defaultStrFunc, + 23 :self.defaultStrFunc, + 24 :self.busyFunc, + 25 :self.highOKLowNotOKFunc, + 26 :self.highOKLowNotOKFunc, + 27 :self.highOKLowNotOKFunc, + 28 :self.highOKLowNotOKFunc, + 29 :self.defaultStrFunc, + } + + self.axisDiagPvName="" + + self.stdItemArrayName=[] + self.stdItemArrayData=[] + self.stdItemArraySelect=[] + + self.create_GUI() + + self.comTable = comTable() + self.comTable.data_signal.connect(self.updateGUI) + + self.comTrend = comTrend() + self.comTrend.data_signal.connect(self.trend.addData_callbackFunc) + return + + def create_GUI(self): + self.table = QtWidgets.QTableView(self) # SELECTING THE VIEW + self.model = QtGui.QStandardItemModel(self) # SELECTING THE MODEL - FRAMEWORK THAT HANDLES QUERIES AND EDITS + self.table.setModel(self.model) # SETTING THE MODEL + self.populate() + self.model.setHorizontalHeaderLabels(['Parameter', 'Value', '']) + self.btnPlot=QtWidgets.QPushButton('Plot',default=False, autoDefault=False) + self.trend=ecmcTrend.ecmcTrend() + # Disable put button + self.trend.enablePut(False) + self.trend.setTitle("ecmc plot") + self.show() + + def populate(self): + for i in range(0,ELEMENT_COUNT): + row= [] + cell=QtGui.QStandardItem(DESCRIPTION[i]) + cell.setFlags(QtCore.Qt.ItemIsEditable) + cell.setBackground(QtGui.QBrush(QtCore.Qt.white)) + cell.setForeground(QtGui.QBrush(QtCore.Qt.black)) + self.stdItemArrayName.append(cell) + row.append(cell) + cell=QtGui.QStandardItem('value'+str(i)) + cell.setFlags(QtCore.Qt.ItemIsEditable) + cell.setBackground(QtGui.QBrush(QtCore.Qt.white)) + cell.setForeground(QtGui.QBrush(QtCore.Qt.black)) + self.stdItemArrayData.append(cell) + row.append(cell) + cell=QtGui.QStandardItem(True) + cell.setCheckable(True) + cell.setCheckState(QtCore.Qt.Unchecked) + row.append(cell) + self.stdItemArraySelect.append(cell) + self.model.appendRow(row) + + #Timestamp + row= [] + cell=QtGui.QStandardItem("Timestamp") + cell.setFlags(QtCore.Qt.ItemIsEditable) + cell.setBackground(QtGui.QBrush(QtCore.Qt.white)) + cell.setForeground(QtGui.QBrush(QtCore.Qt.black)) + self.stdItemArrayName.append(cell) + row.append(cell) + cell=QtGui.QStandardItem('empty') + cell.setFlags(QtCore.Qt.ItemIsEditable) + cell.setBackground(QtGui.QBrush(QtCore.Qt.white)) + cell.setForeground(QtGui.QBrush(QtCore.Qt.black)) + self.stdItemArrayData.append(cell) + row.append(cell) + cell=QtGui.QStandardItem(True) + cell.setCheckable(True) + cell.setCheckState(QtCore.Qt.Unchecked) + row.append(cell) + self.stdItemArraySelect.append(cell) + self.model.appendRow(row) + self.formatTableView() + + def formatTableView(self): + self.table.resizeRowsToContents() + #self.table.resizeColumnToContents(0) + self.table.setColumnWidth(0,100) + self.table.setColumnWidth(1,200) + self.table.setColumnWidth(2,20) + #self.table.setHorizontalHeaderLabels(['Parameter', 'Value','Select']) + + def parseAxisStatArray(self,charData): + self.dataList=charData.split(',') + if len(self.dataList)!=ELEMENT_COUNT: + return PARSE_ERROR_ELEMENT_COUNT_OUT_OF_RANGE + + #Update table view + for i in range(0,ELEMENT_COUNT): + if self.dataList[i] is not None: + if len(self.dataList[i])>0: + func=self.dataSourceConvFuncPoint[i] + func(self.dataList[i],self.stdItemArrayData[i]) + + self.covertStringToData(self.dataList) + self.update() + if self.startToPlot: + self.updateDataPlot() + + def defaultStrFunc(self,strValue,cell): + cell.setData(strValue,role=QtCore.Qt.DisplayRole) + + def sourceFunc(self,strValue,cell): + if int(strValue)==0: + strToSet="Internal" + else: + strToSet="PLC" + + cell.setData(strToSet,role=QtCore.Qt.DisplayRole) + + def commandStrFunc(self,strValue,cell): + switcher = { + 1: "Move Vel", + 2: "Move Rel", + 3: "Move Abs", + 10: "Move Home", + } + strToSet=switcher.get(int(strValue),strValue) + if strToSet==strValue: + cell.setBackground(QtGui.QBrush(QtCore.Qt.red)) + else: + cell.setBackground(QtGui.QBrush(QtCore.Qt.white)) + cell.setData(strToSet,role=QtCore.Qt.DisplayRole) + + def highOKLowNotOKFunc(self,strValue,cell): + if int(strValue)==1: + strToSet="OK" + cell.setBackground(QtGui.QBrush(QtCore.Qt.green)) + else: + strToSet="Not OK" + cell.setBackground(QtGui.QBrush(QtCore.Qt.red)) + cell.setData(strToSet,role=QtCore.Qt.DisplayRole) + + def busyFunc(self,strValue,cell): + if int(strValue)==0: + strToSet="Ready" + cell.setBackground(QtGui.QBrush(QtCore.Qt.green)) + else: + strToSet="Busy" + cell.setBackground(QtGui.QBrush(QtCore.Qt.red)) + cell.setData(strToSet,role=QtCore.Qt.DisplayRole) + + def errorFunc(self,strValue,cell): + if int(strValue,16)==0: + cell.setBackground(QtGui.QBrush(QtCore.Qt.green)) + else: + cell.setBackground(QtGui.QBrush(QtCore.Qt.red)) + cell.setData(strValue,role=QtCore.Qt.DisplayRole) + + def lowOKHighNotOKShowValFunc(self,strValue,cell): + if int(strValue)==0: + cell.setBackground(QtGui.QBrush(QtCore.Qt.green)) + else: + cell.setBackground(QtGui.QBrush(QtCore.Qt.red)) + cell.setData(strValue,role=QtCore.Qt.DisplayRole) + + def iLockFunc(self,strValue,cell): + if int(strValue)==0: + cell.setBackground(QtGui.QBrush(QtCore.Qt.green)) + cell.setData("OK",role=QtCore.Qt.DisplayRole) + return + + switcher = { + 1: "Soft Bwd", + 2: "Soft Fwd", + 3: "Hard Bwd", + 4: "Hard Fwd", + 5: "No Execute", + 6: "Pos. Lag", + 7: "Both Lim.", + 8: "External", + 9: "Transform", + 10: "Max Vel.", + 11: "Cntrl High Lim.", + 12: "Cntrl Inc at Lim.", + 13: "Axis Error", + 14: "Unexp. Lim.", + 15: "Vel. diff", + 16: "Hardware", + 17: "PLC", + 18: "PLC Bwd", + 19: "PLC Fwd", + } + + strToSet=switcher.get(int(strValue),strValue) + cell.setData(strToSet,role=QtCore.Qt.DisplayRole) + cell.setBackground(QtGui.QBrush(QtCore.Qt.red)) + + def cmdDataStrFunc(self,strValue,cell): + if self.command!=ECMC_COMMAND['MOVE_HOME']: + self.defaultStrFunc(strValue,cell) + return + + switcher = { + 0: "Not defined", + 1: "Low Lim", + 2: "High Lim", + 3: "Low Lim, Home Sens", + 4: "High Lim, Home Sens", + 5: "Low Lim, Center Home Sens", + 6: "High Lim, Center Home Sens", + 7: "Bwd, Home sens", + 8: "Fwd, Home sens", + 9: "Bwd, Center Home Sens", + 10: "Fwd, Center Home Sens", + 11: "Low Lim, Enc index", + 12: "High Lim, Enc index", + 15: "Home Direct", + 21: "Low Lim Part Abs", + 22: "High Lim Part Abs", + } + + strToSet=switcher.get(int(strValue),strValue) + if strToSet==strValue: + cell.setBackground(QtGui.QBrush(QtCore.Qt.red)) + else: + cell.setBackground(QtGui.QBrush(QtCore.Qt.white)) + cell.setData(strToSet,role=QtCore.Qt.DisplayRole) + + def covertStringToData(self,dataList): + self.axId=int(dataList[0]) + self.posSet=float(dataList[1]) + self.posAct=float(dataList[2]) + self.posErr=float(dataList[3]) + self.posTarg=float(dataList[4]) + self.posErrTarg=float(dataList[5]) + self.posRaw=float(dataList[6]) + self.cntrlOut=float(dataList[7]) + self.velSet=float(dataList[8]) + self.velAct=float(dataList[9]) + self.velFFraw=float(dataList[10]) + self.velRaw=float(dataList[11]) + self.cycleCounter=int(dataList[12]) + self.error=int(dataList[13],16) + self.command=int(dataList[14]) + self.cmdData=int(dataList[15]) + self.seqState=int(dataList[16]) + self.ilock=int(dataList[17]) + self.ilockLastActive=int(dataList[18]) + self.trajSource=int(dataList[19]) + self.encSource=int(dataList[20]) + self.enable=int(dataList[21]) + self.enabled=int(dataList[22]) + self.execute=int(dataList[23]) + self.busy=int(dataList[24]) + self.atTarget=int(dataList[25]) + self.homed=int(dataList[26]) + self.lowLim=int(dataList[27]) + self.highLim=int(dataList[28]) + self.homeSensor=int(dataList[29]) + self.trendValue = float(dataList[self.trendDataIndex]) + + def onChangeAxisDiagPv(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + self.comTable.data_signal.emit(char_value,timestamp) + + # All update of GUI here.. + def updateGUI(self, char_value, timestamp): + self.errorCode=self.parseAxisStatArray(char_value) + + if self.errorCode: + print("Parse failed with error code: " + str(self.errorCode)) + + self.strTime=datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S%f') + self.stdItemArrayData[TIMESTAMP_INDEX].setData(self.strTime,role=QtCore.Qt.DisplayRole) + + + def connect(self, pvname): + if pvname is None: + raise RuntimeError("pvname must not be 'None'") + + if len(pvname)==0: + raise RuntimeError("pvname must not be ''") + + self.axisDiagPvName = pvname + self.axisDiagPv = epics.PV(self.axisDiagPvName) + self.axisDiagPv.add_callback(self.onChangeAxisDiagPv) + self.trend.setTitle(self.axisDiagPvName) + + + def disconnect(self): + if self.axisDiagPv is not None: + self.axisDiagPv.clear_callbacks() + + def printInfo(self): + print("axId : " + str(self.axId)) + print("posSet : " + str(self.posSet)) + print("posAct : " + str(self.posAct)) + print("posErr : " + str(self.posErr)) + print("posTarg : " + str(self.posTarg)) + print("posErrTarg : " + str(self.posErrTarg)) + print("posRaw : " + str(self.posRaw)) + print("cntrlOut : " + str(self.cntrlOut)) + print("velSet : " + str(self.velSet)) + print("velAct : " + str(self.velAct)) + print("velFFraw : " + str(self.velFFraw)) + print("velRaw : " + str(self.velRaw)) + print("cycleCounter : " + str(self.cycleCounter)) + print("error : " + str(self.error)) + print("command : " + str(self.command)) + print("cmdData : " + str(self.cmdData)) + print("seqState : " + str(self.seqState)) + print("ilock : " + str(self.ilock)) + print("ilockLastActive : " + str(self.ilockLastActive)) + print("trajSource : " + str(self.trajSource)) + print("encSource : " + str(self.encSource)) + print("enable : " + str(self.enable)) + print("enabled : " + str(self.enabled)) + print("execute : " + str(self.execute)) + print("busy : " + str(self.busy)) + print("atTarget : " + str(self.atTarget)) + print("homed : " + str(self.homed)) + print("lowLim : " + str(self.lowLim)) + print("highLim : " + str(self.highLim)) + print("homeSensor : " + str(self.homeSensor)) + return + + def startPlot(self): + self.trendDataIndex, label = self.checkPlotVar() + self.trend.setYLabel(label) + self.trend.show() + self.startToPlot=True; + + def updateDataPlot(self): + if self.startToPlot: + self.comTrend.data_signal.emit(self.trendValue) + + # return first index of selected data (only support one var currently) + def checkPlotVar(self): + for i in range(0,ELEMENT_COUNT): + if self.stdItemArraySelect[i].checkState(): + return i, self.stdItemArrayName[i].text() + + return TREND_DEFAULT_INDEX, self.stdItemArrayName[TREND_DEFAULT_INDEX].text() diff --git a/tools/ecmcGuiMain.py b/tools/ecmcGuiMain.py new file mode 100644 index 0000000..1d6e472 --- /dev/null +++ b/tools/ecmcGuiMain.py @@ -0,0 +1,187 @@ +#!/usr/bin/python3.6 +# coding: utf-8 + +from PyQt5 import QtWidgets,uic +import numpy as np +import epics +from ecmcArrayStat import * +from ecmcOneMotorGUI import * +from ecmcMainWndDesigner import Ui_MainWindow +from ecmcFFTMainGui import * +from ecmcScopeMainGui import * +import ecmcTrendPv +import time + +# Needed packages: +# 1. sudo yum -y install https://rhel7.iuscommunity.org/ius-release.rpm +# 2. sudo pip3.6 install pyqt5 +# 3. sudo yum install qt5-qtbase-devel +# 4. sudo python3.6 -m pip install numpy scipy matplotlib +# 5. sudo pip3 install pyepics +# 6. sudo yum install python3-matplotlib + + +# Regenerate py from ui file: +# pyuic5 -x ecmcMainWndDesigner.ui -o ecmcMainWndDesigner.py + +class ecmcMainWindow(QtWidgets.QMainWindow): + def __init__(self): + + super(ecmcMainWindow,self).__init__() + self.prefix="" + self.pvName="" + self.pv=None + + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + self.ui.pbStartGUI.clicked.connect(self.showGUI) + self.ui.pbStartGUI.setToolTip("Start GUI for ioc-prefix + pv-name") + + self.ui.lineIOCPrefix.textChanged.connect(self.newIOCPrefix) + self.ui.lineIOCPrefix.setToolTip("Enter ioc-prefix to to use.") + self.ui.linepvName.textChanged.connect(self.newIOCpvName) + self.ui.linepvName.setToolTip("Enter pv-name to plot/trend (or control)") + + self.ui.comboPrefix.currentIndexChanged.connect(self.newPrefixComboIndex) + self.ui.comboPrefix.addItem("IOC_TEST:") + self.ui.comboPrefix.addItem("IOC:") + self.ui.comboPrefix.addItem("IOC2:") + self.ui.comboPrefix.addItem("IOC_SLIT:") + self.ui.comboPrefix.addItem("TEST") + self.ui.comboPrefix.setToolTip("Predefined ioc-prefix. Choose one to use..") + + self.ui.comboPvName.currentIndexChanged.connect(self.newPvComboIndex) + self.ui.comboPvName.addItem("Axis1") + self.ui.comboPvName.addItem("Axis2") + self.ui.comboPvName.addItem("MCU-ThdLatMax") + self.ui.comboPvName.addItem("MCU-ThdLatMin") + self.ui.comboPvName.addItem("MCU-ThdPrdMax") + self.ui.comboPvName.addItem("MCU-ThdPrdMin") + self.ui.comboPvName.addItem("MCU-ThdSndMax") + self.ui.comboPvName.addItem("MCU-ThdSndMin") + self.ui.comboPvName.addItem("m0-DomFailCntrTot") + self.ui.comboPvName.addItem("MCU-ErrId") + self.ui.comboPvName.addItem("m0s001-BI01") + self.ui.comboPvName.addItem("m0s001-BI02") + self.ui.comboPvName.addItem("m0s003-Enc01-PosAct") + self.ui.comboPvName.addItem("FFT-0") + self.ui.comboPvName.addItem("Scope-0") + self.ui.comboPvName.setToolTip("Predefined pv-names. Choose one to use..") + + if len(sys.argv)>1: + self.prefix=sys.argv[1] + self.ui.lineIOCPrefix.setText(self.prefix) + if len(sys.argv)>2: + self.pvName=sys.argv[2] + self.ui.linepvName.setText(self.pvName) + + + if (len(sys.argv)>2): + for i in range(2,len(sys.argv)): + self.ui.linepvName.setText(str(sys.argv[i])) + self.showGUI() + + def showGUI(self): + + #Check and start FFT gui + if self.showGuiFFT(self.prefix, self.pvName): + return + + #Check and start Scope gui + if self.showGuiScope(self.prefix, self.pvName): + return + + # See if scalar or motor + self.ui.pbStartGUI.setText("Connecting to: " + self.prefix + self.pvName + "...") + self.ui.pbStartGUI.setEnabled(False) + self.ui.pbStartGUI.update() + QtCore.QCoreApplication.processEvents() + + self.prefix=self.ui.lineIOCPrefix.text() + self.pvName=self.ui.linepvName.text() + entirePvName = self.prefix+self.pvName + pos = entirePvName.rfind('.') + + # ensure record/pv exist + + pvtest = epics.PV(entirePvName) + connected = pvtest.wait_for_connection(timeout=2) + self.ui.pbStartGUI.setEnabled(True) + self.ui.pbStartGUI.setText("Start GUI for: " + self.prefix + self.pvName) + self.ui.pbStartGUI.update() + if not(connected): + print("Timeout. Could not connect to: " + entirePvName + ". Probably not a valid PV name.") + return + del(pvtest) + + + # Check if motor + if pos < 0: + pv = epics.PV(entirePvName + '.RTYP') + if pv.get() == 'motor': + self.showMotorGUI(self.prefix, self.pvName) + return + + # Normal PV + self.showGuiPv(self.prefix+self.pvName) + + def showMotorGUI(self,prefix,pvName): + self.dialog = MotorPanel(self,prefix,pvName) + self.dialog.resize(500, 900) + self.dialog.show() + + def showGuiPv(self, pvName): + dialog = ecmcTrendPv.ecmcTrendPv(pvName) + dialog.show() + + def showGuiFFT(self, prefix, pvName): + # Check if FFT gui + if pvName.find('FFT-') == 0 and len(prefix) > 0: + pvNameTemp = pvName.split('-') + if np.size(pvNameTemp)==2: + if pvNameTemp[1].isdigit(): + self.dialog = ecmcFFTMainGui(prefix,int(pvNameTemp[1])) + self.dialog.show() + return 1 + return 0 + + def showGuiScope(self, prefix, pvName): + # Check if FFT gui + if pvName.find('Scope-') == 0 and len(prefix) > 0: + pvNameTemp = pvName.split('-') + if np.size(pvNameTemp)==2: + if pvNameTemp[1].isdigit(): + self.dialog = ecmcScopeMainGui(prefix,int(pvNameTemp[1])) + self.dialog.show() + return 1 + return 0 + + def newIOCPrefix(self,iocPrefix): + self.prefix=iocPrefix + self.ui.pbStartGUI.setText("Start GUI for: " + self.prefix + self.pvName) + + def newIOCpvName(self,pvName): + self.pvName=pvName + self.ui.pbStartGUI.setText("Start GUI for: " + self.prefix + self.pvName) + + def newPrefixComboIndex(self,index): + self.prefix=self.ui.comboPrefix.itemText(index) + self.ui.lineIOCPrefix.setText(self.prefix) + self.ui.pbStartGUI.setText("Start GUI for: " + self.prefix + self.pvName) + + def newPvComboIndex(self,index): + self.pvName=self.ui.comboPvName.itemText(index) + self.ui.linepvName.setText(self.pvName) + self.ui.pbStartGUI.setText("Start GUI for: " + self.prefix + self.pvName) + + def quit(self): + self.close() + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + window=ecmcMainWindow(); + window.show() + sys.exit(app.exec_()) + \ No newline at end of file diff --git a/tools/ecmcMotionMainPyQtGraph.py b/tools/ecmcMotionMainPyQtGraph.py index e70ec42..c0e945e 100644 --- a/tools/ecmcMotionMainPyQtGraph.py +++ b/tools/ecmcMotionMainPyQtGraph.py @@ -21,6 +21,7 @@ from PyQt5.QtCore import * from PyQt5.QtGui import * import numpy as np import time +from ecmcOneMotorGUI import * import pyqtgraph as pg import threading @@ -65,8 +66,12 @@ pvBinary = ['Ena-Arr', 'AtTrg-Arr'] # MCU info PVs -pvAxisCompleteNamePart1 ='MCU-Cfg-AX' -pvAxisCompleteNamePart2 ='-PfxNam' +pvAxisPrefixNamePart1 ='MCU-Cfg-AX' +pvAxisPrefixNamePart2 ='-Pfx' + +pvAxisNamePart1 ='MCU-Cfg-AX' +pvAxisNamePart2 ='-Nam' + pvFistAxisIndexName = 'MCU-Cfg-AX-FrstObjId' pvNextAxisIndexNamePart1 = 'MCU-Cfg-AX' pvNextAxisIndexNamePart2 = '-NxtObjId' @@ -297,6 +302,7 @@ class ecmcMtnMainGui(QtWidgets.QDialog): layoutMotionGrid = QGridLayout() frameMotion.setLayout(layoutMotionGrid) btn = QPushButton(text = 'Test') + btn.clicked.connect(self.openMotorRecordPanel) btn.setFixedSize(100, 50) layoutMotionGrid.addWidget(btn,0,0) @@ -575,11 +581,19 @@ class ecmcMtnMainGui(QtWidgets.QDialog): if id >= 0: self.cmbBxSelectAxis.setCurrentIndex(id) - name = self.pvPrefixStr + pvAxisCompleteNamePart1 + str(int(value)) + pvAxisCompleteNamePart2 - namePV = epics.PV(name) - newName = namePV.get() - if newName is not None: - print('PV name of axis:' + newName) + axisPrefixPvName = self.pvPrefixStr + pvAxisPrefixNamePart1 + str(int(value)) + pvAxisPrefixNamePart2 + prefixPV = epics.PV(axisPrefixPvName) + axisPrefix = prefixPV.get() + if axisPrefix is not None: + print('prefix of axis:' + axisPrefix) + self.axisPrefix = axisPrefix + + axisNamePvName = self.pvPrefixStr + pvAxisNamePart1 + str(int(value)) + pvAxisNamePart2 + namePV = epics.PV(axisNamePvName) + axisName = namePV.get() + if axisName is not None: + print('name of axis:' + axisName) + self.axisName = axisName def sig_cb_SmpHz_RB(self,value): self.data['SmpHz-RB'] = value @@ -633,6 +647,11 @@ class ecmcMtnMainGui(QtWidgets.QDialog): if self.cmbBxSelectAxis.currentData() is not None: self.pvs['AxCmd-RB'].put(self.cmbBxSelectAxis.currentData(), use_complete=True) + def openMotorRecordPanel(self,xxx): + self.dialog = MotorPanel(self,self.axisPrefix ,self.axisName) + self.dialog.resize(500, 900) + self.dialog.show() + ###### Widget callbacks def pauseBtnAction(self): self.pause = not self.pause diff --git a/tools/ecmcOneMotorGUI.py b/tools/ecmcOneMotorGUI.py new file mode 100644 index 0000000..ce79788 --- /dev/null +++ b/tools/ecmcOneMotorGUI.py @@ -0,0 +1,782 @@ +#!/usr/bin/env python3.6 +import epics +import sys +from PyQt5 import QtWidgets, QtGui, QtCore +from ecmcArrayStat import * + +#Define pvs +# Axis +ECMC_PV_AXIS_DIAG_ARRAY_SUFFIX = '-Arr-Stat' +ECMC_PV_AXIS_ERROR_RESET_SUFFIX = '-ErrRst' +# Controller +ECMC_PV_CNTROLLER_ERROR_ID_SUFFIX = 'MCU-ErrId' +ECMC_PV_CNTROLLER_ERROR_MSG_SUFFIX = 'MCU-ErrMsg' +ECMC_PV_CNTROLLER_ERROR_RESET_SUFFIX = 'MCU-ErrRst' +ECMC_PV_CNTROLLER_ERROR_CND_SUFFIX = 'MCU-Cmd' + +BLANK = ' '*4 +BACKGROUND_DEFAULT = '#efefef' +#BACKGROUND_DONE_MOVING = 'beige' +BACKGROUND_DONE_MOVING = BACKGROUND_DEFAULT +BACKGROUND_MOVING = 'lightgreen' +BACKGROUND_LVIO_ON = 'yellow' +BACKGROUND_LIMIT_ON = 'red' +TOOLTIPS = { + 'DESC': 'DESC: short description', + 'NAME': 'NAME: EPICS PV name', + 'RBV': 'RBV: motor readback value', + 'VAL': 'VAL: motor target value', + 'EGU': 'EGU: engineering units', + 'STOP': 'STOP: command this motor to stop moving', + 'TWV': 'TWV: tweak value', + 'TWF': 'TWF: increment motor by tweak value', + 'TWR': 'TWR: decrement motor by tweak value', + '*10': 'multiply tweak value by 10', + '/10': 'divide tweak value by 10', + 'CNEN': 'enable/disable drive', + 'ArrayStat': 'axis Status', + 'ErrRst': 'reset axis error', + 'JOGR': 'JOGR: jog backward', + 'JOGF': 'JOGF: jog forward', + 'HOMR': 'HOMR: Home reverse', + 'HOMF': 'HOMF: Home forward', + 'MSTA': 'MSTA: Status', + 'JVEL': 'JVEL: Jog velocity', + 'VELO': 'VELO: Velocity (positioning)' +} +STYLES = { ### http://doc.qt.digia.com/qt/stylesheet-reference.html + 'self': ''' + MotorPanel { + border-style: solid; + border-color: black; + border-width: 1px; + } + ''', + 'DESC': ''' + QLabel { + qproperty-alignment: AlignRight; + font: bold; + } + ''', + 'NAME': ''' + QLabel { + qproperty-alignment: AlignRight; + font: bold; + } + ''', + 'RBV': ''' + QLabel { + qproperty-alignment: AlignRight; + } + ''', + 'EGU': ''' + QLabel { + qproperty-alignment: AlignRight; + } + ''', + 'VAL': ''' + QLineEdit { + background-color: white; + text-align: right; + } + ''', + 'STOP': ''' + QPushButton { + background-color: red; + color: black; + text-align: center; + height: 50px; + min-height: 50px; + max-height: 50px; + } + QPushButton:hover { + background-color: red; + color: yellow; + font: bold; + text-align: center; + } + ''', + 'CNEN': ''' + QPushButton { + background-color: red; + color: black; + text-align: center; + height: 40px; + min-height: 40px; + max-height: 40px; + } + QPushButton:hover { + background-color: red; + color: yellow; + font: bold; + text-align: center; + } + ''', + 'BUTTON_ON': ''' + QPushButton { + background-color: green; + color: black; + text-align: center; + height: 40px; + min-height: 40px; + max-height: 40px; + } + QPushButton:hover { + background-color: green; + color: yellow; + font: bold; + text-align: center; + } + ''', + + 'TWV': ''' + QLineEdit { + background-color: white; + text-align: right; + width: 80px; + min-width: 80px; + max-width: 80px; + height: 40px; + min-height: 40px; + max-height: 40px; + } + ''', + 'TWF': ''' + QPushButton { + width: 40px; + min-width: 40px; + max-width: 40px; + height: 40px; + min-height: 40px; + max-height: 40px; + } + ''', + 'TWR': ''' + QPushButton { + width: 40px; + min-width: 40px; + max-width: 40px; + height: 40px; + min-height: 40px; + max-height: 40px; + + } + ''', + '*10': ''' + QPushButton { + width: 40px; + min-width: 40px; + max-width: 40px; + height: 40px; + min-height: 40px; + max-height: 40px; + } + ''', + '/10': ''' + QPushButton { + width: 40px; + min-width: 40px; + max-width: 40px; + height: 40px; + min-height: 40px; + max-height: 40px; + } + ''', + 'ArrayStat': ''' + QTableView { + background-color: white; + font: bold; + width: 350px; + min-width: 350px; + max-width: 350px; + font-size:10pt; + height:770px; + min-height:770px; + max-height:770px; + } + ''', + 'ErrRst': ''' + QPushButton { + background-color: red; + color: black; + text-align: center; + height: 40px; + min-height: 40px; + max-height: 40px; + } + QPushButton:hover { + background-color: red; + color: yellow; + font: bold; + text-align: center; + } + + ''', + 'JOGR': ''' + QPushButton { + background-color: grey; + color: black; + text-align: center; + height: 40px; + min-height: 40px; + max-height: 40px; + } + ''', + 'JOGF': ''' + QPushButton { + background-color: grey; + color: black; + text-align: center; + height: 40px; + min-height: 40px; + max-height: 40px; + } + ''', + 'HOMR': ''' + QPushButton { + background-color: grey; + color: black; + text-align: center; + height: 40px; + min-height: 40px; + max-height: 40px; + } + ''', + 'HOMF': ''' + QPushButton { + background-color: grey; + color: black; + text-align: center; + height: 40px; + min-height: 40px; + max-height: 40px; + } + ''', + 'MSTA': ''' + QLabel { + text-align: right; + } + ''', + 'PLOT': ''' + QPushButton { + background-color: grey; + color: black; + text-align: center; + height: 40px; + min-height: 40px; + max-height: 40px; + } + ''', + + 'VELO': ''' + QLineEdit { + background-color: white; + text-align: right; + width: 80px; + min-width: 80px; + max-width: 80px; + height: 40px; + min-height: 40px; + max-height: 40px; + } + ''', + 'JVEL': ''' + QLineEdit { + background-color: white; + text-align: right; + width: 80px; + min-width: 80px; + max-width: 80px; + height: 40px; + min-height: 40px; + max-height: 40px; + } + ''', + +} + +class MotorPanel(QtWidgets.QDialog): + + def __init__(self, parent=None,iocPrefix=None,axisName=None): + super(MotorPanel, self).__init__(parent) + QtWidgets.QToolTip.setFont(QtGui.QFont('SansSerif', 10)) + self.motorPv = None + self.motorPvName = "" + self.axisDiagPvName="" + self.axisErrorResetPv = None + self.axisErrorResetPvName = "" + self.cntrlErrorIdPv = None + self.cntrlErrorIdPvName = "" + self.cntrlErrorResetPv = None + self.cntrlErrorResetPvName = "" + self.cntrlErrorMsgPv = None + self.cntrlErrorMsgPvName = "" + self.cntrlErrorMsg="" + self.cntrlCmdPv = None + self.cntrlCmdPvName = "" + self.create_GUI() + self.apply_styles() + self.create_actions() + + if isinstance(axisName, str) and isinstance(iocPrefix, str): + self.connect(iocPrefix,axisName) + + def create_GUI(self): + '''define controls AND set the layout''' + self.controls = {} + for field in ['DESC', 'NAME', 'EGU', 'RBV']: + self.controls[field] = QtWidgets.QLabel(BLANK) + self.controls['VAL'] = QtWidgets.QLineEdit() + self.controls['STOP'] = QtWidgets.QPushButton('STOP',default=False, autoDefault=False) + self.controls['TWF'] = QtWidgets.QPushButton('>>',default=False, autoDefault=False) + self.controls['TWV'] = QtWidgets.QLineEdit() + self.controls['TWR'] = QtWidgets.QPushButton('<<',default=False, autoDefault=False) + self.controls['*10'] = QtWidgets.QPushButton('*10',default=False, autoDefault=False) + self.controls['/10'] = QtWidgets.QPushButton('/10',default=False, autoDefault=False) + self.controls['CNEN'] = QtWidgets.QPushButton('CNEN',default=False, autoDefault=False) + self.controls['ArrayStat']=ecmcArrayStat(self) + self.controls['JVEL'] = QtWidgets.QLineEdit() + self.controls['VELO'] = QtWidgets.QLineEdit() + self.controls['ErrRst'] = QtWidgets.QPushButton('Reset Error',default=False, autoDefault=False) + self.controls['JOGR'] = QtWidgets.QPushButton('JOGR',default=False, autoDefault=False) + self.controls['JOGF'] = QtWidgets.QPushButton('JOGF',default=False, autoDefault=False) + self.controls['HOMR'] = QtWidgets.QPushButton('HOMR',default=False, autoDefault=False) + self.controls['HOMF'] = QtWidgets.QPushButton('HOMF',default=False, autoDefault=False) + self.controls['PLOT'] = QtWidgets.QPushButton('Plot',default=False, autoDefault=False) + self.controls['MSTA'] = QtWidgets.QLabel() + self.controls['RBV'].setAutoFillBackground(True) + self.setLabelBackground(self.controls['RBV'], BACKGROUND_DONE_MOVING) + + main_frame= QtWidgets.QFrame(self) + main_layout = QtWidgets.QHBoxLayout() + + left_frame = QtWidgets.QFrame(self) + left_layout = QtWidgets.QVBoxLayout() + for field in ['NAME','DESC', 'EGU', 'RBV', 'VAL','VELO','MSTA']: + tmp_frame = QtWidgets.QFrame(self) + tmp_layout = QtWidgets.QHBoxLayout() + tmp_label=QtWidgets.QLabel() + tmp_label.setText(field+ ':') + tmp_layout.addWidget(tmp_label) + tmp_layout.addWidget(self.controls[field]) + tmp_frame.setLayout(tmp_layout) + left_layout.addWidget(tmp_frame) + + #Tweak + tweak_label= QtWidgets.QLabel() + tweak_label.setText('TWEAK:') + left_layout.addWidget(tweak_label) + + tweak_frame = QtWidgets.QFrame(self) + tweak_layout = QtWidgets.QHBoxLayout() + for field in ['TWR', '/10', 'TWV', '*10', 'TWF']: + tweak_layout.addWidget(self.controls[field]) + tweak_frame.setLayout(tweak_layout) + + left_layout.addWidget(tweak_frame) + + #Jog + jog_label= QtWidgets.QLabel() + jog_label.setText('JOG:') + left_layout.addWidget(jog_label) + jvel_frame = QtWidgets.QFrame(self) + jvel_layout = QtWidgets.QHBoxLayout() + jvel_label=QtWidgets.QLabel() + jvel_label.setText('JVEL' + ':') + jvel_layout.addWidget(jvel_label) + jvel_layout.addWidget(self.controls['JVEL']) + jvel_frame.setLayout(jvel_layout) + left_layout.addWidget(jvel_frame) + jog_frame = QtWidgets.QFrame(self) + jog_layout = QtWidgets.QHBoxLayout() + jog_layout.addWidget(self.controls['JOGR']) + jog_layout.addWidget(self.controls['JOGF']) + jog_frame.setLayout(jog_layout) + left_layout.addWidget(jog_frame) + + #Home + home_label= QtWidgets.QLabel() + home_label.setText('HOME:') + left_layout.addWidget(home_label) + home_frame = QtWidgets.QFrame(self) + home_layout = QtWidgets.QHBoxLayout() + home_layout.addWidget(self.controls['HOMR']) + home_layout.addWidget(self.controls['HOMF']) + home_frame.setLayout(home_layout) + left_layout.addWidget(home_frame) + + control_label= QtWidgets.QLabel() + control_label.setText('CONTROL:') + left_layout.addWidget(control_label) + + left_layout.addWidget(self.controls['STOP']) + left_layout.addWidget(self.controls['CNEN']) + left_layout.addWidget(self.controls['ErrRst']) + + left_frame.setLayout(left_layout) + main_layout.addWidget(left_frame) + + right_frame = QtWidgets.QFrame(self) + right_layout = QtWidgets.QVBoxLayout() + right_layout.addWidget(self.controls['ArrayStat']) + right_layout.addWidget(self.controls['PLOT']) + right_frame.setLayout(right_layout); + + main_layout.addWidget(right_frame) + main_frame.setLayout(main_layout) + + self.setLayout(main_layout) + self.setWindowTitle("ecmc: axis control") + + + def apply_styles(self): + '''apply styles and tips''' + for field in ['DESC', 'NAME', 'EGU', 'RBV', 'VAL', + 'STOP','CNEN','TWV', 'TWF', 'TWR', '*10', + '/10','ArrayStat','ErrRst','JOGR','JOGF', + 'HOMR','HOMF','MSTA','JVEL','VELO']: + if field in STYLES: + self.controls[field].setStyleSheet(STYLES[field]) + if field in TOOLTIPS: + self.controls[field].setToolTip(TOOLTIPS[field]) + + self.setStyleSheet(STYLES['self']) + + def create_actions(self): + '''define actions''' + self.controls['VAL'].returnPressed.connect(self.onReturnVAL) + self.controls['VELO'].returnPressed.connect(self.onReturnVELO) + self.controls['TWV'].returnPressed.connect(self.onReturnTWV) + self.controls['TWR'].clicked.connect(self.onPushTWR) + self.controls['TWF'].clicked.connect(self.onPushTWF) + self.controls['*10'].clicked.connect(self.onPush10x) + self.controls['/10'].clicked.connect(self.onPush_1x) + self.controls['STOP'].clicked.connect(self.onPushSTOP) + self.controls['CNEN'].clicked.connect(self.onPushCNEN) + self.controls['ErrRst'].clicked.connect(self.onPushErrRst) + self.controls['JVEL'].returnPressed.connect(self.onReturnJVEL) + self.controls['JOGR'].clicked.connect(self.onPushJOGR) + self.controls['JOGF'].clicked.connect(self.onPushJOGF) + self.controls['HOMR'].clicked.connect(self.onPushHOMR) + self.controls['HOMF'].clicked.connect(self.onPushHOMF) + self.controls['PLOT'].clicked.connect(self.onPushPLOT) + + + def setPvNames(self,iocPrefix=None,axisName=None): + self.motorPvName = (iocPrefix + axisName).split('.')[0] # keep everything to left of first dot + self.axisDiagPvName = self.motorPvName + ECMC_PV_AXIS_DIAG_ARRAY_SUFFIX + self.axisErrorResetPvName = self.motorPvName + ECMC_PV_AXIS_ERROR_RESET_SUFFIX + self.cntrlErrorIdPvName = iocPrefix + ECMC_PV_CNTROLLER_ERROR_ID_SUFFIX + self.cntrlErrorResetPvName = iocPrefix + ECMC_PV_CNTROLLER_ERROR_RESET_SUFFIX + self.cntrlErrorMsgPvName = iocPrefix + ECMC_PV_CNTROLLER_ERROR_MSG_SUFFIX + self.cntrlCmdPvName = iocPrefix + ECMC_PV_CNTROLLER_ERROR_CND_SUFFIX + + def connect(self, iocPrefix=None,axisName=None): + '''connect this panel with an EPICS motor PV''' + if iocPrefix is None: + raise RuntimeError("iocPrefix must not be 'None'") + if axisName is None: + raise RuntimeError("axisName must not be 'None'") + + self.setPvNames(iocPrefix,axisName) + + if len(iocPrefix) == 0 or len(axisName) == 0: + raise RuntimeError("iocPrefix or axisName must not be ''") + + if self.motorPv is not None: + self.disconnect() + + self.controls['NAME'].setText(self.motorPvName) + self.motorPv = epics.Motor(self.motorPvName) # verifies that self.motor_pv has RTYP='motor' + + callback_dict = { + #field: callback function + 'DESC': self.onChangeDESC, + 'EGU': self.onChangeEGU, + 'RBV': self.onChangeRBV, + 'VAL': self.onChangeVAL, + 'VELO': self.onChangeVELO, + 'TWV': self.onChangeTWV, + 'DMOV': self.onChangeDMOV, + 'HLS': self.onChangeHLS, + 'LLS': self.onChangeLLS, + 'CNEN': self.onChangeCNEN, + 'JOGR': self.onChangeJOGR, + 'JOGF': self.onChangeJOGF, + 'JVEL': self.onChangeJVEL, + 'HOMR': self.onChangeHOMR, + 'HOMF': self.onChangeHOMF, + 'MSTA': self.onChangeMSTA, + } + for field, func in callback_dict.items(): + self.motorPv.set_callback(attr=field, callback=func) + + self.controls['DESC'].setText(self.motorPv.description) + self.controls['EGU'].setText(self.motorPv.units) + + # display initial values + self.onChangeRBV(value=self.motorPv.get('RBV')) + self.onChangeVAL(value=self.motorPv.get('VAL')) + self.onChangeTWV(value=self.motorPv.get('TWV')) + self.onChangeDMOV(value=self.motorPv.get('DMOV')) + self.onChangeCNEN(value=self.motorPv.get('CNEN')) + self.onChangeJOGR(value=self.motorPv.get('JOGR')) + self.onChangeJOGF(value=self.motorPv.get('JOGF')) + self.onChangeHOMR(value=self.motorPv.get('HOMR')) + self.onChangeHOMF(value=self.motorPv.get('HOMF')) + self.onChangeMSTA(value=self.motorPv.get('MSTA')) + self.onChangeVELO(value=self.motorPv.get('VELO')) + self.onChangeJVEL(value=self.motorPv.get('JVEL')) + + # additional records + self.axisErrorResetPv = epics.PV(self.axisErrorResetPvName) + self.cntrlErrorIdPv = epics.PV(self.cntrlErrorIdPvName) + self.cntrlErrorIdPv.add_callback(self.onChangeCntrlErrorIdPv) + self.cntrlErrorResetPv = epics.PV(self.cntrlErrorResetPvName) + self.cntrlErrorMsgPv = epics.PV(self.cntrlErrorMsgPvName) + self.cntrlCmdPv = epics.PV(self.cntrlCmdPvName) + self.cntrlCmdPv.add_callback(self.onChangeCntrlCmdPv) + if self.controls['ArrayStat'] is not None: + self.controls['ArrayStat'].connect(self.axisDiagPvName) + + def disconnect(self): + '''disconnect this panel from EPICS''' + if self.motorPv is not None: + for field in ['VAL', 'RBV', 'DESC', 'EGU', 'TWV', 'DMOV', 'HLS', 'LLS']: + self.motorPv.clear_callback(attr=field) + self.motorPv = None + for field in ['DESC', 'NAME', 'EGU', 'RBV', 'VAL', 'TWV']: + self.controls[field].setText(BLANK) + + if self.cntrlErrorIdPv is not None: + self.cntrlErrorIdPv.clear_callbacks() + if self.cntrlCmdPv is not None: + self.cntrlCmdPv.clear_callbacks() + + if self.controls['ArrayStat'] is not None: + self.controls['ArrayStat'].disconnect() + + def closeEvent(self, event): + '''be sure to disconnect from EPICS when closing''' + self.disconnect() + + def onPushSTOP(self): + '''stop button was pressed''' + if self.motorPv is not None: + self.motorPv.stop() + + def onPushCNEN(self): + '''cnen button was pressed''' + if self.motorPv is not None: + self.motorPv.put('CNEN',not self.motorPv.get('CNEN')) + + def onPushErrRst(self): + '''ErrRst button was pressed''' + if self.axisErrorResetPv is not None: + self.axisErrorResetPv.put(1) + + def onPushJOGR(self): + '''jogr button was pressed''' + if self.motorPv is not None: + self.motorPv.put('JOGR',not self.motorPv.get('JOGR')) + + def onPushJOGF(self): + '''jogf button was pressed''' + if self.motorPv is not None: + self.motorPv.put('JOGF',not self.motorPv.get('JOGF')) + + def onPushHOMR(self): + '''homr button was pressed''' + if self.motorPv is not None: + self.motorPv.put('HOMR',1) + + def onPushHOMF(self): + '''homf button was pressed''' + if self.motorPv is not None: + self.motorPv.put('HOMF',1) + + def onPushPLOT(self): + '''plot button was pressed''' + self.controls['ArrayStat'].startPlot() + + def onPushTWF(self): + '''tweak forward button was pressed''' + if self.motorPv is not None: + self.motorPv.put('TWF', 1) + + def onPushTWR(self): + '''tweak reverse button was pressed''' + if self.motorPv is not None: + self.motorPv.put('TWR', 1) + + def onPush10x(self): + '''multiply TWV*10 button was pressed''' + if self.motorPv is not None: + self.motorPv.put('TWV', 10*self.motorPv.get('TWV')) + + def onPush_1x(self): + '''multiply TWV*0.1 button was pressed''' + if self.motorPv is not None: + self.motorPv.put('TWV', 0.1*self.motorPv.get('TWV')) + + def onReturnTWV(self): + '''new target value was entered in this panel''' + if self.motorPv is not None: + number = float(self.controls['TWV'].text()) + self.motorPv.put('TWV', number) + + def onReturnVAL(self): + '''new target value was entered in this panel''' + if self.motorPv is not None: + number = float(self.controls['VAL'].text()) + #self.motorPv.move(number) + self.motorPv.put('VAL',number) + + def onReturnVELO(self): + '''new target velocity was entered in this panel''' + if self.motorPv is not None: + number = float(self.controls['VELO'].text()) + self.motorPv.put('VELO',number) + + def onReturnJVEL(self): + '''new target jog velocity was entered in this panel''' + if self.motorPv is not None: + number = float(self.controls['JVEL'].text()) + self.motorPv.put('JVEL',number) + + def onChangeCNEN(self, value = None, **kws): + '''EPICS monitor on CNEN called this''' + field='CNEN' + if value: + self.controls[field].setStyleSheet(STYLES['BUTTON_ON']) + else: + self.controls[field].setStyleSheet(STYLES[field]) + + def onChangeJOGR(self, value = None, **kws): + '''EPICS monitor on JOGR called this''' + field='JOGR' + if value: + self.controls[field].setStyleSheet(STYLES['BUTTON_ON']) + else: + self.controls[field].setStyleSheet(STYLES[field]) + + def onChangeJOGF(self, value = None, **kws): + '''EPICS monitor on JOGF called this''' + field='JOGF' + if value: + self.controls[field].setStyleSheet(STYLES['BUTTON_ON']) + else: + self.controls[field].setStyleSheet(STYLES[field]) + + def onChangeHOMR(self, value = None, **kws): + '''EPICS monitor on HOMR called this''' + field='HOMR' + if value: + self.controls[field].setStyleSheet(STYLES['BUTTON_ON']) + else: + self.controls[field].setStyleSheet(STYLES[field]) + + def onChangeHOMF(self, value = None, **kws): + '''EPICS monitor on HOMF called this''' + field='HOMF' + if value: + self.controls[field].setStyleSheet(STYLES['BUTTON_ON']) + else: + self.controls[field].setStyleSheet(STYLES[field]) + + def onChangeMSTA(self, value = None, **kws): + '''EPICS monitor on MSTA called this''' + field='MSTA' + self.controls[field].setText(bin(int(value))[2:]) + + def onChangeDESC(self, char_value=None, **kws): + '''EPICS monitor on DESC called this''' + self.controls['DESC'].setText(char_value) + + def onChangeDMOV(self, value = None, **kws): + '''EPICS monitor on DMOV called this, change the color of the RBV label''' + if value is not None: + color = {1: BACKGROUND_DONE_MOVING, 0: BACKGROUND_MOVING}[value] + self.setLabelBackground(self.controls['RBV'], color) + + def onChangeHLS(self, value=None, **kws): + '''EPICS monitor on HLS called this, change the color of the TWF button''' + if value is not None: + color = {0: BACKGROUND_DEFAULT, 1: BACKGROUND_LIMIT_ON}[value] + self.setLabelBackground(self.controls['TWF'], color) + + def onChangeLLS(self, value=None, **kws): + '''EPICS monitor on LLS called this, change the color of the TWR button''' + if value is not None: + color = {0: BACKGROUND_DEFAULT, 1: BACKGROUND_LIMIT_ON}[value] + self.setLabelBackground(self.controls['TWR'], color) + + def onChangeEGU(self, char_value=None, **kws): + '''EPICS monitor on EGU called this''' + self.controls['EGU'].setText(char_value) + + def onChangeRBV(self, value=None, **kws): + '''EPICS monitor on RBV called this''' + field='RBV' + if value is not None: + self.controls[field].setText(str(value)) + + def onChangeTWV(self, value=None, **kws): + '''EPICS monitor on TWV called this''' + field='TWV' + if value is not None: + self.controls[field].setText(str(value)) + self.controls[field].setAlignment(QtCore.Qt.AlignRight) + + def onChangeVAL(self, value=None, **kws): + '''EPICS monitor on VAL called this''' + field='VAL' + if value is not None: + self.controls[field].setText(str(value)) + self.controls[field].setAlignment(QtCore.Qt.AlignRight) + + def onChangeVELO(self, value=None, **kws): + '''EPICS monitor on VELO called this''' + field='VELO' + if value is not None: + self.controls[field].setText(str(value)) + self.controls[field].setAlignment(QtCore.Qt.AlignRight) + + def onChangeJVEL(self, value=None, **kws): + '''EPICS monitor on JVEL called this''' + field='JVEL' + if value is not None: + self.controls[field].setText(str(value)) + self.controls[field].setAlignment(QtCore.Qt.AlignRight) + + def onChangeCntrlErrorIdPv(self,pvname=None, value=None, char_value=None, **kw): + self.cntrlErrorMsg=self.cntrlErrorMsgPv.get(as_string=True) + print("new Error message: " +str(self.cntrlErrorMsg)) + + def onChangeCntrlErrorMsgPv(self,pvname=None, value=None, char_value=None, **kw): + print("onChangeCntrlErrorMsgPv:" + char_value) + + def onChangeCntrlCmdPv(self,pvname=None, value=None, char_value=None, **kw): + print("onChangeCntrlCmdPv") + + def setLabelBackground(self, widget = None, color = BACKGROUND_DEFAULT): + '''change the background color of a Qt widget''' + if widget is not None: + palette = QtGui.QPalette() + palette.setColor(widget.backgroundRole(), QtGui.QColor(color)) + widget.setPalette(palette) + + +def main(): + '''demo: display the named motors in a horizontal block''' + if len(sys.argv) != 2: + raise RuntimeError ("usage: %s motor".format(sys.argv[0])) + app = QtWidgets.QApplication(sys.argv) + panel = MotorPanel(pvname=sys.argv[1]) + #panel.connect() + panel.show() + sys.exit(app.exec_()) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tools/ecmcRTCanvas.py b/tools/ecmcRTCanvas.py new file mode 100644 index 0000000..b0917dc --- /dev/null +++ b/tools/ecmcRTCanvas.py @@ -0,0 +1,177 @@ + +#************************************************************************* +# Copyright (c) 2020 European Spallation Source ERIC +# ecmc is distributed subject to a Software License Agreement found +# in file LICENSE that is included with this distribution. +# +# ecmcRTCanvas.py +# +# Created on: July 6, 2020 +# Author: Anders Sandström +# +# Heavily inspired by: https://exceptionshub.com/real-time-plotting-in-while-loop-with-matplotlib.html +# +#*************************************************************************** +import sys +import os +import epics +from PyQt5.QtWidgets import * +from PyQt5 import QtWidgets + +from PyQt5.QtCore import * +from PyQt5.QtGui import * +import functools +import numpy as np +import random as rd +import matplotlib +matplotlib.use("Qt5Agg") +from matplotlib.figure import Figure +from matplotlib.animation import TimedAnimation +from matplotlib.lines import Line2D +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar +import time +import threading +class ecmcRTCanvas(FigureCanvas, TimedAnimation): + def __init__(self, title): + self.pause = 0 + self.addedData = [] + self.exceptCount = 0 + self.autoZoom = False + print(matplotlib.__version__) + # The data + self.xlim = 1000 + self.n = np.linspace(-(self.xlim - 1), 0, self.xlim) + self.y = (self.n * 0.0) + # The window + self.fig = Figure(figsize=(5,5), dpi=100) + self.ax1 = self.fig.add_subplot(111) + # self.ax1 settings + self.ax1.set_xlabel('samples') + self.ax1.set_ylabel('data') + self.ax1.set_title(title) + self.line1 = Line2D([], [], color='blue') + self.line1_tail = Line2D([], [], color='red', linewidth=2) + self.line1_head = Line2D([], [], color='red', marker='o', markeredgecolor='r') + self.ax1.add_line(self.line1) + self.ax1.add_line(self.line1_tail) + self.ax1.add_line(self.line1_head) + self.ax1.set_xlim(-(self.xlim - 1),0) + self.ax1.set_ylim(-100, 100) + self.ax1.grid() + self.firstUpdatedData = True + FigureCanvas.__init__(self, self.fig) + TimedAnimation.__init__(self, self.fig, interval = 50, blit = True) + return + + + def new_frame_seq(self): + return iter(range(self.n.size)) + + def setBufferSize(self, bufferSize): + if bufferSize<1000 : + print("Buffer size out of range: " + str(bufferSize)) + return + fillValue = self.y[0] + oldSize = self.xlim + self.xlim = int(bufferSize) + self.n = np.linspace(-(self.xlim - 1),0,self.xlim) + + if self.xlim > oldSize: + tempArray = np.full(self.xlim - oldSize,fillValue) + self.y = np.concatenate((tempArray, self.y)) + else: + self.y = self.y[oldSize-self.xlim:-1] + + self.ax1.set_xlim(-(self.xlim-1), 1) + self.draw() + + + def pauseUpdate(self): + if self.pause: + self.pause = 0 + else: + self.pause = 1 + + def _init_draw(self): + lines = [self.line1, self.line1_tail, self.line1_head] + for l in lines: + l.set_data([], []) + return + + def addData(self, value): + if self.pause == 0: + self.addedData.append(value) + + return + + def zoomAuto(self): + bottom = np.min(self.y) + top = np.max(self.y) + # ensure different values + if bottom == top: + top = bottom +1 + self.ax1.clear() + self.ax1.grid(b=True) + range = top - bottom + top += range * 0.1 + bottom -= range *0.1 + self.ax1.set_ylim(bottom,top) + self.ax1.set_xlim(-(self.xlim-1), 1) + self.draw() + return + + def zoomLow(self, value): + top = self.ax1.get_ylim()[1] + bottom = value + self.ax1.set_ylim(bottom,top) + self.draw() + return + + def zoomHigh(self, value): + bottom = self.ax1.get_ylim()[0] + top = value + self.ax1.set_ylim(bottom,top) + self.draw() + return + + def _step(self, *args): + # Extends the _step() method for the TimedAnimation class. + try: + TimedAnimation._step(self, *args) + except Exception as e: + self.exceptCount += 1 + print(str(self.exceptCount)) + TimedAnimation._stop(self) + pass + + return + + def getYLims(self): + return self.ax1.get_ylim() + + def _draw_frame(self, framedata): + margin = 1 + while(len(self.addedData) > 0): + self.y = np.roll(self.y, -1) + self.y[-1] = self.addedData[0] + if self.firstUpdatedData: + if len(self.addedData) > 0: + self.y[0:-1] = self.addedData[0] # Set entire array to start value + self.firstUpdatedData = False + self.zoomAuto() + del(self.addedData[0]) + + self.line1.set_data(self.n[ 0 : self.n.size - margin ], self.y[ 0 : self.n.size - margin ]) + self.line1_tail.set_data(np.append(self.n[-10:-1 - margin], self.n[-1 - margin]), np.append(self.y[-10:-1 - margin], self.y[-1 - margin])) + self.line1_head.set_data(self.n[-1 - margin], self.y[-1 - margin]) + self._drawn_artists = [self.line1, self.line1_tail, self.line1_head] + return + + def setYLabel(self,label): + self.ax1.set_ylabel(label) + self.draw() + + def setTitle(self,label): + self.ax1.set_title(label) + self.draw() diff --git a/tools/ecmcTrend.py b/tools/ecmcTrend.py new file mode 100644 index 0000000..66f6bb2 --- /dev/null +++ b/tools/ecmcTrend.py @@ -0,0 +1,199 @@ +#************************************************************************* +# Copyright (c) 2020 European Spallation Source ERIC +# ecmc is distributed subject to a Software License Agreement found +# in file LICENSE that is included with this distribution. +# +# ecmcTrend.py +# +# Created on: July 6, 2020 +# Author: Anders Sandström +# +# Heavily inspired by: https://exceptionshub.com/real-time-plotting-in-while-loop-with-matplotlib.html +# +#************************************************************************* + +import sys +import os +import ecmcRTCanvas +from PyQt5.QtWidgets import * +from PyQt5 import QtWidgets +from PyQt5.QtCore import * +from PyQt5.QtGui import * +import functools +import numpy as np +import random as rd +import matplotlib +matplotlib.use("Qt5Agg") +from matplotlib.figure import Figure +from matplotlib.animation import TimedAnimation +from matplotlib.lines import Line2D +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar +import time +import threading + +class ecmcTrend(QtWidgets.QDialog): + def __init__(self): + super(ecmcTrend, self).__init__() + # Define the geometry of the main window + self.setGeometry(300, 300, 900, 700) + self.setWindowTitle("ecmc plot") + self.main_frame= QtWidgets.QFrame(self) + self.main_layout = QtWidgets.QHBoxLayout() + + self.left_frame = QFrame(self) + self.left_layout = QVBoxLayout() + + self.right_frame = QFrame(self) + self.right_layout = QVBoxLayout() + + # Manual zoom High + self.zoomHigh_frame = QFrame(self) + self.zoomHigh_layout = QGridLayout() + self.lblYMax= QLabel(text = "y-max:") + self.lineEditZoomHigh = QLineEdit(text = '100') + self.lineEditZoomHigh.setFixedSize(100, 50) + self.zoomHighBtn = QPushButton(text = '>') + self.zoomHighBtn.setFixedSize(10, 50) + self.zoomHighBtn.clicked.connect(self.zoomHighBtnAction) + self.zoomHigh_layout.addWidget(self.lblYMax,0,0,alignment = Qt.AlignRight | Qt.AlignBottom) + self.zoomHigh_layout.addWidget(self.lineEditZoomHigh,1,0,alignment = Qt.AlignRight | Qt.AlignTop) + self.zoomHigh_layout.addWidget(self.zoomHighBtn,1,1,alignment = Qt.AlignLeft | Qt.AlignTop) + self.zoomHigh_frame.setLayout(self.zoomHigh_layout) + + # Auto zoom + self.zoomBtn = QPushButton(text = 'zoom auto') + self.zoomBtn.setFixedSize(100, 50) + self.zoomBtn.clicked.connect(self.zoomBtnAction) + + # Pause + self.pauseBtn = QPushButton(text = 'pause') + self.pauseBtn.setFixedSize(100, 50) + self.pauseBtn.clicked.connect(self.pauseBtnAction) + + # Manual zoom Low + self.zoomLow_frame = QFrame(self) + self.zoomLow_layout = QGridLayout() + self.lblYMin= QLabel(text = "y-min:") + self.lineEditZoomLow = QLineEdit(text = '-100') + self.lineEditZoomLow.setFixedSize(100, 50) + self.zoomLowBtn = QPushButton(text = '>') + self.zoomLowBtn.setFixedSize(10, 50) + self.zoomLowBtn.clicked.connect(self.zoomLowBtnAction) + self.zoomLow_layout.addWidget(self.lblYMin,0,0,alignment = Qt.AlignRight | Qt.AlignBottom) + self.zoomLow_layout.addWidget(self.lineEditZoomLow,1,0,alignment = Qt.AlignRight | Qt.AlignTop) + self.zoomLow_layout.addWidget(self.zoomLowBtn,1,1,alignment = Qt.AlignLeft | Qt.AlignTop) + self.zoomLow_frame.setLayout(self.zoomLow_layout) + + # Write PV + self.pvPut_frame = QFrame(self) + self.pvPut_layout = QGridLayout() + self.lblYMin= QLabel(text = "Write PV:") + self.lineEditpvPut = QLineEdit(text = '0') + self.lineEditpvPut.setFixedSize(100, 50) + self.pvPutBtn = QPushButton(text = '>') + self.pvPutBtn.setFixedSize(10, 50) + self.pvPutBtn.clicked.connect(self.pvPutBtnAction) + self.pvPut_layout.addWidget(self.lblYMin,0,0,alignment = Qt.AlignRight | Qt.AlignBottom) + self.pvPut_layout.addWidget(self.lineEditpvPut,1,0,alignment = Qt.AlignRight | Qt.AlignTop) + self.pvPut_layout.addWidget(self.pvPutBtn,1,1,alignment = Qt.AlignLeft | Qt.AlignTop) + self.pvPut_frame.setLayout(self.pvPut_layout) + + # Buffer size + self.bufferSize_frame = QFrame(self) + self.bufferSize_layout = QGridLayout() + self.lblBufferSize= QLabel(text = "Buffer size []") + self.lineBufferSize = QLineEdit(text = '1000') + self.lineBufferSize.setFixedSize(100, 50) + self.setBufferSizeBtn = QPushButton(text = '>') + self.setBufferSizeBtn.setFixedSize(10, 50) + self.setBufferSizeBtn.clicked.connect(self.setBufferSizeBtnAction) + self.bufferSize_layout.addWidget(self.lblBufferSize,0,0,alignment = Qt.AlignRight | Qt.AlignBottom) # row, col + self.bufferSize_layout.addWidget(self.lineBufferSize, 1,0,alignment = Qt.AlignRight | Qt.AlignTop) + self.bufferSize_layout.addWidget(self.setBufferSizeBtn, 1,1,alignment = Qt.AlignLeft | Qt.AlignTop) + self.bufferSize_frame.setLayout(self.bufferSize_layout) + + + self.spacerTop = QSpacerItem(100,50) + self.spacerZoomUpper = QSpacerItem(100,10) + self.spacerZoomLower = QSpacerItem(100,10) + + self.left_layout.addWidget(self.zoomHigh_frame) + self.left_layout.addWidget(self.zoomBtn) + self.left_layout.addWidget(self.pauseBtn) + self.left_layout.addWidget(self.zoomLow_frame) + self.left_layout.addWidget(self.pvPut_frame) + + # Place the matplotlib figure + self.myFig = ecmcRTCanvas.ecmcRTCanvas("ecmc plot") + self.myFig.setFixedSize(700,500 ) + self.toolbar = NavigationToolbar(self.myFig, self) + self.right_layout.addWidget(self.toolbar) + self.right_layout.addWidget(self.myFig) + self.right_layout.addWidget(self.bufferSize_frame) + self.lineEditZoomLow.setText(str(self.myFig.getYLims()[0])) + self.lineEditZoomHigh.setText(str(self.myFig.getYLims()[1])) + + self.left_frame.setLayout(self.left_layout) + self.right_frame.setLayout(self.right_layout) + self.main_layout.addWidget(self.left_frame) + self.main_layout.addWidget(self.right_frame) + self.main_frame.setLayout(self.main_layout) + + return + + def setBufferSizeBtnAction(self): + value = float(self.lineBufferSize.text()) + self.myFig.setBufferSize(value) + + def zoomBtnAction(self): + self.myFig.zoomAuto() + return + + def zoomHighBtnAction(self): + value = float(self.lineEditZoomHigh.text()) + self.myFig.zoomHigh(value) + return + + def zoomLowBtnAction(self): + value = float(self.lineEditZoomLow.text()) + self.myFig.zoomLow(value) + return + + def pvPutBtnAction(self): + value = float(self.lineEditpvPut.text()) + self.writePV(value) + return + + def pauseBtnAction(self): + self.myFig.pauseUpdate() + return + + def lineEditHighAction(self): + value = float(self.lineEditZoomHigh.text()) + self.myFig.zoomHigh(value) + return + + def lineEditLowAction(self): + value = float(self.lineEditZoomLow.text()) + self.myFig.zoomLow(value) + return + + def addData_callbackFunc(self, value): + self.myFig.addData(value) + return + + def setYLabel(self,label): + self.myFig.setYLabel(label) + + def setTitle(self,label): + self.myFig.setTitle(label) + self.setWindowTitle("ecmc plot: " + label) + + def enablePut(self,enable): + self.pvPut_frame.setEnabled(enable) + self.pvPutBtn.setEnabled(enable) + self.lineEditpvPut.setEnabled(enable) + QCoreApplication.processEvents() + + diff --git a/tools/ecmcTrendPv.py b/tools/ecmcTrendPv.py new file mode 100644 index 0000000..62710fd --- /dev/null +++ b/tools/ecmcTrendPv.py @@ -0,0 +1,70 @@ +#************************************************************************* +# Copyright (c) 2020 European Spallation Source ERIC +# ecmc is distributed subject to a Software License Agreement found +# in file LICENSE that is included with this distribution. +# +# ecmcTrendPv.py +# +# Created on: July 6, 2020 +# Author: Anders Sandström +# +# Heavily inspired by: https://exceptionshub.com/real-time-plotting-in-while-loop-with-matplotlib.html +# +# Extends the ecmcTrend class will epics pv callbacks +# +#************************************************************************* + +import sys +import os +import epics +import ecmcTrend +from PyQt5.QtWidgets import * +from PyQt5 import QtWidgets +from PyQt5.QtCore import * +from PyQt5.QtGui import * +import functools +import numpy as np +import random as rd +import matplotlib +matplotlib.use("Qt5Agg") +from matplotlib.figure import Figure +from matplotlib.animation import TimedAnimation +from matplotlib.lines import Line2D +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar +import time +import threading + + +class comTrend(QObject): + data_signal = pyqtSignal(float) + + +class ecmcTrendPv(ecmcTrend.ecmcTrend): + def __init__(self,pvName=None): + super(ecmcTrendPv, self).__init__() + self.comTrend = comTrend() + self.comTrend.data_signal.connect(self.addData_callbackFunc) # update trend + self.startval = 0 + self.pvName = pvName + self.connectPv(self.pvName) # Epics + self.setTitle(pvName) + return + + def connectPv(self, pvname): + if pvname is None: + raise RuntimeError("pvname must not be 'None'") + if len(pvname)==0: + raise RuntimeError("pvname must not be ''") + self.pv = epics.PV(self.pvName) + self.startval = self.pv.get() + self.pv.add_callback(self.onChangePv) + self.myFig.addData(self.startval) + QCoreApplication.processEvents() + + def onChangePv(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + self.comTrend.data_signal.emit(value) + + def writePV(self,value): + self.pv.put(value) + self.myFig.addData(value)