Allow changing of the recording time.
[sigrok-meter/gsi.git] / mainwindow.py
blob64caa2a7e9b72b03afef01d0ba4978b06aba2cc8
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.
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.
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
22 import acquisition
23 import datamodel
24 import icons
25 import multiplotwidget
26 import os.path
27 import qtcompat
28 import settings
29 import textwrap
30 import time
31 import util
33 QtCore = qtcompat.QtCore
34 QtGui = qtcompat.QtGui
35 pyqtgraph = qtcompat.pyqtgraph
37 class EmptyMessageListView(QtGui.QListView):
38 '''List view that shows a message if the model im empty.'''
40 def __init__(self, message, parent=None):
41 super(self.__class__, self).__init__(parent)
43 self._message = message
45 def paintEvent(self, event):
46 m = self.model()
47 if m and m.rowCount():
48 super(self.__class__, self).paintEvent(event)
49 return
51 painter = QtGui.QPainter(self.viewport())
52 painter.drawText(self.rect(), QtCore.Qt.AlignCenter, self._message)
54 class MainWindow(QtGui.QMainWindow):
55 '''The main window of the application.'''
57 # Update interval of the plots in milliseconds.
58 UPDATEINTERVAL = 100
60 def __init__(self, context, drivers):
61 super(self.__class__, self).__init__()
63 # Used to coordinate the stopping of the acquisition and
64 # the closing of the window.
65 self._closing = False
67 self.context = context
68 self.drivers = drivers
70 self.delegate = datamodel.MultimeterDelegate(self, self.font())
71 self.model = datamodel.MeasurementDataModel(self)
73 # Maps from 'unit' to the corresponding plot.
74 self._plots = {}
75 # Maps from '(plot, device)' to the corresponding curve.
76 self._curves = {}
78 self._setup_ui()
80 self._plot_update_timer = QtCore.QTimer()
81 self._plot_update_timer.setInterval(MainWindow.UPDATEINTERVAL)
82 self._plot_update_timer.timeout.connect(self._updatePlots)
84 settings.graph.backlog.changed.connect(self.on_setting_graph_backlog_changed)
86 QtCore.QTimer.singleShot(0, self._start_acquisition)
88 def _start_acquisition(self):
89 self.acquisition = acquisition.Acquisition(self.context)
90 self.acquisition.measured.connect(self.model.update)
91 self.acquisition.stopped.connect(self._stopped)
93 try:
94 for (ds, cs) in self.drivers:
95 self.acquisition.add_device(ds, cs)
96 except Exception as e:
97 QtGui.QMessageBox.critical(self, 'Error', str(e))
98 self.close()
99 return
101 self.start_stop_acquisition()
103 def _setup_ui(self):
104 self.setWindowTitle('sigrok-meter')
105 # Resizing the listView below will increase this again.
106 self.resize(350, 10)
108 self.setWindowIcon(QtGui.QIcon(':/logo.png'))
110 self._setup_graphPage()
111 self._setup_addDevicePage()
112 self._setup_logPage()
113 self._setup_preferencesPage()
115 self._pages = [
116 self.graphPage,
117 self.addDevicePage,
118 self.logPage,
119 self.preferencesPage
122 self.stackedWidget = QtGui.QStackedWidget(self)
123 for page in self._pages:
124 self.stackedWidget.addWidget(page)
126 self._setup_sidebar()
128 self.setCentralWidget(QtGui.QWidget())
129 self.centralWidget().setContentsMargins(0, 0, 0, 0)
131 layout = QtGui.QHBoxLayout(self.centralWidget())
132 layout.addWidget(self.sideBar)
133 layout.addWidget(self.stackedWidget)
134 layout.setSpacing(0)
135 layout.setContentsMargins(0, 0, 0, 0)
137 self.resize(settings.mainwindow.size.value())
138 if settings.mainwindow.pos.value():
139 self.move(settings.mainwindow.pos.value())
141 def _setup_sidebar(self):
142 self.sideBar = QtGui.QToolBar(self)
143 self.sideBar.setOrientation(QtCore.Qt.Vertical)
145 actionGraph = self.sideBar.addAction('Instantaneous Values and Graphs')
146 actionGraph.setCheckable(True)
147 actionGraph.setIcon(icons.graph)
148 actionGraph.triggered.connect(self.showGraphPage)
150 #actionAdd = self.sideBar.addAction('Add Device')
151 #actionAdd.setCheckable(True)
152 #actionAdd.setIcon(icons.add)
153 #actionAdd.triggered.connect(self.showAddDevicePage)
155 #actionLog = self.sideBar.addAction('Logs')
156 #actionLog.setCheckable(True)
157 #actionLog.setIcon(icons.log)
158 #actionLog.triggered.connect(self.showLogPage)
160 actionPreferences = self.sideBar.addAction('Preferences')
161 actionPreferences.setCheckable(True)
162 actionPreferences.setIcon(icons.preferences)
163 actionPreferences.triggered.connect(self.showPreferencesPage)
165 # make the buttons at the top exclusive
166 self.actionGroup = QtGui.QActionGroup(self)
167 self.actionGroup.addAction(actionGraph)
168 #self.actionGroup.addAction(actionAdd)
169 #self.actionGroup.addAction(actionLog)
170 self.actionGroup.addAction(actionPreferences)
172 # show graph at startup
173 actionGraph.setChecked(True)
175 # fill space between buttons on the top and on the bottom
176 fill = QtGui.QWidget(self)
177 fill.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
178 self.sideBar.addWidget(fill)
180 self.actionStartStop = self.sideBar.addAction('Start Acquisition')
181 self.actionStartStop.setIcon(icons.start)
182 self.actionStartStop.triggered.connect(self.start_stop_acquisition)
184 actionAbout = self.sideBar.addAction('About')
185 actionAbout.setIcon(icons.about)
186 actionAbout.triggered.connect(self.show_about)
188 actionQuit = self.sideBar.addAction('Quit')
189 actionQuit.setIcon(icons.exit)
190 actionQuit.triggered.connect(self.close)
192 s = self.style().pixelMetric(QtGui.QStyle.PM_LargeIconSize)
193 self.sideBar.setIconSize(QtCore.QSize(s, s))
195 self.sideBar.setStyleSheet('''
196 QToolBar {
197 background-color: white;
198 margin: 0px;
199 border: 0px;
200 border-right: 1px solid black;
203 QToolButton {
204 padding: 10px;
205 border: 0px;
206 border-right: 1px solid black;
209 QToolButton:checked,
210 QToolButton[checkable="false"]:hover {
211 background-color: #c0d0e8;
213 ''')
215 def _setup_graphPage(self):
216 listView = EmptyMessageListView('waiting for data...')
217 listView.setFrameShape(QtGui.QFrame.NoFrame)
218 listView.viewport().setBackgroundRole(QtGui.QPalette.Window)
219 listView.viewport().setAutoFillBackground(True)
220 listView.setMinimumWidth(260)
221 listView.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
222 listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
223 listView.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)
224 listView.setItemDelegate(self.delegate)
225 listView.setModel(self.model)
226 listView.setUniformItemSizes(True)
227 listView.setMinimumSize(self.delegate.sizeHint())
229 self.plotwidget = multiplotwidget.MultiPlotWidget(self)
230 self.plotwidget.plotHidden.connect(self._on_plotHidden)
232 self.graphPage = QtGui.QSplitter(QtCore.Qt.Horizontal, self)
233 self.graphPage.addWidget(listView)
234 self.graphPage.addWidget(self.plotwidget)
235 self.graphPage.setStretchFactor(0, 0)
236 self.graphPage.setStretchFactor(1, 1)
238 def _setup_addDevicePage(self):
239 self.addDevicePage = QtGui.QWidget(self)
240 layout = QtGui.QVBoxLayout(self.addDevicePage)
241 label = QtGui.QLabel('add device page')
242 layout.addWidget(label)
244 def _setup_logPage(self):
245 self.logPage = QtGui.QWidget(self)
246 layout = QtGui.QVBoxLayout(self.logPage)
247 label = QtGui.QLabel('log page')
248 layout.addWidget(label)
250 def _setup_preferencesPage(self):
251 self.preferencesPage = QtGui.QWidget(self)
252 layout = QtGui.QGridLayout(self.preferencesPage)
254 layout.addWidget(QtGui.QLabel('<b>Graph</b>'), 0, 0)
255 layout.addWidget(QtGui.QLabel('Recording time (seconds):'), 1, 0)
257 spin = QtGui.QSpinBox(self)
258 spin.setMinimum(10)
259 spin.setMaximum(3600)
260 spin.setSingleStep(10)
261 spin.setValue(settings.graph.backlog.value())
262 spin.valueChanged[int].connect(settings.graph.backlog.setValue)
263 layout.addWidget(spin, 1, 1)
265 layout.setRowStretch(layout.rowCount(), 100)
267 def showPage(self, page):
268 self.stackedWidget.setCurrentIndex(self._pages.index(page))
270 @QtCore.Slot(bool)
271 def showGraphPage(self):
272 self.showPage(self.graphPage)
274 @QtCore.Slot(bool)
275 def showAddDevicePage(self):
276 self.showPage(self.addDevicePage)
278 @QtCore.Slot(bool)
279 def showLogPage(self):
280 self.showPage(self.logPage)
282 @QtCore.Slot(bool)
283 def showPreferencesPage(self):
284 self.showPage(self.preferencesPage)
286 @QtCore.Slot(int)
287 def on_setting_graph_backlog_changed(self, bl):
288 for unit in self._plots:
289 plot = self._plots[unit]
291 # Remove the limits first, otherwise the range update would
292 # be ignored.
293 plot.view.setLimits(xMin=None, xMax=None)
295 # Now change the range, and then use the calculated limits
296 # (also see the comment in '_getPlot()').
297 plot.view.setXRange(-bl, 0, update=True)
298 r = plot.view.viewRange()
299 plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
301 def _getPlot(self, unit):
302 '''Looks up or creates a new plot for 'unit'.'''
304 if unit in self._plots:
305 return self._plots[unit]
307 # create a new plot for the unit
308 plot = self.plotwidget.addPlot()
309 plot.yaxis.setLabel(util.quantity_from_unit(unit), units=util.format_unit(unit))
310 plot.view.setXRange(-settings.graph.backlog.value(), 0, update=False)
311 plot.view.setYRange(-1, 1)
312 plot.view.enableAutoRange(axis=pyqtgraph.ViewBox.YAxis)
313 # lock to the range calculated by the view using additional padding,
314 # looks nicer this way
315 r = plot.view.viewRange()
316 plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
318 self._plots[unit] = plot
319 return plot
321 def _getCurve(self, plot, deviceID):
322 '''Looks up or creates a new curve for '(plot, deviceID)'.'''
324 key = (plot, deviceID)
325 if key in self._curves:
326 return self._curves[key]
328 # create a new curve
329 curve = pyqtgraph.PlotDataItem(
330 antialias=True,
331 symbolPen=pyqtgraph.mkPen(QtGui.QColor(QtCore.Qt.black)),
332 symbolBrush=pyqtgraph.mkBrush(QtGui.QColor(QtCore.Qt.black)),
333 symbolSize=1
335 plot.view.addItem(curve)
337 self._curves[key] = curve
338 return curve
340 def _updatePlots(self):
341 '''Updates all plots.'''
343 # loop over all devices and channels
344 for row in range(self.model.rowCount()):
345 idx = self.model.index(row, 0)
346 deviceID = self.model.data(idx,
347 datamodel.MeasurementDataModel.idRole)
348 deviceID = tuple(deviceID) # PySide returns a list.
349 traces = self.model.data(idx,
350 datamodel.MeasurementDataModel.tracesRole)
352 for unit, trace in traces.items():
353 now = time.time()
355 # remove old samples
356 l = now - settings.graph.backlog.value()
357 while trace.samples and trace.samples[0][0] < l:
358 trace.samples.pop(0)
360 plot = self._getPlot(unit)
361 if not plot.visible:
362 if trace.new:
363 self.plotwidget.showPlot(plot)
365 if plot.visible:
366 xdata = [s[0] - now for s in trace.samples]
367 ydata = [s[1] for s in trace.samples]
369 color = self.model.data(idx,
370 datamodel.MeasurementDataModel.colorRole)
372 curve = self._getCurve(plot, deviceID)
373 curve.setPen(pyqtgraph.mkPen(color=color))
374 curve.setData(xdata, ydata)
376 @QtCore.Slot(multiplotwidget.Plot)
377 def _on_plotHidden(self, plot):
378 plotunit = [u for u, p in self._plots.items() if p == plot][0]
380 # Mark all traces of all devices/channels with the same unit as the
381 # plot as "old" ('trace.new = False'). As soon as a new sample arrives
382 # on one trace, the plot will be shown again
383 for row in range(self.model.rowCount()):
384 idx = self.model.index(row, 0)
385 traces = self.model.data(idx, datamodel.MeasurementDataModel.tracesRole)
387 for traceunit, trace in traces.items():
388 if traceunit == plotunit:
389 trace.new = False
391 @QtCore.Slot()
392 def _stopped(self):
393 if self._closing:
394 # The acquisition was stopped by the 'closeEvent()', close the
395 # window again now that the acquisition has stopped.
396 self.close()
398 def closeEvent(self, event):
399 if self.acquisition.is_running():
400 # Stop the acquisition before closing the window.
401 self._closing = True
402 self.start_stop_acquisition()
403 event.ignore()
404 else:
405 settings.mainwindow.size.setValue(self.size())
406 settings.mainwindow.pos.setValue(self.pos())
407 event.accept()
409 @QtCore.Slot()
410 def start_stop_acquisition(self):
411 if self.acquisition.is_running():
412 self.acquisition.stop()
413 self._plot_update_timer.stop()
414 self.actionStartStop.setText('Start Acquisition')
415 self.actionStartStop.setIcon(icons.start)
416 else:
417 # before starting (again), remove all old samples and old curves
418 self.model.clear_samples()
420 for key in self._curves:
421 plot, _ = key
422 curve = self._curves[key]
423 plot.view.removeItem(curve)
424 self._curves = {}
426 self.acquisition.start()
427 self._plot_update_timer.start()
428 self.actionStartStop.setText('Stop Acquisition')
429 self.actionStartStop.setIcon(icons.stop)
431 @QtCore.Slot()
432 def show_about(self):
433 text = textwrap.dedent('''\
434 <div align="center">
435 <b>sigrok-meter 0.1.0</b><br/><br/>
436 Using libsigrok {} (lib version {}).<br/><br/>
437 <a href='http://www.sigrok.org'>
438 http://www.sigrok.org</a><br/>
439 <br/>
440 License: GNU GPL, version 3 or later<br/>
441 <br/>
442 This program comes with ABSOLUTELY NO WARRANTY;<br/>
443 for details visit
444 <a href='http://www.gnu.org/licenses/gpl.html'>
445 http://www.gnu.org/licenses/gpl.html</a><br/>
446 <br/>
447 Some icons by <a href='https://www.gnome.org'>
448 the GNOME project</a>
449 </div>
450 '''.format(self.context.package_version, self.context.lib_version))
452 QtGui.QMessageBox.about(self, 'About sigrok-meter', text)