-
Notifications
You must be signed in to change notification settings - Fork 1
/
rs-tool-gui.py
280 lines (243 loc) · 11.9 KB
/
rs-tool-gui.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
#!/usr/bin/env python3
# author: [email protected]
import visa # https://github.com/hgrecco/pyvisa
import numpy
import sys
import time
import k2450 # functions to talk to a keithley 2450 sourcemeter
import rs # grey's sheet resistance library
# for plotting
import matplotlib.pyplot as plt
plt.switch_backend("Qt5Agg")
# debugging/testing stuff
#visa.log_to_screen() # for debugging
#import timeit
# for the GUI
import pyqtGen
from PyQt5 import QtCore, QtGui, QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
#========= start print override stuff =========
# this stuff is for overriding print() so that
# any calls to it are mirrored to my gui's log pane
import builtins as __builtin__
systemPrint = __builtin__.print
myPrinter = None
import io
class MyPrinter(QtCore.QObject): # a class that holds the signals we'll need for passing around the log data
writeToLog = QtCore.pyqtSignal(str) # this signal sends the contents for the log
scrollLog = QtCore.pyqtSignal() # this signal tells the log to scroll to its max position
def print(*args, **kwargs): # overload the print() function
global myPrinter
if myPrinter is not None: # check to see if the gui has created myPrinter
stringBuf = io.StringIO()
kwargs['file'] = stringBuf
systemPrint(*args, **kwargs) # print to our string buffer
myPrinter.writeToLog.emit(stringBuf.getvalue()) # send the print to the gui
myPrinter.scrollLog.emit() # tell the gui to scroll the log
stringBuf.close()
kwargs['file'] = sys.stdout
return systemPrint(*args, **kwargs) # now do the print for real
__builtin__.print = print
#========= end print override stuff =========
# this is the thread where the sweep takes place
class sweepThread(QtCore.QThread):
def __init__(self, mainWindow, parent=None):
QtCore.QThread.__init__(self, parent)
self.mainWindow = mainWindow
def run(self):
self.mainWindow.ui.applyButton.setEnabled(False)
self.mainWindow.ui.sweepButton.setEnabled(False)
if not k2450.doSweep(self.mainWindow.sm):
print ("Failed to do forward sweep.")
else:
# get the forward data
[i,v] = k2450.fetchSweepData(self.mainWindow.sm,self.mainWindow.sweepParams)
self.mainWindow.ax1.clear()
if i is not None:
self.mainWindow.ax1.set_title('Forward Sweep Results',loc="right")
rs.plotSweep(i,v,self.mainWindow.ax1) # plot the sweep results
else:
print("Failed to fetch forward sweep data.")
# now do a reverse sweep
reverseParams = self.mainWindow.sweepParams.copy()
reverseParams['sweepStart'] = self.mainWindow.sweepParams['sweepEnd']
reverseParams['sweepEnd'] = self.mainWindow.sweepParams['sweepStart']
confRes = k2450.configureSweep(self.mainWindow.sm,reverseParams)
if confRes:
time.sleep(5)
if not confRes:
print ("Failed to configure reverse sweep.")
elif not k2450.doSweep(self.mainWindow.sm):
print ("Failed to do reverse sweep.")
else:
# get the reverse data
[i,v] = k2450.fetchSweepData(self.mainWindow.sm,reverseParams)
self.mainWindow.ax2.clear()
if i is not None:
self.mainWindow.ax2.set_title('Reverse Sweep Results',loc="right")
rs.plotSweep(i,v,self.mainWindow.ax2) # plot the sweep results
else:
print("Failed to fetch reverse sweep data.")
print('======================================')
self.mainWindow.ui.applyButton.setEnabled(True)
self.mainWindow.ui.sweepButton.setEnabled(True)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.setup = False # to keep track of if the sourcemeter is setup or not
self.configured = False # to keep track of if the sweep is configured or not
# Set up the user interface from Designer
self.ui = pyqtGen.Ui_MainWindow()
self.ui.setupUi(self)
#recall settings
self.settings = QtCore.QSettings("greyltc", "rs-tool-gui")
for thisKey in self.settings.allKeys():
if hasattr(self.ui,thisKey):
targetObject = getattr(self.ui,thisKey)
if type(targetObject) is QtWidgets.QLineEdit:
targetObject.setText(self.settings.value(thisKey))
elif (type(targetObject) is QtWidgets.QCheckBox):
targetObject.setCheckState(self.settings.value(thisKey,type=int))
elif (type(targetObject) is QtWidgets.QSpinBox):
targetObject.setValue(self.settings.value(thisKey,type=int))
elif type(targetObject) is QtWidgets.QDoubleSpinBox:
targetObject.setValue(self.settings.value(thisKey,type=float))
else:
print('Unexpected object type while loading settings.')
self.ui.stepDelayDoubleSpinBox.setEnabled(not self.ui.autoDelayCheckBox.isChecked())
# tell the UI where to draw put matplotlib plots
fig = plt.figure(facecolor="white")
self.ax1 = fig.add_subplot(2,1,1)
self.ax2 = fig.add_subplot(2,1,2)
vBox = QtWidgets.QVBoxLayout()
vBox.addWidget(FigureCanvas(fig))
self.ui.plotTab.setLayout(vBox)
# set up things for our log pane
global myPrinter
myPrinter = MyPrinter()
myPrinter.writeToLog.connect(self.ui.textBrowser.insertPlainText)
myPrinter.scrollLog.connect(self.scrollLog)
self.ui.textBrowser.setTextBackgroundColor(QtGui.QColor('black'))
self.ui.textBrowser.setTextColor(QtGui.QColor(0, 255, 0))
#self.ui.textBrowser.setFontWeight(QtGui.QFont.Bold)
self.ui.textBrowser.setAutoFillBackground(True)
p = self.ui.textBrowser.palette()
p.setBrush(9, QtGui.QColor('black'))
#p.setColor(self.ui.textBrowser.backgroundRole, QtGui.QColor('black'))
self.ui.textBrowser.setPalette(p)
# for now put these here, should be initiated by user later:
self.rm = visa.ResourceManager('@py') # select pyvisa-py (pure python) backend
# connect up the sweep button
self.ui.sweepButton.clicked.connect(self.doSweep)
# connect up the connect button
self.ui.connectButton.clicked.connect(self.connectToKeithley)
# connect up the apply button
self.ui.applyButton.clicked.connect(self.applySweepValues)
# save any changes the user makes
self.ui.visaAddressLineEdit.editingFinished.connect(self.aSettingHasChanged)
self.ui.terminationLineEdit.editingFinished.connect(self.aSettingHasChanged)
self.ui.timeoutSpinBox.valueChanged.connect(self.aSettingHasChanged)
self.ui.startVoltageDoubleSpinBox.valueChanged.connect(self.aSettingHasChanged)
self.ui.endVoltageDoubleSpinBox.valueChanged.connect(self.aSettingHasChanged)
self.ui.numberOfStepsSpinBox.valueChanged.connect(self.aSettingHasChanged)
self.ui.currentLimitDoubleSpinBox.valueChanged.connect(self.aSettingHasChanged)
self.ui.nPLCDoubleSpinBox.valueChanged.connect(self.aSettingHasChanged)
self.ui.stepDelayDoubleSpinBox.valueChanged.connect(self.aSettingHasChanged)
self.ui.autoDelayCheckBox.stateChanged.connect(self.aSettingHasChanged)
self.ui.autoDelayCheckBox.stateChanged.connect(self.autoDelayStateChange)
self.ui.autoZeroCheckBox.stateChanged.connect(self.aSettingHasChanged)
self.sweepThread = sweepThread(self)
def __del__(self):
try:
print("Closing connection to", self.sm._logging_extra['resource_name'],"...")
self.sm.close() # close connection
print("Connection closed.")
except:
return
def aSettingHasChanged(self, newValue=None):
self.configured = False
sourceWidget = self.sender()
if (type(sourceWidget) is QtWidgets.QLineEdit) and (newValue is None): # this ensures we're not saving a line edit on every new char
self.settings.setValue(sourceWidget.objectName(),sourceWidget.text())
else:
self.settings.setValue(sourceWidget.objectName(),newValue) # save the key-value pair to our settings
def autoDelayStateChange(self):
isChecked = self.ui.autoDelayCheckBox.isChecked()
self.settings.setValue('autoDelay',isChecked)
self.ui.stepDelayDoubleSpinBox.setEnabled(not isChecked)
def applySweepValues(self):
#TODO: somehow detect that a user has changed a sweep parameter in the UI and they need to be resent to the sourcemeter
if not self.setup:
print("The sourcemeter has not been set up. We'll try that now.")
self.connectToKeithley()
if self.setup:
self.sweepParams = {} # here we'll store the parameters that define our sweep
#self.sweepParams['maxCurrent'] = 0.05 # amps
#self.sweepParams['sweepStart'] = -0.003 # volts
#self.sweepParams['sweepEnd'] = 0.003 # volts
#self.sweepParams['nPoints'] = 101
#self.sweepParams['stepDelay'] = -1 # seconds (-1 for auto, nearly zero, delay)
self.sweepParams['maxCurrent'] = self.ui.currentLimitDoubleSpinBox.value()/1000 # amps
self.sweepParams['sweepStart'] = self.ui.startVoltageDoubleSpinBox.value()/1000 # volts
self.sweepParams['sweepEnd'] = self.ui.endVoltageDoubleSpinBox.value()/1000 # volts
self.sweepParams['nPoints'] = self.ui.numberOfStepsSpinBox.value()
self.sweepParams['stepDelay'] = self.ui.stepDelayDoubleSpinBox.value()/1000
self.sweepParams['sourceFun'] = 'voltage'
self.sweepParams['senseFun'] = 'current'
self.sweepParams['fourWire'] = True
self.sweepParams['nplc'] = self.ui.nPLCDoubleSpinBox.value() # intigration time (in number of power line cycles)
if self.ui.autoZeroCheckBox.isChecked():
self.sweepParams['autoZero'] = True
else:
self.sweepParams['autoZero'] = False
if self.ui.autoDelayCheckBox.isChecked():
self.sweepParams['stepDelay'] = -1 # seconds (-1 for auto, nearly zero, delay)
self.sweepParams['durationEstimate'] = k2450.estimateSweepTimeout(self.sweepParams['nPoints'], self.sweepParams['stepDelay'],self.sweepParams['nplc'])
self.configured = k2450.configureSweep(self.sm,self.sweepParams)
if self.configured:
print('Sweep parameters applied.')
else:
print('Sweep parameters not applied.')
def connectToKeithley(self):
# ====for TCPIP comms====
#instrumentIP = ipaddress.ip_address('10.42.0.60') # IP address of sourcemeter
#fullAddress = 'TCPIP::'+str(instrumentIP)+'::INSTR'
#deviceTimeout = 1000 # ms
#fullAddress = 'TCPIP::'+str(instrumentIP)+'::5025::SOCKET' # for raw TCPIP comms directly through a socket @ port 5025 (probably worse than INSTR)
#openParams = {'resource_name':fullAddress, 'timeout': deviceTimeout}
# ====for serial rs232 comms=====
#serialPort = "/dev/ttyUSB0"
#fullAddress = "ASRL"+serialPort+"::INSTR"
#deviceTimeout = 1000 # ms
#sm = rm.open_resource(smAddress)
#sm.set_visa_attribute(visa.constants.VI_ATTR_ASRL_BAUD,57600)
#sm.set_visa_attribute(visa.constants.VI_ASRL_END_TERMCHAR,u'\r')
#openParams = {'resource_name':fullAddress, 'timeout': deviceTimeout}
if (not self.setup):
self.openParams = {'resource_name': self.ui.visaAddressLineEdit.text(), 'timeout': self.ui.timeoutSpinBox.value(), '_read_termination': self.ui.terminationLineEdit.text().replace("\\n", '\n').replace("\\t",
'\t').replace("\\r",'\r')}
self.sm = k2450.visaConnect(self.rm, self.openParams)
if self.sm is not None:
result = k2450.setup2450(self.sm)
if result is True:
self.setup = True
else:
print('Already connected.')
def scrollLog(self): # scrolls log to maximum position
self.ui.textBrowser.verticalScrollBar().setValue(self.ui.textBrowser.verticalScrollBar().maximum())
def doSweep(self):
if not self.configured:
print("The sweep has not been configured. We'll try that now.")
self.applySweepValues()
if self.configured:
print("Waiting 5 seconds before we start the sweep...")
time.sleep(5)
self.sweepThread.start()
#self.ui.tehTabs.setCurrentIndex(0) # switch to plot tab
def main():
app = QtWidgets.QApplication(sys.argv)
sweepUI = MainWindow()
sweepUI.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()