Tue, 19 Jan 2021 20:44:16 +0100
Added tag WORKING_BEFORE_UPGRADE_TO_GITMASTER for changeset f7e9bd735ce1
15 | 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)) |