]> sigrok.org Git - sigrok-meter.git/blob - mainwindow.py
a28e15c5990e53e1893d7f7a25ae37db5250c03f
[sigrok-meter.git] / mainwindow.py
1 ##
2 ## This file is part of the sigrok-meter project.
3 ##
4 ## Copyright (C) 2013 Uwe Hermann <uwe@hermann-uwe.de>
5 ## Copyright (C) 2014 Jens Steinhauser <jens.steinhauser@gmail.com>
6 ##
7 ## This program is free software; you can redistribute it and/or modify
8 ## it under the terms of the GNU General Public License as published by
9 ## the Free Software Foundation; either version 2 of the License, or
10 ## (at your option) any later version.
11 ##
12 ## This program is distributed in the hope that it will be useful,
13 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 ## GNU General Public License for more details.
16 ##
17 ## You should have received a copy of the GNU General Public License
18 ## along with this program; if not, write to the Free Software
19 ## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
20 ##
21
22 import acquisition
23 import datamodel
24 import datetime
25 import icons
26 import multiplotwidget
27 import os.path
28 import qtcompat
29 import settings
30 import sigrok.core as sr
31 import sys
32 import textwrap
33 import time
34 import util
35
36 QtCore = qtcompat.QtCore
37 QtGui = qtcompat.QtGui
38 pyqtgraph = qtcompat.pyqtgraph
39
40 class EmptyMessageListView(QtGui.QListView):
41     '''List view that shows a message if the model im empty.'''
42
43     def __init__(self, message, parent=None):
44         super(self.__class__, self).__init__(parent)
45
46         self._message = message
47
48     def paintEvent(self, event):
49         m = self.model()
50         if m and m.rowCount():
51             super(self.__class__, self).paintEvent(event)
52             return
53
54         painter = QtGui.QPainter(self.viewport())
55         painter.drawText(self.rect(), QtCore.Qt.AlignCenter, self._message)
56
57 class MainWindow(QtGui.QMainWindow):
58     '''The main window of the application.'''
59
60     # Update interval of the plots in milliseconds.
61     UPDATEINTERVAL = 100
62
63     def __init__(self, context, drivers):
64         super(self.__class__, self).__init__()
65
66         # Used to coordinate the stopping of the acquisition and
67         # the closing of the window.
68         self._closing = False
69
70         self.context = context
71         self.drivers = drivers
72
73         self.logModel = QtGui.QStringListModel(self)
74         self.context.set_log_callback(self._log_callback)
75
76         self.delegate = datamodel.MultimeterDelegate(self, self.font())
77         self.model = datamodel.MeasurementDataModel(self)
78
79         # Maps from 'unit' to the corresponding plot.
80         self._plots = {}
81         # Maps from '(plot, device)' to the corresponding curve.
82         self._curves = {}
83
84         self._setup_ui()
85
86         self._plot_update_timer = QtCore.QTimer()
87         self._plot_update_timer.setInterval(MainWindow.UPDATEINTERVAL)
88         self._plot_update_timer.timeout.connect(self._updatePlots)
89
90         settings.graph.backlog.changed.connect(self.on_setting_graph_backlog_changed)
91
92         QtCore.QTimer.singleShot(0, self._start_acquisition)
93
94     def _start_acquisition(self):
95         self.acquisition = acquisition.Acquisition(self.context)
96         self.acquisition.measured.connect(self.model.update)
97         self.acquisition.stopped.connect(self._stopped)
98
99         try:
100             for (ds, cs) in self.drivers:
101                 self.acquisition.add_device(ds, cs)
102         except Exception as e:
103             QtGui.QMessageBox.critical(self, 'Error', str(e))
104             self.close()
105             return
106
107         self.start_stop_acquisition()
108
109     def _log_callback(self, level, message):
110         if level.id > settings.logging.level.value().id:
111             return
112
113         t = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')
114         message = '[{}] sr: {}'.format(t, message)
115
116         sys.stderr.write(message + '\n')
117
118         scrollBar = self.logView.verticalScrollBar()
119         bottom = scrollBar.value() == scrollBar.maximum()
120
121         rows = self.logModel.rowCount()
122         maxrows = settings.logging.lines.value()
123         while rows > maxrows:
124             self.logModel.removeRows(0, 1)
125             rows -= 1
126
127         if self.logModel.insertRow(rows):
128             index = self.logModel.index(rows)
129             self.logModel.setData(index, message, QtCore.Qt.DisplayRole)
130
131             if bottom:
132                 self.logView.scrollToBottom()
133
134     def _setup_ui(self):
135         self.setWindowTitle('sigrok-meter')
136         # Resizing the listView below will increase this again.
137         self.resize(350, 10)
138
139         self.setWindowIcon(QtGui.QIcon(':/logo.png'))
140
141         self._setup_graphPage()
142         self._setup_addDevicePage()
143         self._setup_logPage()
144         self._setup_preferencesPage()
145
146         self._pages = [
147             self.graphPage,
148             self.addDevicePage,
149             self.logPage,
150             self.preferencesPage
151         ]
152
153         self.stackedWidget = QtGui.QStackedWidget(self)
154         for page in self._pages:
155             self.stackedWidget.addWidget(page)
156
157         self._setup_sidebar()
158
159         self.setCentralWidget(QtGui.QWidget())
160         self.centralWidget().setContentsMargins(0, 0, 0, 0)
161
162         layout = QtGui.QHBoxLayout(self.centralWidget())
163         layout.addWidget(self.sideBar)
164         layout.addWidget(self.stackedWidget)
165         layout.setSpacing(0)
166         layout.setContentsMargins(0, 0, 0, 0)
167
168         self.resize(settings.mainwindow.size.value())
169         if settings.mainwindow.pos.value():
170             self.move(settings.mainwindow.pos.value())
171
172     def _setup_sidebar(self):
173         self.sideBar = QtGui.QToolBar(self)
174         self.sideBar.setOrientation(QtCore.Qt.Vertical)
175
176         actionGraph = self.sideBar.addAction('Instantaneous Values and Graphs')
177         actionGraph.setCheckable(True)
178         actionGraph.setIcon(icons.graph)
179         actionGraph.triggered.connect(self.showGraphPage)
180
181         #actionAdd = self.sideBar.addAction('Add Device')
182         #actionAdd.setCheckable(True)
183         #actionAdd.setIcon(icons.add)
184         #actionAdd.triggered.connect(self.showAddDevicePage)
185
186         actionLog = self.sideBar.addAction('Logs')
187         actionLog.setCheckable(True)
188         actionLog.setIcon(icons.log)
189         actionLog.triggered.connect(self.showLogPage)
190
191         actionPreferences = self.sideBar.addAction('Preferences')
192         actionPreferences.setCheckable(True)
193         actionPreferences.setIcon(icons.preferences)
194         actionPreferences.triggered.connect(self.showPreferencesPage)
195
196         # make the buttons at the top exclusive
197         self.actionGroup = QtGui.QActionGroup(self)
198         self.actionGroup.addAction(actionGraph)
199         #self.actionGroup.addAction(actionAdd)
200         self.actionGroup.addAction(actionLog)
201         self.actionGroup.addAction(actionPreferences)
202
203         # show graph at startup
204         actionGraph.setChecked(True)
205
206         # fill space between buttons on the top and on the bottom
207         fill = QtGui.QWidget(self)
208         fill.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
209         self.sideBar.addWidget(fill)
210
211         self.actionStartStop = self.sideBar.addAction('Start Acquisition')
212         self.actionStartStop.setIcon(icons.start)
213         self.actionStartStop.triggered.connect(self.start_stop_acquisition)
214
215         actionAbout = self.sideBar.addAction('About')
216         actionAbout.setIcon(icons.about)
217         actionAbout.triggered.connect(self.show_about)
218
219         actionQuit = self.sideBar.addAction('Quit')
220         actionQuit.setIcon(icons.exit)
221         actionQuit.triggered.connect(self.close)
222
223         s = self.style().pixelMetric(QtGui.QStyle.PM_LargeIconSize)
224         self.sideBar.setIconSize(QtCore.QSize(s, s))
225
226         self.sideBar.setStyleSheet('''
227             QToolBar {
228                 background-color: white;
229                 margin: 0px;
230                 border: 0px;
231                 border-right: 1px solid black;
232             }
233
234             QToolButton {
235                 padding: 10px;
236                 border: 0px;
237                 border-right: 1px solid black;
238             }
239
240             QToolButton:checked,
241             QToolButton[checkable="false"]:hover {
242                 background-color: #c0d0e8;
243             }
244         ''')
245
246     def _setup_graphPage(self):
247         listView = EmptyMessageListView('waiting for data...')
248         listView.setFrameShape(QtGui.QFrame.NoFrame)
249         listView.viewport().setBackgroundRole(QtGui.QPalette.Window)
250         listView.viewport().setAutoFillBackground(True)
251         listView.setMinimumWidth(260)
252         listView.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
253         listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
254         listView.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)
255         listView.setItemDelegate(self.delegate)
256         listView.setModel(self.model)
257         listView.setUniformItemSizes(True)
258         listView.setMinimumSize(self.delegate.sizeHint())
259
260         self.plotwidget = multiplotwidget.MultiPlotWidget(self)
261         self.plotwidget.plotHidden.connect(self._on_plotHidden)
262
263         self.graphPage = QtGui.QSplitter(QtCore.Qt.Horizontal, self)
264         self.graphPage.addWidget(listView)
265         self.graphPage.addWidget(self.plotwidget)
266         self.graphPage.setStretchFactor(0, 0)
267         self.graphPage.setStretchFactor(1, 1)
268
269     def _setup_addDevicePage(self):
270         self.addDevicePage = QtGui.QWidget(self)
271         layout = QtGui.QVBoxLayout(self.addDevicePage)
272         label = QtGui.QLabel('add device page')
273         layout.addWidget(label)
274
275     def _setup_logPage(self):
276         self.logView = QtGui.QListView(self)
277         self.logView.setModel(self.logModel)
278         self.logView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
279         self.logView.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
280
281         self.logPage = QtGui.QWidget(self)
282         layout = QtGui.QVBoxLayout(self.logPage)
283         layout.addWidget(self.logView)
284
285     def _setup_preferencesPage(self):
286         self.preferencesPage = QtGui.QWidget(self)
287         layout = QtGui.QGridLayout(self.preferencesPage)
288
289         layout.addWidget(QtGui.QLabel('<b>Graph</b>'), 0, 0)
290         layout.addWidget(QtGui.QLabel('Recording time (seconds):'), 1, 0)
291
292         spin = QtGui.QSpinBox(self)
293         spin.setMinimum(10)
294         spin.setMaximum(3600)
295         spin.setSingleStep(10)
296         spin.setValue(settings.graph.backlog.value())
297         spin.valueChanged[int].connect(settings.graph.backlog.setValue)
298         layout.addWidget(spin, 1, 1)
299
300         layout.addWidget(QtGui.QLabel('<b>Logging</b>'), 2, 0)
301         layout.addWidget(QtGui.QLabel('Log level:'), 3, 0)
302
303         cbox = QtGui.QComboBox()
304         descriptions = [
305             'no messages at all',
306             'error messages',
307             'warnings',
308             'informational messages',
309             'debug messages',
310             'very noisy debug messages'
311         ]
312         for i, desc in enumerate(descriptions):
313             level = sr.LogLevel.get(i)
314             text = '{} ({})'.format(level.name, desc)
315             # The numeric log level corresponds to the index of the text in the
316             # combo box. Should this ever change, we could use the 'userData'
317             # that can also be stored in the item.
318             cbox.addItem(text)
319
320         cbox.setCurrentIndex(settings.logging.level.value().id)
321         cbox.currentIndexChanged[int].connect(
322             (lambda i: settings.logging.level.setValue(sr.LogLevel.get(i))))
323         layout.addWidget(cbox, 3, 1)
324
325         layout.addWidget(QtGui.QLabel('Number of lines to log:'), 4, 0)
326
327         spin = QtGui.QSpinBox(self)
328         spin.setMinimum(100)
329         spin.setMaximum(10 * 1000 * 1000)
330         spin.setSingleStep(100)
331         spin.setValue(settings.logging.lines.value())
332         spin.valueChanged[int].connect(settings.logging.lines.setValue)
333         layout.addWidget(spin, 4, 1)
334
335         layout.setRowStretch(layout.rowCount(), 100)
336
337     def showPage(self, page):
338         self.stackedWidget.setCurrentIndex(self._pages.index(page))
339
340     @QtCore.Slot(bool)
341     def showGraphPage(self):
342         self.showPage(self.graphPage)
343
344     @QtCore.Slot(bool)
345     def showAddDevicePage(self):
346         self.showPage(self.addDevicePage)
347
348     @QtCore.Slot(bool)
349     def showLogPage(self):
350         self.showPage(self.logPage)
351
352     @QtCore.Slot(bool)
353     def showPreferencesPage(self):
354         self.showPage(self.preferencesPage)
355
356     @QtCore.Slot(int)
357     def on_setting_graph_backlog_changed(self, bl):
358         for unit in self._plots:
359             plot = self._plots[unit]
360
361             # Remove the limits first, otherwise the range update would
362             # be ignored.
363             plot.view.setLimits(xMin=None, xMax=None)
364
365             # Now change the range, and then use the calculated limits
366             # (also see the comment in '_getPlot()').
367             plot.view.setXRange(-bl, 0, update=True)
368             r = plot.view.viewRange()
369             plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
370
371     def _getPlot(self, unit):
372         '''Looks up or creates a new plot for 'unit'.'''
373
374         if unit in self._plots:
375             return self._plots[unit]
376
377         # create a new plot for the unit
378         plot = self.plotwidget.addPlot()
379         plot.yaxis.setLabel(util.quantity_from_unit(unit), units=util.format_unit(unit))
380         plot.view.setXRange(-settings.graph.backlog.value(), 0, update=False)
381         plot.view.setYRange(-1, 1)
382         plot.view.enableAutoRange(axis=pyqtgraph.ViewBox.YAxis)
383         # lock to the range calculated by the view using additional padding,
384         # looks nicer this way
385         r = plot.view.viewRange()
386         plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
387
388         self._plots[unit] = plot
389         return plot
390
391     def _getCurve(self, plot, deviceID):
392         '''Looks up or creates a new curve for '(plot, deviceID)'.'''
393
394         key = (plot, deviceID)
395         if key in self._curves:
396             return self._curves[key]
397
398         # create a new curve
399         curve = pyqtgraph.PlotDataItem(
400             antialias=True,
401             symbolPen=pyqtgraph.mkPen(QtGui.QColor(QtCore.Qt.black)),
402             symbolBrush=pyqtgraph.mkBrush(QtGui.QColor(QtCore.Qt.black)),
403             symbolSize=1
404         )
405         plot.view.addItem(curve)
406
407         self._curves[key] = curve
408         return curve
409
410     def _updatePlots(self):
411         '''Updates all plots.'''
412
413         # loop over all devices and channels
414         for row in range(self.model.rowCount()):
415             idx = self.model.index(row, 0)
416             deviceID = self.model.data(idx,
417                             datamodel.MeasurementDataModel.idRole)
418             deviceID = tuple(deviceID) # PySide returns a list.
419             traces = self.model.data(idx,
420                             datamodel.MeasurementDataModel.tracesRole)
421
422             for unit, trace in traces.items():
423                 now = time.time()
424
425                 # remove old samples
426                 l = now - settings.graph.backlog.value()
427                 while trace.samples and trace.samples[0][0] < l:
428                     trace.samples.pop(0)
429
430                 plot = self._getPlot(unit)
431                 if not plot.visible:
432                     if trace.new:
433                         self.plotwidget.showPlot(plot)
434
435                 if plot.visible:
436                     xdata = [s[0] - now for s in trace.samples]
437                     ydata = [s[1]       for s in trace.samples]
438
439                     color = self.model.data(idx,
440                                 datamodel.MeasurementDataModel.colorRole)
441
442                     curve = self._getCurve(plot, deviceID)
443                     curve.setPen(pyqtgraph.mkPen(color=color))
444                     curve.setData(xdata, ydata)
445
446     @QtCore.Slot(multiplotwidget.Plot)
447     def _on_plotHidden(self, plot):
448         plotunit = [u for u, p in self._plots.items() if p == plot][0]
449
450         # Mark all traces of all devices/channels with the same unit as the
451         # plot as "old" ('trace.new = False'). As soon as a new sample arrives
452         # on one trace, the plot will be shown again
453         for row in range(self.model.rowCount()):
454             idx = self.model.index(row, 0)
455             traces = self.model.data(idx, datamodel.MeasurementDataModel.tracesRole)
456
457             for traceunit, trace in traces.items():
458                 if traceunit == plotunit:
459                     trace.new = False
460
461     @QtCore.Slot()
462     def _stopped(self):
463         if self._closing:
464             # The acquisition was stopped by the 'closeEvent()', close the
465             # window again now that the acquisition has stopped.
466             self.close()
467
468     def closeEvent(self, event):
469         if self.acquisition.is_running():
470             # Stop the acquisition before closing the window.
471             self._closing = True
472             self.start_stop_acquisition()
473             event.ignore()
474         else:
475             settings.mainwindow.size.setValue(self.size())
476             settings.mainwindow.pos.setValue(self.pos())
477             event.accept()
478
479     @QtCore.Slot()
480     def start_stop_acquisition(self):
481         if self.acquisition.is_running():
482             self.acquisition.stop()
483             self._plot_update_timer.stop()
484             self.actionStartStop.setText('Start Acquisition')
485             self.actionStartStop.setIcon(icons.start)
486         else:
487             # before starting (again), remove all old samples and old curves
488             self.model.clear_samples()
489
490             for key in self._curves:
491                 plot, _ = key
492                 curve = self._curves[key]
493                 plot.view.removeItem(curve)
494             self._curves = {}
495
496             self.acquisition.start()
497             self._plot_update_timer.start()
498             self.actionStartStop.setText('Stop Acquisition')
499             self.actionStartStop.setIcon(icons.stop)
500
501     @QtCore.Slot()
502     def show_about(self):
503         text = textwrap.dedent('''\
504             <div align="center">
505                 <b>sigrok-meter 0.1.0</b><br/><br/>
506                 Using libsigrok {} (lib version {}).<br/><br/>
507                 <a href='http://www.sigrok.org'>
508                          http://www.sigrok.org</a><br/>
509                 <br/>
510                 License: GNU GPL, version 3 or later<br/>
511                 <br/>
512                 This program comes with ABSOLUTELY NO WARRANTY;<br/>
513                 for details visit
514                 <a href='http://www.gnu.org/licenses/gpl.html'>
515                          http://www.gnu.org/licenses/gpl.html</a><br/>
516                 <br/>
517                 Some icons by <a href='https://www.gnome.org'>
518                               the GNOME project</a>
519             </div>
520         '''.format(self.context.package_version, self.context.lib_version))
521
522         QtGui.QMessageBox.about(self, 'About sigrok-meter', text)