printrun-src/printrun/gui/graph.py

changeset 15
0bbb006204fc
child 46
cce0af6351f0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/printrun-src/printrun/gui/graph.py	Fri Jun 03 09:16:07 2016 +0200
@@ -0,0 +1,452 @@
+#!/usr/bin/env python
+
+# This file is part of the Printrun suite.
+#
+# Printrun is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Printrun is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Printrun.  If not, see <http://www.gnu.org/licenses/>.
+
+import wx
+from math import log10, floor, ceil
+
+from printrun.utils import install_locale
+install_locale('pronterface')
+
+from .bufferedcanvas import BufferedCanvas
+
+class GraphWindow(wx.Frame):
+    def __init__(self, root, parent_graph = None, size = (600, 600)):
+        super(GraphWindow, self).__init__(None, title = _("Temperature graph"),
+                                          size = size)
+        panel = wx.Panel(self, -1)
+        vbox = wx.BoxSizer(wx.VERTICAL)
+        self.graph = Graph(panel, wx.ID_ANY, root, parent_graph = parent_graph)
+        vbox.Add(self.graph, 1, wx.EXPAND)
+        panel.SetSizer(vbox)
+
+class Graph(BufferedCanvas):
+    '''A class to show a Graph with Pronterface.'''
+
+    def __init__(self, parent, id, root, pos = wx.DefaultPosition,
+                 size = wx.Size(150, 80), style = 0, parent_graph = None):
+        # Forcing a no full repaint to stop flickering
+        style = style | wx.NO_FULL_REPAINT_ON_RESIZE
+        super(Graph, self).__init__(parent, id, pos, size, style)
+        self.root = root
+
+        if parent_graph is not None:
+            self.extruder0temps = parent_graph.extruder0temps
+            self.extruder0targettemps = parent_graph.extruder0targettemps
+            self.extruder1temps = parent_graph.extruder1temps
+            self.extruder1targettemps = parent_graph.extruder1targettemps
+            self.bedtemps = parent_graph.bedtemps
+            self.bedtargettemps = parent_graph.bedtargettemps
+	    self.fanpowers=parent_graph.fanpowers
+        else:
+            self.extruder0temps = [0]
+            self.extruder0targettemps = [0]
+            self.extruder1temps = [0]
+            self.extruder1targettemps = [0]
+            self.bedtemps = [0]
+            self.bedtargettemps = [0]
+	    self.fanpowers= [0]
+
+        self.timer = wx.Timer(self)
+        self.Bind(wx.EVT_TIMER, self.updateTemperatures, self.timer)
+
+        self.minyvalue = 0
+        self.maxyvalue = 260
+        self.rescaley = True  # should the Y axis be rescaled dynamically?
+        if self.rescaley:
+            self._ybounds = Graph._YBounds(self)
+
+        # If rescaley is set then ybars gives merely an estimate
+        # Note that "bars" actually indicate the number of internal+external gridlines.
+        self.ybars = 5
+        self.xbars = 7  # One bar per 10 second
+        self.xsteps = 60  # Covering 1 minute in the graph
+
+        self.window = None
+
+    def show_graph_window(self, event = None):
+        if not self.window:
+            self.window = GraphWindow(self.root, self)
+            self.window.Show()
+            if self.timer.IsRunning():
+                self.window.graph.StartPlotting(self.timer.Interval)
+        else:
+            self.window.Raise()
+
+    def __del__(self):
+        if self.window: self.window.Close()
+
+    def updateTemperatures(self, event):
+        self.AddBedTemperature(self.bedtemps[-1])
+        self.AddBedTargetTemperature(self.bedtargettemps[-1])
+        self.AddExtruder0Temperature(self.extruder0temps[-1])
+        self.AddExtruder0TargetTemperature(self.extruder0targettemps[-1])
+        self.AddExtruder1Temperature(self.extruder1temps[-1])
+        self.AddExtruder1TargetTemperature(self.extruder1targettemps[-1])
+	self.AddFanPower(self.fanpowers[-1])
+        if self.rescaley:
+            self._ybounds.update()
+        self.Refresh()
+
+    def drawgrid(self, dc, gc):
+        # cold, medium, hot = wx.Colour(0, 167, 223),\
+        #                     wx.Colour(239, 233, 119),\
+        #                     wx.Colour(210, 50.100)
+        # col1 = wx.Colour(255, 0, 0, 255)
+        # col2 = wx.Colour(255, 255, 255, 128)
+
+        # b = gc.CreateLinearGradientBrush(0, 0, w, h, col1, col2)
+
+        gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 1))
+
+        # gc.SetBrush(wx.Brush(wx.Colour(245, 245, 255, 52)))
+
+        # gc.SetBrush(gc.CreateBrush(wx.Brush(wx.Colour(0, 0, 0, 255))))
+        gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 255), 1))
+
+        # gc.DrawLines(wx.Point(0, 0), wx.Point(50, 10))
+
+        font = wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD)
+        gc.SetFont(font, wx.Colour(23, 44, 44))
+
+        # draw vertical bars
+        dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1))
+        for x in range(self.xbars + 1):
+            dc.DrawLine(x * (float(self.width - 1) / (self.xbars - 1)),
+                        0,
+                        x * (float(self.width - 1) / (self.xbars - 1)),
+                        self.height)
+
+        # draw horizontal bars
+        spacing = self._calculate_spacing()  # spacing between bars, in degrees
+        yspan = self.maxyvalue - self.minyvalue
+        ybars = int(yspan / spacing)  # Should be close to self.ybars
+        firstbar = int(ceil(self.minyvalue / spacing))  # in degrees
+        dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1))
+        for y in range(firstbar, firstbar + ybars + 1):
+            # y_pos = y*(float(self.height)/self.ybars)
+            degrees = y * spacing
+            y_pos = self._y_pos(degrees)
+            dc.DrawLine(0, y_pos, self.width, y_pos)
+            gc.DrawText(unicode(y * spacing),
+                        1, y_pos - (font.GetPointSize() / 2))
+
+        if self.timer.IsRunning() is False:
+            font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.BOLD)
+            gc.SetFont(font, wx.Colour(3, 4, 4))
+            gc.DrawText("Graph offline",
+                        self.width / 2 - (font.GetPointSize() * 3),
+                        self.height / 2 - (font.GetPointSize() * 1))
+
+        # dc.DrawCircle(50, 50, 1)
+
+        # gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 1))
+        # gc.DrawLines([[20, 30], [10, 53]])
+        # dc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 1))
+
+    def _y_pos(self, temperature):
+        """Converts a temperature, in degrees, to a pixel position"""
+        # fraction of the screen from the bottom
+        frac = (float(temperature - self.minyvalue)
+                / (self.maxyvalue - self.minyvalue))
+        return int((1.0 - frac) * (self.height - 1))
+
+    def _calculate_spacing(self):
+        # Allow grids of spacings 1,2.5,5,10,25,50,100,etc
+
+        yspan = float(self.maxyvalue - self.minyvalue)
+        log_yspan = log10(yspan / self.ybars)
+        exponent = int(floor(log_yspan))
+
+        # calculate boundary points between allowed spacings
+        log1_25 = log10(2) + log10(1) + log10(2.5) - log10(1 + 2.5)
+        log25_5 = log10(2) + log10(2.5) + log10(5) - log10(2.5 + 5)
+        log5_10 = log10(2) + log10(5) + log10(10) - log10(5 + 10)
+
+        if log_yspan - exponent < log1_25:
+            return 10 ** exponent
+        elif log1_25 <= log_yspan - exponent < log25_5:
+            return 25 * 10 ** (exponent - 1)
+        elif log25_5 <= log_yspan - exponent < log5_10:
+            return 5 * 10 ** exponent
+        else:
+            return 10 ** (exponent + 1)
+
+    def drawtemperature(self, dc, gc, temperature_list,
+                        text, text_xoffset, r, g, b, a):
+        if self.timer.IsRunning() is False:
+            dc.SetPen(wx.Pen(wx.Colour(128, 128, 128, 128), 1))
+        else:
+            dc.SetPen(wx.Pen(wx.Colour(r, g, b, a), 1))
+
+        x_add = float(self.width) / self.xsteps
+        x_pos = 0.0
+        lastxvalue = 0.0
+        lastyvalue = temperature_list[-1]
+
+        for temperature in (temperature_list):
+            y_pos = self._y_pos(temperature)
+            if (x_pos > 0.0):  # One need 2 points to draw a line.
+                dc.DrawLine(lastxvalue, lastyvalue, x_pos, y_pos)
+
+            lastxvalue = x_pos
+            x_pos = float(x_pos) + x_add
+            lastyvalue = y_pos
+
+        if len(text) > 0:
+            font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD)
+            # font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
+            if self.timer.IsRunning() is False:
+                gc.SetFont(font, wx.Colour(128, 128, 128))
+            else:
+                gc.SetFont(font, wx.Colour(r, g, b))
+
+            text_size = len(text) * text_xoffset + 1
+            gc.DrawText(text,
+                        x_pos - x_add - (font.GetPointSize() * text_size),
+                        lastyvalue - (font.GetPointSize() / 2))
+
+    def drawfanpower(self, dc, gc):
+        self.drawtemperature(dc, gc, self.fanpowers,
+                             "Fan", 1, 0, 0, 0, 128)
+
+    def drawbedtemp(self, dc, gc):
+        self.drawtemperature(dc, gc, self.bedtemps,
+                             "Bed", 2, 255, 0, 0, 128)
+
+    def drawbedtargettemp(self, dc, gc):
+        self.drawtemperature(dc, gc, self.bedtargettemps,
+                             "Bed Target", 2, 255, 120, 0, 128)
+
+    def drawextruder0temp(self, dc, gc):
+        self.drawtemperature(dc, gc, self.extruder0temps,
+                             "Ex0", 1, 0, 155, 255, 128)
+
+    def drawextruder0targettemp(self, dc, gc):
+        self.drawtemperature(dc, gc, self.extruder0targettemps,
+                             "Ex0 Target", 2, 0, 5, 255, 128)
+
+    def drawextruder1temp(self, dc, gc):
+        self.drawtemperature(dc, gc, self.extruder1temps,
+                             "Ex1", 3, 55, 55, 0, 128)
+
+    def drawextruder1targettemp(self, dc, gc):
+        self.drawtemperature(dc, gc, self.extruder1targettemps,
+                             "Ex1 Target", 2, 55, 55, 0, 128)
+
+    def SetFanPower(self, value):
+        self.fanpowers.pop()
+        self.fanpowers.append(value)
+
+    def AddFanPower(self, value):
+        self.fanpowers.append(value)
+        if float(len(self.fanpowers) - 1) / self.xsteps > 1:
+            self.fanpowers.pop(0)
+
+    def SetBedTemperature(self, value):
+        self.bedtemps.pop()
+        self.bedtemps.append(value)
+
+    def AddBedTemperature(self, value):
+        self.bedtemps.append(value)
+        if float(len(self.bedtemps) - 1) / self.xsteps > 1:
+            self.bedtemps.pop(0)
+
+    def SetBedTargetTemperature(self, value):
+        self.bedtargettemps.pop()
+        self.bedtargettemps.append(value)
+
+    def AddBedTargetTemperature(self, value):
+        self.bedtargettemps.append(value)
+        if float(len(self.bedtargettemps) - 1) / self.xsteps > 1:
+            self.bedtargettemps.pop(0)
+
+    def SetExtruder0Temperature(self, value):
+        self.extruder0temps.pop()
+        self.extruder0temps.append(value)
+
+    def AddExtruder0Temperature(self, value):
+        self.extruder0temps.append(value)
+        if float(len(self.extruder0temps) - 1) / self.xsteps > 1:
+            self.extruder0temps.pop(0)
+
+    def SetExtruder0TargetTemperature(self, value):
+        self.extruder0targettemps.pop()
+        self.extruder0targettemps.append(value)
+
+    def AddExtruder0TargetTemperature(self, value):
+        self.extruder0targettemps.append(value)
+        if float(len(self.extruder0targettemps) - 1) / self.xsteps > 1:
+            self.extruder0targettemps.pop(0)
+
+    def SetExtruder1Temperature(self, value):
+        self.extruder1temps.pop()
+        self.extruder1temps.append(value)
+
+    def AddExtruder1Temperature(self, value):
+        self.extruder1temps.append(value)
+        if float(len(self.extruder1temps) - 1) / self.xsteps > 1:
+            self.extruder1temps.pop(0)
+
+    def SetExtruder1TargetTemperature(self, value):
+        self.extruder1targettemps.pop()
+        self.extruder1targettemps.append(value)
+
+    def AddExtruder1TargetTemperature(self, value):
+        self.extruder1targettemps.append(value)
+        if float(len(self.extruder1targettemps) - 1) / self.xsteps > 1:
+            self.extruder1targettemps.pop(0)
+
+    def StartPlotting(self, time):
+        self.Refresh()
+        self.timer.Start(time)
+        if self.window: self.window.graph.StartPlotting(time)
+
+    def StopPlotting(self):
+        self.timer.Stop()
+        self.Refresh()
+        if self.window: self.window.graph.StopPlotting()
+
+    def draw(self, dc, w, h):
+        dc.SetBackground(wx.Brush(self.root.bgcolor))
+        dc.Clear()
+        gc = wx.GraphicsContext.Create(dc)
+        self.width = w
+        self.height = h
+        self.drawgrid(dc, gc)
+        self.drawbedtargettemp(dc, gc)
+        self.drawbedtemp(dc, gc)
+        self.drawfanpower(dc, gc)
+        self.drawextruder0targettemp(dc, gc)
+        self.drawextruder0temp(dc, gc)
+        self.drawextruder1targettemp(dc, gc)
+        self.drawextruder1temp(dc, gc)
+
+    class _YBounds(object):
+        """Small helper class to claculate y bounds dynamically"""
+
+        def __init__(self, graph, minimum_scale=5.0, buffer=0.10):
+            """_YBounds(Graph,float,float)
+
+            graph           parent object to calculate scales for
+            minimum_scale   minimum range to show on the graph
+            buffer          amount of padding to add above & below the
+                            displayed temperatures. Given as a fraction of the
+                            total range. (Eg .05 to use 90% of the range for
+                            temperatures)
+            """
+            self.graph = graph
+            self.min_scale = minimum_scale
+            self.buffer = buffer
+
+            # Frequency to rescale the graph
+            self.update_freq = 10
+            # number of updates since last full refresh
+            self._last_update = self.update_freq
+
+        def update(self, forceUpdate=False):
+            """Updates graph.minyvalue and graph.maxyvalue based on current
+            temperatures """
+            self._last_update += 1
+            # TODO Smart update. Only do full calculation every 10s. Otherwise,
+            # just look at current graph & expand if necessary
+            if forceUpdate or self._last_update >= self.update_freq:
+                self.graph.minyvalue, self.graph.maxyvalue = self.getBounds()
+                self._last_update = 0
+            else:
+                bounds = self.getBoundsQuick()
+                self.graph.minyvalue, self.graph.maxyvalue = bounds
+
+        def getBounds(self):
+            """
+            Calculates the bounds based on the current temperatures
+
+            Rules:
+             * Include the full extruder0 history
+             * Include the current target temp (but not necessarily old
+               settings)
+             * Include the extruder1 and/or bed temp if
+                1) The target temp is >0
+                2) The history has ever been above 5
+             * Include at least min_scale
+             * Include at least buffer above & below the extreme temps
+            """
+            extruder0_min = min(self.graph.extruder0temps)
+            extruder0_max = max(self.graph.extruder0temps)
+            extruder0_target = self.graph.extruder0targettemps[-1]
+            extruder1_min = min(self.graph.extruder1temps)
+            extruder1_max = max(self.graph.extruder1temps)
+            extruder1_target = self.graph.extruder1targettemps[-1]
+            bed_min = min(self.graph.bedtemps)
+            bed_max = max(self.graph.bedtemps)
+            bed_target = self.graph.bedtargettemps[-1]
+
+            miny = min(extruder0_min, extruder0_target)
+            maxy = max(extruder0_max, extruder0_target)
+            if extruder1_target > 0 or extruder1_max > 5:  # use extruder1
+                miny = min(miny, extruder1_min, extruder1_target)
+                maxy = max(maxy, extruder1_max, extruder1_target)
+            if bed_target > 0 or bed_max > 5:  # use HBP
+                miny = min(miny, bed_min, bed_target)
+                maxy = max(maxy, bed_max, bed_target)
+	    miny=min(0,miny);
+	    maxy=max(260,maxy);
+
+            padding = (maxy - miny) * self.buffer / (1.0 - 2 * self.buffer)
+            miny -= padding
+            maxy += padding
+
+            if maxy - miny < self.min_scale:
+                extrapadding = (self.min_scale - maxy + miny) / 2.0
+                miny -= extrapadding
+                maxy += extrapadding
+
+            return (miny, maxy)
+
+        def getBoundsQuick(self):
+            # Only look at current temps
+            extruder0_min = self.graph.extruder0temps[-1]
+            extruder0_max = self.graph.extruder0temps[-1]
+            extruder0_target = self.graph.extruder0targettemps[-1]
+            extruder1_min = self.graph.extruder1temps[-1]
+            extruder1_max = self.graph.extruder1temps[-1]
+            extruder1_target = self.graph.extruder1targettemps[-1]
+            bed_min = self.graph.bedtemps[-1]
+            bed_max = self.graph.bedtemps[-1]
+            bed_target = self.graph.bedtargettemps[-1]
+
+            miny = min(extruder0_min, extruder0_target)
+            maxy = max(extruder0_max, extruder0_target)
+            if extruder1_target > 0 or extruder1_max > 5:  # use extruder1
+                miny = min(miny, extruder1_min, extruder1_target)
+                maxy = max(maxy, extruder1_max, extruder1_target)
+            if bed_target > 0 or bed_max > 5:  # use HBP
+                miny = min(miny, bed_min, bed_target)
+                maxy = max(maxy, bed_max, bed_target)
+	    miny=min(0,miny);
+	    maxy=max(260,maxy);
+
+            # We have to rescale, so add padding
+            bufratio = self.buffer / (1.0 - self.buffer)
+            if miny < self.graph.minyvalue:
+                padding = (self.graph.maxyvalue - miny) * bufratio
+                miny -= padding
+            if maxy > self.graph.maxyvalue:
+                padding = (maxy - self.graph.minyvalue) * bufratio
+                maxy += padding
+
+            return (min(miny, self.graph.minyvalue),
+                    max(maxy, self.graph.maxyvalue))

mercurial