bed_mesh: improvements an changes:

use current profiles instead of the ones saved in the config file
remove matplotlib and numpy, caused many intall issues, graph was slow and not great for small screens
create a custom 2D graph to show the probed matrix
This commit is contained in:
alfrix 2022-11-07 19:04:40 -03:00
parent 0ca410acba
commit ce6158ad91
4 changed files with 141 additions and 133 deletions

View File

@ -0,0 +1,65 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
class BedMap(Gtk.DrawingArea):
def __init__(self, font_size, bm):
super().__init__()
self.set_hexpand(True)
self.set_vexpand(True)
self.connect('draw', self.draw_graph)
self.font_size = font_size
self.font_spacing = round(self.font_size * 1.5)
self.bm = list(reversed(bm)) if bm is not None else None
def update_bm(self, bm):
self.bm = list(reversed(bm)) if bm is not None else None
def draw_graph(self, da, ctx):
width = da.get_allocated_width()
height = da.get_allocated_height()
# Styling
ctx.set_line_width(1)
ctx.set_font_size(self.font_size)
if self.bm is None:
ctx.move_to(self.font_spacing, height / 2)
ctx.set_source_rgb(0.5, 0.5, 0.5)
ctx.show_text(_("No mesh has been loaded"))
ctx.stroke()
return
rows = len(self.bm)
columns = len(self.bm[0])
for i, row in enumerate(self.bm):
ty = height / rows * i
by = ty + height / rows
for j, column in enumerate(row):
lx = width / columns * j
rx = lx + width / columns
# Colors
ctx.set_source_rgb(*self.colorbar(column))
ctx.move_to(lx, ty)
ctx.line_to(lx, by)
ctx.line_to(rx, by)
ctx.line_to(rx, ty)
ctx.close_path()
ctx.fill()
ctx.stroke()
# Numbers
ctx.set_source_rgb(0, 0, 0)
ctx.move_to((lx + rx) / 2 - self.font_size, (ty + by + self.font_size) / 2)
ctx.show_text(f"{column:.2f}")
ctx.stroke()
@staticmethod
def colorbar(value):
rmax = 0.25
color = min(1, max(0, 1 - 1 / rmax * abs(value)))
if value > 0:
return [1, color, color]
if value < 0:
return [color, color, 1]
return [1, 1, 1]

View File

@ -1,20 +1,13 @@
import gi
import logging
import contextlib
import numpy as np
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Pango
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib import rc
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
from matplotlib.ticker import LinearLocator
from ks_includes.KlippyGcodes import KlippyGcodes
from ks_includes.screen_panel import ScreenPanel
from ks_includes.widgets.bedmap import BedMap
def create_panel(*args):
@ -25,6 +18,7 @@ class BedMeshPanel(ScreenPanel):
def __init__(self, screen, title, back=True):
super().__init__(screen, title, back)
self.clear = None
self.profiles = {}
self.show_create = False
self.active_mesh = None
@ -34,9 +28,9 @@ class BedMeshPanel(ScreenPanel):
addprofile = self._gtk.ButtonImage("increase", " " + _("Add profile"), "color1", .66, Gtk.PositionType.LEFT, 1)
addprofile.connect("clicked", self.show_create_profile)
addprofile.set_hexpand(True)
clear = self._gtk.ButtonImage("cancel", " " + _("Clear"), "color2", .66, Gtk.PositionType.LEFT, 1)
clear.connect("clicked", self._clear_mesh)
clear.set_hexpand(True)
self.clear = self._gtk.ButtonImage("cancel", " " + _("Clear"), "color2", .66, Gtk.PositionType.LEFT, 1)
self.clear.connect("clicked", self.send_clear_mesh)
self.clear.set_hexpand(True)
calibrate = self._gtk.ButtonImage("refresh", " " + _("Calibrate"), "color3", .66, Gtk.PositionType.LEFT, 1)
calibrate.connect("clicked", self.calibrate_mesh)
calibrate.set_hexpand(True)
@ -45,7 +39,7 @@ class BedMeshPanel(ScreenPanel):
topbar.set_hexpand(True)
topbar.set_vexpand(False)
topbar.add(addprofile)
topbar.add(clear)
topbar.add(self.clear)
topbar.add(calibrate)
# Create a grid for all profiles
@ -57,53 +51,81 @@ class BedMeshPanel(ScreenPanel):
scroll.add(self.labels['profiles'])
scroll.set_vexpand(True)
# Create a box to contain all of the above
self.labels['main_box'] = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.labels['main_box'].set_vexpand(True)
self.labels['main_box'].pack_start(topbar, False, False, 0)
self.labels['main_box'].pack_end(scroll, True, True, 0)
self.load_meshes()
self.content.add(self.labels['main_box'])
grid = self._gtk.HomogeneousGrid()
grid.set_row_homogeneous(False)
grid.attach(topbar, 0, 0, 2, 1)
self.labels['map'] = BedMap(self._gtk.get_font_size(), self.active_mesh)
grid.attach(self.labels['map'], 0, 2, 1, 1)
grid.attach(scroll, 1, 2, 1, 1)
self.labels['main_grid'] = grid
self.content.add(self.labels['main_grid'])
def activate(self):
self.load_meshes()
with contextlib.suppress(KeyError):
self.activate_mesh(self._screen.printer.get_stat("bed_mesh", "profile_name"))
def activate_mesh(self, profile):
if self.active_mesh is not None:
self.profiles[self.active_mesh]['name'].set_sensitive(True)
self.profiles[self.active_mesh]['name'].get_style_context().remove_class("button_active")
if profile == "":
logging.info("Clearing active profile")
self.profiles[self.active_mesh]['button_box'].add(self.profiles[self.active_mesh]['load'])
self.active_mesh = None
self.update_graph()
self.clear.set_sensitive(False)
return
if profile not in self.profiles:
self.add_profile(profile)
logging.info(f"Active {self.active_mesh} changing to {profile}")
self.profiles[profile]['button_box'].remove(self.profiles[profile]['load'])
self.profiles[profile]['name'].set_sensitive(False)
self.profiles[profile]['name'].get_style_context().add_class("button_active")
self.active_mesh = profile
self.update_graph(profile=profile)
self.clear.set_sensitive(True)
def retrieve_bm(self, profile):
if profile is None:
return None
if profile == self.active_mesh:
bm = self._printer.get_stat("bed_mesh")
if bm is None:
logging.info(f"Unable to load active mesh: {profile}")
return None
matrix = 'probed_matrix'
else:
bm = self._printer.get_config_section(f"bed_mesh {profile}")
if bm is False:
logging.info(f"Unable to load profile: {profile}")
self.remove_profile(profile)
return None
matrix = 'points'
return bm[matrix]
def update_graph(self, widget=None, profile=None):
self.labels['map'].update_bm(self.retrieve_bm(profile))
self.labels['map'].queue_draw()
def add_profile(self, profile):
logging.debug(f"Adding Profile: {profile}")
name = Gtk.Label()
name.set_markup(f"<big><b>{profile}</b></big>")
name.set_hexpand(True)
name = self._gtk.Button(f"<big><b>{profile}</b></big>")
name.get_children()[0].set_use_markup(True)
name.get_children()[0].set_line_wrap(True)
name.get_children()[0].set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
name.set_vexpand(False)
name.set_halign(Gtk.Align.START)
name.set_line_wrap(True)
name.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
name.connect("clicked", self.send_load_mesh, profile)
name.connect("clicked", self.update_graph, profile)
buttons = {
"save": self._gtk.ButtonImage("complete", _("Save"), "color3"),
"delete": self._gtk.ButtonImage("cancel", _("Delete"), "color3"),
"view": self._gtk.ButtonImage("bed-level", _("View Mesh"), "color1"),
"load": self._gtk.ButtonImage("load", _("Load"), "color2"),
"save": self._gtk.ButtonImage("complete", None, "color4", .75),
"delete": self._gtk.ButtonImage("cancel", None, "color2", .75),
}
buttons["save"].connect("clicked", self.send_save_mesh, profile)
buttons["delete"].connect("clicked", self.send_remove_mesh, profile)
buttons["view"].connect("clicked", self.show_mesh, profile)
buttons["load"].connect("clicked", self.send_load_mesh, profile)
for b in buttons.values():
b.set_hexpand(False)
@ -114,8 +136,6 @@ class BedMeshPanel(ScreenPanel):
if profile != "default":
button_box.add(buttons["save"])
button_box.add(buttons["delete"])
button_box.add(buttons["view"])
button_box.add(buttons["load"])
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
box.pack_start(name, True, True, 0)
@ -126,12 +146,11 @@ class BedMeshPanel(ScreenPanel):
frame.add(box)
self.profiles[profile] = {
"name": name,
"button_box": button_box,
"row": frame,
"load": buttons["load"],
"save": buttons["save"],
"delete": buttons["delete"],
"view": buttons["view"],
}
pl = list(self.profiles)
@ -151,10 +170,14 @@ class BedMeshPanel(ScreenPanel):
return False
def load_meshes(self):
bm_profiles = self._screen.printer.get_config_section_list("bed_mesh ")
bm_profiles = self._printer.get_stat("bed_mesh", "profiles")
logging.info(f"Bed profiles: {bm_profiles}")
for prof in bm_profiles:
self.add_profile(prof[9:])
if prof not in self.profiles:
self.add_profile(prof)
for prof in self.profiles:
if prof not in bm_profiles:
self.remove_profile(prof)
def process_update(self, action, data):
if action == "notify_status_update":
@ -171,7 +194,7 @@ class BedMeshPanel(ScreenPanel):
self.content.remove(child)
self.show_create = False
self.content.add(self.labels['main_box'])
self.content.add(self.labels['main_grid'])
self.content.show()
def remove_profile(self, profile):
@ -185,6 +208,10 @@ class BedMeshPanel(ScreenPanel):
pos = profiles.index(profile) + 1 if profile != "default" else 0
self.labels['profiles'].remove_row(pos)
del self.profiles[profile]
if not self.profiles:
self.active_mesh = None
self.update_graph()
self.clear.set_sensitive(False)
def show_create_profile(self, widget):
@ -223,85 +250,14 @@ class BedMeshPanel(ScreenPanel):
def _show_keyboard(self, widget=None, event=None):
self._screen.show_keyboard(entry=self.labels['profile_name'])
def show_mesh(self, widget, profile):
if profile == self.active_mesh:
bm = self._printer.get_stat("bed_mesh")
if bm is None:
logging.info(f"Unable to load active mesh: {profile}")
return
matrix = 'probed_matrix'
# if 'mesh_matrix' in bm and bm['mesh_matrix'][0]:
# matrix = 'mesh_matrix'
x_range = [int(bm['mesh_min'][0]), int(bm['mesh_max'][0])]
y_range = [int(bm['mesh_min'][1]), int(bm['mesh_max'][1])]
else:
bm = self._printer.get_config_section(f"bed_mesh {profile}")
if bm is False:
logging.info(f"Unable to load profile: {profile}")
self.remove_profile(profile)
return
matrix = 'points'
x_range = [int(bm['min_x']), int(bm['max_x'])]
y_range = [int(bm['min_y']), int(bm['max_y'])]
# Zscale can be offered as a slider instead of hardcoded values reasonable values 0.5 - 2 (mm)
z_range = [min(min(min(bm[matrix])), -1), max(max(max(bm[matrix])), 1)]
counts = [len(bm[matrix][0]), len(bm[matrix])]
deltas = [(x_range[1] - x_range[0]) / (counts[0] - 1), (y_range[1] - y_range[0]) / (counts[1] - 1)]
x = [(i * deltas[0]) + x_range[0] for i in range(counts[0])]
y = [(i * deltas[0]) + y_range[0] for i in range(counts[1])]
x, y = np.meshgrid(x, y)
z = np.asarray(bm[matrix])
rc('axes', edgecolor="#e2e2e2", labelcolor="#e2e2e2")
rc(('xtick', 'ytick'), color="#e2e2e2")
fig = plt.figure(facecolor='#12121277')
ax = Axes3D(fig, azim=245, elev=23)
ax.set(title=profile, xlabel="X", ylabel="Y", facecolor='none')
ax.spines['bottom'].set_color("#e2e2e2")
fig.add_axes(ax)
# Color gradient could also be configurable as a slider reasonable values 0.1 - 0.2 (mm)
surf = ax.plot_surface(x, y, z, cmap=cm.coolwarm, vmin=-0.2, vmax=0.2)
chartbox = ax.get_position()
ax.set_position([chartbox.x0, chartbox.y0 + 0.1, chartbox.width * .92, chartbox.height])
ax.set_zlim(z_range[0], z_range[1])
ax.zaxis.set_major_locator(LinearLocator(5))
# A StrMethodFormatter is used automatically
ax.zaxis.set_major_formatter('{x:.02f}')
fig.colorbar(surf, shrink=0.7, aspect=5, pad=0.25)
title = Gtk.Label()
title.set_markup(f"<b>{profile}</b>")
title.set_hexpand(True)
title.set_halign(Gtk.Align.CENTER)
canvas = FigureCanvas(fig)
canvas.set_size_request(self._screen.width * .9, self._screen.height / 3 * 2)
# Remove the "matplotlib-canvas" class which forces a white background.
# https://github.com/matplotlib/matplotlib/commit/3c832377fb4c4b32fcbdbc60fdfedb57296bc8c0
style_ctx = canvas.get_style_context()
for css_class in style_ctx.list_classes():
style_ctx.remove_class(css_class)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.add(title)
box.add(canvas)
box.show_all()
buttons = [
{"name": _("Close"), "response": Gtk.ResponseType.CANCEL}
]
self._gtk.Dialog(self._screen, buttons, box, self._close_dialog)
@staticmethod
def _close_dialog(widget, response):
widget.destroy()
def create_profile(self, widget):
name = self.labels['profile_name'].get_text()
if " " in name:
name = f'"{name}"'
if self.active_mesh is None:
self.calibrate_mesh(None)
self._screen._ws.klippy.gcode_script(f"BED_MESH_PROFILE SAVE={name}")
self.remove_create()
@ -311,31 +267,21 @@ class BedMeshPanel(ScreenPanel):
if self._screen.printer.get_stat("toolhead", "homed_axes") != "xyz":
self._screen._ws.klippy.gcode_script(KlippyGcodes.HOME)
self._screen._ws.klippy.gcode_script(
"BED_MESH_CALIBRATE"
)
self._screen._ws.klippy.gcode_script("BED_MESH_CALIBRATE")
# Load zcalibrate to do a manual mesh
if not (self._printer.config_section_exists("probe") or self._printer.config_section_exists("bltouch")):
self.menu_item_clicked(widget, "refresh", {"name": "Mesh calibrate", "panel": "zcalibrate"})
self.menu_item_clicked(widget, "refresh", {"name": _("Mesh calibrate"), "panel": "zcalibrate"})
def _clear_mesh(self, widget):
self._screen._ws.klippy.gcode_script(
"BED_MESH_CLEAR"
)
def send_clear_mesh(self, widget):
self._screen._ws.klippy.gcode_script("BED_MESH_CLEAR")
def send_load_mesh(self, widget, profile):
self._screen._ws.klippy.gcode_script(
KlippyGcodes.bed_mesh_load(profile)
)
self._screen._ws.klippy.gcode_script(KlippyGcodes.bed_mesh_load(profile))
def send_save_mesh(self, widget, profile):
self._screen._ws.klippy.gcode_script(
KlippyGcodes.bed_mesh_save(profile)
)
self._screen._ws.klippy.gcode_script(KlippyGcodes.bed_mesh_save(profile))
def send_remove_mesh(self, widget, profile):
self._screen._ws.klippy.gcode_script(
KlippyGcodes.bed_mesh_remove(profile)
)
self._screen._ws.klippy.gcode_script(KlippyGcodes.bed_mesh_remove(profile))
self.remove_profile(profile)

View File

@ -10,7 +10,6 @@ import os
import signal
import subprocess
import pathlib
import traceback # noqa
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib, Pango
@ -244,7 +243,7 @@ class KlipperScreen(Gtk.Window):
def ws_subscribe(self):
requested_updates = {
"objects": {
"bed_mesh": ["profile_name", "mesh_max", "mesh_min", "probed_matrix"],
"bed_mesh": ["profile_name", "mesh_max", "mesh_min", "probed_matrix", "profiles"],
"configfile": ["config"],
"display_status": ["progress", "message"],
"fan": ["speed"],

View File

@ -1,6 +1,4 @@
numpy==1.21.4
jinja2==3.1.2
matplotlib==3.5.0
netifaces==0.11.0
requests==2.28.1
websocket-client==1.4.2