|
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)) |