4 ## This file is part of the sigrok-meter project.
6 ## Copyright (C) 2013 Uwe Hermann <uwe@hermann-uwe.de>
7 ## Copyright (C) 2014 Jens Steinhauser <jens.steinhauser@gmail.com>
9 ## This program is free software; you can redistribute it and/or modify
10 ## it under the terms of the GNU General Public License as published by
11 ## the Free Software Foundation; either version 2 of the License, or
12 ## (at your option) any later version.
14 ## This program is distributed in the hope that it will be useful,
15 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 ## GNU General Public License for more details.
19 ## You should have received a copy of the GNU General Public License
20 ## along with this program; if not, write to the Free Software
21 ## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
28 import sigrok.core as sr
32 default_drivers = [('demo', {'analog_channels': 1})]
33 default_loglevel = sr.LogLevel.WARN
36 parser = argparse.ArgumentParser(
37 description='Simple sigrok GUI for multimeters and dataloggers.',
38 epilog=textwrap.dedent('''\
39 The DRIVER string is the same as for sigrok-cli(1).
43 %(prog)s --driver tecpel-dmm-8061-ser:conn=/dev/ttyUSB0
45 %(prog)s --driver uni-t-ut61e:conn=1a86.e008
47 formatter_class=argparse.RawDescriptionHelpFormatter)
49 parser.add_argument('-d', '--driver',
51 help='The driver to use')
52 parser.add_argument('-l', '--loglevel',
54 help='Set loglevel (5 is most verbose)')
55 parser.add_argument('--pyside',
58 help='Force use of PySide (default is to use PyQt4)')
59 args = parser.parse_args()
62 'drivers': default_drivers,
63 'loglevel': default_loglevel,
68 result['drivers'] = []
70 m = re.match('(?P<name>[^:]+)(?P<opts>(:[^:=]+=[^:=]+)*)', d)
72 sys.exit('error parsing option "{}"'.format(d))
74 opts = m.group('opts').split(':')[1:]
75 opts = [tuple(kv.split('=')) for kv in opts]
78 result['drivers'].append((m.group('name'), opts))
80 if args.loglevel != None:
82 result['loglevel'] = sr.LogLevel.get(args.loglevel)
84 sys.exit('error: invalid log level')
88 if __name__ == '__main__':
89 # The command line parsing and import of the Qt modules is done here,
90 # so that the modules are imported before the classes below derive
91 # from classes therein. The rest of the main function is at the bottom.
96 from PySide import QtCore, QtGui
99 # Use version 2 API in all cases, because that's what PySide uses.
101 sip.setapi('QVariant', 2)
102 sip.setapi('QDate', 2)
103 sip.setapi('QDateTime', 2)
104 sip.setapi('QString', 2)
105 sip.setapi('QTextStream', 2)
106 sip.setapi('QTime', 2)
107 sip.setapi('QUrl', 2)
108 sip.setapi('QVariant', 2)
110 from PyQt4 import QtCore, QtGui
112 # Add PySide compatible names.
113 QtCore.Signal = QtCore.pyqtSignal
114 QtCore.Slot = QtCore.pyqtSlot
116 sys.stderr.write('import of PyQt4 failed, using PySide\n')
117 from PySide import QtCore, QtGui
119 class SamplingThread(QtCore.QObject):
120 '''A class that handles the reception of sigrok packets in the background.'''
122 class Worker(QtCore.QObject):
123 '''Helper class that does the actual work in another thread.'''
125 '''Signal emitted when new data arrived.'''
126 measured = QtCore.Signal(object, object)
128 '''Signal emmited in case of an error.'''
129 error = QtCore.Signal(str)
131 def __init__(self, drivers, loglevel):
132 super(self.__class__, self).__init__()
134 self.sampling = False
135 self.drivers = drivers
137 self.context = sr.Context_create()
138 self.context.log_level = loglevel
140 self.sr_pkg_version = self.context.package_version
141 self.sr_lib_version = self.context.lib_version
143 def start_sampling(self):
145 for name, options in self.drivers:
147 dr = self.context.drivers[name]
148 devices.append(dr.scan(**options)[0])
151 'Unable to get device for driver "{}".'.format(name))
154 self.session = self.context.create_session()
156 self.session.add_device(dev)
158 self.session.add_datafeed_callback(self.callback)
163 # If sampling is 'True' here, it means that 'stop_sampling()' was
164 # not called, therefore 'session.run()' ended too early, indicating
167 self.error.emit('An error occured during the acquisition.')
169 def stop_sampling(self):
171 self.sampling = False
174 def callback(self, device, packet):
175 if packet.type == sr.PacketType.ANALOG:
176 dev = '{} {}'.format(device.vendor, device.model)
178 # only send the most recent value
179 mag = packet.payload.data[0][-1]
181 self.measured.emit((dev, mag, packet.payload.unit,
182 packet.payload.mq_flags))
184 # wait a short time so that in any case we don't flood the GUI
185 # with new data (for example if the demo device is used)
186 self.thread().msleep(100)
188 # signal used to start the worker across threads
189 _start_signal = QtCore.Signal()
191 def __init__(self, drivers, loglevel):
192 super(self.__class__, self).__init__()
194 self.worker = self.Worker(drivers, loglevel)
195 self.thread = QtCore.QThread()
196 self.worker.moveToThread(self.thread)
198 self._start_signal.connect(self.worker.start_sampling)
200 # expose the signals of the worker
201 self.measured = self.worker.measured
202 self.error = self.worker.error
207 '''Starts sampling'''
208 self._start_signal.emit()
211 '''Stops sampling and the background thread.'''
212 self.worker.stop_sampling()
214 # the timeout is needed when the demo device is used, because it
215 # produces so much outstanding data that quitting takes a long time
216 self.thread.wait(500)
218 def sr_pkg_version(self):
219 '''Returns the version number of the libsigrok package.'''
220 return self.worker.sr_pkg_version
222 def sr_lib_version(self):
223 '''Returns the version number fo the libsigrok library.'''
224 return self.worker.sr_lib_version
226 class SigrokMeter(QtGui.QMainWindow):
227 '''The main window of the application.'''
229 def __init__(self, thread):
230 super(SigrokMeter, self).__init__()
233 self.inf = float('inf')
236 self.thread.measured.connect(self.update, QtCore.Qt.QueuedConnection)
237 self.thread.error.connect(self.error)
241 self.setWindowTitle('sigrok-meter')
242 self.setMinimumHeight(130)
243 self.setMinimumWidth(260)
245 p = os.path.abspath(os.path.dirname(__file__))
246 p = os.path.join(p, 'sigrok-logo-notext.png')
247 self.setWindowIcon(QtGui.QIcon(p))
249 actionQuit = QtGui.QAction(self)
250 actionQuit.setText('&Quit')
251 actionQuit.setIcon(QtGui.QIcon.fromTheme('application-exit'))
252 actionQuit.setShortcut('Ctrl+Q')
253 actionQuit.triggered.connect(self.close)
255 actionAbout = QtGui.QAction(self)
256 actionAbout.setText('&About')
257 actionAbout.setIcon(QtGui.QIcon.fromTheme('help-about'))
258 actionAbout.triggered.connect(self.show_about)
260 menubar = self.menuBar()
261 menuFile = menubar.addMenu('&File')
262 menuFile.addAction(actionQuit)
263 menuHelp = menubar.addMenu('&Help')
264 menuHelp.addAction(actionAbout)
266 self.lblValue = QtGui.QLabel('waiting for data...')
267 self.lblValue.setAlignment(QtCore.Qt.AlignCenter)
268 font = self.lblValue.font()
269 font.setPointSize(font.pointSize() * 1.7)
271 self.lblValue.setFont(font)
272 self.setCentralWidget(self.lblValue)
273 self.centralWidget().setContentsMargins(0, 0, 0, 0)
275 self.lblDevName = QtGui.QLabel()
276 self.lblDevName.setToolTip('Name of used measurement device.')
277 self.statusBar().insertWidget(0, self.lblDevName, 10)
278 self.lblTime = QtGui.QLabel()
279 self.lblTime.setToolTip('Time of the last measurement.')
280 self.statusBar().insertWidget(1, self.lblTime)
282 self.statusBar().setSizeGripEnabled(False)
284 def show_about(self):
285 text = textwrap.dedent('''\
287 <b>sigrok-meter</b><br/>
289 Using libsigrok {} (lib version {}).<br/>
290 <a href='http://www.sigrok.org'>
291 http://www.sigrok.org</a><br/>
293 This program comes with ABSOLUTELY NO WARRANTY;<br/>
295 <a href='http://www.gnu.org/licenses/gpl.html'>
296 http://www.gnu.org/licenses/gpl.html</a>
298 '''.format(self.thread.sr_pkg_version(), self.thread.sr_lib_version()))
300 QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
302 def format_unit(self, u):
306 sr.Unit.OHM: u'\u03A9',
309 sr.Unit.CELSIUS: u'\u00B0C',
310 sr.Unit.FAHRENHEIT: u'\u00B0F',
312 sr.Unit.PERCENTAGE: '%',
315 sr.Unit.SIEMENS: 'S',
316 sr.Unit.DECIBEL_MW: 'dBu',
317 sr.Unit.DECIBEL_VOLT: 'dBV',
319 sr.Unit.DECIBEL_SPL: 'dB',
320 # sr.Unit.CONCENTRATION
321 sr.Unit.REVOLUTIONS_PER_MINUTE: 'rpm',
322 sr.Unit.VOLT_AMPERE: 'VA',
324 sr.Unit.WATT_HOUR: 'Wh',
325 sr.Unit.METER_SECOND: 'm/s',
326 sr.Unit.HECTOPASCAL: 'hPa',
327 sr.Unit.HUMIDITY_293K: '%rF',
328 sr.Unit.DEGREE: u'\u00B0',
332 return units.get(u, '')
334 def format_mqflags(self, mqflags):
335 if sr.QuantityFlag.AC in mqflags:
337 elif sr.QuantityFlag.DC in mqflags:
344 def format_mag(self, mag):
347 return '{:f}'.format(mag)
349 def update(self, data):
350 '''Updates the labels with new measurement values.'''
352 device, mag, unit, mqflags = data
354 unit_str = self.format_unit(unit)
355 mqflags_str = self.format_mqflags(mqflags)
356 mag_str = self.format_mag(mag)
357 value = ' '.join([mag_str, unit_str, mqflags_str])
359 n = datetime.datetime.now().time()
360 now = '{:02}:{:02}:{:02}.{:03}'.format(
361 n.hour, n.minute, n.second, n.microsecond / 1000)
363 self.lblValue.setText(value)
364 self.lblDevName.setText(device)
365 self.lblTime.setText(now)
367 def error(self, msg):
368 '''Error handler for the sampling thread.'''
369 QtGui.QMessageBox.critical(self, 'Error', msg)
372 if __name__ == '__main__':
373 thread = SamplingThread(args['drivers'], args['loglevel'])
375 app = QtGui.QApplication([])
376 s = SigrokMeter(thread)