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
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)
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
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)
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):
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.
#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)
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('<b>Graph</b>'), 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('<b>Logging</b>'), 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))
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])
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)),
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,
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)
# 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)
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:
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('''\