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