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:
parent
0ca410acba
commit
ce6158ad91
65
ks_includes/widgets/bedmap.py
Normal file
65
ks_includes/widgets/bedmap.py
Normal 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]
|
@ -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)
|
||||
|
@ -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"],
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user