X-Git-Url: http://sigrok.org/gitweb/?a=blobdiff_plain;f=mainwindow.py;h=26f1005bdbe5558c68edbd212d8d2768b1afcccf;hb=1dc3ae069fa8649774fb6d97f0d9e974318d6577;hp=680dd97b5eb5cded0d7cb7ba4b2ad304298975d7;hpb=2abf5a93f7fbca40d4b761e34c4edee008a6b37d;p=sigrok-meter.git diff --git a/mainwindow.py b/mainwindow.py index 680dd97..26f1005 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -15,17 +15,19 @@ ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License -## along with this program; if not, write to the Free Software -## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +## along with this program; if not, see . ## import acquisition import datamodel +import datetime import icons import multiplotwidget import os.path import qtcompat import settings +import sigrok.core as sr +import sys import textwrap import time import util @@ -35,7 +37,7 @@ QtGui = qtcompat.QtGui pyqtgraph = qtcompat.pyqtgraph class EmptyMessageListView(QtGui.QListView): - '''List view that shows a message if the model im empty.''' + '''List view that shows a message if the model is empty.''' def __init__(self, message, parent=None): super(self.__class__, self).__init__(parent) @@ -54,9 +56,6 @@ class EmptyMessageListView(QtGui.QListView): class MainWindow(QtGui.QMainWindow): '''The main window of the application.''' - # Number of seconds that the plots display. - BACKLOG = 30 - # Update interval of the plots in milliseconds. UPDATEINTERVAL = 100 @@ -70,6 +69,9 @@ class MainWindow(QtGui.QMainWindow): self.context = context self.drivers = drivers + self.logModel = QtGui.QStringListModel(self) + self.context.set_log_callback(self._log_callback) + self.delegate = datamodel.MultimeterDelegate(self, self.font()) self.model = datamodel.MeasurementDataModel(self) @@ -84,6 +86,8 @@ class MainWindow(QtGui.QMainWindow): self._plot_update_timer.setInterval(MainWindow.UPDATEINTERVAL) self._plot_update_timer.timeout.connect(self._updatePlots) + settings.graph.backlog.changed.connect(self.on_setting_graph_backlog_changed) + QtCore.QTimer.singleShot(0, self._start_acquisition) def _start_acquisition(self): @@ -101,6 +105,31 @@ class MainWindow(QtGui.QMainWindow): self.start_stop_acquisition() + def _log_callback(self, level, message): + if level.id > settings.logging.level.value().id: + return + + t = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f') + message = '[{}] sr: {}'.format(t, message) + + sys.stderr.write(message + '\n') + + scrollBar = self.logView.verticalScrollBar() + bottom = scrollBar.value() == scrollBar.maximum() + + rows = self.logModel.rowCount() + maxrows = settings.logging.lines.value() + while rows > maxrows: + self.logModel.removeRows(0, 1) + rows -= 1 + + if self.logModel.insertRow(rows): + index = self.logModel.index(rows) + self.logModel.setData(index, message, QtCore.Qt.DisplayRole) + + if bottom: + self.logView.scrollToBottom() + def _setup_ui(self): self.setWindowTitle('sigrok-meter') # Resizing the listView below will increase this again. @@ -153,27 +182,27 @@ class MainWindow(QtGui.QMainWindow): #actionAdd.setIcon(icons.add) #actionAdd.triggered.connect(self.showAddDevicePage) - #actionLog = self.sideBar.addAction('Logs') - #actionLog.setCheckable(True) - #actionLog.setIcon(icons.log) - #actionLog.triggered.connect(self.showLogPage) + actionLog = self.sideBar.addAction('Logs') + actionLog.setCheckable(True) + actionLog.setIcon(icons.log) + actionLog.triggered.connect(self.showLogPage) - #actionPreferences = self.sideBar.addAction('Preferences') - #actionPreferences.setCheckable(True) - #actionPreferences.setIcon(icons.preferences) - #actionPreferences.triggered.connect(self.showPreferencesPage) + actionPreferences = self.sideBar.addAction('Preferences') + actionPreferences.setCheckable(True) + actionPreferences.setIcon(icons.preferences) + actionPreferences.triggered.connect(self.showPreferencesPage) - # make the buttons at the top exclusive + # Make the buttons at the top exclusive. self.actionGroup = QtGui.QActionGroup(self) self.actionGroup.addAction(actionGraph) #self.actionGroup.addAction(actionAdd) - #self.actionGroup.addAction(actionLog) - #self.actionGroup.addAction(actionPreferences) + self.actionGroup.addAction(actionLog) + self.actionGroup.addAction(actionPreferences) - # show graph at startup + # Show graph at startup. actionGraph.setChecked(True) - # fill space between buttons on the top and on the bottom + # Fill space between buttons on the top and on the bottom. fill = QtGui.QWidget(self) fill.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) self.sideBar.addWidget(fill) @@ -245,14 +274,68 @@ class MainWindow(QtGui.QMainWindow): def _setup_logPage(self): self.logPage = QtGui.QWidget(self) layout = QtGui.QVBoxLayout(self.logPage) - label = QtGui.QLabel('log page') - layout.addWidget(label) + + self.logView = QtGui.QListView(self) + self.logView.setModel(self.logModel) + self.logView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) + self.logView.setSelectionMode(QtGui.QAbstractItemView.NoSelection) + layout.addWidget(self.logView) + + btn = QtGui.QPushButton('Save to file...', self) + btn.clicked.connect(self.on_save_log_clicked) + layout.addWidget(btn) def _setup_preferencesPage(self): self.preferencesPage = QtGui.QWidget(self) - layout = QtGui.QVBoxLayout(self.preferencesPage) - label = QtGui.QLabel('preferences page') - layout.addWidget(label) + layout = QtGui.QGridLayout(self.preferencesPage) + + layout.addWidget(QtGui.QLabel('Graph'), 0, 0) + layout.addWidget(QtGui.QLabel('Recording time (seconds):'), 1, 0) + + spin = QtGui.QSpinBox(self) + spin.setMinimum(10) + spin.setMaximum(3600) + spin.setSingleStep(10) + spin.setValue(settings.graph.backlog.value()) + spin.valueChanged[int].connect(settings.graph.backlog.setValue) + layout.addWidget(spin, 1, 1) + + layout.addWidget(QtGui.QLabel('Logging'), 2, 0) + layout.addWidget(QtGui.QLabel('Log level:'), 3, 0) + + cbox = QtGui.QComboBox() + descriptions = [ + 'no messages at all', + 'error messages', + 'warnings', + 'informational messages', + 'debug messages', + 'very noisy debug messages' + ] + for i, desc in enumerate(descriptions): + level = sr.LogLevel.get(i) + text = '{} ({})'.format(level.name, desc) + # The numeric log level corresponds to the index of the text in the + # combo box. Should this ever change, we could use the 'userData' + # that can also be stored in the item. + cbox.addItem(text) + + cbox.setCurrentIndex(settings.logging.level.value().id) + cbox.currentIndexChanged[int].connect( + (lambda i: settings.logging.level.setValue(sr.LogLevel.get(i)))) + layout.addWidget(cbox, 3, 1) + + layout.addWidget(QtGui.QLabel('Number of lines to log:'), 4, 0) + + spin = QtGui.QSpinBox(self) + spin.setMinimum(100) + spin.setMaximum(10 * 1000 * 1000) + spin.setSingleStep(100) + spin.setValue(settings.logging.lines.value()) + spin.valueChanged[int].connect(settings.logging.lines.setValue) + layout.addWidget(spin, 4, 1) + + layout.setRowStretch(layout.rowCount(), 100) def showPage(self, page): self.stackedWidget.setCurrentIndex(self._pages.index(page)) @@ -273,20 +356,35 @@ class MainWindow(QtGui.QMainWindow): def showPreferencesPage(self): self.showPage(self.preferencesPage) + @QtCore.Slot(int) + def on_setting_graph_backlog_changed(self, bl): + for unit in self._plots: + plot = self._plots[unit] + + # Remove the limits first, otherwise the range update would + # be ignored. + plot.view.setLimits(xMin=None, xMax=None) + + # Now change the range, and then use the calculated limits + # (also see the comment in '_getPlot()'). + plot.view.setXRange(-bl, 0, update=True) + r = plot.view.viewRange() + plot.view.setLimits(xMin=r[0][0], xMax=r[0][1]) + def _getPlot(self, unit): '''Looks up or creates a new plot for 'unit'.''' if unit in self._plots: return self._plots[unit] - # create a new plot for the unit + # Create a new plot for the unit. plot = self.plotwidget.addPlot() plot.yaxis.setLabel(util.quantity_from_unit(unit), units=util.format_unit(unit)) - plot.view.setXRange(-MainWindow.BACKLOG, 0, update=False) + plot.view.setXRange(-settings.graph.backlog.value(), 0, update=False) plot.view.setYRange(-1, 1) plot.view.enableAutoRange(axis=pyqtgraph.ViewBox.YAxis) - # lock to the range calculated by the view using additional padding, - # looks nicer this way + # Lock to the range calculated by the view using additional padding, + # looks nicer this way. r = plot.view.viewRange() plot.view.setLimits(xMin=r[0][0], xMax=r[0][1]) @@ -300,7 +398,7 @@ class MainWindow(QtGui.QMainWindow): if key in self._curves: return self._curves[key] - # create a new curve + # Create a new curve. curve = pyqtgraph.PlotDataItem( antialias=True, symbolPen=pyqtgraph.mkPen(QtGui.QColor(QtCore.Qt.black)), @@ -315,7 +413,7 @@ class MainWindow(QtGui.QMainWindow): def _updatePlots(self): '''Updates all plots.''' - # loop over all devices and channels + # Loop over all devices and channels. for row in range(self.model.rowCount()): idx = self.model.index(row, 0) deviceID = self.model.data(idx, @@ -327,8 +425,8 @@ class MainWindow(QtGui.QMainWindow): for unit, trace in traces.items(): now = time.time() - # remove old samples - l = now - MainWindow.BACKLOG + # Remove old samples. + l = now - settings.graph.backlog.value() while trace.samples and trace.samples[0][0] < l: trace.samples.pop(0) @@ -354,7 +452,7 @@ class MainWindow(QtGui.QMainWindow): # Mark all traces of all devices/channels with the same unit as the # plot as "old" ('trace.new = False'). As soon as a new sample arrives - # on one trace, the plot will be shown again + # on one trace, the plot will be shown again. for row in range(self.model.rowCount()): idx = self.model.index(row, 0) traces = self.model.data(idx, datamodel.MeasurementDataModel.tracesRole) @@ -389,7 +487,7 @@ class MainWindow(QtGui.QMainWindow): self.actionStartStop.setText('Start Acquisition') self.actionStartStop.setIcon(icons.start) else: - # before starting (again), remove all old samples and old curves + # Before starting (again), remove all old samples and old curves. self.model.clear_samples() for key in self._curves: @@ -403,6 +501,26 @@ class MainWindow(QtGui.QMainWindow): self.actionStartStop.setText('Stop Acquisition') self.actionStartStop.setIcon(icons.stop) + @QtCore.Slot() + def on_save_log_clicked(self): + filename = QtGui.QFileDialog.getSaveFileName(self, + 'Save Log File', settings.logging.filename.value()) + + if not filename: + # User pressed 'cancel'. + return + + try: + with open(filename, 'w') as f: + for line in self.logModel.stringList(): + f.write(line) + f.write('\n') + except Exception as e: + QtGui.QMessageBox.critical(self, 'Error saving log file', + 'Unable to save the log messages:\n{}'.format(e)) + + settings.logging.filename.setValue(filename) + @QtCore.Slot() def show_about(self): text = textwrap.dedent('''\