printrun-src/printrun/gui/graph.py

changeset 15
0bbb006204fc
child 46
cce0af6351f0
equal deleted inserted replaced
14:51bf56ba3c10 15:0bbb006204fc
1 #!/usr/bin/env python
2
3 # This file is part of the Printrun suite.
4 #
5 # Printrun is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Printrun is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Printrun. If not, see <http://www.gnu.org/licenses/>.
17
18 import wx
19 from math import log10, floor, ceil
20
21 from printrun.utils import install_locale
22 install_locale('pronterface')
23
24 from .bufferedcanvas import BufferedCanvas
25
26 class GraphWindow(wx.Frame):
27 def __init__(self, root, parent_graph = None, size = (600, 600)):
28 super(GraphWindow, self).__init__(None, title = _("Temperature graph"),
29 size = size)
30 panel = wx.Panel(self, -1)
31 vbox = wx.BoxSizer(wx.VERTICAL)
32 self.graph = Graph(panel, wx.ID_ANY, root, parent_graph = parent_graph)
33 vbox.Add(self.graph, 1, wx.EXPAND)
34 panel.SetSizer(vbox)
35
36 class Graph(BufferedCanvas):
37 '''A class to show a Graph with Pronterface.'''
38
39 def __init__(self, parent, id, root, pos = wx.DefaultPosition,
40 size = wx.Size(150, 80), style = 0, parent_graph = None):
41 # Forcing a no full repaint to stop flickering
42 style = style | wx.NO_FULL_REPAINT_ON_RESIZE
43 super(Graph, self).__init__(parent, id, pos, size, style)
44 self.root = root
45
46 if parent_graph is not None:
47 self.extruder0temps = parent_graph.extruder0temps
48 self.extruder0targettemps = parent_graph.extruder0targettemps
49 self.extruder1temps = parent_graph.extruder1temps
50 self.extruder1targettemps = parent_graph.extruder1targettemps
51 self.bedtemps = parent_graph.bedtemps
52 self.bedtargettemps = parent_graph.bedtargettemps
53 self.fanpowers=parent_graph.fanpowers
54 else:
55 self.extruder0temps = [0]
56 self.extruder0targettemps = [0]
57 self.extruder1temps = [0]
58 self.extruder1targettemps = [0]
59 self.bedtemps = [0]
60 self.bedtargettemps = [0]
61 self.fanpowers= [0]
62
63 self.timer = wx.Timer(self)
64 self.Bind(wx.EVT_TIMER, self.updateTemperatures, self.timer)
65
66 self.minyvalue = 0
67 self.maxyvalue = 260
68 self.rescaley = True # should the Y axis be rescaled dynamically?
69 if self.rescaley:
70 self._ybounds = Graph._YBounds(self)
71
72 # If rescaley is set then ybars gives merely an estimate
73 # Note that "bars" actually indicate the number of internal+external gridlines.
74 self.ybars = 5
75 self.xbars = 7 # One bar per 10 second
76 self.xsteps = 60 # Covering 1 minute in the graph
77
78 self.window = None
79
80 def show_graph_window(self, event = None):
81 if not self.window:
82 self.window = GraphWindow(self.root, self)
83 self.window.Show()
84 if self.timer.IsRunning():
85 self.window.graph.StartPlotting(self.timer.Interval)
86 else:
87 self.window.Raise()
88
89 def __del__(self):
90 if self.window: self.window.Close()
91
92 def updateTemperatures(self, event):
93 self.AddBedTemperature(self.bedtemps[-1])
94 self.AddBedTargetTemperature(self.bedtargettemps[-1])
95 self.AddExtruder0Temperature(self.extruder0temps[-1])
96 self.AddExtruder0TargetTemperature(self.extruder0targettemps[-1])
97 self.AddExtruder1Temperature(self.extruder1temps[-1])
98 self.AddExtruder1TargetTemperature(self.extruder1targettemps[-1])
99 self.AddFanPower(self.fanpowers[-1])
100 if self.rescaley:
101 self._ybounds.update()
102 self.Refresh()
103
104 def drawgrid(self, dc, gc):
105 # cold, medium, hot = wx.Colour(0, 167, 223),\
106 # wx.Colour(239, 233, 119),\
107 # wx.Colour(210, 50.100)
108 # col1 = wx.Colour(255, 0, 0, 255)
109 # col2 = wx.Colour(255, 255, 255, 128)
110
111 # b = gc.CreateLinearGradientBrush(0, 0, w, h, col1, col2)
112
113 gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 1))
114
115 # gc.SetBrush(wx.Brush(wx.Colour(245, 245, 255, 52)))
116
117 # gc.SetBrush(gc.CreateBrush(wx.Brush(wx.Colour(0, 0, 0, 255))))
118 gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 255), 1))
119
120 # gc.DrawLines(wx.Point(0, 0), wx.Point(50, 10))
121
122 font = wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD)
123 gc.SetFont(font, wx.Colour(23, 44, 44))
124
125 # draw vertical bars
126 dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1))
127 for x in range(self.xbars + 1):
128 dc.DrawLine(x * (float(self.width - 1) / (self.xbars - 1)),
129 0,
130 x * (float(self.width - 1) / (self.xbars - 1)),
131 self.height)
132
133 # draw horizontal bars
134 spacing = self._calculate_spacing() # spacing between bars, in degrees
135 yspan = self.maxyvalue - self.minyvalue
136 ybars = int(yspan / spacing) # Should be close to self.ybars
137 firstbar = int(ceil(self.minyvalue / spacing)) # in degrees
138 dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1))
139 for y in range(firstbar, firstbar + ybars + 1):
140 # y_pos = y*(float(self.height)/self.ybars)
141 degrees = y * spacing
142 y_pos = self._y_pos(degrees)
143 dc.DrawLine(0, y_pos, self.width, y_pos)
144 gc.DrawText(unicode(y * spacing),
145 1, y_pos - (font.GetPointSize() / 2))
146
147 if self.timer.IsRunning() is False:
148 font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.BOLD)
149 gc.SetFont(font, wx.Colour(3, 4, 4))
150 gc.DrawText("Graph offline",
151 self.width / 2 - (font.GetPointSize() * 3),
152 self.height / 2 - (font.GetPointSize() * 1))
153
154 # dc.DrawCircle(50, 50, 1)
155
156 # gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 1))
157 # gc.DrawLines([[20, 30], [10, 53]])
158 # dc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 1))
159
160 def _y_pos(self, temperature):
161 """Converts a temperature, in degrees, to a pixel position"""
162 # fraction of the screen from the bottom
163 frac = (float(temperature - self.minyvalue)
164 / (self.maxyvalue - self.minyvalue))
165 return int((1.0 - frac) * (self.height - 1))
166
167 def _calculate_spacing(self):
168 # Allow grids of spacings 1,2.5,5,10,25,50,100,etc
169
170 yspan = float(self.maxyvalue - self.minyvalue)
171 log_yspan = log10(yspan / self.ybars)
172 exponent = int(floor(log_yspan))
173
174 # calculate boundary points between allowed spacings
175 log1_25 = log10(2) + log10(1) + log10(2.5) - log10(1 + 2.5)
176 log25_5 = log10(2) + log10(2.5) + log10(5) - log10(2.5 + 5)
177 log5_10 = log10(2) + log10(5) + log10(10) - log10(5 + 10)
178
179 if log_yspan - exponent < log1_25:
180 return 10 ** exponent
181 elif log1_25 <= log_yspan - exponent < log25_5:
182 return 25 * 10 ** (exponent - 1)
183 elif log25_5 <= log_yspan - exponent < log5_10:
184 return 5 * 10 ** exponent
185 else:
186 return 10 ** (exponent + 1)
187
188 def drawtemperature(self, dc, gc, temperature_list,
189 text, text_xoffset, r, g, b, a):
190 if self.timer.IsRunning() is False:
191 dc.SetPen(wx.Pen(wx.Colour(128, 128, 128, 128), 1))
192 else:
193 dc.SetPen(wx.Pen(wx.Colour(r, g, b, a), 1))
194
195 x_add = float(self.width) / self.xsteps
196 x_pos = 0.0
197 lastxvalue = 0.0
198 lastyvalue = temperature_list[-1]
199
200 for temperature in (temperature_list):
201 y_pos = self._y_pos(temperature)
202 if (x_pos > 0.0): # One need 2 points to draw a line.
203 dc.DrawLine(lastxvalue, lastyvalue, x_pos, y_pos)
204
205 lastxvalue = x_pos
206 x_pos = float(x_pos) + x_add
207 lastyvalue = y_pos
208
209 if len(text) > 0:
210 font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD)
211 # font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
212 if self.timer.IsRunning() is False:
213 gc.SetFont(font, wx.Colour(128, 128, 128))
214 else:
215 gc.SetFont(font, wx.Colour(r, g, b))
216
217 text_size = len(text) * text_xoffset + 1
218 gc.DrawText(text,
219 x_pos - x_add - (font.GetPointSize() * text_size),
220 lastyvalue - (font.GetPointSize() / 2))
221
222 def drawfanpower(self, dc, gc):
223 self.drawtemperature(dc, gc, self.fanpowers,
224 "Fan", 1, 0, 0, 0, 128)
225
226 def drawbedtemp(self, dc, gc):
227 self.drawtemperature(dc, gc, self.bedtemps,
228 "Bed", 2, 255, 0, 0, 128)
229
230 def drawbedtargettemp(self, dc, gc):
231 self.drawtemperature(dc, gc, self.bedtargettemps,
232 "Bed Target", 2, 255, 120, 0, 128)
233
234 def drawextruder0temp(self, dc, gc):
235 self.drawtemperature(dc, gc, self.extruder0temps,
236 "Ex0", 1, 0, 155, 255, 128)
237
238 def drawextruder0targettemp(self, dc, gc):
239 self.drawtemperature(dc, gc, self.extruder0targettemps,
240 "Ex0 Target", 2, 0, 5, 255, 128)
241
242 def drawextruder1temp(self, dc, gc):
243 self.drawtemperature(dc, gc, self.extruder1temps,
244 "Ex1", 3, 55, 55, 0, 128)
245
246 def drawextruder1targettemp(self, dc, gc):
247 self.drawtemperature(dc, gc, self.extruder1targettemps,
248 "Ex1 Target", 2, 55, 55, 0, 128)
249
250 def SetFanPower(self, value):
251 self.fanpowers.pop()
252 self.fanpowers.append(value)
253
254 def AddFanPower(self, value):
255 self.fanpowers.append(value)
256 if float(len(self.fanpowers) - 1) / self.xsteps > 1:
257 self.fanpowers.pop(0)
258
259 def SetBedTemperature(self, value):
260 self.bedtemps.pop()
261 self.bedtemps.append(value)
262
263 def AddBedTemperature(self, value):
264 self.bedtemps.append(value)
265 if float(len(self.bedtemps) - 1) / self.xsteps > 1:
266 self.bedtemps.pop(0)
267
268 def SetBedTargetTemperature(self, value):
269 self.bedtargettemps.pop()
270 self.bedtargettemps.append(value)
271
272 def AddBedTargetTemperature(self, value):
273 self.bedtargettemps.append(value)
274 if float(len(self.bedtargettemps) - 1) / self.xsteps > 1:
275 self.bedtargettemps.pop(0)
276
277 def SetExtruder0Temperature(self, value):
278 self.extruder0temps.pop()
279 self.extruder0temps.append(value)
280
281 def AddExtruder0Temperature(self, value):
282 self.extruder0temps.append(value)
283 if float(len(self.extruder0temps) - 1) / self.xsteps > 1:
284 self.extruder0temps.pop(0)
285
286 def SetExtruder0TargetTemperature(self, value):
287 self.extruder0targettemps.pop()
288 self.extruder0targettemps.append(value)
289
290 def AddExtruder0TargetTemperature(self, value):
291 self.extruder0targettemps.append(value)
292 if float(len(self.extruder0targettemps) - 1) / self.xsteps > 1:
293 self.extruder0targettemps.pop(0)
294
295 def SetExtruder1Temperature(self, value):
296 self.extruder1temps.pop()
297 self.extruder1temps.append(value)
298
299 def AddExtruder1Temperature(self, value):
300 self.extruder1temps.append(value)
301 if float(len(self.extruder1temps) - 1) / self.xsteps > 1:
302 self.extruder1temps.pop(0)
303
304 def SetExtruder1TargetTemperature(self, value):
305 self.extruder1targettemps.pop()
306 self.extruder1targettemps.append(value)
307
308 def AddExtruder1TargetTemperature(self, value):
309 self.extruder1targettemps.append(value)
310 if float(len(self.extruder1targettemps) - 1) / self.xsteps > 1:
311 self.extruder1targettemps.pop(0)
312
313 def StartPlotting(self, time):
314 self.Refresh()
315 self.timer.Start(time)
316 if self.window: self.window.graph.StartPlotting(time)
317
318 def StopPlotting(self):
319 self.timer.Stop()
320 self.Refresh()
321 if self.window: self.window.graph.StopPlotting()
322
323 def draw(self, dc, w, h):
324 dc.SetBackground(wx.Brush(self.root.bgcolor))
325 dc.Clear()
326 gc = wx.GraphicsContext.Create(dc)
327 self.width = w
328 self.height = h
329 self.drawgrid(dc, gc)
330 self.drawbedtargettemp(dc, gc)
331 self.drawbedtemp(dc, gc)
332 self.drawfanpower(dc, gc)
333 self.drawextruder0targettemp(dc, gc)
334 self.drawextruder0temp(dc, gc)
335 self.drawextruder1targettemp(dc, gc)
336 self.drawextruder1temp(dc, gc)
337
338 class _YBounds(object):
339 """Small helper class to claculate y bounds dynamically"""
340
341 def __init__(self, graph, minimum_scale=5.0, buffer=0.10):
342 """_YBounds(Graph,float,float)
343
344 graph parent object to calculate scales for
345 minimum_scale minimum range to show on the graph
346 buffer amount of padding to add above & below the
347 displayed temperatures. Given as a fraction of the
348 total range. (Eg .05 to use 90% of the range for
349 temperatures)
350 """
351 self.graph = graph
352 self.min_scale = minimum_scale
353 self.buffer = buffer
354
355 # Frequency to rescale the graph
356 self.update_freq = 10
357 # number of updates since last full refresh
358 self._last_update = self.update_freq
359
360 def update(self, forceUpdate=False):
361 """Updates graph.minyvalue and graph.maxyvalue based on current
362 temperatures """
363 self._last_update += 1
364 # TODO Smart update. Only do full calculation every 10s. Otherwise,
365 # just look at current graph & expand if necessary
366 if forceUpdate or self._last_update >= self.update_freq:
367 self.graph.minyvalue, self.graph.maxyvalue = self.getBounds()
368 self._last_update = 0
369 else:
370 bounds = self.getBoundsQuick()
371 self.graph.minyvalue, self.graph.maxyvalue = bounds
372
373 def getBounds(self):
374 """
375 Calculates the bounds based on the current temperatures
376
377 Rules:
378 * Include the full extruder0 history
379 * Include the current target temp (but not necessarily old
380 settings)
381 * Include the extruder1 and/or bed temp if
382 1) The target temp is >0
383 2) The history has ever been above 5
384 * Include at least min_scale
385 * Include at least buffer above & below the extreme temps
386 """
387 extruder0_min = min(self.graph.extruder0temps)
388 extruder0_max = max(self.graph.extruder0temps)
389 extruder0_target = self.graph.extruder0targettemps[-1]
390 extruder1_min = min(self.graph.extruder1temps)
391 extruder1_max = max(self.graph.extruder1temps)
392 extruder1_target = self.graph.extruder1targettemps[-1]
393 bed_min = min(self.graph.bedtemps)
394 bed_max = max(self.graph.bedtemps)
395 bed_target = self.graph.bedtargettemps[-1]
396
397 miny = min(extruder0_min, extruder0_target)
398 maxy = max(extruder0_max, extruder0_target)
399 if extruder1_target > 0 or extruder1_max > 5: # use extruder1
400 miny = min(miny, extruder1_min, extruder1_target)
401 maxy = max(maxy, extruder1_max, extruder1_target)
402 if bed_target > 0 or bed_max > 5: # use HBP
403 miny = min(miny, bed_min, bed_target)
404 maxy = max(maxy, bed_max, bed_target)
405 miny=min(0,miny);
406 maxy=max(260,maxy);
407
408 padding = (maxy - miny) * self.buffer / (1.0 - 2 * self.buffer)
409 miny -= padding
410 maxy += padding
411
412 if maxy - miny < self.min_scale:
413 extrapadding = (self.min_scale - maxy + miny) / 2.0
414 miny -= extrapadding
415 maxy += extrapadding
416
417 return (miny, maxy)
418
419 def getBoundsQuick(self):
420 # Only look at current temps
421 extruder0_min = self.graph.extruder0temps[-1]
422 extruder0_max = self.graph.extruder0temps[-1]
423 extruder0_target = self.graph.extruder0targettemps[-1]
424 extruder1_min = self.graph.extruder1temps[-1]
425 extruder1_max = self.graph.extruder1temps[-1]
426 extruder1_target = self.graph.extruder1targettemps[-1]
427 bed_min = self.graph.bedtemps[-1]
428 bed_max = self.graph.bedtemps[-1]
429 bed_target = self.graph.bedtargettemps[-1]
430
431 miny = min(extruder0_min, extruder0_target)
432 maxy = max(extruder0_max, extruder0_target)
433 if extruder1_target > 0 or extruder1_max > 5: # use extruder1
434 miny = min(miny, extruder1_min, extruder1_target)
435 maxy = max(maxy, extruder1_max, extruder1_target)
436 if bed_target > 0 or bed_max > 5: # use HBP
437 miny = min(miny, bed_min, bed_target)
438 maxy = max(maxy, bed_max, bed_target)
439 miny=min(0,miny);
440 maxy=max(260,maxy);
441
442 # We have to rescale, so add padding
443 bufratio = self.buffer / (1.0 - self.buffer)
444 if miny < self.graph.minyvalue:
445 padding = (self.graph.maxyvalue - miny) * bufratio
446 miny -= padding
447 if maxy > self.graph.maxyvalue:
448 padding = (maxy - self.graph.minyvalue) * bufratio
449 maxy += padding
450
451 return (min(miny, self.graph.minyvalue),
452 max(maxy, self.graph.maxyvalue))

mercurial