]> sigrok.org Git - sigrok-meter.git/blame_incremental - samplingthread.py
README: Bump libsigrok requirement to 0.4.0.
[sigrok-meter.git] / samplingthread.py
... / ...
CommitLineData
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
22import qtcompat
23import re
24import sigrok.core as sr
25import time
26
27QtCore = qtcompat.QtCore
28QtGui = qtcompat.QtGui
29
30class 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()