]>
Commit | Line | Data |
---|---|---|
48723bbb JS |
1 | ## |
2 | ## This file is part of the sigrok-meter project. | |
3 | ## | |
4 | ## Copyright (C) 2014 Jens Steinhauser <jens.steinhauser@gmail.com> | |
5 | ## | |
6 | ## This program is free software; you can redistribute it and/or modify | |
7 | ## it under the terms of the GNU General Public License as published by | |
8 | ## the Free Software Foundation; either version 2 of the License, or | |
9 | ## (at your option) any later version. | |
10 | ## | |
11 | ## This program is distributed in the hope that it will be useful, | |
12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | ## GNU General Public License for more details. | |
15 | ## | |
16 | ## You should have received a copy of the GNU General Public License | |
0b63748b | 17 | ## along with this program; if not, see <http://www.gnu.org/licenses/>. |
48723bbb JS |
18 | ## |
19 | ||
3010b5a0 | 20 | import itertools |
5baa1f4b | 21 | import math |
48723bbb JS |
22 | import qtcompat |
23 | import sigrok.core as sr | |
f76b9df8 | 24 | import util |
48723bbb | 25 | |
3010b5a0 JS |
26 | try: |
27 | from itertools import izip | |
28 | except ImportError: | |
29 | izip = zip | |
30 | ||
48723bbb JS |
31 | QtCore = qtcompat.QtCore |
32 | QtGui = qtcompat.QtGui | |
33 | ||
d0aa45b4 JS |
34 | class Trace(object): |
35 | '''Class to hold the measured samples.''' | |
36 | ||
37 | def __init__(self): | |
38 | self.samples = [] | |
39 | self.new = False | |
40 | ||
41 | def append(self, sample): | |
42 | self.samples.append(sample) | |
43 | self.new = True | |
44 | ||
48723bbb JS |
45 | class MeasurementDataModel(QtGui.QStandardItemModel): |
46 | '''Model to hold the measured values.''' | |
47 | ||
48 | '''Role used to identify and find the item.''' | |
f76b9df8 | 49 | idRole = QtCore.Qt.UserRole + 1 |
48723bbb JS |
50 | |
51 | '''Role used to store the device vendor and model.''' | |
52 | descRole = QtCore.Qt.UserRole + 2 | |
53 | ||
911ab26e | 54 | '''Role used to store a dictionary with the traces.''' |
d0aa45b4 | 55 | tracesRole = QtCore.Qt.UserRole + 3 |
f76b9df8 | 56 | |
3010b5a0 JS |
57 | '''Role used to store the color to draw the graph of the channel.''' |
58 | colorRole = QtCore.Qt.UserRole + 4 | |
59 | ||
48723bbb JS |
60 | def __init__(self, parent): |
61 | super(self.__class__, self).__init__(parent) | |
62 | ||
63 | # Use the description text to sort the items for now, because the | |
f76b9df8 | 64 | # idRole holds tuples, and using them to sort doesn't work. |
48723bbb JS |
65 | self.setSortRole(MeasurementDataModel.descRole) |
66 | ||
3010b5a0 JS |
67 | # A generator for the colors of the channels. |
68 | self._colorgen = self._make_colorgen() | |
69 | ||
70 | def _make_colorgen(self): | |
71 | cols = [ | |
72 | QtGui.QColor(0x8F, 0x52, 0x02), # brown | |
86862cb4 | 73 | QtGui.QColor(0x73, 0xD2, 0x16), # green |
3010b5a0 | 74 | QtGui.QColor(0xCC, 0x00, 0x00), # red |
86862cb4 | 75 | QtGui.QColor(0x34, 0x65, 0xA4), # blue |
3010b5a0 JS |
76 | QtGui.QColor(0xF5, 0x79, 0x00), # orange |
77 | QtGui.QColor(0xED, 0xD4, 0x00), # yellow | |
3010b5a0 JS |
78 | QtGui.QColor(0x75, 0x50, 0x7B) # violet |
79 | ] | |
80 | ||
81 | def myrepeat(g, n): | |
82 | '''Repeats every element from 'g' 'n' times'.''' | |
83 | for e in g: | |
84 | for f in itertools.repeat(e, n): | |
85 | yield f | |
86 | ||
87 | colorcycle = itertools.cycle(cols) | |
88 | darkness = myrepeat(itertools.count(100, 10), len(cols)) | |
89 | ||
90 | for c, d in izip(colorcycle, darkness): | |
91 | yield QtGui.QColor(c).darker(d) | |
92 | ||
48723bbb JS |
93 | def format_mqflags(self, mqflags): |
94 | if sr.QuantityFlag.AC in mqflags: | |
95 | return 'AC' | |
96 | elif sr.QuantityFlag.DC in mqflags: | |
97 | return 'DC' | |
98 | else: | |
99 | return '' | |
100 | ||
101 | def format_value(self, mag): | |
5baa1f4b | 102 | if math.isinf(mag): |
48723bbb JS |
103 | return u'\u221E' |
104 | return '{:f}'.format(mag) | |
105 | ||
106 | def getItem(self, device, channel): | |
480cdb7b UH |
107 | '''Return the item for the device + channel combination from the |
108 | model, or create a new item if no existing one matches.''' | |
48723bbb | 109 | |
480cdb7b UH |
110 | # Unique identifier for the device + channel. |
111 | # TODO: Isn't there something better? | |
48723bbb JS |
112 | uid = ( |
113 | device.vendor, | |
114 | device.model, | |
115 | device.serial_number(), | |
116 | device.connection_id(), | |
117 | channel.index | |
118 | ) | |
119 | ||
480cdb7b | 120 | # Find the correct item in the model. |
48723bbb JS |
121 | for row in range(self.rowCount()): |
122 | item = self.item(row) | |
f76b9df8 | 123 | rid = item.data(MeasurementDataModel.idRole) |
480cdb7b | 124 | rid = tuple(rid) # PySide returns a list. |
48723bbb JS |
125 | if uid == rid: |
126 | return item | |
127 | ||
480cdb7b | 128 | # Nothing found, create a new item. |
0e810ddf | 129 | desc = '{} {}, {}'.format( |
48723bbb JS |
130 | device.vendor, device.model, channel.name) |
131 | ||
132 | item = QtGui.QStandardItem() | |
f76b9df8 | 133 | item.setData(uid, MeasurementDataModel.idRole) |
48723bbb | 134 | item.setData(desc, MeasurementDataModel.descRole) |
1879265a | 135 | item.setData({}, MeasurementDataModel.tracesRole) |
3010b5a0 | 136 | item.setData(next(self._colorgen), MeasurementDataModel.colorRole) |
48723bbb JS |
137 | self.appendRow(item) |
138 | self.sort(0) | |
139 | return item | |
140 | ||
6c05c913 JS |
141 | @QtCore.Slot(float, sr.classes.Device, sr.classes.Channel, tuple) |
142 | def update(self, timestamp, device, channel, data): | |
480cdb7b | 143 | '''Update the data for the device (+channel) with the most recent |
48723bbb JS |
144 | measurement from the given payload.''' |
145 | ||
146 | item = self.getItem(device, channel) | |
147 | ||
148 | value, unit, mqflags = data | |
149 | value_str = self.format_value(value) | |
f76b9df8 | 150 | unit_str = util.format_unit(unit) |
48723bbb JS |
151 | mqflags_str = self.format_mqflags(mqflags) |
152 | ||
02862990 JS |
153 | # The display role is a tuple containing the value and the unit/flags. |
154 | disp = (value_str, ' '.join([unit_str, mqflags_str])) | |
48723bbb JS |
155 | item.setData(disp, QtCore.Qt.DisplayRole) |
156 | ||
f76b9df8 JS |
157 | # The samples role is a dictionary that contains the old samples for each unit. |
158 | # Should be trimmed periodically, otherwise it grows larger and larger. | |
5baa1f4b JS |
159 | if not math.isinf(value) and not math.isnan(value): |
160 | sample = (timestamp, value) | |
161 | traces = item.data(MeasurementDataModel.tracesRole) | |
1879265a JS |
162 | |
163 | # It's not possible to use 'collections.defaultdict' here, because | |
164 | # PySide doesn't return the original type that was passed in. | |
165 | if not (unit in traces): | |
166 | traces[unit] = Trace() | |
5baa1f4b | 167 | traces[unit].append(sample) |
f76b9df8 | 168 | |
1879265a JS |
169 | item.setData(traces, MeasurementDataModel.tracesRole) |
170 | ||
68348e5a JS |
171 | def clear_samples(self): |
172 | '''Removes all old samples from the model.''' | |
173 | for row in range(self.rowCount()): | |
174 | idx = self.index(row, 0) | |
175 | self.setData(idx, {}, | |
176 | MeasurementDataModel.tracesRole) | |
177 | ||
48723bbb JS |
178 | class MultimeterDelegate(QtGui.QStyledItemDelegate): |
179 | '''Delegate to show the data items from a MeasurementDataModel.''' | |
180 | ||
181 | def __init__(self, parent, font): | |
480cdb7b | 182 | '''Initialize the delegate. |
48723bbb | 183 | |
32b16651 | 184 | :param font: Font used for the text. |
48723bbb JS |
185 | ''' |
186 | ||
187 | super(self.__class__, self).__init__(parent) | |
188 | ||
189 | self._nfont = font | |
48723bbb JS |
190 | |
191 | fi = QtGui.QFontInfo(self._nfont) | |
192 | self._nfontheight = fi.pixelSize() | |
193 | ||
32b16651 | 194 | fm = QtGui.QFontMetrics(self._nfont) |
48723bbb | 195 | r = fm.boundingRect('-XX.XXXXXX X XX') |
02862990 | 196 | |
32b16651 JS |
197 | w = 1.4 * r.width() + 2 * self._nfontheight |
198 | h = 2.6 * self._nfontheight | |
199 | self._size = QtCore.QSize(w, h) | |
48723bbb JS |
200 | |
201 | def sizeHint(self, option=None, index=None): | |
202 | return self._size | |
203 | ||
32b16651 JS |
204 | def _color_rect(self, outer): |
205 | '''Returns the dimensions of the clickable rectangle.''' | |
206 | x1 = (outer.height() - self._nfontheight) / 2 | |
207 | r = QtCore.QRect(x1, x1, self._nfontheight, self._nfontheight) | |
208 | r.translate(outer.topLeft()) | |
209 | return r | |
210 | ||
48723bbb | 211 | def paint(self, painter, options, index): |
02862990 | 212 | value, unit = index.data(QtCore.Qt.DisplayRole) |
48723bbb | 213 | desc = index.data(MeasurementDataModel.descRole) |
32b16651 | 214 | color = index.data(MeasurementDataModel.colorRole) |
48723bbb | 215 | |
48723bbb | 216 | painter.setFont(self._nfont) |
32b16651 JS |
217 | |
218 | # Draw the clickable rectangle. | |
219 | painter.fillRect(self._color_rect(options.rect), color) | |
220 | ||
221 | # Draw the text | |
222 | h = options.rect.height() | |
48723bbb | 223 | p = options.rect.topLeft() |
32b16651 | 224 | p += QtCore.QPoint(h, (h + self._nfontheight) / 2 - 2) |
0e810ddf | 225 | painter.drawText(p, desc + ': ' + value + ' ' + unit) |
32b16651 JS |
226 | |
227 | def editorEvent(self, event, model, options, index): | |
228 | if type(event) is QtGui.QMouseEvent: | |
229 | if event.type() == QtCore.QEvent.MouseButtonPress: | |
230 | rect = self._color_rect(options.rect) | |
231 | if rect.contains(event.x(), event.y()): | |
232 | c = index.data(MeasurementDataModel.colorRole) | |
233 | c = QtGui.QColorDialog.getColor(c, None, | |
234 | 'Choose new color for channel') | |
4452730c AS |
235 | if c.isValid(): |
236 | # False if cancel is pressed (resulting in a black | |
911ab26e | 237 | # color). |
4452730c AS |
238 | item = model.itemFromIndex(index) |
239 | item.setData(c, MeasurementDataModel.colorRole) | |
32b16651 JS |
240 | |
241 | return True | |
242 | ||
243 | return False |