## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
##
+import collections
import qtcompat
import sigrok.core as sr
+import time
+import util
QtCore = qtcompat.QtCore
QtGui = qtcompat.QtGui
'''Model to hold the measured values.'''
'''Role used to identify and find the item.'''
- _idRole = QtCore.Qt.UserRole + 1
+ idRole = QtCore.Qt.UserRole + 1
'''Role used to store the device vendor and model.'''
descRole = QtCore.Qt.UserRole + 2
+ '''Role used to store past samples.'''
+ samplesRole = QtCore.Qt.UserRole + 3
+
def __init__(self, parent):
super(self.__class__, self).__init__(parent)
# Use the description text to sort the items for now, because the
- # _idRole holds tuples, and using them to sort doesn't work.
+ # idRole holds tuples, and using them to sort doesn't work.
self.setSortRole(MeasurementDataModel.descRole)
# Used in 'format_value()' to check against.
self.inf = float('inf')
- def format_unit(self, u):
- units = {
- sr.Unit.VOLT: 'V',
- sr.Unit.AMPERE: 'A',
- sr.Unit.OHM: u'\u03A9',
- sr.Unit.FARAD: 'F',
- sr.Unit.KELVIN: 'K',
- sr.Unit.CELSIUS: u'\u00B0C',
- sr.Unit.FAHRENHEIT: u'\u00B0F',
- sr.Unit.HERTZ: 'Hz',
- sr.Unit.PERCENTAGE: '%',
- # sr.Unit.BOOLEAN
- sr.Unit.SECOND: 's',
- sr.Unit.SIEMENS: 'S',
- sr.Unit.DECIBEL_MW: 'dBu',
- sr.Unit.DECIBEL_VOLT: 'dBV',
- # sr.Unit.UNITLESS
- sr.Unit.DECIBEL_SPL: 'dB',
- # sr.Unit.CONCENTRATION
- sr.Unit.REVOLUTIONS_PER_MINUTE: 'rpm',
- sr.Unit.VOLT_AMPERE: 'VA',
- sr.Unit.WATT: 'W',
- sr.Unit.WATT_HOUR: 'Wh',
- sr.Unit.METER_SECOND: 'm/s',
- sr.Unit.HECTOPASCAL: 'hPa',
- sr.Unit.HUMIDITY_293K: '%rF',
- sr.Unit.DEGREE: u'\u00B0',
- sr.Unit.HENRY: 'H'
- }
-
- return units.get(u, '')
-
def format_mqflags(self, mqflags):
if sr.QuantityFlag.AC in mqflags:
return 'AC'
# Find the correct item in the model.
for row in range(self.rowCount()):
item = self.item(row)
- rid = item.data(MeasurementDataModel._idRole)
+ rid = item.data(MeasurementDataModel.idRole)
rid = tuple(rid) # PySide returns a list.
if uid == rid:
return item
device.vendor, device.model, channel.name)
item = QtGui.QStandardItem()
- item.setData(uid, MeasurementDataModel._idRole)
+ item.setData(uid, MeasurementDataModel.idRole)
item.setData(desc, MeasurementDataModel.descRole)
+ item.setData(collections.defaultdict(list), MeasurementDataModel.samplesRole)
self.appendRow(item)
self.sort(0)
return item
value, unit, mqflags = data
value_str = self.format_value(value)
- unit_str = self.format_unit(unit)
+ unit_str = util.format_unit(unit)
mqflags_str = self.format_mqflags(mqflags)
# The display role is a tuple containing the value and the unit/flags.
disp = (value_str, ' '.join([unit_str, mqflags_str]))
item.setData(disp, QtCore.Qt.DisplayRole)
+ # The samples role is a dictionary that contains the old samples for each unit.
+ # Should be trimmed periodically, otherwise it grows larger and larger.
+ sample = (time.time(), value)
+ d = item.data(MeasurementDataModel.samplesRole)
+ d[unit].append(sample)
+
class MultimeterDelegate(QtGui.QStyledItemDelegate):
'''Delegate to show the data items from a MeasurementDataModel.'''
##
import datamodel
+import multiplotwidget
import os.path
import qtcompat
import samplingthread
import textwrap
+import time
+import util
QtCore = qtcompat.QtCore
QtGui = qtcompat.QtGui
+pyqtgraph = qtcompat.pyqtgraph
class EmptyMessageListView(QtGui.QListView):
'''List view that shows a message if the model im empty.'''
class MainWindow(QtGui.QMainWindow):
'''The main window of the application.'''
+ # Number of seconds that the plots display.
+ BACKLOG = 10
+
+ # Update interval of the plots in milliseconds.
+ UPDATEINTERVAL = 100
+
def __init__(self, context, drivers):
super(self.__class__, self).__init__()
self.listView.setUniformItemSizes(True)
self.listView.setMinimumSize(self.delegate.sizeHint())
- self.setCentralWidget(self.listView)
+ self.plotwidget = multiplotwidget.MultiPlotWidget(self)
+
+ # Maps from 'unit' to the corresponding plot.
+ self._plots = {}
+ # Maps from '(plot, device)' to the corresponding curve.
+ self._curves = {}
+
+ self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal);
+ self.splitter.addWidget(self.listView)
+ self.splitter.addWidget(self.plotwidget)
+ self.splitter.setStretchFactor(0, 0)
+ self.splitter.setStretchFactor(1, 1)
+
+ self.setCentralWidget(self.splitter)
self.centralWidget().setContentsMargins(0, 0, 0, 0)
+ self.resize(800, 500)
+
+ self.startTimer(MainWindow.UPDATEINTERVAL)
+
+ def _getPlot(self, unit):
+ '''Looks up or creates a new plot for 'unit'.'''
+
+ if unit in self._plots:
+ return self._plots[unit]
+
+ # create a new plot for the unit
+ plot = self.plotwidget.addPlot()
+ plot.yaxis.setLabel(util.quantity_from_unit(unit), units=util.format_unit(unit))
+ plot.view.setXRange(-MainWindow.BACKLOG, 0, update=False)
+ plot.view.setYRange(-1, 1)
+ plot.view.enableAutoRange(axis=pyqtgraph.ViewBox.YAxis)
+ # lock to the range calculated by the view using additional padding,
+ # looks nicer this way
+ r = plot.view.viewRange()
+ plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
+
+ self._plots[unit] = plot
+ return plot
+
+ def _getCurve(self, plot, deviceID):
+ '''Looks up or creates a new curve for '(plot, deviceID)'.'''
+
+ key = (id(plot), deviceID)
+ if key in self._curves:
+ return self._curves[key]
+
+ # create a new curve
+ curve = pyqtgraph.PlotDataItem()
+ plot.view.addItem(curve)
+
+ self._curves[key] = curve
+ return curve
+
+ def timerEvent(self, event):
+ '''Periodically updates all graphs.'''
+
+ for row in range(self.model.rowCount()):
+ idx = self.model.index(row, 0)
+ deviceID = self.model.data(idx, datamodel.MeasurementDataModel.idRole)
+ sampledict = self.model.data(idx, datamodel.MeasurementDataModel.samplesRole)
+ for unit in sampledict:
+ self._updatePlot(deviceID, unit, sampledict[unit])
+
+ def _updatePlot(self, deviceID, unit, samples):
+ '''Updates the curve of device 'deviceID' and 'unit' with 'samples'.'''
+
+ plot = self._getPlot(unit)
+ curve = self._getCurve(plot, deviceID)
+
+ now = time.time()
+
+ # remove old samples
+ l = now - MainWindow.BACKLOG
+ while samples and samples[0][0] < l:
+ samples.pop(0)
+
+ xdata = [s[0] - now for s in samples]
+ ydata = [s[1] for s in samples]
+
+ curve.setData(xdata, ydata)
def closeEvent(self, event):
self.thread.stop()
--- /dev/null
+##
+## This file is part of the sigrok-meter project.
+##
+## Copyright (C) 2015 Jens Steinhauser <jens.steinhauser@gmail.com>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+##
+
+import qtcompat
+
+QtCore = qtcompat.QtCore
+QtGui = qtcompat.QtGui
+pyqtgraph = qtcompat.pyqtgraph
+
+# black foreground on white background
+pyqtgraph.setConfigOption('background', 'w')
+pyqtgraph.setConfigOption('foreground', 'k')
+
+class MultiPlotItem(pyqtgraph.GraphicsWidget):
+
+ class Plot:
+ def __init__(self, view, xaxis, yaxis):
+ self.view = view
+ self.xaxis = xaxis
+ self.yaxis = yaxis
+
+ def __init__(self, parent=None):
+ pyqtgraph.GraphicsWidget.__init__(self, parent)
+
+ self.layout = QtGui.QGraphicsGridLayout()
+ self.layout.setContentsMargins(10, 10, 10, 1)
+ self.layout.setHorizontalSpacing(0)
+ self.layout.setVerticalSpacing(0)
+ self.setLayout(self.layout)
+
+ self._plots = []
+
+ for i in range(2):
+ self.layout.setColumnPreferredWidth(i, 0)
+ self.layout.setColumnMinimumWidth(i, 0)
+ self.layout.setColumnSpacing(i, 0)
+
+ self.layout.setColumnStretchFactor(0, 0)
+ self.layout.setColumnStretchFactor(1, 100)
+
+ def addPlot(self):
+ row = self.layout.rowCount()
+
+ view = pyqtgraph.ViewBox(parent=self)
+ if self._plots:
+ view.setXLink(self._plots[-1].view)
+ self.layout.addItem(view, row, 1)
+
+ yaxis = pyqtgraph.AxisItem(parent=self, orientation='left')
+ yaxis.linkToView(view)
+ yaxis.setGrid(255)
+ self.layout.addItem(yaxis, row, 0, QtCore.Qt.AlignRight)
+
+ xaxis = pyqtgraph.AxisItem(parent=self, orientation='bottom')
+ xaxis.linkToView(view)
+ xaxis.setGrid(255)
+ self.layout.addItem(xaxis, row + 1, 1)
+
+ for i in range(row, row + 2):
+ self.layout.setRowPreferredHeight(i, 0)
+ self.layout.setRowMinimumHeight(i, 0)
+ self.layout.setRowSpacing(i, 0)
+
+ self.layout.setRowStretchFactor(row, 100)
+ self.layout.setRowStretchFactor(row + 1, 0)
+
+ p = MultiPlotItem.Plot(view, xaxis, yaxis)
+ self._plots.append(p)
+ return p
+
+class MultiPlotWidget(pyqtgraph.GraphicsView):
+ '''Widget that aligns multiple plots on top of each other.
+
+ (The built in classes fail at doing this correctly when the axis grow,
+ just try zooming in the "GraphicsLayout" or the "Linked View" examples.)'''
+
+ def __init__(self, parent=None):
+ pyqtgraph.GraphicsView.__init__(self, parent)
+
+ self.multiPlotItem = MultiPlotItem()
+ self.setCentralItem(self.multiPlotItem)
+
+ for m in [
+ 'addPlot'
+ ]:
+ setattr(self, m, getattr(self.multiPlotItem, m))
global QtGui
QtCore = _QtCore
QtGui = _QtGui
+
+ import pyqtgraph as _pyqtgraph
+ import pyqtgraph.dockarea as _pyqtgraph_dockarea
+ global pyqtgraph
+ pyqtgraph = _pyqtgraph
+ pyqtgraph.dockarea = _pyqtgraph_dockarea
--- /dev/null
+##
+## This file is part of the sigrok-meter project.
+##
+## Copyright (C) 2015 Jens Steinhauser <jens.steinhauser@gmail.com>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+##
+
+import sigrok.core as sr
+
+def format_unit(u):
+ units = {
+ sr.Unit.VOLT: 'V',
+ sr.Unit.AMPERE: 'A',
+ sr.Unit.OHM: u'\u03A9',
+ sr.Unit.FARAD: 'F',
+ sr.Unit.KELVIN: 'K',
+ sr.Unit.CELSIUS: u'\u00B0C',
+ sr.Unit.FAHRENHEIT: u'\u00B0F',
+ sr.Unit.HERTZ: 'Hz',
+ sr.Unit.PERCENTAGE: '%',
+ # sr.Unit.BOOLEAN
+ sr.Unit.SECOND: 's',
+ sr.Unit.SIEMENS: 'S',
+ sr.Unit.DECIBEL_MW: 'dBm',
+ sr.Unit.DECIBEL_VOLT: 'dBV',
+ # sr.Unit.UNITLESS
+ sr.Unit.DECIBEL_SPL: 'dB',
+ # sr.Unit.CONCENTRATION
+ sr.Unit.REVOLUTIONS_PER_MINUTE: 'rpm',
+ sr.Unit.VOLT_AMPERE: 'VA',
+ sr.Unit.WATT: 'W',
+ sr.Unit.WATT_HOUR: 'Wh',
+ sr.Unit.METER_SECOND: 'm/s',
+ sr.Unit.HECTOPASCAL: 'hPa',
+ sr.Unit.HUMIDITY_293K: '%rF',
+ sr.Unit.DEGREE: u'\u00B0',
+ sr.Unit.HENRY: 'H'
+ }
+
+ return units.get(u, '')
+
+def quantity_from_unit(u):
+ quantities = {
+ sr.Unit.VOLT: 'Voltage',
+ sr.Unit.AMPERE: 'Current',
+ sr.Unit.OHM: 'Resistance',
+ sr.Unit.FARAD: 'Capacity',
+ sr.Unit.KELVIN: 'Temperature',
+ sr.Unit.CELSIUS: 'Temperature',
+ sr.Unit.FAHRENHEIT: 'Temperature',
+ sr.Unit.HERTZ: 'Frequency',
+ sr.Unit.PERCENTAGE: 'Duty Cycle',
+ sr.Unit.BOOLEAN: 'Continuity',
+ sr.Unit.SECOND: 'Time',
+ sr.Unit.SIEMENS: 'Conductance',
+ sr.Unit.DECIBEL_MW: 'Power Ratio',
+ sr.Unit.DECIBEL_VOLT: 'Voltage Ratio',
+ sr.Unit.UNITLESS: 'Unitless Quantity',
+ sr.Unit.DECIBEL_SPL: 'Sound Pressure',
+ sr.Unit.CONCENTRATION: 'Concentration',
+ sr.Unit.REVOLUTIONS_PER_MINUTE: 'Revolutions',
+ sr.Unit.VOLT_AMPERE: 'Apparent Power',
+ sr.Unit.WATT: 'Power',
+ sr.Unit.WATT_HOUR: 'Energy',
+ sr.Unit.METER_SECOND: 'Velocity',
+ sr.Unit.HECTOPASCAL: 'Pressure',
+ sr.Unit.HUMIDITY_293K: 'Humidity',
+ sr.Unit.DEGREE: 'Angle',
+ sr.Unit.HENRY: 'Inductance'
+ }
+
+ return quantities.get(u, '')