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
24 import multiplotwidget
31 QtCore = qtcompat.QtCore
32 QtGui = qtcompat.QtGui
33 pyqtgraph = qtcompat.pyqtgraph
35 class EmptyMessageListView(QtGui.QListView):
36 '''List view that shows a message if the model im empty.'''
38 def __init__(self, message, parent=None):
39 super(self.__class__, self).__init__(parent)
41 self._message = message
43 def paintEvent(self, event):
45 if m and m.rowCount():
46 super(self.__class__, self).paintEvent(event)
49 painter = QtGui.QPainter(self.viewport())
50 painter.drawText(self.rect(), QtCore.Qt.AlignCenter, self._message)
52 class MainWindow(QtGui.QMainWindow):
53 '''The main window of the application.'''
55 # Number of seconds that the plots display.
58 # Update interval of the plots in milliseconds.
61 def __init__(self, context, drivers):
62 super(self.__class__, self).__init__()
64 # Used to coordinate the stopping of the acquisition and
65 # the closing of the window.
68 self.context = context
69 self.drivers = drivers
71 self.delegate = datamodel.MultimeterDelegate(self, self.font())
72 self.model = datamodel.MeasurementDataModel(self)
73 self.model.rowsInserted.connect(self.modelRowsInserted)
77 QtCore.QTimer.singleShot(0, self._start_acquisition)
79 def _start_acquisition(self):
80 self.acquisition = acquisition.Acquisition(self.context)
81 self.acquisition.measured.connect(self.model.update)
82 self.acquisition.stopped.connect(self._stopped)
85 for (ds, cs) in self.drivers:
86 self.acquisition.add_device(ds, cs)
87 except Exception as e:
88 QtGui.QMessageBox.critical(self, 'Error', str(e))
92 self.acquisition.start()
95 self.setWindowTitle('sigrok-meter')
96 # Resizing the listView below will increase this again.
99 p = os.path.abspath(os.path.dirname(__file__))
100 p = os.path.join(p, 'sigrok-logo-notext.png')
101 self.setWindowIcon(QtGui.QIcon(p))
103 actionQuit = QtGui.QAction(self)
104 actionQuit.setText('&Quit')
105 actionQuit.setIcon(QtGui.QIcon.fromTheme('application-exit'))
106 actionQuit.setShortcut('Ctrl+Q')
107 actionQuit.triggered.connect(self.close)
109 actionAbout = QtGui.QAction(self)
110 actionAbout.setText('&About')
111 actionAbout.setIcon(QtGui.QIcon.fromTheme('help-about'))
112 actionAbout.triggered.connect(self.show_about)
114 menubar = self.menuBar()
115 menuFile = menubar.addMenu('&File')
116 menuFile.addAction(actionQuit)
117 menuHelp = menubar.addMenu('&Help')
118 menuHelp.addAction(actionAbout)
120 self.listView = EmptyMessageListView('waiting for data...')
121 self.listView.setFrameShape(QtGui.QFrame.NoFrame)
122 self.listView.viewport().setBackgroundRole(QtGui.QPalette.Window)
123 self.listView.viewport().setAutoFillBackground(True)
124 self.listView.setMinimumWidth(260)
125 self.listView.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
126 self.listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
127 self.listView.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)
128 self.listView.setItemDelegate(self.delegate)
129 self.listView.setModel(self.model)
130 self.listView.setUniformItemSizes(True)
131 self.listView.setMinimumSize(self.delegate.sizeHint())
133 self.plotwidget = multiplotwidget.MultiPlotWidget(self)
134 self.plotwidget.plotHidden.connect(self._on_plotHidden)
136 # Maps from 'unit' to the corresponding plot.
138 # Maps from '(plot, device)' to the corresponding curve.
141 self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal);
142 self.splitter.addWidget(self.listView)
143 self.splitter.addWidget(self.plotwidget)
144 self.splitter.setStretchFactor(0, 0)
145 self.splitter.setStretchFactor(1, 1)
147 self.setCentralWidget(self.splitter)
148 self.centralWidget().setContentsMargins(0, 0, 0, 0)
149 self.resize(800, 500)
151 self.startTimer(MainWindow.UPDATEINTERVAL)
154 self.acquisition.stop()
155 print(self.acquisition.is_running())
157 def _getPlot(self, unit):
158 '''Looks up or creates a new plot for 'unit'.'''
160 if unit in self._plots:
161 return self._plots[unit]
163 # create a new plot for the unit
164 plot = self.plotwidget.addPlot()
165 plot.yaxis.setLabel(util.quantity_from_unit(unit), units=util.format_unit(unit))
166 plot.view.setXRange(-MainWindow.BACKLOG, 0, update=False)
167 plot.view.setYRange(-1, 1)
168 plot.view.enableAutoRange(axis=pyqtgraph.ViewBox.YAxis)
169 # lock to the range calculated by the view using additional padding,
170 # looks nicer this way
171 r = plot.view.viewRange()
172 plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
174 self._plots[unit] = plot
177 def _getCurve(self, plot, deviceID):
178 '''Looks up or creates a new curve for '(plot, deviceID)'.'''
180 key = (id(plot), deviceID)
181 if key in self._curves:
182 return self._curves[key]
185 curve = pyqtgraph.PlotDataItem(
187 symbolPen=pyqtgraph.mkPen(QtGui.QColor(QtCore.Qt.black)),
188 symbolBrush=pyqtgraph.mkBrush(QtGui.QColor(QtCore.Qt.black)),
191 plot.view.addItem(curve)
193 self._curves[key] = curve
196 def timerEvent(self, event):
197 '''Periodically updates all graphs.'''
201 def _updatePlots(self):
202 '''Updates all plots.'''
204 # loop over all devices and channels
205 for row in range(self.model.rowCount()):
206 idx = self.model.index(row, 0)
207 deviceID = self.model.data(idx,
208 datamodel.MeasurementDataModel.idRole)
209 deviceID = tuple(deviceID) # PySide returns a list.
210 traces = self.model.data(idx,
211 datamodel.MeasurementDataModel.tracesRole)
213 for unit, trace in traces.items():
217 l = now - MainWindow.BACKLOG
218 while trace.samples and trace.samples[0][0] < l:
221 plot = self._getPlot(unit)
224 self.plotwidget.showPlot(plot)
227 xdata = [s[0] - now for s in trace.samples]
228 ydata = [s[1] for s in trace.samples]
230 color = self.model.data(idx,
231 datamodel.MeasurementDataModel.colorRole)
233 curve = self._getCurve(plot, deviceID)
234 curve.setPen(pyqtgraph.mkPen(color=color))
235 curve.setData(xdata, ydata)
237 @QtCore.Slot(multiplotwidget.Plot)
238 def _on_plotHidden(self, plot):
239 plotunit = [u for u, p in self._plots.items() if p == plot][0]
241 # Mark all traces of all devices/channels with the same unit as the
242 # plot as "old" ('trace.new = False'). As soon as a new sample arrives
243 # on one trace, the plot will be shown again
244 for row in range(self.model.rowCount()):
245 idx = self.model.index(row, 0)
246 traces = self.model.data(idx, datamodel.MeasurementDataModel.tracesRole)
248 for traceunit, trace in traces.items():
249 if traceunit == plotunit:
257 def closeEvent(self, event):
258 if self.acquisition.is_running():
260 self.acquisition.stop()
266 def show_about(self):
267 text = textwrap.dedent('''\
269 <b>sigrok-meter 0.1.0</b><br/><br/>
270 Using libsigrok {} (lib version {}).<br/><br/>
271 <a href='http://www.sigrok.org'>
272 http://www.sigrok.org</a><br/>
274 License: GNU GPL, version 3 or later<br/>
276 This program comes with ABSOLUTELY NO WARRANTY;<br/>
278 <a href='http://www.gnu.org/licenses/gpl.html'>
279 http://www.gnu.org/licenses/gpl.html</a>
281 '''.format(self.context.package_version, self.context.lib_version))
283 QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
285 @QtCore.Slot(object, int, int)
286 def modelRowsInserted(self, parent, start, end):
287 '''Resize the list view to the size of the content.'''
288 rows = self.model.rowCount()
289 dh = self.delegate.sizeHint().height()
290 self.listView.setMinimumHeight(dh * rows)