Bengt Sjölén 1 month ago
commit
eed0ed45b0

BIN
docs/AL808CommsProtocol.pdf


BIN
docs/AL808_V60_EN.pdf


BIN
docs/IR6500-Manual-EN.pdf


BIN
docs/PC410.pdf


+ 6 - 0
pc410/src/build/settings/base.json

@@ -0,0 +1,6 @@
+{
+    "app_name": "PC410",
+    "author": "Bengt Sjölén",
+    "main_module": "src/main/python/main.py",
+    "version": "0.0.0"
+}

+ 6 - 0
pc410/src/build/settings/linux.json

@@ -0,0 +1,6 @@
+{
+    "categories": "Utility;",
+    "description": "",
+    "author_email": "",
+    "url": ""
+}

+ 3 - 0
pc410/src/build/settings/mac.json

@@ -0,0 +1,3 @@
+{
+    "mac_bundle_identifier": "com.automata.pc410"
+}

BIN
pc410/src/main/icons/Icon.ico


+ 12 - 0
pc410/src/main/icons/README.md

@@ -0,0 +1,12 @@
+![Sample app icon](linux/128.png)
+
+This directory contains the icons that are displayed for your app. Feel free to
+change them by placing your own copies into `src/main/icons` in your project
+directory.
+
+The difference between the icons on Mac and the other platforms is that on Mac,
+they contain a ~5% transparent margin. This is because otherwise they look too
+big (eg. in the Dock or in the app switcher).
+
+Icon.ico is used on Windows and Linux. You can create it from the .png files
+with [an online tool](http://icoconvert.com/Multi_Image_to_one_icon/).

BIN
pc410/src/main/icons/base/16.png


BIN
pc410/src/main/icons/base/24.png


BIN
pc410/src/main/icons/base/32.png


BIN
pc410/src/main/icons/base/48.png


BIN
pc410/src/main/icons/base/64.png


BIN
pc410/src/main/icons/linux/1024.png


BIN
pc410/src/main/icons/linux/128.png


BIN
pc410/src/main/icons/linux/256.png


BIN
pc410/src/main/icons/linux/512.png


BIN
pc410/src/main/icons/mac/1024.png


BIN
pc410/src/main/icons/mac/128.png


BIN
pc410/src/main/icons/mac/256.png


BIN
pc410/src/main/icons/mac/512.png


+ 164 - 0
pc410/src/main/python/PC410Controller.py

@@ -0,0 +1,164 @@
+from pc410 import PC410
+import traceback,time,sys,os
+
+class PC410Controller:
+    def __init__(self,threader,cmdcallback,infocallback,statecallback,recordcallback):
+        self.threader=threader
+        self.cmdcallback=cmdcallback
+        self.infocallback=infocallback
+        self.statecallback=statecallback
+        self.recordcallback=recordcallback
+
+        now=time.time()
+        self.tstart0=now
+        
+        self.port="/dev/ttyUSB0"
+        self.baudrate=9600
+        self.pc410=PC410(self.port,self.baudrate,verbose=0)
+
+        self.cycleRunning=0
+        self.cycleEnabled=1        
+        self.quit=0
+
+        self.runnning=0
+        self.runStart=now
+        self.runLastTime=now
+
+    def disableCycle(self):
+        self.cycleEnabled=0
+        while self.cycleRunning: time.sleep(0.1)
+        time.sleep(0.5)
+    def enableCycle(self):
+        self.cycleEnabled=1
+        
+    def cmdRun(self):
+        self.disableCycle()
+        self.pc410.write("OS",2)
+        self.enableCycle()
+        self.running=1
+        now=time.time()
+        self.runStart=now
+        self.runLastTime=now
+        
+    def cmdStop(self):
+        self.disableCycle()
+        self.pc410.write("OS",0)
+        self.enableCycle()
+        self.running=0
+
+
+    def writeProgram(self,program):
+        self.disableCycle()
+        i=1
+        e1=0
+        for i1 in range(0,len(program)):
+            r1,l1,t1=program[i1]
+            e1,f1=self.pc410.write("r%d"%i,r1)
+            if e1!=0: break
+            e1,f1=self.pc410.write("l%d"%i,l1)
+            if e1!=0: break
+            e1,f1=self.pc410.write("t%d"%i,t1)
+            if e1!=0: break
+            i+=1
+        if e1==0 and i<8:
+            e1,f1=self.pc410.write("r%d"%i,-0.01)
+        self.enableCycle()
+        return e1
+
+        
+    def readProgram(self):
+        self.disableCycle()
+        program=[]
+        print("reading program")
+        for i in range(1,9):
+            e1,r1=self.pc410.read("r%d"%i)
+            if e1!=0: return []
+            e1,l1=self.pc410.read("l%d"%i)
+            if e1!=0: return []
+            e1,t1=self.pc410.read("t%d"%i)
+            if e1!=0: return []
+            program+=[(r1,l1,t1)]
+            #print("reading program",r1,l1,t1)
+            
+        self.enableCycle()
+        return program
+            
+
+    def cycle1(self):
+        state={}
+        for readparam 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"]:
+            e1,value=self.pc410.read(readparam)
+            now=time.time()
+            if self.running: self.runLastTime=now
+            print(value)
+            s=[]
+            if readparam=="SW":
+                value1=int(value[1:],16)
+                bits=["Data format",
+                      "Input fault",
+                      "Barring key operation",
+                      "---",
+                      "---",
+                      "Modifying parameters by keys",
+                      "Deviation alarm status",
+                      "Deviation alarm condition",
+                      "Lower limit alarm status",
+                      "Lower limit alarm condition",
+                      "Upper limit alarm status",
+                      "Upper limit alarm condition",
+                      "Alarm output",
+                      "---",
+                      "---",
+                      "Auto/manual"]
+                s+=["%04x"%value1]
+                if 0:
+                    for i in range(0,len(bits)):
+                        if (value1>>i)&1: s+=[bits[i]]
+            elif readparam=="XS":
+                value1=int(value[1:],16)
+                s+=["%04x"%value1]
+                if value1: s+=["Turn on self-tuning"]
+            elif readparam=="OS":
+                value1=int(value[1:],16)
+                s+=["%d"%value1]
+                value1&=255
+                if value1==0:
+                    s+=["program stopped"]
+                    self.running=0                
+                elif value1==2:
+                    s+=["program running"]
+                    self.running=1
+                elif value1==3: s+=["program hold"]
+                elif value1==6: s+=["unknown 6"]
+                else: s+=["unknown state"]            
+            elif readparam=="PV":
+                if self.running:
+                    dt=self.runLastTime-self.runStart
+                    if self.recordcallback: self.recordcallback(dt,value)
+                s+=[str(value)]
+            else:
+                s+=[str(value)]
+            state[readparam]=" ".join(s)
+        self.statecallback(state)
+    
+    def cycle(self,tstart0):
+        t2=time.time()
+        while not self.quit:
+            if self.cycleEnabled:
+                self.cycleRunning=1
+                self.cycle1()
+                time.sleep(0.5)
+                self.cycleRunning=0
+
+            # wait 10 x 0.1 s
+            for i in range(0,10):
+                if self.quit: break
+                time.sleep(0.1)
+
+                
+                
+        
+    def run(self):
+        self.cycleThread=self.threader("cycle",self)
+        self.cycleThread.start()
+   

BIN
pc410/src/main/python/__pycache__/PC410Controller.cpython-310.pyc


BIN
pc410/src/main/python/__pycache__/pc410.cpython-310.pyc


+ 1025 - 0
pc410/src/main/python/main.py

@@ -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)
+    

+ 372 - 0
pc410/src/main/python/pc410.py

@@ -0,0 +1,372 @@
+import sys,os,select,time,random,re,binascii
+import serial
+
+class Altec:
+    def __init__(self,port,baudrate=9600,verbose=0,instrumentaddress="01"):
+        self.instrumentaddress=instrumentaddress
+        self.prompt=">"
+        self.timeout=5.0
+        self.baudrate=baudrate
+        self.verbose=verbose
+        self.device=port
+
+        self.connect()
+
+    def connect(self):
+        self.connectionfailed=0
+        # startbits is always 1?
+        try:
+            self.port=serial.Serial(self.device,baudrate=self.baudrate,parity=serial.PARITY_EVEN,stopbits=1,bytesize=7,timeout=3)
+        except:
+            self.connectionfailed=1        
+
+    def initialize(self):
+        pass
+
+    def write(self,cmd):
+        if self.connectionfailed: return 
+        try:
+            self.port.flushInput()
+            self.port.write(cmd)
+            self.port.flush()
+        except:
+            self.port.close()
+            self.connectionfailed=1
+
+    def send(self,cmd,wait=3.0,verbose=1):
+        self.write(cmd.encode("utf-8")+b"\r")
+        response=self.waitresponse(wait)
+        if self.verbose: print("cmd",cmd,"got response",response)        
+        return response
+
+    def waitresponse(self,wait=3.0):
+
+        STX=bytes([2])
+        ETX=bytes([3])
+        EOT=bytes([4])
+        ENQ=bytes([5])
+        ACK=bytes([6])
+        NAK=bytes([0x15])
+        
+        t0=time.time()
+        endprompt=0
+        data=b""
+        err=-1
+        while 1:
+            if self.connectionfailed: return -100,[]
+            t1=time.time()
+            avail_read, avail_write, avail_error = select.select([self.port],[],[], 0.1)
+            for d in avail_read:
+                try:
+                    data+=d.read()
+                except:
+                    self.connectionfailed=1                    
+            #print("data",data,data.endswith(ACK),data.endswith(NAK),(len(data)>1 and data[-2]==ETX[0]),data[-2] if len(data)>1 else 0,ETX)
+            if data.endswith(ACK) or data.endswith(NAK) or (len(data)>1 and data[-2]==ETX[0]):
+                err=0
+                break
+            if time.time()-t0>wait:
+                # timeout
+                #print("timing out",repr(data))
+                break
+        data=data.decode("utf-8","ignore")
+        return err,data
+
+
+class PC410:
+    def __init__(self,port,baudrate=9600,verbose=1,instrumentaddress="01"):
+        self.altec=Altec(port,baudrate=baudrate,verbose=verbose,instrumentaddress=instrumentaddress)
+
+    def initialize(self):
+        self.altec.initialize()
+
+    def bcc(self,block):
+        xcc=0
+        for b in block:
+            xcc^=ord(b)
+        return xcc
+        
+    def handleresponse(self,e1,r1,start=None,cmd=""):
+        data=None
+        index=0
+
+        # [STX](C1)(C2)<DATA>[EXT](BCC) 
+        STX=chr(2)
+        ETX=chr(3)
+        EOT=chr(4)
+        ENQ=chr(5)
+        ACK=chr(6)
+        NAK=chr(0x15)
+        if e1==0 and r1:
+            while 1:
+                if index>=len(r1):
+                    # should we have our own?
+                    r="NO RESPONSE"
+                    e1=-5
+                else:
+                    r=r1[index:]
+                    #print(index,repr(r),repr(start))
+                    if r.startswith(ACK):
+                        # no response. we would already have err -3 from send call so won't get here
+                        e1=0
+                    elif r.startswith(NAK):
+                        e1=-1
+                    elif start!=None and r.startswith(start):
+                        # valid response - data to return
+                        data=r1[index][len(start):]
+
+                        if r[0]==STX and len(r)>1 and r[-2]==ETX:                            
+                            bcc=ord(r[-1])
+                            block=[r[x] for x in range(1,len(r)-1)]
+                            xcc=self.bcc(block)
+                            if xcc!=bcc:
+                                print("bcc wrong",xcc,bcc)
+                                e1=-6
+                            else:
+                                #print("bcc ok",xcc,bcc)
+                                pass
+                            data=r[len(start):len(r)-2]
+                            #print("returning data",repr(data))
+                        else:
+                            # no etx second last byte?!?
+                            e1=-5
+                    else:
+                        # broken response
+                        e1=-4
+                        pass
+                break
+        return e1,data
+
+
+    def send(self,msg,wait):
+        return self.altec.send(msg,wait)
+    
+    def read(self,parameter,verbose=1,wait=3.0):
+        """
+        02 stx start of text
+        03 etx end of text
+        04 eot end of transmission
+        05 enq enquiry
+        06 ack positive ack
+        15 nak negative ack
+        20 space
+        2d -
+        2e .
+        3e >
+        30-39 0-9
+
+        read 
+        [EOT](ADR_H)(ADR_H)(ADR_L)(ADR_L)(C1)(C2)[ENQ]
+
+        addr is instrument address, ours is 01
+        baud is set to 9600
+
+        cmd 0011PV got response (-1, ['\x02PV   9.\x032'])
+        read response 0011PV -1 ['\x02PV   9.\x032']
+
+        [STX](C1)(C2)<DATA>[EXT](BCC)
+
+        BCC
+        This is a block checksum that is generated for data validation. It is computed by XORing(exclusive or)
+        all the characters after and excluding the STX, and including the ETX. Note that it may take the value
+        of 'EOT' and care must be take when writing a protocol driver to ensure that this is not seen as an 'End of
+        Transmission' sequence.
+
+        write
+        [EOT](ADR_H)(ADR_H)(ADR_L)(ADR_L)[STX](C1) (C2)<DATA>[ETX](BCC)
+
+        The value of the parameter in a given display format. e.g. 99.9,1.2, -999, >1234 etc.
+        BCC
+        This is a block checksum that is generated for data validation. It is computed by XORing(exclusive or)
+        all the characters after and excluding the STX, and including the ETX.
+
+        response is ACK or NAK
+
+        program parameter list
+        Lc  loop counter
+        r1  ramp rate 1
+        L1  target setpoint 1
+        dl  dwell time
+        r2  ramp rate 2
+        L2  target setpoint 2
+        d2  dwell time 2
+        PL1 ramp 1 and dwell 1 output power limit
+        PL2 ramp 2 and dwell 2 output power limit
+
+
+        PV               last measure value read only
+        OP               output power read only
+        SP               setpoint read only
+        SL               local set point 1
+        ...
+
+
+        P.End can be Off or SP constant temp or a program number to continue with that program
+        there are 10 programs of 16 segments that each can repeat 1-200 times or continuously?
+
+        
+        """
+        STX=chr(2)
+        ETX=chr(3)
+        EOT=chr(4)
+        ENQ=chr(5)
+        ACK=chr(6)
+        NAK=chr(0x15)
+
+        
+        addrhi=self.altec.instrumentaddress[0]
+        addrlo=self.altec.instrumentaddress[1]
+        msg=[EOT,addrhi,addrhi,addrlo,addrlo,parameter[0],parameter[1],ENQ]
+        msg="".join(msg)
+        #print(repr(msg))
+        e1,r1=self.send(msg,wait=wait)
+        
+        e1,data=self.handleresponse(e1,r1,start=STX+parameter[0]+parameter[1])
+        #if verbose: print("read response",msg,e1,repr(r1))
+        return e1,data
+
+    def write(self,parameter,data,verbose=1,wait=3.0,hex=0):
+        # data must be hex string ready to write
+        STX=chr(2)
+        ETX=chr(3)
+        EOT=chr(4)
+        ENQ=chr(5)
+        ACK=chr(6)
+        NAK=chr(0x15)
+
+        
+        addrhi=self.altec.instrumentaddress[0]
+        addrlo=self.altec.instrumentaddress[1]
+
+        
+        # data is value in display format e.g. 99.9,1.2, -999,>1234
+        if type(data)!=str: data=str(data)
+        sdata=[c for c in data]
+        msg=[EOT,addrhi,addrhi,addrlo,addrlo,STX,parameter[0],parameter[1]]+sdata+[ETX]
+        # bcc is block checksum xor all characters after and excluding stx and including etx
+        bcc=self.bcc(msg[6:])
+        msg+=[chr(bcc)]
+
+        msg="".join(msg)
+
+        retries=3
+        for i in range(0,retries):
+            # just in case wait
+            time.sleep(0.1)
+            wait1=0.2
+            e1,r1=self.send(msg,wait=wait1)
+            e2,data=self.handleresponse(e1,r1)
+            if verbose: print("write response",repr(msg),e1,e2,r1,repr(data))
+            if e2==0: break
+        return e2,data
+
+def main(argv,stdout,environ):
+    progname = argv[0]
+    args=argv[1:]
+
+    port="/dev/ttyUSB0"
+    readparam=None
+    writeaddr=None
+    writevalue=None
+    baudrate=9600
+    i=0
+    while i<len(args):
+        if args[i]=="-p":
+            i+=1
+            port=args[i]
+        elif args[i]=="-b":
+            i+=1
+            baudrate=int(args[i])
+        elif args[i]=="--read":
+            i+=1
+            readparam=args[i].strip()
+        elif args[i]=="--write":
+            i+=1
+            writeaddr=args[i]
+            i+=1
+            try:
+                writevalue=int(args[i],16)
+            except:
+                try:
+                    writevalue=float(args[i])
+                except:
+                    writevalue=args[i]
+                
+        i+=1
+
+    pc410=PC410(port,baudrate,verbose=0)
+    
+    if readparam!=None:
+        t0=time.time()
+        value=pc410.read(readparam)
+        dt=time.time()-t0
+        print("read",readparam,"returns",value,"took",dt)
+        params=["PV","OP","SP","SL","HA","LA","DA","XP","TI","TD","HB","LB","CH","CC","RG","HS","LS","BP","HO","SR","HB","Lc","SW","XS","OS"]
+
+        for j in range(1,9):
+            params+=["r%d"%j,"l%d"%j,"t%d"%j]
+            # rn is slope/rate
+            # dn == tn is how long to dwell
+            # ln is what level ie what temperature
+        
+        for readparam in params:
+            e1,value=pc410.read(readparam)
+            print("read",readparam,"returns",e1,value,"took",dt)
+            if readparam=="SW":
+                value1=int(value[1:],16)
+                bits=["Data format",
+                      "Input fault",
+                      "Barring key operation",
+                      "---",
+                      "---",
+                      "Modifying parameters by keys",
+                      "Deviation alarm status",
+                      "Deviation alarm condition",
+                      "Lower limit alarm status",
+                      "Lower limit alarm condition",
+                      "Upper limit alarm status",
+                      "Upper limit alarm condition",
+                      "Alarm output",
+                      "---",
+                      "---",
+                      "Auto/manual"]
+                for i in range(0,len(bits)):
+                    print("   %d %s"%((value1>>i)&1,bits[i]))
+            if readparam=="XS":
+                value1=int(value[1:],16)
+                print("   %d %s"%(value1,"Turn on self-tuning"))
+            if readparam=="OS":
+                value1=int(value[1:],16)
+                print("   %d %s"%(value1,"0=stop program, 2=run program, 3=hold program"))
+
+        # not only display format we can write with or without decimal point, with or without leading space
+        #e1,r1=pc410.write("l1","143")
+        e1,r1=pc410.write("l1",147)
+        print("write returned",e1,r1)
+
+        readparam="l1"
+        e1,value=pc410.read(readparam)
+        print("read",readparam,"returns",e1,value)
+                      
+        if 0:
+            for i in range(0,26):
+                for j in range(0,26):
+                    c1=chr(65+i)
+                    c2=chr(65+j)
+                    param=c1+c2
+                    print(param)
+                    e1,r1=pc410.read(param,wait=3.0)
+                    if e1!=-1:
+                        print("read",param,"returns",repr(value))
+
+    if writeaddr:
+        ok=pc410.write(writeaddr,writevalue)
+
+
+    print("done")
+
+    
+if __name__=='__main__':
+    main(sys.argv, sys.stdout, os.environ)
+
+