2 ## This file is part of the sigrok-meter project.
4 ## Copyright (C) 2013 Uwe Hermann <uwe@hermann-uwe.de>
5 ## Copyright (C) 2014 Jens Steinhauser <jens.steinhauser@gmail.com>
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
25 import multiplotwidget
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):
47 if m and m.rowCount():
48 super(self.__class__, self).paintEvent(event)
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.
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.
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.
75 # Maps from '(plot, device)' to the corresponding curve.
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)
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))
101 self.start_stop_acquisition()
104 self.setWindowTitle('sigrok-meter')
105 # Resizing the listView below will increase this again.
108 self.setWindowIcon(QtGui.QIcon(':/logo.png'))
110 self._setup_graphPage()
111 self._setup_addDevicePage()
112 self._setup_logPage()
113 self._setup_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)
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('''
197 background-color: white;
200 border-right: 1px solid black;
206 border-right: 1px solid black;
210 QToolButton[checkable="false"]:hover {
211 background-color: #c0d0e8;
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)
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))
271 def showGraphPage(self):
272 self.showPage(self.graphPage)
275 def showAddDevicePage(self):
276 self.showPage(self.addDevicePage)
279 def showLogPage(self):
280 self.showPage(self.logPage)
283 def showPreferencesPage(self):
284 self.showPage(self.preferencesPage)
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
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
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]
329 curve = pyqtgraph.PlotDataItem(
331 symbolPen=pyqtgraph.mkPen(QtGui.QColor(QtCore.Qt.black)),
332 symbolBrush=pyqtgraph.mkBrush(QtGui.QColor(QtCore.Qt.black)),
335 plot.view.addItem(curve)
337 self._curves[key] = 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():
356 l = now - settings.graph.backlog.value()
357 while trace.samples and trace.samples[0][0] < l:
360 plot = self._getPlot(unit)
363 self.plotwidget.showPlot(plot)
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:
394 # The acquisition was stopped by the 'closeEvent()', close the
395 # window again now that the acquisition has stopped.
398 def closeEvent(self, event):
399 if self.acquisition.is_running():
400 # Stop the acquisition before closing the window.
402 self.start_stop_acquisition()
405 settings.mainwindow.size.setValue(self.size())
406 settings.mainwindow.pos.setValue(self.pos())
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)
417 # before starting (again), remove all old samples and old curves
418 self.model.clear_samples()
420 for key in self._curves:
422 curve = self._curves[key]
423 plot.view.removeItem(curve)
426 self.acquisition.start()
427 self._plot_update_timer.start()
428 self.actionStartStop.setText('Stop Acquisition')
429 self.actionStartStop.setIcon(icons.stop)
432 def show_about(self):
433 text = textwrap.dedent('''\
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/>
440 License: GNU GPL, version 3 or later<br/>
442 This program comes with ABSOLUTELY NO WARRANTY;<br/>
444 <a href='http://www.gnu.org/licenses/gpl.html'>
445 http://www.gnu.org/licenses/gpl.html</a><br/>
447 Some icons by <a href='https://www.gnome.org'>
448 the GNOME project</a>
450 '''.format(self.context.package_version, self.context.lib_version))
452 QtGui.QMessageBox.about(self, 'About sigrok-meter', text)