From 5bfcd0108a5c96fd5a46b8a7250f265242232217 Mon Sep 17 00:00:00 2001 From: Jordan Date: Tue, 21 Sep 2021 21:19:39 -0400 Subject: [PATCH] Adding graph capability to main menu --- ks_includes/graph.py | 165 +++++++++++++++++++++++++++++++++ ks_includes/printer.py | 46 ++++++++++ ks_includes/screen_panel.py | 15 +++ panels/main_menu.py | 177 +++++++++++++++++++++++++++++------- screen.py | 9 +- styles/base.css | 29 +++++- 6 files changed, 401 insertions(+), 40 deletions(-) create mode 100644 ks_includes/graph.py diff --git a/ks_includes/graph.py b/ks_includes/graph.py new file mode 100644 index 00000000..9cb08e8f --- /dev/null +++ b/ks_includes/graph.py @@ -0,0 +1,165 @@ +import datetime +import gi +import logging +import math + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk, GLib, Pango + + +class HeaterGraph(Gtk.DrawingArea): + def __init__(self, printer): + super().__init__() + self.set_hexpand(True) + self.set_vexpand(True) + self.get_style_context().add_class('heatergraph') + self.printer = printer + self.store = {} + self.max_length = 0 + self.connect('draw', self.draw_graph) + + def add_object(self, name, type, rgb=[0, 0, 0], dashed=False, fill=False): + if name not in self.store: + self.store.update({name: {}}) + self.store[name].update({type: { + "dashed": dashed, + "fill": fill, + "rgb": rgb + }}) + self.max_length = max(self.max_length, len(self.printer.get_temp_store(name, type))) + + def get_max_num(self, data_points=0): + mnum = [] + for x in self.store: + for t in self.store[x]: + mnum.append(max(self.printer.get_temp_store(x, t, data_points))) + return max(mnum) + + def draw_graph(self, da, ctx): + width = da.get_allocated_width() + height = da.get_allocated_height() + + g_width_start = 30 + g_width = width - 5 + g_height_start = 5 + g_height = height - 30 + + ctx.set_source_rgb(.5, .5, .5) + ctx.set_line_width(1) + ctx.set_tolerance(0.1) + + ctx.move_to(g_width_start, g_height_start) + ctx.line_to(g_width, g_height_start) + ctx.line_to(g_width, g_height) + ctx.line_to(g_width_start, g_height) + ctx.line_to(g_width_start, g_height_start) + ctx.stroke() + + ctx.set_source_rgb(1, 0, 0) + ctx.move_to(g_width_start, height) + + gsize = [ + [g_width_start, g_height_start], + [g_width, g_height] + ] + + points_per_pixel = 2 + data_points = (gsize[1][0]-gsize[0][0]) * points_per_pixel + max_num = math.ceil(self.get_max_num(data_points) * 1.1 / 10) * 10 + d_width = 1 / points_per_pixel + + d_height_scale = self.graph_lines(ctx, gsize, max_num) + self.graph_time(ctx, gsize, points_per_pixel) + + for name in self.store: + for type in self.store[name]: + d = self.printer.get_temp_store(name, type, data_points) + if d is False: + continue + self.graph_data(ctx, d, gsize, d_height_scale, d_width, self.store[name][type]["rgb"], + self.store[name][type]["dashed"], self.store[name][type]["fill"]) + + def graph_data(self, ctx, data, gsize, hscale, swidth, rgb, dashed=False, fill=False): + i = 0 + ctx.set_source_rgba(rgb[0], rgb[1], rgb[2], 1) + ctx.move_to(gsize[0][0] + 1, gsize[0][1] - 1) + if dashed: + ctx.set_dash([10, 5]) + else: + ctx.set_dash([1, 0]) + d_len = len(data) - 1 + for d in data: + p_x = i*swidth + gsize[0][0] if i != d_len else gsize[1][0] - 1 + p_y = gsize[1][1] - 1 - (d*hscale) + if i == 0: + ctx.move_to(gsize[0][0]+1, p_y) + i += 1 + continue + ctx.line_to(p_x, p_y) + i += 1 + if fill is False: + ctx.stroke() + return + + ctx.stroke_preserve() + ctx.line_to(gsize[1][0] - 1, gsize[1][1] - 1) + ctx.line_to(gsize[0][0] + 1, gsize[1][1] - 1) + if fill: + ctx.set_source_rgba(rgb[0], rgb[1], rgb[2], .1) + ctx.fill() + + def graph_lines(self, ctx, gsize, max_num): + if max_num <= 30: + nscale = 5 + elif max_num <= 60: + nscale = 10 + elif max_num <= 130: + nscale = 25 + else: + nscale = 50 + # nscale = math.floor((max_num / 10) / 4) * 10 + r = int(max_num/nscale) + 1 + hscale = (gsize[1][1] - gsize[0][1]) / (r * nscale) + + for i in range(r): + ctx.set_source_rgb(.5, .5, .5) + lheight = gsize[1][1] - nscale*i*hscale + ctx.move_to(10, lheight + 3) + ctx.show_text(str(nscale*i).rjust(3, " ")) + ctx.stroke() + ctx.set_source_rgba(.5, .5, .5, .2) + ctx.move_to(gsize[0][0], lheight) + ctx.line_to(gsize[1][0], lheight) + ctx.stroke() + return hscale + + def graph_time(self, ctx, gsize, points_per_pixel): + glen = gsize[1][0] - gsize[0][0] + + now = datetime.datetime.now() + first = gsize[1][0] - ((now.second + ((now.minute % 2) * 60)) / points_per_pixel) + steplen = 120 / points_per_pixel # For 120s + i = 0 + while True: + x = first - i*steplen + if x < gsize[0][0]: + break + ctx.set_source_rgba(.5, .5, .5, .2) + ctx.move_to(x, gsize[0][1]) + ctx.line_to(x, gsize[1][1]) + ctx.stroke() + + ctx.set_source_rgb(.5, .5, .5) + ctx.move_to(x - 15, gsize[1][1] + 10) + + hour = now.hour + min = now.minute - (now.minute % 2) - i*2 + if min < 0: + hour -= 1 + min = 60 + min + if hour < 0: + hour += 24 + + ctx.show_text("%02d:%02d" % (hour, min)) + ctx.stroke() + i += 1 diff --git a/ks_includes/printer.py b/ks_includes/printer.py index d1f1223a..5d0cd4d3 100644 --- a/ks_includes/printer.py +++ b/ks_includes/printer.py @@ -25,6 +25,7 @@ class Printer: self.state = "disconnected" self.state_cb = state_execute_cb self.power_devices = {} + self.store_timeout = False def reinit(self, printer_info, data): logging.debug("Moonraker object status: %s" % data) @@ -35,6 +36,9 @@ class Printer: self.devices = {} self.data = data self.klipper = {} + self.tempstore = {} + if self.store_timeout is False: + GLib.timeout_add_seconds(1, self._update_temp_store) self.klipper = { "version": printer_info['software_version'] @@ -49,6 +53,10 @@ class Printer: "temperature": 0, "target": 0 } + self.tempstore[x] = { + "temperatures": [0 for x in range(1200)], + "targets": [0 for x in range(1200)] + } self.tools.append(x) self.tools = sorted(self.tools) self.toolcount += 1 @@ -61,6 +69,10 @@ class Printer: "temperature": 0, "target": 0 } + self.tempstore[x] = { + "temperatures": [0 for x in range(1200)], + "targets": [0 for x in range(1200)] + } if x.startswith('bed_mesh '): r = self.config[x] r['x_count'] = int(r['x_count']) @@ -270,6 +282,24 @@ class Printer: def get_extruder_count(self): return self.extrudercount + def get_temp_store(self, device, section=False, results=0): + if device not in self.tempstore: + return False + + if section is not False: + if section not in self.tempstore[device]: + return False + if results == 0 or results >= len(self.tempstore[device][section]): + return self.tempstore[device][section] + return self.tempstore[device][section][-results:] + + temp = {} + for section in self.tempstore[device]: + if results == 0 or results >= len(self.tempstore[device][section]): + temp[section] = self.tempstore[device][section] + temp[section] = self.tempstore[device][section][-results:] + return temp + def get_tools(self): return self.tools @@ -280,6 +310,14 @@ class Printer: if "heater_bed" in self.devices: return True + def init_temp_store(self, result): + for dev in result: + if dev in self.tempstore: + if "targets" in result[dev]: + self.tempstore[dev]["targets"] = result[dev]["targets"] + if "temperatures" in result[dev]: + self.tempstore[dev]["temperatures"] = result[dev]["temperatures"] + def section_exists(self, section): if section in self.get_config_section_list(): return True @@ -295,3 +333,11 @@ class Printer: return self.devices[dev][stat] = value + + def _update_temp_store(self): + for device in self.tempstore: + for x in self.tempstore[device]: + t = len(self.tempstore[device][x]) - 1 + self.tempstore[device][x].pop(0) + self.tempstore[device][x].append(round(self.get_dev_stat(device, x[:-1]), 2)) + return True diff --git a/ks_includes/screen_panel.py b/ks_includes/screen_panel.py index dc796a50..26a322b6 100644 --- a/ks_includes/screen_panel.py +++ b/ks_includes/screen_panel.py @@ -40,6 +40,21 @@ class ScreenPanel: else: self._screen._ws.klippy.emergency_stop() + def format_target(self, temp): + _ = self.lang.gettext + + if temp <= 0: + return _("Off") + else: + return self.format_temp(temp, 0) + + def format_temp(self, temp, places=1): + if places == 0: + n = int(temp) + else: + n = round(temp, places) + return "%s°C" % str(n) + def get(self): return self.layout diff --git a/panels/main_menu.py b/panels/main_menu.py index 6d8aec45..a3de604a 100644 --- a/panels/main_menu.py +++ b/panels/main_menu.py @@ -1,10 +1,12 @@ +import datetime import gi +import math import logging gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, GLib - +from gi.repository import Gtk, Gdk, GLib, Pango from panels.menu import MenuPanel +from ks_includes.graph import HeaterGraph def create_panel(*args): return MainPanel(*args) @@ -12,6 +14,8 @@ def create_panel(*args): class MainPanel(MenuPanel): def __init__(self, screen, title, back=False): super().__init__(screen, title, False) + self.devices = {} + self.graph_update = None def initialize(self, panel_name, items, extrudercount): print("### Making MainMenu") @@ -20,11 +24,124 @@ class MainPanel(MenuPanel): grid.set_hexpand(True) grid.set_vexpand(True) - # Create Extruders and bed icons - eq_grid = Gtk.Grid() - eq_grid.set_hexpand(True) - eq_grid.set_vexpand(True) + self.items = items + self.create_menu_items() + self.grid = Gtk.Grid() + self.grid.set_row_homogeneous(True) + self.grid.set_column_homogeneous(True) + + leftpanel = self.create_left_panel() + grid.attach(leftpanel, 0, 0, 1, 1) + grid.attach(self.arrangeMenuItems(items, 2, True), 1, 0, 1, 1) + + self.grid = grid + + self.content.add(self.grid) + self.layout.show_all() + + def activate(self): + if self.graph_update is None: + self.graph_update = GLib.timeout_add_seconds(1, self.update_graph) + return + + def deactivate(self): + if self.graph_update is not None: + GLib.source_remove(self.graph_update) + self.graph_update = None + + def add_device(self, device): + logging.info("Adding device: %s" % device) + + if not (device.startswith("extruder") or device.startswith("heater_bed")): + devname = " ".join(device.split(" ")[1:]) + else: + devname = device + + if device.startswith("extruder"): + i = 0 + for d in self.devices: + if d.startswith('extruder'): + i += 1 + image = "extruder-%s" % i + elif device == "heater_bed": + image = "bed" + devname = "Heater Bed" + else: + image = "heat-up" + + name = self._gtk.ImageLabel(image, devname.capitalize(), 20, False, .5, .5) + name['b'].set_hexpand(True) + + temp = Gtk.Label("") + temp.set_markup(self.format_temp(self._printer.get_dev_stat(device, "temperature"))) + target = Gtk.Label("") + target.set_markup(self.format_target(self._printer.get_dev_stat(device, "target"))) + + labels = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + + dev = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + dev.set_hexpand(True) + dev.set_vexpand(False) + dev.add(labels) + + self.devices[device] = { + "name": name, + "target": target, + "temp": temp, + + } + + devices = sorted(self.devices) + pos = devices.index(device) + 1 + + self.labels['devices'].insert_row(pos) + self.labels['devices'].attach(name['b'], 0, pos, 1, 1) + self.labels['devices'].attach(temp, 1, pos, 1, 1) + self.labels['devices'].attach(target, 2, pos, 1, 1) + self.labels['devices'].show_all() + + def create_left_panel(self): + _ = self.lang.gettext + + self.labels['devices'] = Gtk.Grid() + self.labels['devices'].get_style_context().add_class('heater-grid') + self.labels['devices'].set_vexpand(False) + + name = Gtk.Label("") + temp = Gtk.Label(_("Temp")) + temp.set_size_request(round(self._gtk.get_font_size() * 5.5), 0) + target = Gtk.Label(_("Target")) + + self.labels['devices'].attach(name, 0, 0, 1, 1) + self.labels['devices'].attach(temp, 1, 0, 1, 1) + self.labels['devices'].attach(target, 2, 0, 1, 1) + + rgbs = [ + [0, 1, 0], + [1, 0, 0], + [0, 0, 1] + ] + heaters = ['heater_bed', 'extruder'] + i = 0 + da = HeaterGraph(self._printer) + da.set_vexpand(True) + for h in heaters: + da.add_object(h, "temperatures", rgbs[i], False, True) + da.add_object(h, "targets", rgbs[i], True, False) + i += 1 + self.labels['da'] = da + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + box.set_vexpand(True) + box.add(self.labels['devices']) + box.add(da) + + self.load_devices() + + return box + + def load_devices(self): self.heaters = [] i = 0 @@ -42,34 +159,9 @@ class MainPanel(MenuPanel): self.labels[h] = self._gtk.ButtonImage("heat-up", name) self.heaters.append(h) - i = 0 - cols = 3 if len(self.heaters) > 4 else (1 if len(self.heaters) <= 2 else 2) - for h in self.heaters: - eq_grid.attach(self.labels[h], i % cols, int(i/cols), 1, 1) - i += 1 - - self.items = items - self.create_menu_items() - - self.grid = Gtk.Grid() - self.grid.set_row_homogeneous(True) - self.grid.set_column_homogeneous(True) - - grid.attach(eq_grid, 0, 0, 1, 1) - grid.attach(self.arrangeMenuItems(items, 2, True), 1, 0, 1, 1) - - self.grid = grid - - self.target_temps = { - "heater_bed": 0, - "extruder": 0 - } - - self.content.add(self.grid) - self.layout.show_all() - - def activate(self): - return + for d in self.heaters: + self.add_device(d) + logging.info("Heaters: %s" % self.heaters) def process_update(self, action, data): if action != "notify_status_update": @@ -86,6 +178,21 @@ class MainPanel(MenuPanel): h, self._printer.get_dev_stat(h, "temperature"), self._printer.get_dev_stat(h, "target"), - None if h == "heater_bed" else " ".join(h.split(" ")[1:]) ) return + + def update_graph(self): + self.labels['da'].queue_draw() + alloc = self.labels['devices'].get_allocation() + logging.info("Devices height: %s" % alloc.height) + alloc = self.labels['da'].get_allocation() + logging.info("DA height: %s" % alloc.height) + return True + + def update_temp(self, device, temp, target): + if device not in self.devices: + return + + self.devices[device]["temp"].set_markup(self.format_temp(temp)) + if "target" in self.devices[device]: + self.devices[device]["target"].set_markup(self.format_target(target)) diff --git a/screen.py b/screen.py index b9e67f01..cfb869a0 100644 --- a/screen.py +++ b/screen.py @@ -501,12 +501,16 @@ class KlipperScreen(Gtk.Window): def _remove_current_panel(self, pop=True, show=True): if len(self._cur_panels) > 0: self.base_panel.remove(self.panels[self._cur_panels[-1]].get_content()) + if hasattr(self.panels[self._cur_panels[-1]], "deactivate"): + self.panels[self._cur_panels[-1]].deactivate() self.remove_subscription(self._cur_panels[-1]) if pop is True: self._cur_panels.pop() if len(self._cur_panels) > 0: self.base_panel.add_content(self.panels[self._cur_panels[-1]]) self.base_panel.show_back(False if len(self._cur_panels) == 1 else True) + if hasattr(self.panels[self._cur_panels[-1]], "activate"): + self.panels[self._cur_panels[-1]].activate() if hasattr(self.panels[self._cur_panels[-1]], "process_update"): self.panels[self._cur_panels[-1]].process_update("notify_status_update", self.printer.get_updates()) @@ -842,7 +846,10 @@ class KlipperScreen(Gtk.Window): if data is False: logging.info("Error getting printer object data") return False - logging.info("Startup data: %s" % data['result']['status']) + + tempstore = self.apiclient.send_request("server/temperature_store") + if tempstore is not False: + self.printer.init_temp_store(tempstore['result']) self.printer.process_update(data['result']['status']) self.files.initialize() diff --git a/styles/base.css b/styles/base.css index eaca36e2..361bf1e8 100644 --- a/styles/base.css +++ b/styles/base.css @@ -146,7 +146,7 @@ scrollbar, scrollbar button, scrollbar trough { } scrollbar slider { - min-width: 2.5em; + min-width: 1.5em; border-radius: .7em; background-color: #404E57; } @@ -221,6 +221,18 @@ trough { margin-left: 0; } +.extruder-0 { + color: #ff0000; +} + +.extruder-1 { + color: #00ff00; +} + +.extruder-2 { + color: #0000ff; +} + .fan_slider { margin: 0 1em 0 1em; color: white; @@ -231,9 +243,13 @@ trough { padding: .2em .3em; } -.updater-item { - min-height: 3em; - padding: .2em; +.heatergraph { + min-height: 350px; +} + +.heater-grid label { + margin-top: .3em; + margin-bottom: .3em; } .message_popup { @@ -375,6 +391,11 @@ trough { margin-top: 0; } +.updater-item { + min-height: 3em; + padding: .2em; +} + .message { border: .1em solid #981E1F; font-size: 1em;