def handleQueuedData(self): self.dataAccessMutex.lock() rawData = self.dataQueue if self.invertData: data = -rawData.data else: data = rawData.data temperature = rawData.temperature self.updateDigitalTemperatureDisplay(temperature) targetPath = self.cachedTargetPath save = self.recordingState if save: if targetPath: filenameForChunk = targetPath self.chunkIndex += 1 else: self.ui.statusbar.message("Invalid save file path, not saving data!") save = False try: if save: if self.rawDataFileWriter is None: self.rawDataFileWriter = DelayedFileWriter(filenameForChunk) self.rawDataFileWriter.write(rawData) time = datetime.now().strftime("%H:%M:%S") self.ui.statusbar.message("%s: (delayed) wrote a new value to datafile \"%s\"" % (time, filenameForChunk)) except OverflowError: print "!! overflow while processing data, not saving" data = None finally: self.dataAccessMutex.unlock() if data is not None: self.updateDigitalVoltageDisplay(data) self.previousChunk = data self.dataAwaitingIntegration.append(data) integratedPointsAdded = self.maybeDoIntegration() self.dataAccessMutex.lock() self.data.append(data) self.rawDataBackupCopy.append(rawData) self.dataAccessMutex.unlock() created = False if not self.plotobject: created = True self.plotobject = KPlotObject(QColor(0, 190, 255), KPlotObject.Lines, 1.0) self.addDataPoint(data) if created: self.ui.plot_sum.addPlotObject(self.plotobject) created = False if not self.integratePlotObject: created = True # FIXME: the width argument for kplotobject doesn't seem to work... self.integratePlotObject = KPlotObject(QColor(0, 0, 0), KPlotObject.Lines, 1.0) p = QPen(QColor(200, 200, 255)) p.setWidth(2.5) self.integratePlotObject.setLinePen(p) self.addIntegratedPoints(integratedPointsAdded) if created: self.ui.plot_sum.addPlotObject(self.integratePlotObject) if (self.currentDataIndex) % self.updateInterval == 0 and self.autoUpdate: self.redrawPlot() if self.newIntegrationInterval is not None: if len(self.dataAwaitingIntegration) == 0: print "updating integration interval to", self.newIntegrationInterval self.integrationInterval = self.newIntegrationInterval self.newIntegrationInterval = None self.currentIntegratedIndex = self.currentDataIndex / self.integrationInterval + 1
elif o in ("--smooth"): gTempArraySize = int(a) elif o in ("--profile"): profile_file = a elif o in ("--pcontrol"): pcontrol_dev = a else: assert False, "unhandled option" app = QtGui.QApplication(sys.argv) pyRoast = QtGui.QMainWindow() ui = Ui_pyRoast() ui.setupUi(pyRoast) # create plot of multimeter dmmPlot = KPlotObject(gPlotColor, KPlotObject.Lines, 6, KPlotObject.Circle) LoadedProfile = KPlotObject(gProfileColor, KPlotObject.Lines, 6) SetupPlot(ui.TemperaturePlot, dmmPlot, LoadedProfile) pyRoast.setWindowTitle("pyRoast") findRoasts() # connect up the buttons QtCore.QObject.connect(ui.bQuit, QtCore.SIGNAL("clicked()"), bQuit) QtCore.QObject.connect(ui.bSave, QtCore.SIGNAL("clicked()"), bSave) QtCore.QObject.connect(ui.bSaveAs, QtCore.SIGNAL("clicked()"), bSaveAs) QtCore.QObject.connect(ui.bReset, QtCore.SIGNAL("clicked()"), bReset) # QtCore.QObject.connect(ui.bLoadProfile, QtCore.SIGNAL("clicked()"), bLoadProfile) QtCore.QObject.connect(ui.bFirstCrack, QtCore.SIGNAL("clicked()"), bFirstCrack)
class mainwin(QMainWindow): def __init__(self): super(mainwin, self).__init__() self.ui = mainwindow.Ui_MainWindow() self.ui.setupUi(self) self.ui.record_url.setMode(KFile.Directory) self.rawDataFileWriter = None self.ui.window_splitter.setSizes([600, 150]) self.ui.preview_group.toggled.connect(self.toggleAutoUpdate) self.abortRecording = False self.ui.mode_group.clicked.connect(self.toggleDataAcquisitionAndOperationMode) self.ui.mode_gradient.toggled.connect(self.toggleDataAcquisitionAndOperationMode) self.ui.mode_interval.toggled.connect(self.toggleDataAcquisitionAndOperationMode) self.mode = None self.dataAccessMutex = QMutex() self.dataQueue = False self.newIntegrationInterval = None self.updateIntegrationInterval() self.startRecording() self.updatePreviewIntervals() self.ui.preview_repeatInterval.textChanged.connect(self.updatePreviewIntervals) self.ui.control_clear.clicked.connect(self.clearData) self.toggleDataAcquisitionAndOperationMode() self.toggleAutoUpdate() self.chunkIndex = 0 self.data = [] self.currentDataIndex = 0 self.currentIntegratedIndex = 0 self.isFirstChunk = True self.ui.actionSave_displayed.activated.connect(self.saveAllData) self.ui.gradientSettings_integrate.textChanged.connect(self.updateIntegrationInterval) self.ui.record_startstop.toggled.connect(self.updateRecordingState) self.ui.record_url.urlSelected.connect(self.updatePath) self.ui.record_prefix.textChanged.connect(self.updatePath) self.ui.preview_now.clicked.connect(self.redrawPlot) self.ui.gradientSettings_group.clicked.connect(self.changeIntegrationMode) self.ui.gradientSettings_integrate.textChanged.connect(self.changeIntegrationMode) self.changeIntegrationMode() self.updateIntegrationInterval() self.updateRecordingState() self.updatePath() self.ui.plot_sum.setAntialiasing(True) self.plotobject = None self.integratePlotObject = None self.invertDataToggled() self.ui.actionOpen_and_continue.activated.connect(self.openDataFile) self.ui.limits_slider_x.valueChanged.connect(self.syncLimitsSlidersToTextfield) self.ui.limits_slider_y.valueChanged.connect(self.syncLimitsSlidersToTextfield) self.ui.limits_slider_x.valueChanged.connect(self.checkLimits_x) self.ui.limits_slider_y.valueChanged.connect(self.checkLimits_y) self.ui.limits_text_x.textChanged.connect(self.syncLimitsTextfieldsToSlider) self.ui.limits_text_y.textChanged.connect(self.syncLimitsTextfieldsToSlider) self.ui.limits_autoFromData.toggled.connect(self.changeLimits) self.ui.limits_autoFromIntegrated.toggled.connect(self.changeLimits) self.ui.limits_manual.toggled.connect(self.changeLimits) self.changeLimits() self.dataAwaitingIntegration = [] self.ui.invertSignalCheckbox.clicked.connect(self.invertDataToggled) self.temperatureUpdateTimer = QTimer() self.temperatureUpdateTimer.setInterval(1000) self.temperatureUpdateTimer.setSingleShot(False) self.temperatureUpdateTimer.timeout.connect(self.recorderThread.requestTemperatureUpdate) self.temperatureUpdateTimer.start() self.rawDataBackupCopy = [] def checkLimits_x(self): if self.ui.limits_slider_x.value() > self.ui.limits_slider_y.value(): self.ui.limits_slider_y.setValue(self.ui.limits_slider_x.value()+20) def checkLimits_y(self): if self.ui.limits_slider_y.value() < self.ui.limits_slider_x.value(): self.ui.limits_slider_x.setValue(self.ui.limits_slider_y.value()+20) def syncLimitsTextfieldsToSlider(self): syncItems = { self.ui.limits_slider_x : self.ui.limits_text_x, self.ui.limits_slider_y : self.ui.limits_text_y } for slider, textfield in syncItems.iteritems(): try: newValue = int(textfield.text()) except: continue if slider.value() == newValue: continue slider.setValue(newValue) self.redrawPlot() def syncLimitsSlidersToTextfield(self): syncItems = { self.ui.limits_slider_x : self.ui.limits_text_x, self.ui.limits_slider_y : self.ui.limits_text_y } for slider, textfield in syncItems.iteritems(): newValue = str(slider.value()) if textfield.text() == newValue: continue textfield.setText(newValue) self.redrawPlot() def changeLimits(self): modes = { self.ui.limits_autoFromData : "autoData", self.ui.limits_autoFromIntegrated : "autoIntegrated", self.ui.limits_manual : "manual" } for elem, mode in modes.iteritems(): if elem.isChecked(): self.limitsMode = mode print "limits cotntrol mode changed to", self.limitsMode manualControls = [self.ui.limits_slider_x, self.ui.limits_slider_y, self.ui.limits_text_x, self.ui.limits_text_y] for elem in manualControls: if self.limitsMode != "manual": elem.setDisabled(True) else: elem.setDisabled(False) if self.invertData: sliderRange = (-expectedAdRange[1]-20, expectedAdRange[0]+20) else: sliderRange = (expectedAdRange[0]-20, expectedAdRange[1]+20) self.ui.limits_slider_x.setRange(*sliderRange) self.ui.limits_slider_y.setRange(*sliderRange) if self.limitsMode == "manual" and hasattr(self, "lastUsedBounds"): print self.lastUsedBounds lastBounds = self.lastUsedBounds self.ui.limits_slider_x.setValue(sliderRange[0]) self.ui.limits_slider_y.setValue(sliderRange[1]) self.ui.limits_slider_x.setValue(lastBounds[2]) self.ui.limits_slider_y.setValue(lastBounds[3]) self.redrawPlot() def invertDataToggled(self): self.invertData = self.ui.invertSignalCheckbox.isChecked() self.changeLimits() def verifyDataSaved(self): if self.rawDataFileWriter is not None: self.rawDataFileWriter.doWriteData() def changeIntegrationMode(self): self.doIntegration = self.ui.gradientSettings_group.isChecked() self.recalculateIntegratedData() def recalculateIntegratedData(self): chunkSize = self.integrationInterval self.integratedData = [] for i in xrange(0, len(self.data), chunkSize): chunk = self.data[i:i+chunkSize] self.integratedData.append(sum(chunk) / float(chunkSize)) def saveAllData(self): f = KFileDialog.getSaveFileName() try: writeArrayToDatafile(self.rawDataBackupCopy, f) self.ui.statusbar.message("Successfully saved %s values to %s." % (str(len(self.data)), f)) except Exception as e: self.ui.statusbar.message("Failed to save data to %s: %s" % (f, str(e))) def updatePath(self): self.cachedTargetPath = self.targetPath() def updateRecordingState(self): self.recordingState = self.ui.record_startstop.isChecked() self.verifyDataSaved() def updateIntegrationInterval(self): self.newIntegrationInterval = verify(self.ui.gradientSettings_integrate.text().toInt(), 1, "integration interval", self) # more than 25 updates per second don't make sense self.newIntegrationInterval = max([self.newIntegrationInterval, 0.04]) if not hasattr(self, "integrationInterval"): self.integrationInterval = self.newIntegrationInterval def updatePreviewIntervals(self): self.updateInterval = verify(self.ui.preview_repeatInterval.text().toInt(), 4, "preview interval", self) def toggleDataAcquisitionAndOperationMode(self): self.verifyDataSaved() if not self.ui.mode_group.isChecked(): self.abortRecording = True elif self.abortRecording: self.startRecording() self.abortRecording = False previousMode = self.mode print "setting new operation mode:", if self.ui.mode_gradient.isChecked(): self.mode = 'gradient' else: self.mode = 'interval' print self.mode if self.mode != previousMode: print "Operation mode changed, clearing data" self.clearData() self.updatePreviewIntervals() def indexToTime(self, index): return index / (samplesPerSecond * 60.0) def addIntegratedPoints(self, points): for point in points: self.integratedRealIndex = self.indexToTime(self.currentIntegratedIndex * self.integrationInterval) #print "adding point with time", self.integratedRealIndex, self.integrationInterval, self.newIntegrationInterval self.integratePlotObject.addPoint(self.integratedRealIndex, point) self.currentIntegratedIndex += 1 def addDataPoint(self, point): self.realIndex = self.indexToTime(self.currentDataIndex) self.plotobject.addPoint(self.realIndex, point) self.currentDataIndex += 1 def openDataFile(self): print "Sorry, this does not work currently." return self.verifyDataSaved() f = KFileDialog.getOpenFileName() s = str(f).split('/') self.clearData() self.dataAccessMutex.lock() try: with open(f) as fileptr: data = fileptr.readlines() self.data = [float(item.split('\t')[1]) for item in data] if len(self.data) == 0: raise IOError("No valid data points found, probably the file is empty") self.ui.record_url.setText('/'.join(s[:-1])) self.ui.record_prefix.setText(s[-1].replace('result', '') + "_cont_") self.ui.record_startstop.setChecked(False) self.updateRecordingState() self.ui.statusbar.message("Successfully loaded %s data points from %s." % (len(data), str(f))) except Exception as e: print e try: self.ui.statusbar.message("Could not load file %s: %s" % (f, str(e))) except: self.ui.statusbar.message("Could not load file %s: invalid characters" % f) finally: if len(self.data) > 0: self.addDataPoint(self.data) self.redrawPlot() self.dataAccessMutex.unlock() def clearData(self): self.verifyDataSaved() self.dataAccessMutex.lock() self.data = [] self.integratedData = [] self.integratedRealIndex = 0 self.currentIntegratedIndex = 0 self.dataAwaitingIntegration = [] self.currentDataIndex = 0 self.rawDataFileWriter = None self.previousChunk = False if hasattr(self, "plotobject"): self.plotobject.clearPoints() if hasattr(self, "integratePlotObject"): self.integratePlotObject.clearPoints() self.dataAccessMutex.unlock() def toggleAutoUpdate(self): if self.ui.preview_group.isChecked(): self.updateRecordingInterval() self.autoUpdate = True else: self.autoUpdate = False def length(self): return verify(self.ui.gradientSettings_integrate.text().toInt(), 1, "integration interval", self) def updateRecordingInterval(self): self.recorderThread.length = 1 # TODO make configurable def connectionStatusChanged(self, isConnected): self.deviceConnected = isConnected print "Connection status changed:", isConnected if isConnected: text = '<font style="color:#8BBE00; font-weight:bold">connected</font>' self.ui.reconnectButton.setEnabled(False) self.ui.statusbar.message("i2c-tiny-usb device connected successfully.") self.recorderThread.start() else: text = '<font style="color:#FF2600; font-weight:bold">not connected</font>' self.ui.reconnectButton.setEnabled(True) self.ui.connectionStatus.setText("Status: %s" % text) def IOErrorOccured(self, error): self.ui.statusbar.message('Connection error: %s' % error) def startRecording(self): self.recorderThread = Recorder(1) self.ui.reconnectButton.setEnabled(True) self.recorderThread.dataAvailable.connect(self.handleRecordFinished) self.recorderThread.connectionStatusChanged.connect(self.connectionStatusChanged) self.recorderThread.connectionError.connect(self.IOErrorOccured) self.recorderThread.doConnect() if self.deviceConnected: self.recorderThread.start(QThread.TimeCriticalPriority) self.ui.reconnectButton.clicked.connect(self.recorderThread.reconnect) def targetPath(self): url = self.ui.record_url.url() targetPath = False if url.isValid(): targetPath = url.path() + "/" + self.ui.record_prefix.text() return targetPath def updateDigitalVoltageDisplay(self, rawVoltage): voltage = rawOutputToVoltage(rawVoltage) self.ui.voltage_value.setText(u"%.02fmV" % (voltage*1000)) # theoreticallyμ \u2004 is a halfspace but doesn't work if len(self.integratedData): self.ui.voltage_mean_label.setText(u"Mean (%s samples): <b>%.02fmV</b>" % ( self.integrationInterval, rawOutputToVoltage(self.integratedData[-1]*1000) )) def updateDigitalTemperatureDisplay(self, temperature): if temperature: self.ui.temperature_value.setText(u"%s°C" % temperature) # else: # self.ui.temperature_value.setText("-.-") def handleQueuedData(self): self.dataAccessMutex.lock() rawData = self.dataQueue if self.invertData: data = -rawData.data else: data = rawData.data temperature = rawData.temperature self.updateDigitalTemperatureDisplay(temperature) targetPath = self.cachedTargetPath save = self.recordingState if save: if targetPath: filenameForChunk = targetPath self.chunkIndex += 1 else: self.ui.statusbar.message("Invalid save file path, not saving data!") save = False try: if save: if self.rawDataFileWriter is None: self.rawDataFileWriter = DelayedFileWriter(filenameForChunk) self.rawDataFileWriter.write(rawData) time = datetime.now().strftime("%H:%M:%S") self.ui.statusbar.message("%s: (delayed) wrote a new value to datafile \"%s\"" % (time, filenameForChunk)) except OverflowError: print "!! overflow while processing data, not saving" data = None finally: self.dataAccessMutex.unlock() if data is not None: self.updateDigitalVoltageDisplay(data) self.previousChunk = data self.dataAwaitingIntegration.append(data) integratedPointsAdded = self.maybeDoIntegration() self.dataAccessMutex.lock() self.data.append(data) self.rawDataBackupCopy.append(rawData) self.dataAccessMutex.unlock() created = False if not self.plotobject: created = True self.plotobject = KPlotObject(QColor(0, 190, 255), KPlotObject.Lines, 1.0) self.addDataPoint(data) if created: self.ui.plot_sum.addPlotObject(self.plotobject) created = False if not self.integratePlotObject: created = True # FIXME: the width argument for kplotobject doesn't seem to work... self.integratePlotObject = KPlotObject(QColor(0, 0, 0), KPlotObject.Lines, 1.0) p = QPen(QColor(200, 200, 255)) p.setWidth(2.5) self.integratePlotObject.setLinePen(p) self.addIntegratedPoints(integratedPointsAdded) if created: self.ui.plot_sum.addPlotObject(self.integratePlotObject) if (self.currentDataIndex) % self.updateInterval == 0 and self.autoUpdate: self.redrawPlot() if self.newIntegrationInterval is not None: if len(self.dataAwaitingIntegration) == 0: print "updating integration interval to", self.newIntegrationInterval self.integrationInterval = self.newIntegrationInterval self.newIntegrationInterval = None self.currentIntegratedIndex = self.currentDataIndex / self.integrationInterval + 1 def maybeDoIntegration(self): pointsAdded = [] while len(self.dataAwaitingIntegration) >= self.integrationInterval: integratedValue = sum(self.dataAwaitingIntegration[:self.integrationInterval]) / self.integrationInterval self.integratedData.append(integratedValue) pointsAdded.append(integratedValue) self.dataAwaitingIntegration = self.dataAwaitingIntegration[self.integrationInterval:] return pointsAdded def redrawPlot(self): if len(self.data) > 0: if self.limitsMode == "autoIntegrated" and len(self.integratedData) > 0: bounds = (0, self.realIndex*1.05, min(self.integratedData)-20, max(self.integratedData)+20) elif self.limitsMode == "autoData": bounds = (0, self.realIndex*1.05, min(self.data)-20, max(self.data)+20) else: bounds = (0, self.realIndex*1.05, int(self.ui.limits_text_x.text()), int(self.ui.limits_text_y.text())) self.ui.plot_sum.setLimits(*bounds) self.lastUsedBounds = bounds self.ui.plot_sum.update() def handleRecordFinished(self): if self.recorderThread.stop: return self.recorderThread.dataAccessMutex.lock() data = copy.deepcopy(self.recorderThread.data.pop()) self.recorderThread.dataAccessMutex.unlock() if self.abortRecording: self.recorderThread.stop = True if not self.isFirstChunk: # discard the first chunk of data, it's often faulty self.dataAccessMutex.lock() self.dataQueue = data self.dataAccessMutex.unlock() self.handleQueuedData() else: self.isFirstChunk = False