|
@@ -0,0 +1,1025 @@
|
|
|
+#from fbs_runtime.application_context.PyQt5 import ApplicationContext
|
|
|
+from PyQt5.QtCore import QRunnable,pyqtSlot,QThreadPool,QMetaObject,Qt,Q_ARG,QTimer,QTime,QProcess,QItemSelection,QItemSelectionModel,QSignalBlocker,QRectF,pyqtSignal
|
|
|
+from PyQt5.QtWidgets import (QMainWindow, QApplication, QWidget, QPushButton, QStackedLayout,QItemDelegate,
|
|
|
+ QSlider,
|
|
|
+ QHBoxLayout, QVBoxLayout, QGridLayout, QApplication, QAction,QAbstractItemView,
|
|
|
+ QTabWidget,QLineEdit,QLabel,QComboBox,QDialogButtonBox,QPlainTextEdit,QProgressBar,
|
|
|
+ QSizePolicy,QMessageBox,QTableWidget,QTableWidgetItem,QTextEdit,QHeaderView,QMenu)
|
|
|
+from PyQt5.QtGui import QPalette,QColor,QTextCursor,QPainter,QFont
|
|
|
+import csv
|
|
|
+import numpy as np
|
|
|
+import traceback
|
|
|
+
|
|
|
+import sys,os,time,struct,math,itertools,json
|
|
|
+import pyqtgraph as pg
|
|
|
+from PC410Controller import PC410Controller
|
|
|
+
|
|
|
+"""
|
|
|
+class PlotDataset(object):
|
|
|
+class PlotDataItem(GraphicsObject):
|
|
|
+
|
|
|
+"""
|
|
|
+
|
|
|
+class QCycleThread(QRunnable):
|
|
|
+ # Custom threader for running cycle() in pc410controller in a thread
|
|
|
+ def __init__(self, name, pc410controller):
|
|
|
+ QRunnable.__init__(self)
|
|
|
+ self.name=name
|
|
|
+ self.pc410controller=pc410controller
|
|
|
+ def run(self):
|
|
|
+ try:
|
|
|
+ self.pc410controller.running=1
|
|
|
+ print("Starting ct " + self.name)
|
|
|
+ self.pc410controller.cycle(self.pc410controller.tstart0)
|
|
|
+ print("Exiting ct " + self.name)
|
|
|
+ except:
|
|
|
+ print(traceback.format_exc())
|
|
|
+ finally:
|
|
|
+ self.pc410controller.running=0
|
|
|
+ def start(self):
|
|
|
+ QThreadPool.globalInstance().setObjectName(self.name)
|
|
|
+ QThreadPool.globalInstance().start(self)
|
|
|
+ def join(self):
|
|
|
+ pass
|
|
|
+
|
|
|
+class QRunnableThread(QRunnable):
|
|
|
+ def __init__(self, name, parent):
|
|
|
+ QRunnable.__init__(self)
|
|
|
+ self.name=name
|
|
|
+ self.parent=parent
|
|
|
+ def run(self):
|
|
|
+ try:
|
|
|
+ self.parent.running=1
|
|
|
+ print("Starting ct " + self.name)
|
|
|
+ self.parent.cycle(self.parent.tstart0)
|
|
|
+ print("Exiting rt " + self.name)
|
|
|
+ except:
|
|
|
+ print(traceback.format_exc())
|
|
|
+ finally:
|
|
|
+ self.parent.running=0
|
|
|
+ def start(self):
|
|
|
+ QThreadPool.globalInstance().setObjectName(self.name)
|
|
|
+ QThreadPool.globalInstance().start(self)
|
|
|
+ def join(self):
|
|
|
+ pass
|
|
|
+
|
|
|
+class ProcessRunnable(QRunnable):
|
|
|
+ def __init__(self, target, args=None,start=True):
|
|
|
+ QRunnable.__init__(self)
|
|
|
+ self.name="ProcessRunnable"
|
|
|
+ self.t = target
|
|
|
+ if args==None: args=()
|
|
|
+ self.args = args
|
|
|
+ if start: self.start()
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ try:
|
|
|
+ self.t(*self.args)
|
|
|
+ except:
|
|
|
+ print(traceback.format_exc())
|
|
|
+ finally:
|
|
|
+ print("exiting",self.t)
|
|
|
+
|
|
|
+ def start(self):
|
|
|
+ QThreadPool.globalInstance().setObjectName(self.name)
|
|
|
+ QThreadPool.globalInstance().start(self)
|
|
|
+
|
|
|
+class TableView(QTableWidget):
|
|
|
+ def __init__(self, headers, data, *args):
|
|
|
+ QTableWidget.__init__(self, *args)
|
|
|
+ self.data = data
|
|
|
+ self.headers = headers
|
|
|
+ self.setData()
|
|
|
+ self.resizeColumnsToContents()
|
|
|
+ self.resizeRowsToContents()
|
|
|
+ self.cbSelectionChanged=None
|
|
|
+ self.cbCellChanged=None
|
|
|
+
|
|
|
+ self.cellChanged.connect(self.sCellChanged)
|
|
|
+
|
|
|
+ def sCellChanged(self,row,column):
|
|
|
+ if self.state()==QAbstractItemView.EditingState:
|
|
|
+ txt=self.item(row,column).text()
|
|
|
+ if self.cbCellChanged:
|
|
|
+ self.cbCellChanged(row,column,txt)
|
|
|
+
|
|
|
+ def selectionChanged(self,selected, deselected):
|
|
|
+ super().selectionChanged(selected,deselected)
|
|
|
+ indexes=selected.indexes()
|
|
|
+ dindexes=deselected.indexes()
|
|
|
+ rows=set([item.row() for item in indexes])
|
|
|
+ drows=set([item.row() for item in dindexes])
|
|
|
+ if rows:
|
|
|
+ row0=min(rows)
|
|
|
+ row1=max(rows)
|
|
|
+ if self.cbSelectionChanged:
|
|
|
+ self.cbSelectionChanged(row0,row1)
|
|
|
+
|
|
|
+ def setData(self):
|
|
|
+ horHeaders = []
|
|
|
+ for n, key in enumerate(self.headers):
|
|
|
+ horHeaders.append(key)
|
|
|
+ for m, item in enumerate(self.data[key]):
|
|
|
+ newitem = QTableWidgetItem(str(item))
|
|
|
+ self.setItem(m, n, newitem)
|
|
|
+ self.setHorizontalHeaderLabels(horHeaders)
|
|
|
+
|
|
|
+class ValueSelectDelegate(QItemDelegate):
|
|
|
+ def __init__(self, parent=None,app=None):
|
|
|
+ QItemDelegate.__init__(self, parent)
|
|
|
+ self.app=app
|
|
|
+ def createEditor(self, parent, option, index):
|
|
|
+ model_value = index.model().data(index, Qt.DisplayRole)
|
|
|
+ editor = QComboBox(parent)
|
|
|
+ lines=[ x for x in model_value.split("\n") if x]
|
|
|
+ editor.addItems(lines)
|
|
|
+ return editor
|
|
|
+ def setEditorData(self, editor, index):
|
|
|
+ model_value = index.model().data(index, Qt.EditRole)
|
|
|
+ # must get value from actual data and select value matching in list
|
|
|
+ current_index = editor.findText(model_value)
|
|
|
+ if current_index > 0:
|
|
|
+ editor.setCurrentIndex(current_index)
|
|
|
+ def setModelData(self, editor, model, index):
|
|
|
+ editor_value = editor.currentText()
|
|
|
+ parts=editor_value.split("=")
|
|
|
+ if len(parts)>1:
|
|
|
+ value=parts[0].strip()
|
|
|
+ self.app.realTableCellChanged(index.row(),index.column(),value)
|
|
|
+ #print("setModelData",index.row(),index.column(),editor_value,model)
|
|
|
+ # model.setData(index, editor_value, QtCore.Qt.EditRole)
|
|
|
+
|
|
|
+class SimplifiedPlotItemMovement(pg.PlotItem):
|
|
|
+ pointmoved = pyqtSignal([int,float,float],name="pointMoved")
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ pg.PlotItem.__init__(self)
|
|
|
+
|
|
|
+ # Initialize the class instance variables.
|
|
|
+ self.dragPoint = None
|
|
|
+ self.dragIndex = -1
|
|
|
+ self.dragOffset = 0
|
|
|
+ self.plot_item_control = None
|
|
|
+
|
|
|
+ def mouseDragEvent(self, ev):
|
|
|
+ # Check to make sure the button is the left mouse button. If not, ignore it.
|
|
|
+ # Would have to change this if porting to Mac I assume.
|
|
|
+ if ev.button() != Qt.MouseButton.LeftButton:
|
|
|
+ ev.ignore()
|
|
|
+ return
|
|
|
+
|
|
|
+ if ev.isStart():
|
|
|
+ # We are already one step into the drag.
|
|
|
+ # Find the point(s) at the mouse cursor when the button was first
|
|
|
+ # pressed:
|
|
|
+ # pos = ev.buttonDownPos()
|
|
|
+ pos = ev.buttonDownScenePos()
|
|
|
+ # Switch position into local coords using viewbox
|
|
|
+ local_pos = self.vb.mapSceneToView(pos)
|
|
|
+
|
|
|
+ for item in self.dataItems:
|
|
|
+ new_pts = item.scatter.pointsAt(local_pos)
|
|
|
+ if len(new_pts) == 1:
|
|
|
+ # Store the drag point and the index of the point for future reference.
|
|
|
+ self.dragPoint = new_pts[0]
|
|
|
+ self.dragIndex = item.scatter.points().tolist().index(new_pts[0])
|
|
|
+ # Find the initial offset of the drag operation from the current point.
|
|
|
+ # This value should allow for the initial mismatch from cursor to point to be accounted for.
|
|
|
+
|
|
|
+ self.dragOffset = new_pts[0].pos() - local_pos
|
|
|
+ # If we get here, accept the event to prevent the screen from panning during the drag.
|
|
|
+
|
|
|
+
|
|
|
+ ev.accept()
|
|
|
+ elif ev.isFinish():
|
|
|
+ # The drag is finished. Reset the drag point and index.
|
|
|
+ self.dragPoint = None
|
|
|
+ self.dragIndex = -1
|
|
|
+ return
|
|
|
+ else:
|
|
|
+ # If we get here, this isn't the start or end. Somewhere in the middle.
|
|
|
+ if self.dragPoint is None:
|
|
|
+ # We aren't dragging a point so ignore the event and allow the panning to continue
|
|
|
+ ev.ignore()
|
|
|
+ return
|
|
|
+ else:
|
|
|
+ # We are dragging a point. Find the local position of the event.
|
|
|
+ local_pos = self.vb.mapSceneToView(ev.scenePos())
|
|
|
+
|
|
|
+ # Update the point in the PlotDataItem using get/set data.
|
|
|
+ # If we had more than one plotdataitem we would need to search/store which item
|
|
|
+ # is has a point being moved. For this example we know it is the plot_item_control object.
|
|
|
+ x,y = self.plot_item_control.getData()
|
|
|
+ # Be sure to add in the initial drag offset to each coordinate to account for the initial mismatch.
|
|
|
+ x[self.dragIndex] = local_pos.x() + self.dragOffset.x()
|
|
|
+ y[self.dragIndex] = local_pos.y() + self.dragOffset.y()
|
|
|
+ # Update the PlotDataItem (this will automatically update the graphics when a change is detected)
|
|
|
+ #self.plot_item_control.setData(x, y)
|
|
|
+
|
|
|
+ pos=local_pos.x(),local_pos.y()
|
|
|
+ self.pointmoved.emit(self.dragIndex,*pos)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+class MainWindow(QMainWindow):
|
|
|
+ def __init__(self,ctx,argv):
|
|
|
+ super(MainWindow, self).__init__()
|
|
|
+
|
|
|
+ self.indicatorHeight=32
|
|
|
+ self.resultHeight=36
|
|
|
+ self.buttonHeight=43
|
|
|
+ self.cursorMoved=0
|
|
|
+ self.selectedRow=0
|
|
|
+ self.fontsize=12
|
|
|
+
|
|
|
+ self.infoColumns=2
|
|
|
+ self.clockHeight=70 # 100
|
|
|
+ self.progressCompact=1
|
|
|
+ self.buttonRows=1
|
|
|
+ self.withActionLabel=0
|
|
|
+
|
|
|
+ self.running=0
|
|
|
+ self.starttime=time.time()
|
|
|
+ self.lasttime=self.starttime
|
|
|
+ self.errorMessage=""
|
|
|
+
|
|
|
+
|
|
|
+ self.tfontsize=10
|
|
|
+ self.tfont = QFont('Courier', self.tfontsize) #, QFont.Bold)
|
|
|
+
|
|
|
+ self.mintemp=20.0
|
|
|
+
|
|
|
+ self.e={}
|
|
|
+
|
|
|
+ home=os.environ["HOME"]
|
|
|
+ if not home: raise Exeption("HOME environment variable not set!")
|
|
|
+ self.home=home
|
|
|
+ user=os.environ["USER"]
|
|
|
+ if not home: raise Exeption("USER environment variable not set!")
|
|
|
+ self.user=user
|
|
|
+
|
|
|
+
|
|
|
+ self.loadSettings()
|
|
|
+
|
|
|
+
|
|
|
+ self.build()
|
|
|
+
|
|
|
+ self.pc410controller=PC410Controller(QCycleThread,self.cmdlog,self.infocallback,self.statecallback,self.recordcallback)
|
|
|
+ self.pc410controller.run()
|
|
|
+
|
|
|
+ def loadSettings(self):
|
|
|
+ path=os.path.join(os.environ["HOME"],".pc410Settings")
|
|
|
+ self.settings={}
|
|
|
+ try:
|
|
|
+ self.settings=json.loads(open(path).read())
|
|
|
+ except:
|
|
|
+ print("exception reading and decoding",path)
|
|
|
+
|
|
|
+ fieldrows=self.settings.get("fieldrows",[])
|
|
|
+ if not fieldrows:
|
|
|
+ fieldrows=[["Sn63Pb37", 1,85,70, 1,150,35, 1,185,50, -0.01],
|
|
|
+ ["Sn965.5Ag3Cu0.5", 1,85,60, 1,140,45, 1,170,25, 1,220,50, -0.01],
|
|
|
+ ]
|
|
|
+ self.settings["fieldrows"]=fieldrows
|
|
|
+
|
|
|
+
|
|
|
+ def saveSettings(self):
|
|
|
+ rowPosition = self.table.rowCount()
|
|
|
+
|
|
|
+ fieldrows=[]
|
|
|
+ for i in range(0,rowPosition):
|
|
|
+ name,program=self.getProgramParameters(i)
|
|
|
+ values=list(itertools.chain(*program))
|
|
|
+ fieldrows+=[[name]+values]
|
|
|
+ self.settings["fieldrows"]=fieldrows
|
|
|
+ path=os.path.join(os.environ["HOME"],".pc410Settings")
|
|
|
+ try:
|
|
|
+ open(path,"w").write(json.dumps(self.settings))
|
|
|
+ except:
|
|
|
+ print("exception writing settings",path)
|
|
|
+ raise
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ # PTN is which program
|
|
|
+ # STEP is which step of program...
|
|
|
+
|
|
|
+ def addButton(self,layout,name,fn,key=None,colour="white"):
|
|
|
+ button = QPushButton(self)
|
|
|
+ button.setText(name)
|
|
|
+ hmargin=(self.buttonHeight-self.fontsize)//2-5
|
|
|
+ button.setSizePolicy(
|
|
|
+ QSizePolicy.Preferred,
|
|
|
+ QSizePolicy.Expanding)
|
|
|
+ button.setContentsMargins(20,hmargin,20,hmargin)
|
|
|
+ #button.setStyleSheet("background-color: %s;"%colour)
|
|
|
+ #button.setStyleSheet("padding: 20px; background-color: %s;"%colour)
|
|
|
+ button.setStyleSheet("padding: %dpx; background-color: %s;"%(hmargin,colour))
|
|
|
+
|
|
|
+ #button.setGeometry(200, 150, 100, 40)
|
|
|
+ #button.setFont(QFont('Times', 15))
|
|
|
+
|
|
|
+ if not key: key=name
|
|
|
+ self.e[key]=(name,key,fn,button)
|
|
|
+ layout.addWidget(button)
|
|
|
+ button.clicked.connect(fn)
|
|
|
+
|
|
|
+ def saveAction(self):
|
|
|
+ self.saveSettings()
|
|
|
+
|
|
|
+ def stopAction(self):
|
|
|
+ self.pc410controller.cmdStop()
|
|
|
+ self.running=0
|
|
|
+ def runAction(self):
|
|
|
+ self.pc410controller.cmdRun()
|
|
|
+ self.running=1
|
|
|
+ self.clearRunRecord()
|
|
|
+ self.starttime=time.time()
|
|
|
+
|
|
|
+ def runningToStopped(self):
|
|
|
+ self.running=0
|
|
|
+ def stoppedToRunning(self):
|
|
|
+ self.running=1
|
|
|
+ self.clearRunRecord()
|
|
|
+ self.starttime=time.time()
|
|
|
+
|
|
|
+ def readAction(self):
|
|
|
+ program=self.pc410controller.readProgram()
|
|
|
+ print(program)
|
|
|
+ if program:
|
|
|
+ # update current row in table
|
|
|
+ smodel=self.table.selectionModel()
|
|
|
+ sindexes=smodel.selectedIndexes()
|
|
|
+ if sindexes:
|
|
|
+ rowPosition=max(set([s.row() for s in sindexes]))
|
|
|
+ else:
|
|
|
+ rowPosition = self.table.rowCount()
|
|
|
+ self.table.insertRow(rowPosition)
|
|
|
+ print(rowPosition)
|
|
|
+ name="read"
|
|
|
+ self.setProgramParameters(name,program,rowPosition)
|
|
|
+
|
|
|
+ def writeAction(self):
|
|
|
+ smodel=self.table.selectionModel()
|
|
|
+ sindexes=smodel.selectedIndexes()
|
|
|
+ if sindexes:
|
|
|
+ rowSet=set([s.row() for s in sindexes])
|
|
|
+ if len(rowSet)!=1:
|
|
|
+ # fail
|
|
|
+ return
|
|
|
+ rowPosition=min(rowSet)
|
|
|
+
|
|
|
+ name,program=self.getProgramParameters(rowPosition)
|
|
|
+ if program:
|
|
|
+ result=self.pc410controller.writeProgram(program)
|
|
|
+ if result!=0:
|
|
|
+ print("### failed to write program!!!")
|
|
|
+
|
|
|
+ def clearRunRecord(self):
|
|
|
+ self.runrecord=([],[])
|
|
|
+
|
|
|
+ def addRowAction(self):
|
|
|
+ self.addRow(-1)
|
|
|
+
|
|
|
+ def addRow(self,rowPosition=-1):
|
|
|
+ if rowPosition==-1:
|
|
|
+ smodel=self.table.selectionModel()
|
|
|
+ sindexes=smodel.selectedIndexes()
|
|
|
+ if sindexes:
|
|
|
+ rowPosition=max(set([s.row() for s in sindexes]))+1
|
|
|
+ else:
|
|
|
+ rowPosition = self.table.rowCount()
|
|
|
+ self.table.insertRow(rowPosition)
|
|
|
+
|
|
|
+ def removeRow(self,rowPosition):
|
|
|
+ if self.table.rowCount()<=1:
|
|
|
+ self.addRow(-1)
|
|
|
+ self.table.removeRow(rowPosition)
|
|
|
+
|
|
|
+
|
|
|
+ #@pyqtSlot(int,float,float)
|
|
|
+ def tableContextMenu(self,point):
|
|
|
+ index = self.table.indexAt(point)
|
|
|
+ if index.isValid() :
|
|
|
+ # show context menu
|
|
|
+ self.contextMenu = QMenu(self)
|
|
|
+ taddrowabove = self.contextMenu.addAction("Insert row above")
|
|
|
+ taddrowbelow = self.contextMenu.addAction("Insert row below")
|
|
|
+ self.contextMenu.addSeparator()
|
|
|
+ tdeleterow = self.contextMenu.addAction("Delete row")
|
|
|
+
|
|
|
+ # I want to perform actions only for a single column(E.g: Context menu only for column 4 of my table
|
|
|
+ # Need help here....???
|
|
|
+ action = self.contextMenu.exec_(self.table.mapToGlobal(point))
|
|
|
+
|
|
|
+ row0=index.row()
|
|
|
+ if action == taddrowabove:
|
|
|
+ self.addRow(row0)
|
|
|
+ elif action == taddrowbelow:
|
|
|
+ self.addRow(row0+1)
|
|
|
+ elif action == tdeleterow:
|
|
|
+ self.removeRow(row0)
|
|
|
+
|
|
|
+ def getTimeOfRun(self):
|
|
|
+ dt=self.lasttime-self.starttime
|
|
|
+ def showTime(self):
|
|
|
+ if self.running:
|
|
|
+ self.lasttime=time.time()
|
|
|
+ dt=self.lasttime-self.starttime
|
|
|
+ h=int((dt/3600)%100)
|
|
|
+ m=int((dt/60)%60)
|
|
|
+ s=int(dt%60)
|
|
|
+ label_time="%02d:%02d:%02d"%(h,m,s)
|
|
|
+ #current_time = QTime.currentTime()
|
|
|
+ #label_time = current_time.toString('hh:mm:ss')
|
|
|
+ self.clocklabel.setText(label_time)
|
|
|
+
|
|
|
+
|
|
|
+ def build(self):
|
|
|
+ if 1:
|
|
|
+ font = QFont('Arial', self.fontsize) #, QFont.Bold)
|
|
|
+ self.setFont(font)
|
|
|
+
|
|
|
+
|
|
|
+ toplayout = QHBoxLayout()
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ layout = QVBoxLayout()
|
|
|
+ toplayout.setContentsMargins(0,0,0,0)
|
|
|
+ layout.setContentsMargins(0,0,0,0)
|
|
|
+ toplayout.addLayout(layout)
|
|
|
+
|
|
|
+
|
|
|
+ layoutside = QVBoxLayout()
|
|
|
+ toplayout.addLayout(layoutside)
|
|
|
+ self.addButton(layoutside,"Save",self.saveAction,"action-save",colour="green")
|
|
|
+ self.addButton(layoutside,"Add row",self.addRowAction,"action-addrow",colour="cyan")
|
|
|
+ self.addButton(layoutside,"Run",self.runAction,"action-run",colour="yellow")
|
|
|
+ self.addButton(layoutside,"Stop",self.stopAction,"action-stop",colour="red")
|
|
|
+ self.addButton(layoutside,"Read program",self.readAction,"action-readprogram",colour="blue")
|
|
|
+ self.addButton(layoutside,"Write program",self.writeAction,"action-writeprogram",colour="green")
|
|
|
+
|
|
|
+
|
|
|
+ l=QLabel("Temperature over time")
|
|
|
+ layout.addWidget(l)
|
|
|
+
|
|
|
+ self.plot_graph = pg.GraphicsLayoutWidget()
|
|
|
+ #self.plot_graph = pg.PlotWidget()
|
|
|
+ self.plot_graph.setBackground("w")
|
|
|
+ self.lineplot=SimplifiedPlotItemMovement()
|
|
|
+ self.plot_graph.addItem(self.lineplot)
|
|
|
+ self.lineplot.pointMoved.connect(self.graphPointMoved)
|
|
|
+ self.plottexts=[]
|
|
|
+
|
|
|
+ if 1:
|
|
|
+ self.scatter = pg.ScatterPlotItem(size=2, brush=pg.mkBrush(255, 0, 0, 0))
|
|
|
+ #plot_item = self.plot_graph.addPlot()
|
|
|
+ #scatter_item = pg.ScatterPlotItem()
|
|
|
+ self.lineplot.addItem(self.scatter)
|
|
|
+ #self.plot_graph.addItem(self.scatter)
|
|
|
+ else:
|
|
|
+ self.recordplot = pg.PlotCurveItem(size=2, pen=pg.mkPen(color='#ff0000', width=1))
|
|
|
+ self.plot_graph.addItem(self.recordplot)
|
|
|
+
|
|
|
+ self.clearRunRecord()
|
|
|
+
|
|
|
+ self.lineplot.setTitle('<span style="color: blue; font-size: 20pt">Temperature vs Time</span>')
|
|
|
+ self.lineplot.setLabel("left", "Temperature (°C)")
|
|
|
+ self.lineplot.setLabel("bottom", "Time (seconds)")
|
|
|
+
|
|
|
+
|
|
|
+ self.timer = QTimer(self)
|
|
|
+ self.timer.timeout.connect(self.showTime)
|
|
|
+ self.timer.start(1000)
|
|
|
+ hsize=self.clockHeight-5*2
|
|
|
+ font = QFont('Arial', hsize, QFont.Bold)
|
|
|
+ self.clocklabel = QLabel()
|
|
|
+ self.clocklabel.setAlignment(Qt.AlignCenter)
|
|
|
+ self.clocklabel.setFont(font)
|
|
|
+ self.clocklabel.setContentsMargins(5,5,5,5)
|
|
|
+ layoutside.addWidget(self.clocklabel)
|
|
|
+
|
|
|
+
|
|
|
+ info=QGridLayout()
|
|
|
+ infoslots={}
|
|
|
+ def addInfoSlot(infoslots,name,label=None):
|
|
|
+ if not label: label=name
|
|
|
+ row=len(infoslots)
|
|
|
+ l=QLabel(label)
|
|
|
+ e=QLabel()
|
|
|
+ infoslots[name]=(name,l,e)
|
|
|
+ info.addWidget(l,row,0)
|
|
|
+ info.addWidget(e,row,1)
|
|
|
+ def addInfoSlot2D(infoslots,name,label=None):
|
|
|
+ if not label: label=name
|
|
|
+ count=len(infoslots)
|
|
|
+ row=count//2
|
|
|
+ col=count%2
|
|
|
+
|
|
|
+ l=QLabel(label)
|
|
|
+ e=QLabel()
|
|
|
+ infoslots[name]=(name,l,e)
|
|
|
+ info.addWidget(l,row,col*2+0)
|
|
|
+ info.addWidget(e,row,col*2+1)
|
|
|
+
|
|
|
+ faddinfoslot=addInfoSlot
|
|
|
+ if self.infoColumns==2: faddinfoslot=addInfoSlot2D
|
|
|
+
|
|
|
+ for param in ["PV","OP","SP","SL","HA","LA","DA","XP","TI","TD","HB","LB","CH","CC","RG","HS","LS","BP","HO","SR","Lc","SW","XS","OS"]:
|
|
|
+ # "HB",
|
|
|
+ faddinfoslot(infoslots,param)
|
|
|
+
|
|
|
+
|
|
|
+ layoutside.addLayout(info)
|
|
|
+ self.infoslots=infoslots
|
|
|
+
|
|
|
+
|
|
|
+ layout.addWidget(self.plot_graph)
|
|
|
+
|
|
|
+ if 1:
|
|
|
+
|
|
|
+ l=QLabel("Settings")
|
|
|
+ layout.addWidget(l)
|
|
|
+
|
|
|
+
|
|
|
+ if 0:
|
|
|
+ hbuttons = QHBoxLayout()
|
|
|
+ self.addButton(hbuttons,"Add row",self.addRow,"action-addrow",colour="cyan")
|
|
|
+ layout.addLayout(hbuttons)
|
|
|
+
|
|
|
+ """
|
|
|
+ Lead Sn63Pb37
|
|
|
+ step r L d
|
|
|
+ 1 1 85 70
|
|
|
+ 2 1 150 35
|
|
|
+ 3 1 185 50
|
|
|
+ 4 END Hb=230
|
|
|
+
|
|
|
+ Lead-Free Sn965.5Ag3Cu0.5
|
|
|
+ step r L d
|
|
|
+ 1 1 85 60
|
|
|
+ 2 1 140 45
|
|
|
+ 3 1 170 25
|
|
|
+ 3 1 220 50
|
|
|
+ 5 END hb=230
|
|
|
+
|
|
|
+ """
|
|
|
+
|
|
|
+ fieldrows=[["Sn63Pb37", 1,85,70, 1,150,35, 1,185,50, -0.01],
|
|
|
+ ["Sn965.5Ag3Cu0.5", 1,85,60, 1,140,45, 1,170,25, 1,220,50, -0.01],
|
|
|
+ ]
|
|
|
+ fieldrows=self.settings.get("fieldrows",fieldrows)
|
|
|
+
|
|
|
+ headers=["Name"]
|
|
|
+ for i in range(1,9):
|
|
|
+ headers+=["r%d"%i,"l%d"%i,"t%d"%i]
|
|
|
+
|
|
|
+ data={}
|
|
|
+ for i in range(0,len(headers)):
|
|
|
+ data[headers[i]]=[]
|
|
|
+ for row in fieldrows:
|
|
|
+ fieldrow={}
|
|
|
+ for ki in range(0,len(headers)):
|
|
|
+ k=headers[ki]
|
|
|
+ if ki>=len(row): v=""
|
|
|
+ else: v=row[ki]
|
|
|
+ data[k]+=[v]
|
|
|
+ #data[self.decoded]+=[0]
|
|
|
+
|
|
|
+ table = TableView(headers,data,len(data[headers[0]]),len(headers))
|
|
|
+ self.table=table
|
|
|
+ table.setContentsMargins(0,0,0,0)
|
|
|
+
|
|
|
+ table.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
|
|
|
+ table.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
|
|
|
+ table.setSelectionBehavior(QAbstractItemView.SelectRows);
|
|
|
+
|
|
|
+ table.cbSelectionChanged=self.tableSelectionChanged
|
|
|
+ table.cbCellChanged=self.tableSelectionChanged #CellChanged
|
|
|
+
|
|
|
+ def tableClicked(*args):
|
|
|
+ item=args[0]
|
|
|
+ row=item.row()
|
|
|
+
|
|
|
+ table.clicked.connect(tableClicked)
|
|
|
+
|
|
|
+
|
|
|
+ # setting context menu policy on my table, "self.table"
|
|
|
+ self.table.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
|
+
|
|
|
+ # setting context menu request by calling a function,"self.tableContextMenu"
|
|
|
+ self.table.customContextMenuRequested.connect(self.tableContextMenu)
|
|
|
+
|
|
|
+
|
|
|
+ table.show()
|
|
|
+ table.resize(250, 150)
|
|
|
+ layout.addWidget(self.table)
|
|
|
+
|
|
|
+ if layout:
|
|
|
+ widget = QWidget()
|
|
|
+ widget.setLayout(toplayout)
|
|
|
+ self.setCentralWidget(widget)
|
|
|
+
|
|
|
+ @pyqtSlot(int,float,float)
|
|
|
+ def graphPointMoved(self,index,x,y):
|
|
|
+
|
|
|
+ if index!=0:
|
|
|
+ row0=self.selectedRow
|
|
|
+ time,temp=self.buildCoordinates(row0)
|
|
|
+
|
|
|
+ """
|
|
|
+ moving time of point 1,3,5 etc means changing rate and adding diff to dwell time
|
|
|
+ moving time of point 2,4,6 etc means changing dwell time, and changing rate of the one after
|
|
|
+
|
|
|
+ """
|
|
|
+
|
|
|
+ step=(index-1)//2
|
|
|
+ stepi=1+step*3
|
|
|
+ rgi0=stepi+0
|
|
|
+ lgi0=stepi+1
|
|
|
+ tgi0=stepi+2
|
|
|
+ if (index%2)==0:
|
|
|
+ # 0,2,4 etc
|
|
|
+
|
|
|
+ # change rate of next to compensate for time change of this
|
|
|
+ # should cap but that time is constructed from rate and delta so max x would then be
|
|
|
+ # time[index]+(temp[index+1]-temp[index])/rate
|
|
|
+
|
|
|
+ # cap to minx and mintemp
|
|
|
+ rate0=float(self.table.item(row0,rgi0).text())
|
|
|
+ dy0=(y-temp[index-2])
|
|
|
+ minx=time[index-2]
|
|
|
+ if rate0: minx+=abs(dy0)/rate0
|
|
|
+ x=max(minx,x)
|
|
|
+ y=max(self.mintemp,y)
|
|
|
+
|
|
|
+ if index+1<len(temp):
|
|
|
+ rgi1=rgi0+3
|
|
|
+ rg=float(self.table.item(row0,rgi1).text())
|
|
|
+
|
|
|
+ # cap to maxx and maxy
|
|
|
+ dx0=0
|
|
|
+ oldrate=rg
|
|
|
+ if oldrate: dx0=abs((temp[index+1]-temp[index]))/oldrate
|
|
|
+ maxx=time[index]+dx0
|
|
|
+ x=min(x,maxx)
|
|
|
+ # dwell time must not be negative so y might not change more than rate can fit until next level?
|
|
|
+ #maxy=temp[index-2]+(temp[index
|
|
|
+ if rate0:
|
|
|
+ maxy=temp[index-2]+(maxx-time[index-2])*rate0
|
|
|
+ y=min(maxy,y)
|
|
|
+
|
|
|
+ # change rate
|
|
|
+ dy1=temp[index+1]-y
|
|
|
+ dx1=time[index+1]-x
|
|
|
+ newrate=0.0
|
|
|
+ if dx1: newrate=abs(dy1/dx1)
|
|
|
+ else: newrate=9999.0
|
|
|
+
|
|
|
+ rg=newrate
|
|
|
+ newitem = QTableWidgetItem(str(rg))
|
|
|
+ self.table.setItem(row0,rgi1,newitem)
|
|
|
+
|
|
|
+ # compute dtime and dtemp with capped x and y
|
|
|
+ dtime=x-time[index]
|
|
|
+ dtemp=y-temp[index]
|
|
|
+
|
|
|
+ # moving time means change dwell time
|
|
|
+ dy0=y-temp[index-1]
|
|
|
+ dt0=dy0/rate0
|
|
|
+ tg=float(self.table.item(row0,tgi0).text())
|
|
|
+ tg+=dtime-dt0
|
|
|
+ newitem = QTableWidgetItem(str(tg))
|
|
|
+ self.table.setItem(row0,tgi0,newitem)
|
|
|
+
|
|
|
+ # change level
|
|
|
+ lg=float(self.table.item(row0,lgi0).text())
|
|
|
+ lg+=dtemp
|
|
|
+ newitem = QTableWidgetItem(str(lg))
|
|
|
+ self.table.setItem(row0,lgi0,newitem)
|
|
|
+ else:
|
|
|
+ # 1,3,5 etc
|
|
|
+
|
|
|
+ # change level of edited point and change rate of rise to compensate
|
|
|
+ # cap x and y
|
|
|
+ x=max(x,time[index-1])
|
|
|
+ x=min(x,time[index+1])
|
|
|
+ y=max(self.mintemp,y)
|
|
|
+ dtime=x-time[index]
|
|
|
+ dtemp=y-temp[index]
|
|
|
+
|
|
|
+ dy1=y-temp[index-1]
|
|
|
+ dx1=x-time[index-1]
|
|
|
+ newrate=0.0
|
|
|
+ if dx1: newrate=abs(dy1/dx1)
|
|
|
+ #newrate=max(0.01,newrate)
|
|
|
+
|
|
|
+ # set new rate
|
|
|
+ rg=float(self.table.item(row0,rgi0).text())
|
|
|
+ rg=newrate
|
|
|
+ newitem = QTableWidgetItem(str(rg))
|
|
|
+ self.table.setItem(row0,rgi0,newitem)
|
|
|
+
|
|
|
+ # set new level
|
|
|
+ lg=float(self.table.item(row0,lgi0).text())
|
|
|
+ lg+=dtemp
|
|
|
+ newitem = QTableWidgetItem(str(lg))
|
|
|
+ self.table.setItem(row0,lgi0,newitem)
|
|
|
+
|
|
|
+ # change dwell time to compensate for move of editied point and also change next rate to compensate
|
|
|
+ tg=float(self.table.item(row0,tgi0).text())
|
|
|
+ tg-=dtime
|
|
|
+ newitem = QTableWidgetItem(str(tg))
|
|
|
+ self.table.setItem(row0,tgi0,newitem)
|
|
|
+
|
|
|
+ if index+2<len(temp):
|
|
|
+ dy1=temp[index+2]-y
|
|
|
+ dx1=time[index+2]-time[index+1]
|
|
|
+ newrate=0.0
|
|
|
+ if dx1: newrate=abs(dy1/dx1)
|
|
|
+ else: newrate=9999.0
|
|
|
+
|
|
|
+ # set new next rate?
|
|
|
+ rgi1=rgi0+3
|
|
|
+ rg=float(self.table.item(row0,rgi1).text())
|
|
|
+ rg=newrate
|
|
|
+ newitem = QTableWidgetItem(str(rg))
|
|
|
+ self.table.setItem(row0,rgi1,newitem)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ time,temp=self.buildCoordinates(row0)
|
|
|
+ newdata=np.array([time,temp])
|
|
|
+ self.lineplot.plot_item_control.setData(time, temp)
|
|
|
+ self.updateLinePlotTexts(time,temp)
|
|
|
+
|
|
|
+
|
|
|
+ def setProgramParameters(self,name,program,row0):
|
|
|
+ for g in range(1,9):
|
|
|
+ rn="r%d"%g
|
|
|
+ ln="l%d"%g
|
|
|
+ tn="t%d"%g
|
|
|
+ rgi=1+(g-1)*3+0
|
|
|
+ lgi=1+(g-1)*3+1
|
|
|
+ tgi=1+(g-1)*3+2
|
|
|
+
|
|
|
+ rg,lg,tg=-0.01,0.0,0.0
|
|
|
+
|
|
|
+ newitem = QTableWidgetItem(name)
|
|
|
+ self.table.setItem(row0,0,newitem)
|
|
|
+
|
|
|
+ if g-1<len(program):
|
|
|
+ rg,lg,tg=program[g-1]
|
|
|
+ newitem = QTableWidgetItem(str(rg))
|
|
|
+ self.table.setItem(row0,rgi,newitem)
|
|
|
+ newitem = QTableWidgetItem(str(lg))
|
|
|
+ self.table.setItem(row0,lgi,newitem)
|
|
|
+ newitem = QTableWidgetItem(str(tg))
|
|
|
+ self.table.setItem(row0,tgi,newitem)
|
|
|
+
|
|
|
+ def getProgramParameters(self,row0):
|
|
|
+ # r1,L1,d1
|
|
|
+ # r2,L2,d2
|
|
|
+ # ...
|
|
|
+ program=[]
|
|
|
+
|
|
|
+ name="program"
|
|
|
+ try:
|
|
|
+ name=self.table.item(row0,0).text()
|
|
|
+ except:
|
|
|
+ raise
|
|
|
+
|
|
|
+ for g in range(1,9):
|
|
|
+ rn="r%d"%g
|
|
|
+ ln="l%d"%g
|
|
|
+ tn="t%d"%g
|
|
|
+ rgi=1+(g-1)*3+0
|
|
|
+ lgi=1+(g-1)*3+1
|
|
|
+ tgi=1+(g-1)*3+2
|
|
|
+
|
|
|
+ rg,lg,tg=0.0,0.0,0.0
|
|
|
+
|
|
|
+ try:
|
|
|
+ rg=float(self.table.item(row0,rgi).text())
|
|
|
+ except: pass
|
|
|
+ try:
|
|
|
+ lg=float(self.table.item(row0,lgi).text())
|
|
|
+ except: pass
|
|
|
+ try:
|
|
|
+ tg=float(self.table.item(row0,tgi).text())
|
|
|
+ except: pass
|
|
|
+
|
|
|
+ program+=[(rg,lg,tg)]
|
|
|
+
|
|
|
+ # -0.01 as end of program
|
|
|
+ # 0.0 is step ie skip slope and go straight to dwell
|
|
|
+ if rg<0: break
|
|
|
+ return name,program
|
|
|
+
|
|
|
+
|
|
|
+ def buildCoordinates(self,row0):
|
|
|
+ time=[]
|
|
|
+ temp=[]
|
|
|
+
|
|
|
+ t0=0.0
|
|
|
+ templ=self.mintemp
|
|
|
+ temp0=self.mintemp
|
|
|
+ time+=[t0]
|
|
|
+ temp+=[temp0]
|
|
|
+ for g in range(1,9):
|
|
|
+ rn="r%d"%g
|
|
|
+ ln="l%d"%g
|
|
|
+ tn="t%d"%g
|
|
|
+ rgi=1+(g-1)*3+0
|
|
|
+ lgi=1+(g-1)*3+1
|
|
|
+ tgi=1+(g-1)*3+2
|
|
|
+
|
|
|
+ rg,lg,tg=0.0,0.0,0.0
|
|
|
+
|
|
|
+ try:
|
|
|
+ rg=float(self.table.item(row0,rgi).text())
|
|
|
+ except: pass
|
|
|
+ try:
|
|
|
+ lg=float(self.table.item(row0,lgi).text())
|
|
|
+ except: pass
|
|
|
+ try:
|
|
|
+ tg=float(self.table.item(row0,tgi).text())
|
|
|
+ except: pass
|
|
|
+
|
|
|
+ if rg<0: break
|
|
|
+
|
|
|
+
|
|
|
+ dtemp=abs(lg-templ)
|
|
|
+ if rg==0: dttemp=0
|
|
|
+ else: dttemp=abs(dtemp/rg)
|
|
|
+
|
|
|
+ #print(repr((rgi,lgi,tgi,rg,lg,tg,t0,dtemp,dttemp)))
|
|
|
+
|
|
|
+ t0+=dttemp
|
|
|
+ time+=[t0]
|
|
|
+ temp+=[lg]
|
|
|
+
|
|
|
+ t0+=tg
|
|
|
+ time+=[t0]
|
|
|
+ temp+=[lg]
|
|
|
+
|
|
|
+ templ=lg
|
|
|
+ return time,temp
|
|
|
+
|
|
|
+ def updateLinePlotTexts(self,time,temp):
|
|
|
+ if 1:
|
|
|
+ for t in self.plottexts:
|
|
|
+ self.lineplot.removeItem(t)
|
|
|
+ del t
|
|
|
+ self.plottexts=[]
|
|
|
+ else:
|
|
|
+ self.lineplot.clear()
|
|
|
+
|
|
|
+
|
|
|
+ pen = pg.mkPen(color=(0, 0, 0))
|
|
|
+
|
|
|
+ if 1:
|
|
|
+ if self.lineplot.plot_item_control==None:
|
|
|
+ self.lineplot.plot_item_control = self.lineplot.plot(time,temp, symbolBrush=(255,0,0), symbolPen='w')
|
|
|
+ else:
|
|
|
+ self.lineplot.plot_item_control.setData(time, temp)
|
|
|
+
|
|
|
+ for i in range(1,len(time)):
|
|
|
+ t0,t1=time[i-1],time[i]
|
|
|
+ k0,k1=temp[i-1],temp[i]
|
|
|
+ s=""
|
|
|
+ rotateAxis=None
|
|
|
+ if k0==k1:
|
|
|
+ # dwell time at tempature
|
|
|
+ s="%4.1fs at %4.1fC"%(t1-t0,k0)
|
|
|
+ pos=(t0+t1)/2,k0
|
|
|
+ angle=0
|
|
|
+ elif t0!=t1:
|
|
|
+ s="%.1fC/s"%((k1-k0)/(t1-t0))
|
|
|
+ pos=(t0+t1)/2,(k0+k1)/2
|
|
|
+ # angle
|
|
|
+ # won't work since graph is not scale same in x and y
|
|
|
+ angle=0 # math.atan2(pos[1],pos[0])*180/math.pi
|
|
|
+ rotateAxis=(t1-t0,k1-k0)
|
|
|
+ else:
|
|
|
+ # same time so just skip
|
|
|
+ pass
|
|
|
+ if s:
|
|
|
+ text = pg.TextItem(s,color=(0,0,0),anchor=(0.5,1),angle=angle,rotateAxis=rotateAxis)
|
|
|
+ text.setFont(self.tfont)
|
|
|
+ self.lineplot.addItem(text)
|
|
|
+ text.setPos(*pos)
|
|
|
+ self.plottexts+=[text]
|
|
|
+
|
|
|
+
|
|
|
+ @pyqtSlot(int,int)
|
|
|
+ def tableSelectionChanged(self,row0,row1,text=None):
|
|
|
+ #print("selection changed",row0,row1,self.cursorMoved)
|
|
|
+ if self.cursorMoved: return
|
|
|
+
|
|
|
+ # time = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
|
+ # temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 45]
|
|
|
+
|
|
|
+ self.selectedRow=row0
|
|
|
+ time,temp=self.buildCoordinates(row0)
|
|
|
+
|
|
|
+ self.updateLinePlotTexts(time,temp)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ @pyqtSlot(int,int)
|
|
|
+ def tableCellChanged(self,row,col,text):
|
|
|
+ return self.realTableCellChanged(row,col,text)
|
|
|
+ def realTableCellChanged(self,row,col,text):
|
|
|
+ pass
|
|
|
+
|
|
|
+
|
|
|
+ @pyqtSlot(str,int,int)
|
|
|
+ def updateProgress(self,s,cmdp,p):
|
|
|
+ # update action etc
|
|
|
+
|
|
|
+ pass
|
|
|
+
|
|
|
+ @pyqtSlot(dict)
|
|
|
+ def updateState(self,state):
|
|
|
+ # update enablement and such
|
|
|
+ self.state=state
|
|
|
+ for k,v in state.items():
|
|
|
+ if k in self.infoslots:
|
|
|
+ name,l,e=self.infoslots[k]
|
|
|
+ # v needs to be processed and appenededlike C/s etc?
|
|
|
+ e.setText(str(v))
|
|
|
+
|
|
|
+ _os=state.get("OS","")
|
|
|
+ if self.running and "program stopped" in _os:
|
|
|
+ self.runningToStopped()
|
|
|
+ if not self.running and "program running" in _os:
|
|
|
+ self.stoppedToRunning()
|
|
|
+
|
|
|
+
|
|
|
+ @pyqtSlot(dict)
|
|
|
+ def updateRecord(self,state):
|
|
|
+ # update enablement and such
|
|
|
+ dt=state["dt"]
|
|
|
+ value=state["value"]
|
|
|
+ # feed graph and update it!!!
|
|
|
+
|
|
|
+ tlist,vlist=self.runrecord
|
|
|
+ tlist+=[dt]
|
|
|
+ vlist+=[value]
|
|
|
+ self.runrecord=(tlist,vlist)
|
|
|
+ #print(self.runrecord)
|
|
|
+
|
|
|
+ points=np.array([tlist,vlist])
|
|
|
+ #print(points)
|
|
|
+ self.scatter.setData(*points)
|
|
|
+ self.scatter.updateSpots()
|
|
|
+ self.scatter.invalidate()
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ @pyqtSlot(str)
|
|
|
+ def updateInfo(self,infopath):
|
|
|
+ # update info about device
|
|
|
+ pass
|
|
|
+
|
|
|
+ def cmdlog(self,key,returncode,elements):
|
|
|
+ print("key",repr(key),"returncode",returncode,"elem",elements)
|
|
|
+
|
|
|
+ def statecallback(self,state):
|
|
|
+ QMetaObject.invokeMethod(self,"updateState", Qt.QueuedConnection,Q_ARG(dict, state))
|
|
|
+
|
|
|
+ def infocallback(self,infopath):
|
|
|
+ QMetaObject.invokeMethod(self,"updateInfo", Qt.QueuedConnection,Q_ARG(str, infopath))
|
|
|
+
|
|
|
+ def recordcallback(self,dt,value):
|
|
|
+ QMetaObject.invokeMethod(self,"updateRecord", Qt.QueuedConnection,Q_ARG(dict, {"dt":dt,"value":value}))
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == '__main__':
|
|
|
+ if 0:
|
|
|
+ # run with fbs ie fbs run etc
|
|
|
+ appctxt = ApplicationContext() # 1. Instantiate ApplicationContext
|
|
|
+ app=appctxt.app
|
|
|
+ else:
|
|
|
+ # run as plain python
|
|
|
+ appctxt = QApplication(sys.argv)
|
|
|
+ app=appctxt
|
|
|
+
|
|
|
+ window = MainWindow(appctxt,sys.argv)
|
|
|
+ window.resize(800, 600)
|
|
|
+ window.show()
|
|
|
+ exit_code = app.exec() # 2. Invoke appctxt.app.exec()
|
|
|
+ sys.exit(exit_code)
|
|
|
+
|