]> sigrok.org Git - sigrok-meter.git/blobdiff - sigrok-meter
Don't pass the packet.payload between threads.
[sigrok-meter.git] / sigrok-meter
index e630218330c0b4a1de819887bdc21f1d053a6d38..6f83443a5368df92dc4b4b7a6d20b8d2546edcc3 100755 (executable)
@@ -29,7 +29,7 @@ import sigrok.core as sr
 import sys
 import textwrap
 
 import sys
 import textwrap
 
-default_drivers = [('demo', {'analog_channels': 1})]
+default_drivers = [('demo', {'analog_channels': 4})]
 default_loglevel = sr.LogLevel.WARN
 
 def parse_cli():
 default_loglevel = sr.LogLevel.WARN
 
 def parse_cli():
@@ -123,23 +123,20 @@ class SamplingThread(QtCore.QObject):
         '''Helper class that does the actual work in another thread.'''
 
         '''Signal emitted when new data arrived.'''
         '''Helper class that does the actual work in another thread.'''
 
         '''Signal emitted when new data arrived.'''
-        measured = QtCore.Signal(object, object)
+        measured = QtCore.Signal(object, object, object)
 
         '''Signal emmited in case of an error.'''
         error = QtCore.Signal(str)
 
 
         '''Signal emmited in case of an error.'''
         error = QtCore.Signal(str)
 
-        def __init__(self, drivers, loglevel):
+        def __init__(self, context, drivers):
             super(self.__class__, self).__init__()
 
             super(self.__class__, self).__init__()
 
-            self.sampling = False
+            self.context = context
             self.drivers = drivers
 
             self.drivers = drivers
 
-            self.context = sr.Context_create()
-            self.context.log_level = loglevel
-
-            self.sr_pkg_version = self.context.package_version
-            self.sr_lib_version = self.context.lib_version
+            self.sampling = False
 
 
+        @QtCore.Slot()
         def start_sampling(self):
             devices = []
             for name, options in self.drivers:
         def start_sampling(self):
             devices = []
             for name, options in self.drivers:
@@ -172,26 +169,34 @@ class SamplingThread(QtCore.QObject):
                 self.session.stop()
 
         def callback(self, device, packet):
                 self.session.stop()
 
         def callback(self, device, packet):
-            if packet.type == sr.PacketType.ANALOG:
-                dev = '{} {}'.format(device.vendor, device.model)
+            if not sr:
+                # In rare cases it can happen that the callback fires while
+                # the interpreter is shutting down. Then the sigrok module
+                # is already set to 'None'.
+                return
+
+            if packet.type != sr.PacketType.ANALOG:
+                return
+
+            if not len(packet.payload.channels):
+                return
 
 
-                # only send the most recent value
-                mag = packet.payload.data[0][-1]
+            # TODO: find a device with multiple channels in one packet
+            channel = packet.payload.channels[0]
 
 
-                self.measured.emit((dev, mag, packet.payload.unit,
-                        packet.payload.mq_flags))
+            # the most recent value
+            value = packet.payload.data[0][-1]
 
 
-            # wait a short time so that in any case we don't flood the GUI
-            # with new data (for example if the demo device is used)
-            self.thread().msleep(100)
+            self.measured.emit(device, channel,
+                    (value, packet.payload.unit, packet.payload.mq_flags))
 
     # signal used to start the worker across threads
     _start_signal = QtCore.Signal()
 
 
     # signal used to start the worker across threads
     _start_signal = QtCore.Signal()
 
-    def __init__(self, drivers, loglevel):
+    def __init__(self, context, drivers):
         super(self.__class__, self).__init__()
 
         super(self.__class__, self).__init__()
 
-        self.worker = self.Worker(drivers, loglevel)
+        self.worker = self.Worker(context, drivers)
         self.thread = QtCore.QThread()
         self.worker.moveToThread(self.thread)
 
         self.thread = QtCore.QThread()
         self.worker.moveToThread(self.thread)
 
@@ -211,36 +216,207 @@ class SamplingThread(QtCore.QObject):
         '''Stops sampling and the background thread.'''
         self.worker.stop_sampling()
         self.thread.quit()
         '''Stops sampling and the background thread.'''
         self.worker.stop_sampling()
         self.thread.quit()
-        # the timeout is needed when the demo device is used, because it
-        # produces so much outstanding data that quitting takes a long time
-        self.thread.wait(500)
+        self.thread.wait()
+
+class MeasurementDataModel(QtGui.QStandardItemModel):
+    '''Model to hold the measured values.'''
+
+    '''Role used to identify and find the item.'''
+    _idRole = QtCore.Qt.UserRole + 1
+
+    '''Role used to store the device vendor and model.'''
+    descRole = QtCore.Qt.UserRole + 2
+
+    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.
+        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'
+        elif sr.QuantityFlag.DC in mqflags:
+            return 'DC'
+        else:
+            return ''
+
+    def format_value(self, mag):
+        if mag == self.inf:
+            return u'\u221E'
+        return '{:f}'.format(mag)
+
+    def getItem(self, device, channel):
+        '''Returns the item for the device + channel combination from the model,
+        or creates a new item if no existing one matches.'''
+
+        # unique identifier for the device + channel
+        # TODO: isn't there something better?
+        uid = (
+            device.vendor,
+            device.model,
+            device.serial_number(),
+            device.connection_id(),
+            channel.index
+        )
+
+        # find the correct item in the model
+        for row in range(self.rowCount()):
+            item = self.item(row)
+            rid = item.data(MeasurementDataModel._idRole)
+            rid = tuple(rid) # PySide returns a list
+            if uid == rid:
+                return item
+
+        # nothing found, create a new item
+        desc = '{} {}, channel "{}"'.format(
+                device.vendor, device.model, channel.name)
+
+        item = QtGui.QStandardItem()
+        item.setData(uid, MeasurementDataModel._idRole)
+        item.setData(desc, MeasurementDataModel.descRole)
+        self.appendRow(item)
+        self.sort(0)
+        return item
+
+    @QtCore.Slot(object, object, object)
+    def update(self, device, channel, data):
+        '''Updates the data for the device (+channel) with the most recent
+        measurement from the given payload.'''
+
+        item = self.getItem(device, channel)
+
+        value, unit, mqflags = data
+        value_str = self.format_value(value)
+        unit_str = self.format_unit(unit)
+        mqflags_str = self.format_mqflags(mqflags)
+
+        disp = ' '.join([value_str, unit_str, mqflags_str])
+        item.setData(disp, QtCore.Qt.DisplayRole)
+
+class MultimeterDelegate(QtGui.QStyledItemDelegate):
+    '''Delegate to show the data items from a MeasurementDataModel.'''
+
+    def __init__(self, parent, font):
+        '''Initializes the delegate.
+
+        :param font: Font used for the description text, the value is drawn
+                     with a slightly bigger and bold variant of the font.
+        '''
+
+        super(self.__class__, self).__init__(parent)
+
+        self._nfont = font
+        self._bfont = QtGui.QFont(self._nfont)
+
+        self._bfont.setBold(True)
+        if self._bfont.pixelSize() != -1:
+            self._bfont.setPixelSize(self._bfont.pixelSize() * 1.8)
+        else:
+            self._bfont.setPointSizeF(self._bfont.pointSizeF() * 1.8)
+
+        fi = QtGui.QFontInfo(self._nfont)
+        self._nfontheight = fi.pixelSize()
 
 
-    def sr_pkg_version(self):
-        '''Returns the version number of the libsigrok package.'''
-        return self.worker.sr_pkg_version
+        fm = QtGui.QFontMetrics(self._bfont)
+        r = fm.boundingRect('-XX.XXXXXX X XX')
+        self._size = QtCore.QSize(r.width() * 1.2, r.height() * 3.5)
 
 
-    def sr_lib_version(self):
-        '''Returns the version number fo the libsigrok library.'''
-        return self.worker.sr_lib_version
+    def sizeHint(self, option=None, index=None):
+        return self._size
+
+    def paint(self, painter, options, index):
+        value = index.data(QtCore.Qt.DisplayRole)
+        desc = index.data(MeasurementDataModel.descRole)
+
+        # description in the top left corner
+        painter.setFont(self._nfont)
+        p = options.rect.topLeft()
+        p += QtCore.QPoint(self._nfontheight, 2 * self._nfontheight)
+        painter.drawText(p, desc)
+
+        # value in the center
+        painter.setFont(self._bfont)
+        r = options.rect.adjusted(self._nfontheight, 2.5 * self._nfontheight,
+                0, 0)
+        painter.drawText(r, QtCore.Qt.AlignCenter, value)
+
+class EmptyMessageListView(QtGui.QListView):
+    '''List view that shows a message if the model im empty.'''
+
+    def __init__(self, message, parent=None):
+        super(self.__class__, self).__init__(parent)
+
+        self._message = message
+
+    def paintEvent(self, event):
+        m = self.model()
+        if m and m.rowCount():
+            super(self.__class__, self).paintEvent(event)
+            return
+
+        painter = QtGui.QPainter(self.viewport())
+        painter.drawText(self.rect(), QtCore.Qt.AlignCenter, self._message)
 
 class SigrokMeter(QtGui.QMainWindow):
     '''The main window of the application.'''
 
 
 class SigrokMeter(QtGui.QMainWindow):
     '''The main window of the application.'''
 
-    def __init__(self, thread):
+    def __init__(self, context, drivers):
         super(SigrokMeter, self).__init__()
         super(SigrokMeter, self).__init__()
-        self.setup_ui()
 
 
-        self.inf = float('inf')
+        self.context = context
+
+        self.delegate = MultimeterDelegate(self, self.font())
+        self.model = MeasurementDataModel(self)
+        self.model.rowsInserted.connect(self.modelRowsInserted)
+
+        self.setup_ui()
 
 
-        self.thread = thread
-        self.thread.measured.connect(self.update, QtCore.Qt.QueuedConnection)
+        self.thread = SamplingThread(self.context, drivers)
+        self.thread.measured.connect(self.model.update)
         self.thread.error.connect(self.error)
         self.thread.start()
 
     def setup_ui(self):
         self.setWindowTitle('sigrok-meter')
         self.thread.error.connect(self.error)
         self.thread.start()
 
     def setup_ui(self):
         self.setWindowTitle('sigrok-meter')
-        self.setMinimumHeight(130)
-        self.setMinimumWidth(260)
+        # resizing the listView below will increase this again
+        self.resize(10, 10)
 
         p = os.path.abspath(os.path.dirname(__file__))
         p = os.path.join(p, 'sigrok-logo-notext.png')
 
         p = os.path.abspath(os.path.dirname(__file__))
         p = os.path.join(p, 'sigrok-logo-notext.png')
@@ -263,24 +439,27 @@ class SigrokMeter(QtGui.QMainWindow):
         menuHelp = menubar.addMenu('&Help')
         menuHelp.addAction(actionAbout)
 
         menuHelp = menubar.addMenu('&Help')
         menuHelp.addAction(actionAbout)
 
-        self.lblValue = QtGui.QLabel('waiting for data...')
-        self.lblValue.setAlignment(QtCore.Qt.AlignCenter)
-        font = self.lblValue.font()
-        font.setPointSize(font.pointSize() * 1.7)
-        font.setBold(True)
-        self.lblValue.setFont(font)
-        self.setCentralWidget(self.lblValue)
+        self.listView = EmptyMessageListView('waiting for data...')
+        self.listView.setFrameShape(QtGui.QFrame.NoFrame)
+        self.listView.viewport().setBackgroundRole(QtGui.QPalette.Window)
+        self.listView.viewport().setAutoFillBackground(True)
+        self.listView.setMinimumWidth(260)
+        self.listView.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
+        self.listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
+        self.listView.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)
+        self.listView.setItemDelegate(self.delegate)
+        self.listView.setModel(self.model)
+        self.listView.setUniformItemSizes(True)
+        self.listView.setMinimumSize(self.delegate.sizeHint())
+
+        self.setCentralWidget(self.listView)
         self.centralWidget().setContentsMargins(0, 0, 0, 0)
 
         self.centralWidget().setContentsMargins(0, 0, 0, 0)
 
-        self.lblDevName = QtGui.QLabel()
-        self.lblDevName.setToolTip('Name of used measurement device.')
-        self.statusBar().insertWidget(0, self.lblDevName, 10)
-        self.lblTime = QtGui.QLabel()
-        self.lblTime.setToolTip('Time of the last measurement.')
-        self.statusBar().insertWidget(1, self.lblTime)
-
-        self.statusBar().setSizeGripEnabled(False)
+    def closeEvent(self, event):
+        self.thread.stop()
+        event.accept()
 
 
+    @QtCore.Slot()
     def show_about(self):
         text = textwrap.dedent('''\
             <div align="center">
     def show_about(self):
         text = textwrap.dedent('''\
             <div align="center">
@@ -295,87 +474,30 @@ class SigrokMeter(QtGui.QMainWindow):
                 <a href='http://www.gnu.org/licenses/gpl.html'>
                          http://www.gnu.org/licenses/gpl.html</a>
             </div>
                 <a href='http://www.gnu.org/licenses/gpl.html'>
                          http://www.gnu.org/licenses/gpl.html</a>
             </div>
-        '''.format(self.thread.sr_pkg_version(), self.thread.sr_lib_version()))
+        '''.format(self.context.package_version, self.context.lib_version))
 
         QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
 
 
         QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
 
-    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:
-            s = 'AC'
-        elif sr.QuantityFlag.DC in mqflags:
-            s = 'DC'
-        else:
-            s = ''
-
-        return s
-
-    def format_mag(self, mag):
-        if mag == self.inf:
-            return u'\u221E'
-        return '{:f}'.format(mag)
-
-    def update(self, data):
-        '''Updates the labels with new measurement values.'''
-
-        device, mag, unit, mqflags = data
-
-        unit_str = self.format_unit(unit)
-        mqflags_str = self.format_mqflags(mqflags)
-        mag_str = self.format_mag(mag)
-        value = ' '.join([mag_str, unit_str, mqflags_str])
-
-        n = datetime.datetime.now().time()
-        now = '{:02}:{:02}:{:02}.{:03}'.format(
-                n.hour, n.minute, n.second, n.microsecond / 1000)
-
-        self.lblValue.setText(value)
-        self.lblDevName.setText(device)
-        self.lblTime.setText(now)
-
+    @QtCore.Slot(str)
     def error(self, msg):
         '''Error handler for the sampling thread.'''
         QtGui.QMessageBox.critical(self, 'Error', msg)
         self.close()
 
     def error(self, msg):
         '''Error handler for the sampling thread.'''
         QtGui.QMessageBox.critical(self, 'Error', msg)
         self.close()
 
+    @QtCore.Slot(object, int, int)
+    def modelRowsInserted(self, parent, start, end):
+        '''Resizes the list view to the size of the content.'''
+
+        rows = self.model.rowCount()
+        dh = self.delegate.sizeHint().height()
+        self.listView.setMinimumHeight(dh * rows)
+
 if __name__ == '__main__':
 if __name__ == '__main__':
-    thread = SamplingThread(args['drivers'], args['loglevel'])
+    context = sr.Context_create()
+    context.log_level = args['loglevel']
 
     app = QtGui.QApplication([])
 
     app = QtGui.QApplication([])
-    s = SigrokMeter(thread)
+    s = SigrokMeter(context, args['drivers'])
     s.show()
 
     s.show()
 
-    r = app.exec_()
-    thread.stop()
-    sys.exit(r)
+    sys.exit(app.exec_())