From: Jens Steinhauser Date: Sat, 10 Oct 2015 14:45:25 +0000 (+0200) Subject: Remove the thread used for sampling. X-Git-Url: https://sigrok.org/gitaction?a=commitdiff_plain;h=2e8c2e6e68049604c25a5f035ff4c71513bce31b;p=sigrok-meter.git Remove the thread used for sampling. Changes in libsigrok allow for doing everything in one (the main) thread now. --- diff --git a/acquisition.py b/acquisition.py new file mode 100644 index 0000000..fdb1658 --- /dev/null +++ b/acquisition.py @@ -0,0 +1,144 @@ +## +## This file is part of the sigrok-meter project. +## +## Copyright (C) 2013 Uwe Hermann +## Copyright (C) 2014 Jens Steinhauser +## +## 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 +import re +import sigrok.core as sr +import time + +QtCore = qtcompat.QtCore + +class Acquisition(QtCore.QObject): + '''Class that handles the sigrok session and the reception of data.''' + + '''Signal emitted when new data arrived.''' + measured = QtCore.Signal(float, sr.classes.Device, sr.classes.Channel, tuple) + + '''Signal emitted when the session has stopped.''' + stopped = QtCore.Signal() + + def __init__(self, context): + super(self.__class__, self).__init__() + + self.context = context + self.session = self.context.create_session() + self.session.add_datafeed_callback(self._datafeed_callback) + self.session.set_stopped_callback(self._stopped_callback) + + def _parse_configstring(self, cs): + '''Dissect a config string and return the options as a dictionary.''' + + def parse_option(k, v): + '''Parse the value for a single option.''' + try: + ck = sr.ConfigKey.get_by_identifier(k) + except: + raise ValueError('No option named "{}".'.format(k)) + + try: + val = ck.parse_string(v) + except: + raise ValueError( + 'Invalid value "{}" for option "{}".'.format(v, k)) + + return (k, val) + + if not re.match('(([^:=]+=[^:=]+)(:[^:=]+=[^:=]+)*)?$', cs): + raise ValueError( + '"{}" is not a valid configuration string.'.format(cs)) + + if not cs: + return {} + + opts = cs.split(':') + opts = [tuple(kv.split('=')) for kv in opts] + opts = [parse_option(k, v) for (k, v) in opts] + return dict(opts) + + def _parse_driverstring(self, ds): + '''Dissect the driver string and return a tuple consisting of + the driver name and the options (as a dictionary).''' + + m = re.match('(?P[^:]+)(?P(:[^:=]+=[^:=]+)*)$', ds) + if not m: + raise ValueError('"{}" is not a valid driver string.'.format(ds)) + + opts = m.group('opts')[1:] + return (m.group('name'), self._parse_configstring(opts)) + + def add_device(self, driverstring, configstring): + '''Add a device to the session.''' + + # Process driver string. + (name, opts) = self._parse_driverstring(driverstring) + if not name in self.context.drivers: + raise RuntimeError('No driver named "{}".'.format(name)) + + driver = self.context.drivers[name] + devs = driver.scan(**opts) + if not devs: + raise RuntimeError('No devices found.') + + device = devs[0] + + # Process configuration string. + cfgs = self._parse_configstring(configstring) + for k, v in cfgs.items(): + device.config_set(sr.ConfigKey.get_by_identifier(k), v) + + self.session.add_device(device) + device.open() + + def is_running(self): + '''Return whether the session is running.''' + return self.session.is_running() + + @QtCore.Slot() + def start(self): + '''Start the session.''' + self.session.start() + + @QtCore.Slot() + def stop(self): + '''Stop the session.''' + if self.is_running(): + self.session.stop() + + def _datafeed_callback(self, device, packet): + now = time.time() + + if packet.type != sr.PacketType.ANALOG: + return + + if not len(packet.payload.channels): + return + + # TODO: find a device with multiple channels in one packet + channel = packet.payload.channels[0] + + # The most recent value. + value = packet.payload.data[0][-1] + + self.measured.emit(now, device, channel, + (value, packet.payload.unit, packet.payload.mq_flags)) + + def _stopped_callback(self, **kwargs): + self.stopped.emit() diff --git a/mainwindow.py b/mainwindow.py index 7398a5a..315d805 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -19,11 +19,11 @@ ## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA ## +import acquisition import datamodel import multiplotwidget import os.path import qtcompat -import samplingthread import textwrap import time import util @@ -61,7 +61,12 @@ class MainWindow(QtGui.QMainWindow): def __init__(self, context, drivers): super(self.__class__, self).__init__() + # Used to coordinate the stopping of the acquisition and + # the closing of the window. + self._closing = False + self.context = context + self.drivers = drivers self.delegate = datamodel.MultimeterDelegate(self, self.font()) self.model = datamodel.MeasurementDataModel(self) @@ -69,10 +74,22 @@ class MainWindow(QtGui.QMainWindow): self.setup_ui() - self.thread = samplingthread.SamplingThread(self.context, drivers) - self.thread.measured.connect(self.model.update) - self.thread.error.connect(self.error) - self.thread.start() + QtCore.QTimer.singleShot(0, self._start_acquisition) + + def _start_acquisition(self): + self.acquisition = acquisition.Acquisition(self.context) + self.acquisition.measured.connect(self.model.update) + self.acquisition.stopped.connect(self._stopped) + + try: + for (ds, cs) in self.drivers: + self.acquisition.add_device(ds, cs) + except Exception as e: + QtGui.QMessageBox.critical(self, 'Error', str(e)) + self.close() + return + + self.acquisition.start() def setup_ui(self): self.setWindowTitle('sigrok-meter') @@ -133,6 +150,10 @@ class MainWindow(QtGui.QMainWindow): self.startTimer(MainWindow.UPDATEINTERVAL) + def stop(self): + self.acquisition.stop() + print(self.acquisition.is_running()) + def _getPlot(self, unit): '''Looks up or creates a new plot for 'unit'.''' @@ -228,9 +249,18 @@ class MainWindow(QtGui.QMainWindow): if traceunit == plotunit: trace.new = False + @QtCore.Slot() + def _stopped(self): + if self._closing: + self.close() + def closeEvent(self, event): - self.thread.stop() - event.accept() + if self.acquisition.is_running(): + self._closing = True + self.acquisition.stop() + event.ignore() + else: + event.accept() @QtCore.Slot() def show_about(self): @@ -252,12 +282,6 @@ class MainWindow(QtGui.QMainWindow): QtGui.QMessageBox.about(self, 'About sigrok-meter', text) - @QtCore.Slot(str) - 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): '''Resize the list view to the size of the content.''' diff --git a/samplingthread.py b/samplingthread.py deleted file mode 100644 index fa95f42..0000000 --- a/samplingthread.py +++ /dev/null @@ -1,195 +0,0 @@ -## -## This file is part of the sigrok-meter project. -## -## Copyright (C) 2013 Uwe Hermann -## Copyright (C) 2014 Jens Steinhauser -## -## 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 -import re -import sigrok.core as sr -import time - -QtCore = qtcompat.QtCore -QtGui = qtcompat.QtGui - -class SamplingThread(QtCore.QObject): - '''Class that handles the reception of sigrok packets in the background.''' - - class Worker(QtCore.QObject): - '''Helper class that does the actual work in another thread.''' - - '''Signal emitted when new data arrived.''' - measured = QtCore.Signal(float, sr.classes.Device, sr.classes.Channel, tuple) - - '''Signal emmited in case of an error.''' - error = QtCore.Signal(str) - - def __init__(self, context, drivers): - super(self.__class__, self).__init__() - - self.context = context - self.drivers = drivers - - self.sampling = False - - def parse_configstring(self, cs): - '''Dissect a config string and return the options as a - dictionary.''' - - def parse_option(k, v): - '''Parse the value for a single option.''' - try: - ck = sr.ConfigKey.get_by_identifier(k) - except: - raise ValueError('No option named "{}".'.format(k)) - - try: - val = ck.parse_string(v) - except: - raise ValueError( - 'Invalid value "{}" for option "{}".'.format(v, k)) - - return (k, val) - - if not re.match('(([^:=]+=[^:=]+)(:[^:=]+=[^:=]+)*)?$', cs): - raise ValueError( - '"{}" is not a valid configuration string.'.format(cs)) - - if not cs: - return {} - - opts = cs.split(':') - opts = [tuple(kv.split('=')) for kv in opts] - opts = [parse_option(k, v) for (k, v) in opts] - return dict(opts) - - def parse_driverstring(self, ds): - '''Dissect the driver string and return a tuple consisting of - the driver name and the options (as a dictionary).''' - - m = re.match('(?P[^:]+)(?P(:[^:=]+=[^:=]+)*)$', ds) - if not m: - raise ValueError('"{}" is not a valid driver string.'.format(ds)) - - opts = m.group('opts')[1:] - return (m.group('name'), self.parse_configstring(opts)) - - @QtCore.Slot() - def start_sampling(self): - devices = [] - for (ds, cs) in self.drivers: - # Process driver string. - try: - (name, opts) = self.parse_driverstring(ds) - if not name in self.context.drivers: - raise RuntimeError('No driver named "{}".'.format(name)) - - driver = self.context.drivers[name] - devs = driver.scan(**opts) - if not devs: - raise RuntimeError('No devices found.') - - device = devs[0] - except Exception as e: - self.error.emit( - 'Error processing driver string:\n{}'.format(e)) - return - - # Process configuration string. - try: - cfgs = self.parse_configstring(cs) - for k, v in cfgs.items(): - device.config_set(sr.ConfigKey.get_by_identifier(k), v) - except Exception as e: - self.error.emit( - 'Error processing configuration string:\n{}'.format(e)) - return - - devices.append(device) - - self.session = self.context.create_session() - for dev in devices: - self.session.add_device(dev) - dev.open() - self.session.add_datafeed_callback(self.callback) - self.session.start() - self.sampling = True - self.session.run() - - # If sampling is 'True' here, it means that 'stop_sampling()' was - # not called, therefore 'session.run()' ended too early, indicating - # an error. - if self.sampling: - self.error.emit('An error occured during the acquisition.') - - def stop_sampling(self): - if self.sampling: - self.sampling = False - self.session.stop() - - def callback(self, device, packet): - now = time.time() - - 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 - - # TODO: find a device with multiple channels in one packet - channel = packet.payload.channels[0] - - # The most recent value. - value = packet.payload.data[0][-1] - - self.measured.emit(now, device, channel, - (value, packet.payload.unit, packet.payload.mq_flags)) - - # Signal used to start the worker across threads. - _start_signal = QtCore.Signal() - - def __init__(self, context, drivers): - super(self.__class__, self).__init__() - - self.worker = self.Worker(context, drivers) - self.thread = QtCore.QThread() - self.worker.moveToThread(self.thread) - - self._start_signal.connect(self.worker.start_sampling) - - # Expose the signals of the worker. - self.measured = self.worker.measured - self.error = self.worker.error - - self.thread.start() - - def start(self): - '''Start sampling.''' - self._start_signal.emit() - - def stop(self): - '''Stop sampling and stop the background thread.''' - self.worker.stop_sampling() - self.thread.quit() - self.thread.wait() diff --git a/test.py b/test.py index 2adb108..70b9f74 100755 --- a/test.py +++ b/test.py @@ -20,49 +20,51 @@ ## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA ## +import sigrok.core as sr import unittest if __name__ == '__main__': import qtcompat qtcompat.load_modules(False) - import samplingthread + import acquisition class TestDriverstringParsing(unittest.TestCase): def setUp(self): - self.w = samplingthread.SamplingThread.Worker(None, None) + self.context = sr.Context_create() + self.a = acquisition.Acquisition(self.context) def test_valid_driverstring(self): self.assertEqual( - self.w.parse_driverstring('demo'), + self.a._parse_driverstring('demo'), ('demo', {})) self.assertEqual( - self.w.parse_driverstring('demo:samplerate=1'), + self.a._parse_driverstring('demo:samplerate=1'), ('demo', {'samplerate': 1})) self.assertEqual( - self.w.parse_driverstring('demo:samplerate=1:analog_channels=1'), + self.a._parse_driverstring('demo:samplerate=1:analog_channels=1'), ('demo', {'samplerate': 1, 'analog_channels': 1})) def test_invalid_driverstring(self): self.assertRaisesRegexp(ValueError, 'is not a valid driver string', - self.w.parse_driverstring, '') + self.a._parse_driverstring, '') self.assertRaisesRegexp(ValueError, 'is not a valid driver string', - self.w.parse_driverstring, ':') + self.a._parse_driverstring, ':') self.assertRaisesRegexp(ValueError, 'is not a valid driver string', - self.w.parse_driverstring, ':a') + self.a._parse_driverstring, ':a') self.assertRaisesRegexp(ValueError, 'is not a valid driver string', - self.w.parse_driverstring, 'd:a') + self.a._parse_driverstring, 'd:a') self.assertRaisesRegexp(ValueError, 'is not a valid driver string', - self.w.parse_driverstring, 'd:a=') + self.a._parse_driverstring, 'd:a=') self.assertRaisesRegexp(ValueError, 'is not a valid driver string', - self.w.parse_driverstring, 'd:a=:') + self.a._parse_driverstring, 'd:a=:') self.assertRaisesRegexp(ValueError, 'is not a valid driver string', - self.w.parse_driverstring, 'd:a=b:') + self.a._parse_driverstring, 'd:a=b:') self.assertRaisesRegexp(ValueError, 'is not a valid driver string', - self.w.parse_driverstring, 'd:=b:') + self.a._parse_driverstring, 'd:=b:') self.assertRaisesRegexp(ValueError, 'is not a valid driver string', - self.w.parse_driverstring, 'd:=:') + self.a._parse_driverstring, 'd:=:') self.assertRaisesRegexp(ValueError, 'is not a valid driver string', - self.w.parse_driverstring, 'd:=') + self.a._parse_driverstring, 'd:=') if __name__ == '__main__': unittest.main()