]> sigrok.org Git - sigrok-meter.git/blob - samplingthread.py
README: Add PyQtGraph dependency.
[sigrok-meter.git] / samplingthread.py
1 ##
2 ## This file is part of the sigrok-meter project.
3 ##
4 ## Copyright (C) 2013 Uwe Hermann <uwe@hermann-uwe.de>
5 ## Copyright (C) 2014 Jens Steinhauser <jens.steinhauser@gmail.com>
6 ##
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.
11 ##
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.
16 ##
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
20 ##
21
22 import qtcompat
23 import re
24 import sigrok.core as sr
25 import time
26
27 QtCore = qtcompat.QtCore
28 QtGui = qtcompat.QtGui
29
30 class SamplingThread(QtCore.QObject):
31     '''Class that handles the reception of sigrok packets in the background.'''
32
33     class Worker(QtCore.QObject):
34         '''Helper class that does the actual work in another thread.'''
35
36         '''Signal emitted when new data arrived.'''
37         measured = QtCore.Signal(float, sr.classes.Device, sr.classes.Channel, tuple)
38
39         '''Signal emmited in case of an error.'''
40         error = QtCore.Signal(str)
41
42         def __init__(self, context, drivers):
43             super(self.__class__, self).__init__()
44
45             self.context = context
46             self.drivers = drivers
47
48             self.sampling = False
49
50         def parse_configstring(self, cs):
51             '''Dissect a config string and return the options as a
52             dictionary.'''
53
54             def parse_option(k, v):
55                 '''Parse the value for a single option.'''
56                 try:
57                     ck = sr.ConfigKey.get_by_identifier(k)
58                 except:
59                     raise ValueError('No option named "{}".'.format(k))
60
61                 try:
62                     val = ck.parse_string(v)
63                 except:
64                     raise ValueError(
65                         'Invalid value "{}" for option "{}".'.format(v, k))
66
67                 return (k, val)
68
69             if not re.match('(([^:=]+=[^:=]+)(:[^:=]+=[^:=]+)*)?$', cs):
70                 raise ValueError(
71                     '"{}" is not a valid configuration string.'.format(cs))
72
73             if not cs:
74                 return {}
75
76             opts = cs.split(':')
77             opts = [tuple(kv.split('=')) for kv in opts]
78             opts = [parse_option(k, v) for (k, v) in opts]
79             return dict(opts)
80
81         def parse_driverstring(self, ds):
82             '''Dissect the driver string and return a tuple consisting of
83             the driver name and the options (as a dictionary).'''
84
85             m = re.match('(?P<name>[^:]+)(?P<opts>(:[^:=]+=[^:=]+)*)$', ds)
86             if not m:
87                 raise ValueError('"{}" is not a valid driver string.'.format(ds))
88
89             opts = m.group('opts')[1:]
90             return (m.group('name'), self.parse_configstring(opts))
91
92         @QtCore.Slot()
93         def start_sampling(self):
94             devices = []
95             for (ds, cs) in self.drivers:
96                 # Process driver string.
97                 try:
98                     (name, opts) = self.parse_driverstring(ds)
99                     if not name in self.context.drivers:
100                         raise RuntimeError('No driver named "{}".'.format(name))
101
102                     driver = self.context.drivers[name]
103                     devs = driver.scan(**opts)
104                     if not devs:
105                         raise RuntimeError('No devices found.')
106
107                     device = devs[0]
108                 except Exception as e:
109                     self.error.emit(
110                         'Error processing driver string:\n{}'.format(e))
111                     return
112
113                 # Process configuration string.
114                 try:
115                     cfgs = self.parse_configstring(cs)
116                     for k, v in cfgs.items():
117                         device.config_set(sr.ConfigKey.get_by_identifier(k), v)
118                 except Exception as e:
119                     self.error.emit(
120                         'Error processing configuration string:\n{}'.format(e))
121                     return
122
123                 devices.append(device)
124
125             self.session = self.context.create_session()
126             for dev in devices:
127                 self.session.add_device(dev)
128                 dev.open()
129             self.session.add_datafeed_callback(self.callback)
130             self.session.start()
131             self.sampling = True
132             self.session.run()
133
134             # If sampling is 'True' here, it means that 'stop_sampling()' was
135             # not called, therefore 'session.run()' ended too early, indicating
136             # an error.
137             if self.sampling:
138                 self.error.emit('An error occured during the acquisition.')
139
140         def stop_sampling(self):
141             if self.sampling:
142                 self.sampling = False
143                 self.session.stop()
144
145         def callback(self, device, packet):
146             now = time.time()
147
148             if not sr:
149                 # In rare cases it can happen that the callback fires while
150                 # the interpreter is shutting down. Then the sigrok module
151                 # is already set to 'None'.
152                 return
153
154             if packet.type != sr.PacketType.ANALOG:
155                 return
156
157             if not len(packet.payload.channels):
158                 return
159
160             # TODO: find a device with multiple channels in one packet
161             channel = packet.payload.channels[0]
162
163             # The most recent value.
164             value = packet.payload.data[0][-1]
165
166             self.measured.emit(now, device, channel,
167                     (value, packet.payload.unit, packet.payload.mq_flags))
168
169     # Signal used to start the worker across threads.
170     _start_signal = QtCore.Signal()
171
172     def __init__(self, context, drivers):
173         super(self.__class__, self).__init__()
174
175         self.worker = self.Worker(context, drivers)
176         self.thread = QtCore.QThread()
177         self.worker.moveToThread(self.thread)
178
179         self._start_signal.connect(self.worker.start_sampling)
180
181         # Expose the signals of the worker.
182         self.measured = self.worker.measured
183         self.error = self.worker.error
184
185         self.thread.start()
186
187     def start(self):
188         '''Start sampling.'''
189         self._start_signal.emit()
190
191     def stop(self):
192         '''Stop sampling and stop the background thread.'''
193         self.worker.stop_sampling()
194         self.thread.quit()
195         self.thread.wait()