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
)