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
144 def start_sampling(self):
146 for name, options in self.drivers:
148 dr = self.context.drivers[name]
149 devices.append(dr.scan(**options)[0])
152 'Unable to get device for driver "{}".'.format(name))
155 self.session = self.context.create_session()
157 self.session.add_device(dev)
159 self.session.add_datafeed_callback(self.callback)
164 # If sampling is 'True' here, it means that 'stop_sampling()' was
165 # not called, therefore 'session.run()' ended too early, indicating
168 self.error.emit('An error occured during the acquisition.')
170 def stop_sampling(self):
172 self.sampling = False
175 def callback(self, device, packet):
176 if packet.type == sr.PacketType.ANALOG:
177 dev = '{} {}'.format(device.vendor, device.model)
179 # only send the most recent value
180 mag = packet.payload.data[0][-1]
182 self.measured.emit((dev, mag, packet.payload.unit,
183 packet.payload.mq_flags))
185 # wait a short time so that in any case we don't flood the GUI
186 # with new data (for example if the demo device is used)
187 self.thread().msleep(100)
189 # signal used to start the worker across threads
190 _start_signal = QtCore.Signal()
192 def __init__(self, drivers, loglevel):
193 super(self.__class__, self).__init__()
195 self.worker = self.Worker(drivers, loglevel)
196 self.thread = QtCore.QThread()
197 self.worker.moveToThread(self.thread)
199 self._start_signal.connect(self.worker.start_sampling)
201 # expose the signals of the worker
202 self.measured = self.worker.measured
203 self.error = self.worker.error
208 '''Starts sampling'''
209 self._start_signal.emit()
212 '''Stops sampling and the background thread.'''
213 self.worker.stop_sampling()
215 # the timeout is needed when the demo device is used, because it
216 # produces so much outstanding data that quitting takes a long time
217 self.thread.wait(500)
219 def sr_pkg_version(self):
220 '''Returns the version number of the libsigrok package.'''
221 return self.worker.sr_pkg_version
223 def sr_lib_version(self):
224 '''Returns the version number fo the libsigrok library.'''
225 return self.worker.sr_lib_version
227 class SigrokMeter(QtGui.QMainWindow):
228 '''The main window of the application.'''
230 def __init__(self, thread):
231 super(SigrokMeter, self).__init__()
234 self.inf = float('inf')
237 self.thread.measured.connect(self.update, QtCore.Qt.QueuedConnection)
238 self.thread.error.connect(self.error)
242 self.setWindowTitle('sigrok-meter')
243 self.setMinimumHeight(130)
244 self.setMinimumWidth(260)
246 p = os.path.abspath(os.path.dirname(__file__))
247 p = os.path.join(p, 'sigrok-logo-notext.png')
248 self.setWindowIcon(QtGui.QIcon(p))
250 actionQuit = QtGui.QAction(self)
251 actionQuit.setText('&Quit')
252 actionQuit.setIcon(QtGui.QIcon.fromTheme('application-exit'))
253 actionQuit.setShortcut('Ctrl+Q')
254 actionQuit.triggered.connect(self.close)
256 actionAbout = QtGui.QAction(self)
257 actionAbout.setText('&About')
258 actionAbout.setIcon(QtGui.QIcon.fromTheme('help-about'))
259 actionAbout.triggered.connect(self.show_about)
261 menubar = self.menuBar()
262 menuFile = menubar.addMenu('&File')
263 menuFile.addAction(actionQuit)
264 menuHelp = menubar.addMenu('&Help')
265 menuHelp.addAction(actionAbout)
267 self.lblValue = QtGui.QLabel('waiting for data...')
268 self.lblValue.setAlignment(QtCore.Qt.AlignCenter)
269 font = self.lblValue.font()
270 font.setPointSize(font.pointSize() * 1.7)
272 self.lblValue.setFont(font)
273 self.setCentralWidget(self.lblValue)
274 self.centralWidget().setContentsMargins(0, 0, 0, 0)
276 self.lblDevName = QtGui.QLabel()
277 self.lblDevName.setToolTip('Name of used measurement device.')
278 self.statusBar().insertWidget(0, self.lblDevName, 10)
279 self.lblTime = QtGui.QLabel()
280 self.lblTime.setToolTip('Time of the last measurement.')
281 self.statusBar().insertWidget(1, self.lblTime)
283 self.statusBar().setSizeGripEnabled(False)
286 def show_about(self):
287 text = textwrap.dedent('''\
289 <b>sigrok-meter</b><br/>
291 Using libsigrok {} (lib version {}).<br/>
292 <a href='http://www.sigrok.org'>
293 http://www.sigrok.org</a><br/>
295 This program comes with ABSOLUTELY NO WARRANTY;<br/>
297 <a href='http://www.gnu.org/licenses/gpl.html'>
298 http://www.gnu.org/licenses/gpl.html</a>
300 '''.format(self.thread.sr_pkg_version(), self.thread.sr_lib_version()))
302 QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
304 def format_unit(self, u):
308 sr.Unit.OHM: u'\u03A9',
311 sr.Unit.CELSIUS: u'\u00B0C',
312 sr.Unit.FAHRENHEIT: u'\u00B0F',
314 sr.Unit.PERCENTAGE: '%',
317 sr.Unit.SIEMENS: 'S',
318 sr.Unit.DECIBEL_MW: 'dBu',
319 sr.Unit.DECIBEL_VOLT: 'dBV',
321 sr.Unit.DECIBEL_SPL: 'dB',
322 # sr.Unit.CONCENTRATION
323 sr.Unit.REVOLUTIONS_PER_MINUTE: 'rpm',
324 sr.Unit.VOLT_AMPERE: 'VA',
326 sr.Unit.WATT_HOUR: 'Wh',
327 sr.Unit.METER_SECOND: 'm/s',
328 sr.Unit.HECTOPASCAL: 'hPa',
329 sr.Unit.HUMIDITY_293K: '%rF',
330 sr.Unit.DEGREE: u'\u00B0',
334 return units.get(u, '')
336 def format_mqflags(self, mqflags):
337 if sr.QuantityFlag.AC in mqflags:
339 elif sr.QuantityFlag.DC in mqflags:
346 def format_mag(self, mag):
349 return '{:f}'.format(mag)
352 def update(self, data):
353 '''Updates the labels with new measurement values.'''
355 device, mag, unit, mqflags = data
357 unit_str = self.format_unit(unit)
358 mqflags_str = self.format_mqflags(mqflags)
359 mag_str = self.format_mag(mag)
360 value = ' '.join([mag_str, unit_str, mqflags_str])
362 n = datetime.datetime.now().time()
363 now = '{:02}:{:02}:{:02}.{:03}'.format(
364 n.hour, n.minute, n.second, n.microsecond / 1000)
366 self.lblValue.setText(value)
367 self.lblDevName.setText(device)
368 self.lblTime.setText(now)
371 def error(self, msg):
372 '''Error handler for the sampling thread.'''
373 QtGui.QMessageBox.critical(self, 'Error', msg)
376 if __name__ == '__main__':
377 thread = SamplingThread(args['drivers'], args['loglevel'])
379 app = QtGui.QApplication([])
380 s = SigrokMeter(thread)