# ########################################################## # FlatCAM Evo: 2D Post-processing for Manufacturing # # File by: Marius Adrian Stanciu (c) # # Date: 11/12/2020 # # License: MIT Licence # # ########################################################## from PyQt6 import QtWidgets, QtCore, QtGui from PyQt6.QtCore import Qt from appTool import AppTool from appGUI.GUIElements import VerticalScrollArea, FCLabel, FCButton, FCFrame, GLay, FCComboBox, FCCheckBox, \ FCJog, RadioSet, FCDoubleSpinner, FCSpinner, FCFileSaveDialog, FCDetachableTab, FCTable, \ FCZeroAxes, FCSliderWithDoubleSpinner, FCEntry, RotatedToolButton import logging from copy import deepcopy import sys from shapely import Point, MultiPoint, MultiPolygon, box from shapely.ops import unary_union from shapely.affinity import translate from datetime import datetime as dt import gettext import appTranslation as fcTranslate import builtins from appObjects.AppObjectTemplate import ObjectDeleted from appGUI.VisPyVisuals import * from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy from appEditors.AppTextEditor import AppTextEditor from camlib import CNCjob import time import serial import glob import random from io import StringIO from matplotlib.backend_bases import KeyEvent as mpl_key_event # try: # from foronoi import Voronoi # from foronoi import Polygon as voronoi_poly # VORONOI_ENABLED = True # except Exception: # try: # from shapely.ops import voronoi_diagram # VORONOI_ENABLED = True # # from appCommon.Common import voronoi_diagram # except Exception: # VORONOI_ENABLED = False try: from shapely.ops import voronoi_diagram VORONOI_ENABLED = True # from appCommon.Common import voronoi_diagram except Exception: VORONOI_ENABLED = False fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: _ = gettext.gettext log = logging.getLogger('base') class ToolLevelling(AppTool, CNCjob): build_al_table_sig = QtCore.pyqtSignal() def __init__(self, app): self.app = app self.decimals = self.app.decimals AppTool.__init__(self, app) CNCjob.__init__(self, steps_per_circle=self.app.options["cncjob_steps_per_circle"]) # updated in the self.set_tool_ui() self.form_fields = {} self.first_click = False self.cursor_pos = None # if mouse is dragging set the object True self.mouse_is_dragging = False # if mouse events are bound to local methods self.mouse_events_connected = False # event handlers references self.kp = None self.mm = None self.mr = None self.probing_gcode_text = '' self.grbl_probe_result = '' ''' dictionary of dictionaries to store the information's for the autolevelling format when using Voronoi diagram: { id: { 'point': Shapely Point 'geo': Shapely Polygon from Voronoi diagram, 'height': float } } ''' self.al_voronoi_geo_storage = {} ''' list of (x, y, x) tuples to store the information's for the autolevelling format when using bilinear interpolation: [(x0, y0, z0), (x1, y1, z1), ...] ''' self.al_bilinear_geo_storage = [] self.solid_geo = None self.grbl_ser_port = None self.probing_shapes = None self.gcode_viewer_tab = None # store the current selection shape status to be restored after manual adding test points self.old_selection_state = self.app.options['global_selection_shape'] # ############################################################################# # ######################### Tool GUI ########################################## # ############################################################################# self.ui = LevelUI(layout=self.layout, app=self.app) self.pluginName = self.ui.pluginName self.connect_signals_at_init() def install(self, icon=None, separator=None, **kwargs): AppTool.install(self, icon, separator, shortcut='', **kwargs) def run(self, toggle=True): self.app.defaults.report_usage("ToolLevelling()") if toggle: # if the splitter is hidden, display it if self.app.ui.splitter.sizes()[0] == 0: self.app.ui.splitter.setSizes([1, 1]) # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same found_idx = None for idx in range(self.app.ui.notebook.count()): if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": found_idx = idx break # show the Tab if not found_idx: try: self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) except RuntimeError: self.app.ui.plugin_tab = QtWidgets.QWidget() self.app.ui.plugin_tab.setObjectName("plugin_tab") self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) self.app.ui.plugin_scroll_area = VerticalScrollArea() self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) # focus on Tool Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) try: if self.app.ui.plugin_scroll_area.widget().objectName() == self.pluginName and found_idx: # if the Tool Tab is not focused, focus on it if not self.app.ui.notebook.currentWidget() is self.app.ui.plugin_tab: # focus on Tool Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) else: # else remove the Tool Tab self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) self.app.ui.notebook.removeTab(2) # if there are no objects loaded in the app then hide the Notebook widget if not self.app.collection.get_list(): self.app.ui.splitter.setSizes([0, 1]) except AttributeError: pass else: if self.app.ui.splitter.sizes()[0] == 0: self.app.ui.splitter.setSizes([1, 1]) super().run() self.set_tool_ui() self.app.ui.notebook.setTabText(2, _("Levelling")) def connect_signals_at_init(self): self.build_al_table_sig.connect(self.build_al_table) self.ui.level.toggled.connect(self.on_level_changed) self.ui.avoid_exc_holes_cb.toggled.connect(self.on_avoid_exc_holes) self.ui.al_mode_radio.activated_custom.connect(self.on_mode_radio) self.ui.al_method_radio.activated_custom.connect(self.on_method_radio) self.ui.al_controller_combo.currentIndexChanged.connect(self.on_controller_change) self.ui.plot_probing_pts_cb.toggled.connect(self.show_probing_geo) # GRBL self.ui.com_search_button.clicked.connect(self.on_grbl_search_ports) self.ui.add_bd_button.clicked.connect(self.on_grbl_add_baudrate) self.ui.del_bd_button.clicked.connect(self.on_grbl_delete_baudrate_grbl) self.ui.controller_reset_button.clicked.connect(self.on_grbl_reset) self.ui.com_connect_button.clicked.connect(self.on_grbl_connect) self.ui.grbl_send_button.clicked.connect(self.on_grbl_send_command) self.ui.grbl_command_entry.returnPressed.connect(self.on_grbl_send_command) # Jog self.ui.jog_wdg.jog_up_button.clicked.connect(lambda: self.on_grbl_jog(direction='yplus')) self.ui.jog_wdg.jog_down_button.clicked.connect(lambda: self.on_grbl_jog(direction='yminus')) self.ui.jog_wdg.jog_right_button.clicked.connect(lambda: self.on_grbl_jog(direction='xplus')) self.ui.jog_wdg.jog_left_button.clicked.connect(lambda: self.on_grbl_jog(direction='xminus')) self.ui.jog_wdg.jog_z_up_button.clicked.connect(lambda: self.on_grbl_jog(direction='zplus')) self.ui.jog_wdg.jog_z_down_button.clicked.connect(lambda: self.on_grbl_jog(direction='zminus')) self.ui.jog_wdg.jog_origin_button.clicked.connect(lambda: self.on_grbl_jog(direction='origin')) # Zero self.ui.zero_axs_wdg.grbl_zerox_button.clicked.connect(lambda: self.on_grbl_zero(axis='x')) self.ui.zero_axs_wdg.grbl_zeroy_button.clicked.connect(lambda: self.on_grbl_zero(axis='y')) self.ui.zero_axs_wdg.grbl_zeroz_button.clicked.connect(lambda: self.on_grbl_zero(axis='z')) self.ui.zero_axs_wdg.grbl_zero_all_button.clicked.connect(lambda: self.on_grbl_zero(axis='all')) self.ui.zero_axs_wdg.grbl_homing_button.clicked.connect(self.on_grbl_homing) # Sender self.ui.grbl_report_button.clicked.connect(lambda: self.send_grbl_command(command='?')) self.ui.grbl_get_param_button.clicked.connect( lambda: self.on_grbl_get_parameter(param=self.ui.grbl_parameter_entry.get_value())) self.ui.view_h_gcode_button.clicked.connect(self.on_edit_probing_gcode) self.ui.h_gcode_button.clicked.connect(self.on_save_probing_gcode) self.ui.import_heights_button.clicked.connect(self.on_import_height_map) self.ui.pause_resume_button.clicked.connect(self.on_grbl_pause_resume) self.ui.grbl_get_heightmap_button.clicked.connect(self.on_grbl_autolevel) self.ui.grbl_save_height_map_button.clicked.connect(self.on_grbl_heightmap_save) # When object selection on canvas change self.app.proj_selection_changed.connect(self.on_object_selection_changed) # Reset Tool self.ui.reset_button.clicked.connect(self.set_tool_ui) # Cleanup on Graceful exit (CTRL+ALT+X combo key) self.app.cleanup.connect(self.set_tool_ui) def set_tool_ui(self): self.units = self.app.app_units.upper() self.clear_ui(self.layout) self.ui = LevelUI(layout=self.layout, app=self.app) self.pluginName = self.ui.pluginName self.connect_signals_at_init() # try to select in the CNCJob combobox the active object try: selected_obj = self.app.collection.get_active() if selected_obj.kind == 'cncjob': current_name = selected_obj.obj_options['name'] self.ui.object_combo.set_value(current_name) except Exception: pass loaded_obj = self.app.collection.get_by_name(self.ui.object_combo.get_value()) if loaded_obj and loaded_obj.kind == 'cncjob': name = loaded_obj.obj_options['name'] else: name = '' # Shapes container for the Voronoi cells in Autolevelling if self.app.use_3d_engine: self.probing_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1, pool=self.app.pool) else: self.probing_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=name + "_probing_shapes") self.form_fields.update({ "tools_al_probe_tip_dia": self.ui.probe_tip_dia_entry, "tools_al_travel_z": self.ui.ptravelz_entry, "tools_al_probe_depth": self.ui.pdepth_entry, "tools_al_probe_fr": self.ui.feedrate_probe_entry, "tools_al_controller": self.ui.al_controller_combo, "tools_al_method": self.ui.al_method_radio, "tools_al_mode": self.ui.al_mode_radio, "tools_al_avoid_exc_holes_size": self.ui.avoid_exc_holes_size_entry, "tools_al_rows": self.ui.al_rows_entry, "tools_al_columns": self.ui.al_columns_entry, "tools_al_grbl_jog_step": self.ui.jog_step_entry, "tools_al_grbl_jog_fr": self.ui.jog_fr_entry, }) # Fill Form fields self.to_form() self.on_controller_change_alter_ui() self.ui.plot_probing_pts_cb.set_value(self.app.options["tools_al_plot_points"]) self.ui.avoid_exc_holes_cb.set_value(self.app.options["tools_al_avoid_exc_holes"]) self.ui.al_probe_points_table.setRowCount(0) self.ui.al_probe_points_table.resizeColumnsToContents() self.ui.al_probe_points_table.resizeRowsToContents() v_header = self.ui.al_probe_points_table.verticalHeader() v_header.hide() self.ui.al_probe_points_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) h_header = self.ui.al_probe_points_table.horizontalHeader() h_header.setMinimumSectionSize(10) h_header.setDefaultSectionSize(70) h_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Fixed) h_header.resizeSection(0, 20) h_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Stretch) h_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) self.ui.al_probe_points_table.setMinimumHeight(self.ui.al_probe_points_table.getHeight()) self.ui.al_probe_points_table.setMaximumHeight(self.ui.al_probe_points_table.getHeight()) # Set initial UI self.ui.al_rows_entry.setDisabled(True) self.ui.al_rows_label.setDisabled(True) self.ui.al_columns_entry.setDisabled(True) self.ui.al_columns_label.setDisabled(True) self.ui.al_method_lbl.setDisabled(True) self.ui.al_method_radio.set_value('v') self.ui.al_method_radio.setDisabled(True) # Show/Hide Advanced Options app_mode = self.app.options["global_app_level"] self.change_level(app_mode) try: self.ui.object_combo.currentIndexChanged.disconnect() except (AttributeError, TypeError): pass self.ui.object_combo.currentIndexChanged.connect(self.on_object_changed) self.build_tool_ui() if loaded_obj and loaded_obj.is_segmented_gcode is True and loaded_obj.obj_options["type"] == 'Geometry': self.ui.al_frame.setDisabled(False) self.ui.al_mode_radio.set_value(loaded_obj.obj_options['tools_al_mode']) self.on_controller_change() self.on_mode_radio(val=loaded_obj.obj_options['tools_al_mode']) self.on_method_radio(val=loaded_obj.obj_options['tools_al_method']) else: self.ui.al_frame.setDisabled(True) self.on_avoid_exc_holes(self.app.options["tools_al_avoid_exc_holes"]) def on_object_changed(self): # load the object obj_name = self.ui.object_combo.currentText() # Get source object. try: target_obj = self.app.collection.get_by_name(obj_name) except Exception: self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(obj_name))) return if target_obj is not None and target_obj.is_segmented_gcode is True and \ target_obj.obj_options["type"] == 'Geometry': self.ui.al_frame.setDisabled(False) # Shapes container for the Voronoi cells in Autolevelling if self.app.use_3d_engine: self.probing_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1, pool=self.app.pool) else: self.probing_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=obj_name + "_probing_shapes") else: self.ui.al_frame.setDisabled(True) def on_object_selection_changed(self, current, previous): found_idx = None for tab_idx in range(self.app.ui.notebook.count()): if self.app.ui.notebook.tabText(tab_idx) == self.ui.pluginName: found_idx = True break if found_idx: try: sel_obj = current.indexes()[0].internalPointer().obj name = sel_obj.obj_options['name'] kind = sel_obj.kind if kind == 'cncjob': self.ui.object_combo.set_value(name) except IndexError: pass def change_level(self, level): """ :param level: application level: either 'b' or 'a' :type level: str :return: """ if level == 'a': self.ui.level.setChecked(True) else: self.ui.level.setChecked(False) self.on_level_changed(self.ui.level.isChecked()) def on_level_changed(self, checked): target_obj = self.app.collection.get_by_name(self.ui.object_combo.get_value()) # if 'Roland' in target_obj.pp_excellon_name or 'Roland' in target_obj.pp_geometry_name or 'hpgl' in \ # target_obj.pp_geometry_name: # # TODO DO NOT AUTOLEVELL # pass if not checked: self.ui.level.setText('%s' % _('Beginner')) self.ui.level.setStyleSheet(""" QToolButton { color: green; } """) self.ui.al_title.hide() self.ui.show_al_table.hide() self.ui.al_probe_points_table.hide() # Context Menu section # self.ui.al_probe_points_table.removeContextMenu() else: self.ui.level.setText('%s' % _('Advanced')) self.ui.level.setStyleSheet(""" QToolButton { color: red; } """) self.ui.al_title.show() self.ui.show_al_table.show() if self.ui.show_al_table.get_value(): self.ui.al_probe_points_table.show() # Context Menu section # self.ui.al_probe_points_table.setupContextMenu() def build_tool_ui(self): self.ui_disconnect() self.build_al_table() self.ui_connect() def build_al_table(self): tool_idx = 0 n = len(self.al_voronoi_geo_storage) self.ui.al_probe_points_table.setRowCount(n) for id_key, value in self.al_voronoi_geo_storage.items(): tool_idx += 1 row_no = tool_idx - 1 t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx)) x = value['point'].x y = value['point'].y xy_coords = self.app.dec_format(x, dec=self.app.decimals), self.app.dec_format(y, dec=self.app.decimals) coords_item = QtWidgets.QTableWidgetItem(str(xy_coords)) height = self.app.dec_format(value['height'], dec=self.app.decimals) height_item = QtWidgets.QTableWidgetItem(str(height)) t_id.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) coords_item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) height_item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) self.ui.al_probe_points_table.setItem(row_no, 0, t_id) # Tool name/id self.ui.al_probe_points_table.setItem(row_no, 1, coords_item) # X-Y coords self.ui.al_probe_points_table.setItem(row_no, 2, height_item) # Determined Height self.ui.al_probe_points_table.resizeColumnsToContents() self.ui.al_probe_points_table.resizeRowsToContents() h_header = self.ui.al_probe_points_table.horizontalHeader() h_header.setMinimumSectionSize(10) h_header.setDefaultSectionSize(70) h_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Fixed) h_header.resizeSection(0, 20) h_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Stretch) h_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) self.ui.al_probe_points_table.setMinimumHeight(self.ui.al_probe_points_table.getHeight()) self.ui.al_probe_points_table.setMaximumHeight(self.ui.al_probe_points_table.getHeight()) if self.ui.al_probe_points_table.model().rowCount(): self.ui.grbl_get_heightmap_button.setDisabled(False) self.ui.grbl_save_height_map_button.setDisabled(False) self.ui.h_gcode_button.setDisabled(False) self.ui.view_h_gcode_button.setDisabled(False) else: self.ui.grbl_get_heightmap_button.setDisabled(True) self.ui.grbl_save_height_map_button.setDisabled(True) self.ui.h_gcode_button.setDisabled(True) self.ui.view_h_gcode_button.setDisabled(True) def to_form(self, storage=None): if storage is None: storage = self.app.options for k in self.form_fields: for option in storage: if option.startswith('tools_al_'): if k == option: try: self.form_fields[k].set_value(storage[option]) except Exception: # it may fail for form fields found in the tools tables if there are no rows pass def on_add_al_probepoints(self): # create the solid_geo loaded_obj = self.app.collection.get_by_name(self.ui.object_combo.get_value()) if loaded_obj is None: self.app.log.error("ToolLevelling.on_add_al_probepoints() -> No object loaded.") return 'fail' try: self.solid_geo = unary_union([geo['geom'] for geo in loaded_obj.gcode_parsed if geo['kind'][0] == 'C']) except TypeError: return 'fail' # reset al table self.ui.al_probe_points_table.setRowCount(0) # reset the al dict self.al_voronoi_geo_storage.clear() if self.ui.al_mode_radio.get_value() == 'grid': self.on_add_grid_points() else: self.on_add_manual_points() def check_point_over_excellon(self, pol: Polygon, check: bool) -> MultiPolygon: if not check: return MultiPolygon() fused_geometries = [ exc_geo for obj_in_collection in self.app.collection.get_list() if obj_in_collection.kind == 'excellon' and obj_in_collection.obj_options['plot'] for exc_geo in MultiPolygon(obj_in_collection.solid_geometry).geoms if isinstance(exc_geo, Polygon) and pol.intersects(exc_geo) ] return unary_union(fused_geometries) def on_add_grid_points(self): check_overlap = self.ui.avoid_exc_holes_cb.get_value() avoid_step = self.ui.avoid_exc_holes_size_entry.get_value() radius = self.ui.probe_tip_dia_entry.get_value() / 2 xmin, ymin, xmax, ymax = self.solid_geo.bounds width = abs(xmax - xmin) height = abs(ymax - ymin) cols = self.ui.al_columns_entry.get_value() rows = self.ui.al_rows_entry.get_value() dx = 0 if cols == 1 else width / (cols - 1) dy = 0 if rows == 1 else height / (rows - 1) points = [] new_y = ymin for x in range(rows): new_x = xmin for y in range(cols): formatted_point = ( self.app.dec_format(new_x, self.app.decimals), self.app.dec_format(new_y, self.app.decimals) ) point_buffered = Point(formatted_point).buffer(radius) if self.check_point_over_excellon(pol=point_buffered, check=check_overlap).is_empty: # do not add the point if is already added if formatted_point not in points: points.append(formatted_point) new_x += dx continue box_poly: Polygon = box( new_x - dx if (new_x - dx) > xmin else xmin, new_y - dy if (new_y - dy) > ymin else ymin, new_x + dx if (new_x + dx) < xmax else xmax, new_y + dy if (new_y + dy) < ymax else ymax ) increments = [ (avoid_step, 0), (avoid_step * -1, 0), (0, avoid_step), (0, avoid_step * -1) ] for increment in increments: break_for_loop = False while True: # check if the point is within the box formatted_point = ( formatted_point[0] + increment[0], formatted_point[1] + increment[1] ) point_buffered = Point(formatted_point).buffer(radius) if not box_poly.contains(point_buffered): break # check if the point overlaps an excellon hole if self.check_point_over_excellon(pol=point_buffered, check=check_overlap).is_empty: # do not add the point if it is already added if formatted_point not in points: points.append(formatted_point) break_for_loop = True break if break_for_loop: break new_x += dx new_y += dy pt_id = 0 vor_pts_list = [] bl_pts_list = [] for point in points: pt_id += 1 pt = Point(point) vor_pts_list.append(pt) bl_pts_list.append((point[0], point[1], 0.0)) new_dict = { 'point': pt, 'geo': None, 'height': 0.0 } self.al_voronoi_geo_storage[pt_id] = deepcopy(new_dict) al_method = self.ui.al_method_radio.get_value() if al_method == 'v': if VORONOI_ENABLED is True: self.generate_voronoi_geometry(pts=vor_pts_list) # generate Probing GCode self.probing_gcode_text = self.probing_gcode(storage=self.al_voronoi_geo_storage) else: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Voronoi function can not be loaded.\n" "Shapely >= 1.8 is required")) else: self.generate_bilinear_geometry(pts=bl_pts_list) # generate Probing GCode self.probing_gcode_text = self.probing_gcode(storage=self.al_bilinear_geo_storage) self.build_al_table_sig.emit() if self.ui.plot_probing_pts_cb.get_value(): self.show_probing_geo(state=True, reset=True) else: # clear probe shapes self.plot_probing_geo(None, False) def on_add_manual_points(self): xmin, ymin, xmax, ymax = self.solid_geo.bounds f_probe_pt = Point([xmin, xmin]) int_keys = [int(k) for k in self.al_voronoi_geo_storage.keys()] new_id = max(int_keys) + 1 if int_keys else 1 new_dict = { 'point': f_probe_pt, 'geo': None, 'height': 0.0 } self.al_voronoi_geo_storage[new_id] = deepcopy(new_dict) radius = self.ui.probe_tip_dia_entry.get_value() / 2 fprobe_pt_buff = f_probe_pt.buffer(radius) self.app.inform.emit(_("Click on canvas to add a Probe Point...")) self.app.options['global_selection_shape'] = False if self.app.use_3d_engine: self.app.plotcanvas.graph_event_disconnect('key_press', self.app.ui.keyPressEvent) self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot) self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot) else: self.app.plotcanvas.graph_event_disconnect(self.app.kp) self.app.plotcanvas.graph_event_disconnect(self.app.mp) self.app.plotcanvas.graph_event_disconnect(self.app.mr) self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press) self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release) self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move) self.mouse_events_connected = True self.build_al_table_sig.emit() if self.ui.plot_probing_pts_cb.get_value(): self.show_probing_geo(state=True, reset=True) else: # clear probe shapes self.plot_probing_geo(None, False) self.plot_probing_geo(geometry=fprobe_pt_buff, visibility=True, custom_color="#0000FFFA") def show_probing_geo(self, state, reset=False): self.app.log.debug("ToolLevelling.show_probing_geo() -> %s" % ('cleared' if state is False else 'displayed')) if reset: self.probing_shapes.clear(update=True) points_geo = [] poly_geo = [] al_method = self.ui.al_method_radio.get_value() radius = self.ui.probe_tip_dia_entry.get_value() / 2 # voronoi diagram if al_method == 'v': # create the geometry for pt in self.al_voronoi_geo_storage: if not self.al_voronoi_geo_storage[pt]['geo']: continue p_geo = self.al_voronoi_geo_storage[pt]['point'].buffer(radius) s_geo = self.al_voronoi_geo_storage[pt]['geo'].buffer(0.0000001) points_geo.append(p_geo) poly_geo.append(s_geo) if not points_geo and not poly_geo: return self.plot_probing_geo(geometry=points_geo, visibility=state, custom_color='#000000FF') self.plot_probing_geo(geometry=poly_geo, visibility=state) # bilinear interpolation elif al_method == 'b': for pt in self.al_bilinear_geo_storage: x_pt = pt[0] y_pt = pt[1] p_geo = Point([x_pt, y_pt]).buffer(radius) if p_geo.is_valid: points_geo.append(p_geo) if not points_geo: return self.plot_probing_geo(geometry=points_geo, visibility=state, custom_color='#000000FF') def plot_probing_geo(self, geometry, visibility, custom_color=None): if visibility: if self.app.use_3d_engine: def random_color(): r_color = np.random.rand(4) r_color[3] = 0.5 return r_color else: def random_color(): while True: r_color = np.random.rand(4) r_color[3] = 0.5 new_color = '#' for idx in range(len(r_color)): new_color += '%x' % int(r_color[idx] * 255) # do it until a valid color is generated # a valid color has the # symbol, another 6 chars for the color and the last 2 chars for alpha # for a total of 9 chars if len(new_color) == 9: break return new_color try: # if self.app.use_3d_engine: # color = "#0000FFFE" # else: # color = "#0000FFFE" # for sh in points_geo: # self.add_probing_shape(shape=sh, color=color, face_color=color, visible=True) edge_color = "#000000FF" try: for sh in geometry: if custom_color is None: k = self.add_probing_shape(shape=sh, color=edge_color, face_color=random_color(), visible=True) else: k = self.add_probing_shape(shape=sh, color=custom_color, face_color=custom_color, visible=True) except TypeError: if custom_color is None: self.add_probing_shape( shape=geometry, color=edge_color, face_color=random_color(), visible=True) else: self.add_probing_shape( shape=geometry, color=custom_color, face_color=custom_color, visible=True) self.probing_shapes.redraw() except (ObjectDeleted, AttributeError) as e: self.app.log.error("ToolLevelling.plot_probing_geo() -> %s" % str(e)) self.probing_shapes.clear(update=True) except Exception as e: self.app.log.error("CNCJobObject.plot_probing_geo() --> %s" % str(e)) else: self.probing_shapes.clear(update=True) def add_probing_shape(self, **kwargs): key = self.probing_shapes.add(tolerance=self.drawing_tolerance, layer=0, **kwargs) return key def generate_voronoi_geometry(self, pts): env = self.solid_geo.envelope fact = 1 if self.units == 'MM' else 0.039 env = env.buffer(fact) new_pts = deepcopy(pts) try: pts_union = MultiPoint(pts) voronoi_union = voronoi_diagram(geom=pts_union, envelope=env) except Exception as e: self.app.log.error("CNCJobObject.generate_voronoi_geometry() --> %s" % str(e)) for pt_index in range(len(pts)): new_pts[pt_index] = translate( new_pts[pt_index], random.random() * 1e-09, random.random() * 1e-09) pts_union = MultiPoint(new_pts) try: voronoi_union = voronoi_diagram(geom=pts_union, envelope=env) except Exception: return new_voronoi = [] for p in voronoi_union.geoms: new_voronoi.append(p.intersection(env)) for pt_key in list(self.al_voronoi_geo_storage.keys()): for poly in new_voronoi: if self.al_voronoi_geo_storage[pt_key]['point'].within(poly): self.al_voronoi_geo_storage[pt_key]['geo'] = poly # def generate_voronoi_geometry_2(self, pts): # env = self.solid_geo.envelope # fact = 1 if self.units == 'MM' else 0.039 # env = env.buffer(fact) # env_poly = voronoi_poly(tuple(env.exterior.coords)) # # new_pts = [[pt.x, pt.y] for pt in pts] # print(new_pts) # print(env_poly) # # # Initialize the algorithm # v = Voronoi(env_poly) # # # calculate the Voronoi diagram # try: # v.create_diagram(new_pts) # except AttributeError as e: # self.app.log.error("CNCJobObject.generate_voronoi_geometry_2() --> %s" % str(e)) # new_pts_2 = [] # for pt_index in range(len(new_pts)): # new_pts_2.append([ # new_pts[pt_index][0] + random.random() * 1e-03, # new_pts[pt_index][1] + random.random() * 1e-03 # ]) # # try: # v.create_diagram(new_pts_2) # except Exception: # print("Didn't work.") # return # # new_voronoi = [] # for p in v.sites: # # p_coords = [(coord.x, coord.y) for coord in p.get_coordinates()] # p_coords = [(p.x, p.y)] # new_pol = Polygon(p_coords) # new_voronoi.append(new_pol) # # new_voronoi = MultiPolygon(new_voronoi) # # # new_voronoi = [] # # for p in voronoi_union: # # new_voronoi.append(p.intersection(env)) # # # for pt_key in list(self.al_voronoi_geo_storage.keys()): # for poly in new_voronoi: # if self.al_voronoi_geo_storage[pt_key]['point'].within(poly) or \ # self.al_voronoi_geo_storage[pt_key]['point'].intersects(poly): # self.al_voronoi_geo_storage[pt_key]['geo'] = poly def generate_bilinear_geometry(self, pts): self.al_bilinear_geo_storage = pts def on_mouse_move(self, event): """ Callback for the mouse motion event over the plot. :param event: Contains information about the event. :return: None """ if self.app.use_3d_engine: self.mouse_is_dragging = event.is_dragging else: self.mouse_is_dragging = self.app.plotcanvas.is_dragging # So it can receive key presses but not when the Tcl Shell is active if not self.app.ui.shell_dock.isVisible(): if not self.app.plotcanvas.native.hasFocus(): self.app.plotcanvas.native.setFocus() # To be called after clicking on the plot. def on_mouse_click_release(self, event): if self.app.use_3d_engine: event_pos = event.pos right_button = 2 else: event_pos = (event.xdata, event.ydata) right_button = 3 try: x = float(event_pos[0]) y = float(event_pos[1]) except TypeError: return event_pos = (x, y) # do paint single only for left mouse clicks if event.button == 1: check_for_exc_hole = self.ui.avoid_exc_holes_cb.get_value() pos = self.app.plotcanvas.translate_coords(event_pos) # use the snapped position as reference snapped_pos = self.app.geo_editor.snap(pos[0], pos[1]) # do not add the point if is already added old_points_coords = [(pt['point'].x, pt['point'].y) for pt in self.al_voronoi_geo_storage.values()] if (snapped_pos[0], snapped_pos[1]) in old_points_coords: return # Clicked Point probe_pt = Point(snapped_pos) xxmin, yymin, xxmax, yymax = self.solid_geo.bounds box_geo = box(xxmin, yymin, xxmax, yymax) if not probe_pt.within(box_geo): self.app.inform.emit(_("Point is not within the object area. Choose another point.")) return # check if chosen point is within an Excellon drill hole geometry if check_for_exc_hole is True: for obj_in_collection in self.app.collection.get_list(): if obj_in_collection.kind == 'excellon' and obj_in_collection.obj_options['plot'] is True: exc_solid_geometry = MultiPolygon(obj_in_collection.solid_geometry) for exc_geo in exc_solid_geometry.geoms: if probe_pt.within(exc_geo): self.app.inform.emit(_("Point on an Excellon drill hole. Choose another point.")) return int_keys = [int(k) for k in self.al_voronoi_geo_storage.keys()] new_id = max(int_keys) + 1 if int_keys else 1 new_dict = { 'point': probe_pt, 'geo': None, 'height': 0.0 } self.al_voronoi_geo_storage[new_id] = deepcopy(new_dict) # rebuild the al table self.build_al_table_sig.emit() radius = self.ui.probe_tip_dia_entry.get_value() / 2 probe_pt_buff = probe_pt.buffer(radius) self.plot_probing_geo(geometry=probe_pt_buff, visibility=True, custom_color="#0000FFFA") self.app.inform.emit(_("Added a Probe Point... Click again to add another or right click to finish ...")) # if RMB then we exit elif event.button == right_button and self.mouse_is_dragging is False: if self.app.use_3d_engine: self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press) self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release) self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move) else: self.app.plotcanvas.graph_event_disconnect(self.kp) self.app.plotcanvas.graph_event_disconnect(self.mr) self.app.plotcanvas.graph_event_disconnect(self.mm) self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent) self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot) self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot) # signal that the mouse events are disconnected from local methods self.mouse_events_connected = False # restore selection self.app.options['global_selection_shape'] = self.old_selection_state self.app.inform.emit(_("Finished adding Probe Points...")) al_method = self.ui.al_method_radio.get_value() if al_method == 'v': if VORONOI_ENABLED is True: pts_list = [] for k in self.al_voronoi_geo_storage: pts_list.append(self.al_voronoi_geo_storage[k]['point']) self.generate_voronoi_geometry(pts=pts_list) self.probing_gcode_text = self.probing_gcode(self.al_voronoi_geo_storage) else: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Voronoi function can not be loaded.\n" "Shapely >= 1.8 is required")) # rebuild the al table self.build_al_table_sig.emit() if self.ui.plot_probing_pts_cb.get_value(): self.show_probing_geo(state=True, reset=True) else: # clear probe shapes self.plot_probing_geo(None, False) def on_key_press(self, event): # events out of the self.app.collection view (it's about Project Tab) are of type int if isinstance(event, int): key = event # events from the GUI are of type QKeyEvent elif isinstance(event, QtGui.QKeyEvent): key = event.key() elif isinstance(event, mpl_key_event): # MatPlotLib key events are trickier to interpret than the rest key = event.key key = QtGui.QKeySequence(key) # check for modifiers key_string = key.toString().lower() if '+' in key_string: mod, __, key_text = key_string.rpartition('+') if mod.lower() == 'ctrl': # modifiers = QtCore.Qt.KeyboardModifier.ControlModifier pass elif mod.lower() == 'alt': # modifiers = QtCore.Qt.KeyboardModifier.AltModifier pass elif mod.lower() == 'shift': # modifiers = QtCore.Qt.KeyboardModifier. pass else: # modifiers = QtCore.Qt.KeyboardModifier.NoModifier pass key = QtGui.QKeySequence(key_text) # events from Vispy are of type KeyEvent else: key = event.key # Escape = Deselect All if key == QtCore.Qt.Key.Key_Escape or key == 'Escape': if self.mouse_events_connected is True: self.mouse_events_connected = False if self.app.use_3d_engine: self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press) self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release) self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move) else: self.app.plotcanvas.graph_event_disconnect(self.kp) self.app.plotcanvas.graph_event_disconnect(self.mr) self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent) self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot) self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot) # restore selection self.app.options['global_selection_shape'] = self.old_selection_state # Grid toggle if key == QtCore.Qt.Key.Key_G or key == 'G': self.app.ui.grid_snap_btn.trigger() # Jump to coords if key == QtCore.Qt.Key.Key_J or key == 'J': self.app.on_jump_to() def autolevell_gcode(self): pass def autolevell_gcode_line(self, gcode_line): al_method = self.ui.al_method_radio.get_value() coords = () if al_method == 'v': self.autolevell_voronoi(gcode_line, coords) elif al_method == 'b': self.autolevell_bilinear(gcode_line, coords) def autolevell_bilinear(self, gcode_line, coords): pass def autolevell_voronoi(self, gcode_line, coords): pass def on_show_al_table(self, state): self.ui.al_probe_points_table.show() if state else self.ui.al_probe_points_table.hide() def on_mode_radio(self, val): # reset al table self.ui.al_probe_points_table.setRowCount(0) # reset the al dict self.al_voronoi_geo_storage.clear() # reset Voronoi Shapes self.probing_shapes.clear(update=True) # build AL table self.build_al_table() if val == "manual": self.ui.al_method_radio.set_value('v') self.ui.al_rows_entry.setDisabled(True) self.ui.al_rows_label.setDisabled(True) self.ui.al_columns_entry.setDisabled(True) self.ui.al_columns_label.setDisabled(True) self.ui.al_method_lbl.setDisabled(True) self.ui.al_method_radio.setDisabled(True) # self.ui.avoid_exc_holes_cb.setDisabled(False) else: self.ui.al_rows_entry.setDisabled(False) self.ui.al_rows_label.setDisabled(False) self.ui.al_columns_entry.setDisabled(False) self.ui.al_columns_label.setDisabled(False) self.ui.al_method_lbl.setDisabled(False) self.ui.al_method_radio.setDisabled(False) self.ui.al_method_radio.set_value(self.app.options['tools_al_method']) # self.ui.avoid_exc_holes_cb.setDisabled(True) def on_avoid_exc_holes(self, state): self.ui.avoid_exc_holes_size_label.show() if state else self.ui.avoid_exc_holes_size_label.hide() self.ui.avoid_exc_holes_size_entry.show() if state else self.ui.avoid_exc_holes_size_entry.hide() def on_method_radio(self, val): if val == 'b': self.ui.al_columns_entry.setMinimum(2) self.ui.al_rows_entry.setMinimum(2) else: self.ui.al_columns_entry.setMinimum(1) self.ui.al_rows_entry.setMinimum(1) def on_controller_change(self): self.on_controller_change_alter_ui() # if the is empty then there is a chance that we've added probe points but the GRBL controller was selected # therefore no Probing GCode was generated (it is different for GRBL on how it gets it's Probing GCode target_obj = self.app.collection.get_by_name(self.ui.object_combo.get_value()) if (not self.probing_gcode_text or self.probing_gcode_text == '') and target_obj is not None: # generate Probing GCode al_method = self.ui.al_method_radio.get_value() storage = self.al_voronoi_geo_storage if al_method == 'v' else self.al_bilinear_geo_storage self.probing_gcode_text = self.probing_gcode(storage=storage) def on_controller_change_alter_ui(self): if self.ui.al_controller_combo.get_value() == 'GRBL': self.ui.h_gcode_button.hide() self.ui.view_h_gcode_button.hide() self.ui.import_heights_button.hide() self.ui.grbl_frame.show() self.on_grbl_search_ports(muted=True) else: self.ui.h_gcode_button.show() self.ui.view_h_gcode_button.show() self.ui.import_heights_button.show() self.ui.grbl_frame.hide() @staticmethod def on_grbl_list_serial_ports(): """ Lists serial port names. From here: https://stackoverflow.com/questions/12090503/listing-available-com-ports-with-python :raises EnvironmentError: On unsupported or unknown platforms :returns: A list of the serial ports available on the system """ if sys.platform.startswith('win'): ports = ['COM%s' % (i + 1) for i in range(256)] elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): # this excludes your current terminal "/dev/tty" ports = glob.glob('/dev/tty[A-Za-z]*') elif sys.platform.startswith('darwin'): ports = glob.glob('/dev/tty.*') else: raise EnvironmentError('Unsupported platform') result = [] s = serial.Serial() for port in ports: s.port = port try: s.open() s.close() result.append(port) except (OSError, serial.SerialException): # result.append(port + " (in use)") pass return result def on_grbl_search_ports(self, muted=None): port_list = self.on_grbl_list_serial_ports() self.ui.com_list_combo.clear() self.ui.com_list_combo.addItems(port_list) if muted is not True: self.app.inform.emit('[WARNING_NOTCL] %s' % _("COM list updated ...")) def on_grbl_connect(self): port_name = self.ui.com_list_combo.currentText() if " (" in port_name: port_name = port_name.rpartition(" (")[0] baudrate = int(self.ui.baudrates_list_combo.currentText()) try: self.grbl_ser_port = serial.serial_for_url(port_name, baudrate, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=0.1, xonxoff=False, rtscts=False) # Toggle DTR to reset the controller loaded with GRBL (Arduino, ESP32, etc.) try: self.grbl_ser_port.dtr = False except IOError: pass self.grbl_ser_port.reset_input_buffer() try: self.grbl_ser_port.dtr = True except IOError: pass answer = self.on_grbl_wake() answer = ['ok'] # FIXME: hack for development without a GRBL controller connected for line in answer: if 'ok' in line.lower(): self.ui.com_connect_button.setStyleSheet("QPushButton {background-color: seagreen;}") self.ui.com_connect_button.setText(_("Connected")) self.ui.controller_reset_button.setDisabled(False) for idx in range(self.ui.al_toolbar.count()): if self.ui.al_toolbar.tabText(idx) == _("Connect"): self.ui.al_toolbar.tabBar.setTabTextColor(idx, QtGui.QColor('seagreen')) if self.ui.al_toolbar.tabText(idx) == _("Control"): self.ui.al_toolbar.tabBar.setTabEnabled(idx, True) if self.ui.al_toolbar.tabText(idx) == _("Sender"): self.ui.al_toolbar.tabBar.setTabEnabled(idx, True) self.app.inform.emit("%s: %s" % (_("Port connected"), port_name)) return self.grbl_ser_port.close() self.app.inform.emit("[ERROR_NOTCL] %s: %s" % (_("Could not connect to GRBL on port"), port_name)) except serial.SerialException: self.grbl_ser_port = serial.Serial() self.grbl_ser_port.port = port_name self.grbl_ser_port.close() self.ui.com_connect_button.setStyleSheet("QPushButton {background-color: red;}") self.ui.com_connect_button.setText(_("Disconnected")) self.ui.controller_reset_button.setDisabled(True) for idx in range(self.ui.al_toolbar.count()): if self.ui.al_toolbar.tabText(idx) == _("Connect"): self.ui.al_toolbar.tabBar.setTabTextColor(idx, QtGui.QColor('red')) if self.ui.al_toolbar.tabText(idx) == _("Control"): self.ui.al_toolbar.tabBar.setTabEnabled(idx, False) if self.ui.al_toolbar.tabText(idx) == _("Sender"): self.ui.al_toolbar.tabBar.setTabEnabled(idx, False) self.app.inform.emit("%s: %s" % (_("Port is connected. Disconnecting"), port_name)) except Exception: self.app.inform.emit("[ERROR_NOTCL] %s: %s" % (_("Could not connect to port"), port_name)) def on_grbl_add_baudrate(self): new_bd = str(self.ui.new_baudrate_entry.get_value()) if int(new_bd) >= 40 and new_bd not in self.ui.baudrates_list_combo.model().stringList(): self.ui.baudrates_list_combo.addItem(new_bd) self.ui.baudrates_list_combo.setCurrentText(new_bd) def on_grbl_delete_baudrate_grbl(self): current_idx = self.ui.baudrates_list_combo.currentIndex() self.ui.baudrates_list_combo.removeItem(current_idx) def on_grbl_wake(self): # Wake up grbl self.grbl_ser_port.write("\r\n\r\n".encode('utf-8')) # Wait for GRBL controller to initialize time.sleep(1) grbl_out = deepcopy(self.grbl_ser_port.readlines()) self.grbl_ser_port.reset_input_buffer() return grbl_out def on_grbl_send_command(self): cmd = self.ui.grbl_command_entry.get_value() # show the Shell Dock self.app.ui.shell_dock.show() def worker_task(): with self.app.proc_container.new('%s...' % _("Sending")): self.send_grbl_command(command=cmd) self.app.worker_task.emit({'fcn': worker_task, 'params': []}) def send_grbl_command(self, command, echo=True): """ :param command: GCode command :type command: str :param echo: if to send a '\n' char after :type echo: bool :return: the text returned by the GRBL controller after each command :rtype: str """ cmd = command.strip() if echo: self.app.inform_shell[str, bool].emit(cmd, False) # Send Gcode command to GRBL snd = cmd + '\n' self.grbl_ser_port.write(snd.encode('utf-8')) grbl_out = self.grbl_ser_port.readlines() if not grbl_out: self.app.inform_shell[str, bool].emit('\t\t\t: No answer\n', False) result = '' for line in grbl_out: if echo: try: self.app.inform_shell.emit('\t\t\t: ' + line.decode('utf-8').strip().upper()) except Exception as e: self.app.log.error("CNCJobObject.send_grbl_command() --> %s" % str(e)) if 'ok' in line: result = grbl_out return result def send_grbl_block(self, command, echo=True): stripped_cmd = command.strip() for grbl_line in stripped_cmd.split('\n'): if echo: self.app.inform_shell[str, bool].emit(grbl_line, False) # Send Gcode block to GRBL snd = grbl_line + '\n' self.grbl_ser_port.write(snd.encode('utf-8')) grbl_out = self.grbl_ser_port.readlines() for line in grbl_out: if echo: try: self.app.inform_shell.emit(' : ' + line.decode('utf-8').strip().upper()) except Exception as e: self.app.log.error("CNCJobObject.send_grbl_block() --> %s" % str(e)) def on_grbl_get_parameter(self, param): if '$' in param: param = param.replace('$', '') snd = '$$\n' self.grbl_ser_port.write(snd.encode('utf-8')) grbl_out = self.grbl_ser_port.readlines() for line in grbl_out: decoded_line = line.decode('utf-8') par = '$%s' % str(param) if par in decoded_line: result = float(decoded_line.rpartition('=')[2]) self.app.shell_message("GRBL Parameter: %s = %s" % (str(param), str(result)), show=True) return result def on_grbl_jog(self, direction=None): if direction is None: return cmd = '' step = self.ui.jog_step_entry.get_value(), feedrate = self.ui.jog_fr_entry.get_value() travelz = float(self.app.options["tools_al_grbl_travelz"]) if direction == 'xplus': cmd = "$J=G91 %s X%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate)) if direction == 'xminus': cmd = "$J=G91 %s X-%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate)) if direction == 'yplus': cmd = "$J=G91 %s Y%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate)) if direction == 'yminus': cmd = "$J=G91 %s Y-%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate)) if direction == 'zplus': cmd = "$J=G91 %s Z%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate)) if direction == 'zminus': cmd = "$J=G91 %s Z-%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate)) if direction == 'origin': cmd = "$J=G90 %s Z%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(travelz), str(feedrate)) self.send_grbl_command(command=cmd, echo=False) cmd = "$J=G90 %s X0.0 Y0.0 F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(feedrate)) self.send_grbl_command(command=cmd, echo=False) return self.send_grbl_command(command=cmd, echo=False) def on_grbl_zero(self, axis): current_mode = self.on_grbl_get_parameter('10') if current_mode is None: return cmd = '$10=0' self.send_grbl_command(command=cmd, echo=False) if axis == 'x': cmd = 'G10 L2 P1 X0' elif axis == 'y': cmd = 'G10 L2 P1 Y0' elif axis == 'z': cmd = 'G10 L2 P1 Z0' else: # all cmd = 'G10 L2 P1 X0 Y0 Z0' self.send_grbl_command(command=cmd, echo=False) # restore previous mode cmd = '$10=%d' % int(current_mode) self.send_grbl_command(command=cmd, echo=False) def on_grbl_homing(self): cmd = '$H' self.app.inform.emit("%s" % _("GRBL is doing a home cycle.")) self.on_grbl_wake() self.send_grbl_command(command=cmd) def on_grbl_reset(self): cmd = '\x18' self.app.inform.emit("%s" % _("GRBL software reset was sent.")) self.on_grbl_wake() self.send_grbl_command(command=cmd) def on_grbl_pause_resume(self, checked): if checked is False: cmd = '~' self.send_grbl_command(command=cmd) self.app.inform.emit("%s" % _("GRBL resumed.")) else: cmd = '!' self.send_grbl_command(command=cmd) self.app.inform.emit("%s" % _("GRBL paused.")) def probing_gcode(self, storage): """ :param storage: either a dict of dicts (voronoi) or a list of tuples (bilinear) :return: Probing GCode :rtype: str """ target_obj = self.app.collection.get_by_name(self.ui.object_combo.get_value()) p_gcode = '' header = '' time_str = "{:%A, %d %B %Y at %H:%M}".format(dt.now()) coords = [] al_method = self.ui.al_method_radio.get_value() if al_method == 'v': for id_key, value in storage.items(): x = value['point'].x y = value['point'].y coords.append( ( self.app.dec_format(x, dec=self.app.decimals), self.app.dec_format(y, dec=self.app.decimals) ) ) else: for pt in storage: x = pt[0] y = pt[1] coords.append( ( self.app.dec_format(x, dec=self.app.decimals), self.app.dec_format(y, dec=self.app.decimals) ) ) pr_travel = self.ui.ptravelz_entry.get_value() probe_fr = self.ui.feedrate_probe_entry.get_value() pr_depth = self.ui.pdepth_entry.get_value() controller = self.ui.al_controller_combo.get_value() header += '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \ (str(self.app.version), str(self.app.version_date)) + '\n' header += '(This is a autolevelling probing GCode.)\n' \ '(Make sure that before you start the job you first do a zero for all axis.)\n\n' header += '(Name: ' + str(target_obj.obj_options['name']) + ')\n' header += '(Type: ' + "Autolevelling Probing GCode " + ')\n' header += '(Units: ' + self.units.upper() + ')\n' header += '(Created on ' + time_str + ')\n' # commands if controller == 'MACH3': probing_command = 'G31' # probing_var = '#2002' openfile_command = 'M40' closefile_command = 'M41' elif controller == 'MACH4': probing_command = 'G31' # probing_var = '#5063' openfile_command = 'M40' closefile_command = 'M41' elif controller == 'LinuxCNC': probing_command = 'G38.2' # probing_var = '#5422' openfile_command = '(PROBEOPEN a_probing_points_file.txt)' closefile_command = '(PROBECLOSE)' elif controller == 'GRBL': # do nothing here because the Probing GCode for GRBL is obtained differently return else: self.app.log.debug("CNCJobObject.probing_gcode() -> controller not supported") return # ############################################################################################################# # ########################### GCODE construction ############################################################## # ############################################################################################################# # header p_gcode += header + '\n' # supplementary message for LinuxCNC if controller == 'LinuxCNC': p_gcode += "The file with the stored probing points can be found\n" \ "in the configuration folder for LinuxCNC.\n" \ "The name of the file is: a_probing_points_file.txt.\n" # units p_gcode += 'G21\n' if self.units == 'MM' else 'G20\n' # reference mode = absolute p_gcode += 'G90\n' # open a new file p_gcode += openfile_command + '\n' # move to safe height (probe travel Z) p_gcode += 'G0 Z%s\n' % str(self.app.dec_format(pr_travel, target_obj.coords_decimals)) # probing points for idx, xy_tuple in enumerate(coords, 1): # index starts from 1 x = xy_tuple[0] y = xy_tuple[1] # move to probing point p_gcode += "G0 X%sY%s\n" % ( str(self.app.dec_format(x, target_obj.coords_decimals)), str(self.app.dec_format(y, target_obj.coords_decimals)) ) # do the probing p_gcode += "%s Z%s F%s\n" % ( probing_command, str(self.app.dec_format(pr_depth, target_obj.coords_decimals)), str(self.app.dec_format(probe_fr, target_obj.fr_decimals)), ) # store in a global numeric variable the value of the detected probe Z # I offset the global numeric variable by 500 so, it does not conflict with something else # temp_var = int(idx + 500) # p_gcode += "#%d = %s\n" % (temp_var, probing_var) # move to safe height (probe travel Z) p_gcode += 'G0 Z%s\n' % str(self.app.dec_format(pr_travel, target_obj.coords_decimals)) # close the file p_gcode += closefile_command + '\n' # finish the GCode p_gcode += 'M2' return p_gcode def on_save_probing_gcode(self): lines = StringIO(self.probing_gcode_text) _filter_ = self.app.options['cncjob_save_filters'] name = "probing_gcode" try: dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name) filename, _f = FCFileSaveDialog.get_saved_filename( caption=_("Export Code ..."), directory=dir_file_to_save, ext_filter=_filter_ ) except TypeError: filename, _f = FCFileSaveDialog.get_saved_filename( caption=_("Export Code ..."), ext_filter=_filter_) if filename == '': self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ...")) return else: try: force_windows_line_endings = self.app.options['cncjob_line_ending'] if force_windows_line_endings and sys.platform != 'win32': with open(filename, 'w', newline='\r\n') as f: for line in lines: f.write(line) else: with open(filename, 'w') as f: for line in lines: f.write(line) except FileNotFoundError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("No such file or directory")) return except PermissionError: self.app.inform.emit( '[WARNING] %s' % _("Permission denied, saving not possible.\n" "Most likely another app is holding the file open and not accessible.") ) return 'fail' def on_edit_probing_gcode(self): self.app.proc_container.view.set_busy('%s...' % _("Loading")) gco = self.probing_gcode_text if gco is None or gco == '': self.app.inform.emit('[WARNING_NOTCL] %s...' % _('There is nothing to view')) return self.gcode_viewer_tab = AppTextEditor(app=self.app, plain_text=True) # add the tab if it was closed self.app.ui.plot_tab_area.addTab(self.gcode_viewer_tab, '%s' % _("Code Viewer")) self.gcode_viewer_tab.setObjectName('code_viewer_tab') # delete the absolute and relative position and messages in the infobar self.app.ui.position_label.setText("") self.app.ui.rel_position_label.setText("") self.gcode_viewer_tab.code_editor.completer_enable = False self.gcode_viewer_tab.buttonRun.hide() # Switch plot_area to CNCJob tab self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_viewer_tab) self.gcode_viewer_tab.t_frame.hide() # then append the text from GCode to the text editor try: self.gcode_viewer_tab.load_text(gco, move_to_start=True, clear_text=True) except Exception as e: self.app.log.error('FlatCAMCNCJob.on_edit_probing_gcode() -->%s' % str(e)) return self.gcode_viewer_tab.t_frame.show() self.app.proc_container.view.set_idle() self.gcode_viewer_tab.buttonSave.hide() self.gcode_viewer_tab.buttonOpen.hide() self.gcode_viewer_tab.buttonPrint.hide() self.gcode_viewer_tab.buttonPreview.hide() self.gcode_viewer_tab.buttonReplace.hide() self.gcode_viewer_tab.sel_all_cb.hide() self.gcode_viewer_tab.entryReplace.hide() self.gcode_viewer_tab.button_update_code.show() # self.gcode_viewer_tab.code_editor.setReadOnly(True) self.gcode_viewer_tab.button_update_code.clicked.connect(self.on_update_probing_gcode) self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Viewer')) def on_update_probing_gcode(self): self.probing_gcode_text = self.gcode_viewer_tab.code_editor.toPlainText() def on_import_height_map(self): """ Import the height map file into the app :return: :rtype: """ _filter_ = "Text File .txt (*.txt);;All Files (*.*)" try: filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import Height Map"), directory=self.app.get_last_folder(), filter=_filter_) except TypeError: filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import Height Map"), filter=_filter_) filename = str(filename) if filename == '': self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled.")) else: self.app.worker_task.emit({'fcn': self.import_height_map, 'params': [filename]}) def import_height_map(self, filename): """ :param filename: :type filename: :return: :rtype: """ try: if filename: with open(filename, 'r') as f: stream = f.readlines() else: return except IOError: self.app.log.error("Failed to open height map file: %s" % filename) self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open height map file"), filename)) return idx = 0 if stream is not None and stream != '': for line in stream: if line != '': idx += 1 line = line.replace(' ', ',').replace('\n', '').split(',') if idx not in self.al_voronoi_geo_storage: self.al_voronoi_geo_storage[idx] = {} self.al_voronoi_geo_storage[idx]['height'] = float(line[2]) if 'point' not in self.al_voronoi_geo_storage[idx]: x = float(line[0]) y = float(line[1]) self.al_voronoi_geo_storage[idx]['point'] = Point((x, y)) self.build_al_table_sig.emit() def on_grbl_autolevel(self): # show the Shell Dock self.app.ui.shell_dock.show() def worker_task(): with self.app.proc_container.new('%s...' % _("Sending")): self.grbl_probe_result = '' pr_travelz = str(self.ui.ptravelz_entry.get_value()) probe_fr = str(self.ui.feedrate_probe_entry.get_value()) pr_depth = str(self.ui.pdepth_entry.get_value()) cmd = 'G21\n' self.send_grbl_command(command=cmd) cmd = 'G90\n' self.send_grbl_command(command=cmd) for pt_key in self.al_voronoi_geo_storage: x = str(self.al_voronoi_geo_storage[pt_key]['point'].x) y = str(self.al_voronoi_geo_storage[pt_key]['point'].y) cmd = 'G0 Z%s\n' % pr_travelz self.send_grbl_command(command=cmd) cmd = 'G0 X%s Y%s\n' % (x, y) self.send_grbl_command(command=cmd) cmd = 'G38.2 Z%s F%s' % (pr_depth, probe_fr) output = self.send_grbl_command(command=cmd) self.grbl_probe_result += output + '\n' cmd = 'M2\n' self.send_grbl_command(command=cmd) self.app.inform.emit('%s' % _("Finished probing. Doing the autolevelling.")) # apply autolevelling here self.on_grbl_apply_autolevel() self.app.inform.emit('%s' % _("Sending probing GCode to the GRBL controller.")) self.app.worker_task.emit({'fcn': worker_task, 'params': []}) def on_grbl_heightmap_save(self): if self.grbl_probe_result != '': _filter_ = "Text File .txt (*.txt);;All Files (*.*)" name = "probing_gcode" try: dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name) filename, _f = FCFileSaveDialog.get_saved_filename( caption=_("Export Code ..."), directory=dir_file_to_save, ext_filter=_filter_ ) except TypeError: filename, _f = FCFileSaveDialog.get_saved_filename( caption=_("Export Code ..."), ext_filter=_filter_) if filename == '': self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ...")) return else: try: force_windows_line_endings = self.app.options['cncjob_line_ending'] if force_windows_line_endings and sys.platform != 'win32': with open(filename, 'w', newline='\r\n') as f: for line in self.grbl_probe_result: f.write(line) else: with open(filename, 'w') as f: for line in self.grbl_probe_result: f.write(line) except FileNotFoundError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("No such file or directory")) return except PermissionError: self.app.inform.emit( '[WARNING] %s' % _("Permission denied, saving not possible.\n" "Most likely another app is holding the file open and not accessible.") ) return 'fail' else: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Empty GRBL heightmap.")) def on_grbl_apply_autolevel(self): # TODO here we call the autolevell method self.app.inform.emit('%s' % _("Finished autolevelling.")) def ui_connect(self): self.ui.al_add_button.clicked.connect(self.on_add_al_probepoints) self.ui.show_al_table.stateChanged.connect(self.on_show_al_table) def ui_disconnect(self): try: self.ui.al_add_button.clicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.show_al_table.stateChanged.disconnect() except (TypeError, AttributeError): pass def reset_fields(self): self.ui.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) class LevelUI: pluginName = _("Levelling") def __init__(self, layout, app): self.app = app self.decimals = self.app.decimals self.layout = layout self.tools_frame = QtWidgets.QFrame() self.tools_frame.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.tools_frame) self.tools_box = QtWidgets.QVBoxLayout() self.tools_box.setContentsMargins(0, 0, 0, 0) self.tools_frame.setLayout(self.tools_box) self.title_box = QtWidgets.QHBoxLayout() self.tools_box.addLayout(self.title_box) # ## Title title_label = FCLabel("%s" % self.pluginName, size=16, bold=True) title_label.setToolTip( _("Generate CNC Code with auto-levelled paths.") ) self.title_box.addWidget(title_label) # App Level label self.level = QtWidgets.QToolButton() self.level.setToolTip( _( "Beginner Mode - many parameters are hidden.\n" "Advanced Mode - full control.\n" "Permanent change is done in 'Preferences' menu." ) ) # self.level.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) self.level.setCheckable(True) self.title_box.addWidget(self.level) self.obj_combo_label = FCLabel('%s' % _("Source Object"), color='darkorange', bold=True) self.obj_combo_label.setToolTip( _("CNCJob source object to be levelled.") ) self.tools_box.addWidget(self.obj_combo_label) # ############################################################################################################# # ################################ The object to be Auto-levelled ############################################## # ############################################################################################################# self.object_combo = FCComboBox() self.object_combo.setModel(self.app.collection) self.object_combo.setRootModelIndex(self.app.collection.index(3, 0, QtCore.QModelIndex())) # self.object_combo.setCurrentIndex(1) self.object_combo.is_last = True self.tools_box.addWidget(self.object_combo) # separator_line = QtWidgets.QFrame() # separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) # separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) # self.tools_box.addWidget(separator_line) # Autolevelling self.al_frame = QtWidgets.QFrame() self.al_frame.setContentsMargins(0, 0, 0, 0) self.tools_box.addWidget(self.al_frame) self.al_box = QtWidgets.QVBoxLayout() self.al_box.setContentsMargins(0, 0, 0, 0) self.al_frame.setLayout(self.al_box) self.al_frame.setDisabled(True) grid0 = GLay(v_spacing=5, h_spacing=3) self.al_box.addLayout(grid0) self.al_title = FCLabel('%s' % _("Probe Points Table"), bold=True) self.al_title.setToolTip(_("Generate GCode that will obtain the height map")) self.show_al_table = FCCheckBox(_("Show")) self.show_al_table.setToolTip(_("Toggle the display of the Probe Points table.")) self.show_al_table.setChecked(True) hor_lay = QtWidgets.QHBoxLayout() hor_lay.addWidget(self.al_title) hor_lay.addStretch() hor_lay.addWidget(self.show_al_table, alignment=QtCore.Qt.AlignmentFlag.AlignRight) grid0.addLayout(hor_lay, 0, 0, 1, 2) # ############################################################################################################# # Tool Table Frame # ############################################################################################################# tt_frame = FCFrame() self.al_box.addWidget(tt_frame) # Grid Layout tool_grid = GLay(v_spacing=5, h_spacing=3, c_stretch=[0, 1]) tt_frame.setLayout(tool_grid) # Probe Points table self.al_probe_points_table = FCTable() self.al_probe_points_table.setColumnCount(3) self.al_probe_points_table.setColumnWidth(0, 20) self.al_probe_points_table.setHorizontalHeaderLabels(['#', _('X-Y Coordinates'), _('Height')]) tool_grid.addWidget(self.al_probe_points_table, 0, 0, 1, 2) # Plot Probe Points self.plot_probing_pts_cb = FCCheckBox(_("Plot probing points")) self.plot_probing_pts_cb.setToolTip( _("Plot the probing points in the table.\n" "If a Voronoi method is used then\n" "the Voronoi areas are also plotted.") ) tool_grid.addWidget(self.plot_probing_pts_cb, 2, 0, 1, 2) # Avoid Excellon holes self.avoid_exc_holes_cb = FCCheckBox(_("Avoid Excellon holes")) self.avoid_exc_holes_cb.setToolTip( _("When active, the user cannot add probe points over a drill hole.") ) tool_grid.addWidget(self.avoid_exc_holes_cb, 4, 0, 1, 2) # Avoid Excellon holes Size self.avoid_exc_holes_size_label = FCLabel('%s:' % _("Avoid Step")) self.avoid_exc_holes_size_label.setToolTip( _("The incremental size to move to the side, to avoid an Excellon hole.") ) self.avoid_exc_holes_size_entry = FCDoubleSpinner() self.avoid_exc_holes_size_entry.set_precision(self.decimals) self.avoid_exc_holes_size_entry.set_range(0.0000, 99999.0000) tool_grid.addWidget(self.avoid_exc_holes_size_label, 6, 0) tool_grid.addWidget(self.avoid_exc_holes_size_entry, 6, 1, 1, 1) # ############################################################################################################# # ############### Probe GCode Generation ###################################################################### # ############################################################################################################# self.probe_gc_label = FCLabel('%s' % _("Parameters"), color='blue', bold=True) self.probe_gc_label.setToolTip( _("Will create a GCode which will be sent to the controller,\n" "either through a file or directly, with the intent to get the height map\n" "that is to modify the original GCode to level the cutting height.") ) self.al_box.addWidget(self.probe_gc_label) tp_frame = FCFrame() self.al_box.addWidget(tp_frame) # Grid Layout param_grid = GLay(v_spacing=5, h_spacing=3) tp_frame.setLayout(param_grid) # Probe Diameter self.probe_tip_dia_label = FCLabel('%s:' % _("Probe Tip Dia")) self.probe_tip_dia_label.setToolTip( _("The probe tip diameter.") ) self.probe_tip_dia_entry = FCDoubleSpinner() self.probe_tip_dia_entry.set_precision(self.decimals) self.probe_tip_dia_entry.set_range(0.0000, 10.0000) param_grid.addWidget(self.probe_tip_dia_label, 0, 0) param_grid.addWidget(self.probe_tip_dia_entry, 0, 1) # Travel Z Probe self.ptravelz_label = FCLabel('%s:' % _("Probe Z travel")) self.ptravelz_label.setToolTip( _("The safe Z for probe travelling between probe points.") ) self.ptravelz_entry = FCDoubleSpinner() self.ptravelz_entry.set_precision(self.decimals) self.ptravelz_entry.set_range(0.0000, 10000.0000) param_grid.addWidget(self.ptravelz_label, 2, 0) param_grid.addWidget(self.ptravelz_entry, 2, 1) # Probe depth self.pdepth_label = FCLabel('%s:' % _("Probe Z depth")) self.pdepth_label.setToolTip( _("The maximum depth that the probe is allowed\n" "to probe. Negative value, in current units.") ) self.pdepth_entry = FCDoubleSpinner() self.pdepth_entry.set_precision(self.decimals) self.pdepth_entry.set_range(-910000.0000, 0.0000) param_grid.addWidget(self.pdepth_label, 4, 0) param_grid.addWidget(self.pdepth_entry, 4, 1) # Probe feedrate self.feedrate_probe_label = FCLabel('%s:' % _("Probe Feedrate")) self.feedrate_probe_label.setToolTip( _("The feedrate used while the probe is probing.") ) self.feedrate_probe_entry = FCDoubleSpinner() self.feedrate_probe_entry.set_precision(self.decimals) self.feedrate_probe_entry.set_range(0, 910000.0000) param_grid.addWidget(self.feedrate_probe_label, 6, 0) param_grid.addWidget(self.feedrate_probe_entry, 6, 1) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) param_grid.addWidget(separator_line, 8, 0, 1, 2) # AUTOLEVELL MODE al_mode_lbl = FCLabel('%s' % _("Mode"), bold=True) al_mode_lbl.setToolTip(_("Choose a mode for height map generation.\n" "- Manual: will pick a selection of probe points by clicking on canvas\n" "- Grid: will automatically generate a grid of probe points")) self.al_mode_radio = RadioSet( [ {'label': _('Manual'), 'value': 'manual'}, {'label': _('Grid'), 'value': 'grid'} ]) param_grid.addWidget(al_mode_lbl, 10, 0) param_grid.addWidget(self.al_mode_radio, 10, 1) # AUTOLEVELL METHOD self.al_method_lbl = FCLabel('%s:' % _("Method")) self.al_method_lbl.setToolTip(_("Choose a method for approximation of heights from autolevelling data.\n" "- Voronoi: will generate a Voronoi diagram\n" "- Bilinear: will use bilinear interpolation. Usable only for grid mode.")) self.al_method_radio = RadioSet( [ {'label': _('Voronoi'), 'value': 'v'}, {'label': _('Bilinear'), 'value': 'b'} ]) self.al_method_lbl.setDisabled(True) self.al_method_radio.setDisabled(True) self.al_method_radio.set_value('v') param_grid.addWidget(self.al_method_lbl, 12, 0) param_grid.addWidget(self.al_method_radio, 12, 1) # ## Columns self.al_columns_entry = FCSpinner() self.al_columns_entry.setMinimum(2) self.al_columns_label = FCLabel('%s:' % _("Columns")) self.al_columns_label.setToolTip( _("The number of grid columns.") ) param_grid.addWidget(self.al_columns_label, 14, 0) param_grid.addWidget(self.al_columns_entry, 14, 1) # ## Rows self.al_rows_entry = FCSpinner() self.al_rows_entry.setMinimum(2) self.al_rows_label = FCLabel('%s:' % _("Rows")) self.al_rows_label.setToolTip( _("The number of grid rows.") ) param_grid.addWidget(self.al_rows_label, 16, 0) param_grid.addWidget(self.al_rows_entry, 16, 1) self.al_add_button = FCButton(_("Add Probe Points")) self.al_box.addWidget(self.al_add_button) # ############################################################################################################# # Controller Frame # ############################################################################################################# self.al_controller_label = FCLabel('%s' % _("Controller"), color='red', bold=True) self.al_controller_label.setToolTip( _("The kind of controller for which to generate\n" "height map gcode.") ) self.al_box.addWidget(self.al_controller_label) self.c_frame = FCFrame() self.al_box.addWidget(self.c_frame) ctrl_grid = GLay(v_spacing=5, h_spacing=3) self.c_frame.setLayout(ctrl_grid) self.al_controller_combo = FCComboBox() self.al_controller_combo.addItems(["MACH3", "MACH4", "LinuxCNC", "GRBL"]) ctrl_grid.addWidget(self.al_controller_combo, 0, 0, 1, 2) # ############################################################################################################# # ########################## GRBL frame ####################################################################### # ############################################################################################################# self.grbl_frame = QtWidgets.QFrame() self.grbl_frame.setContentsMargins(0, 0, 0, 0) ctrl_grid.addWidget(self.grbl_frame, 2, 0, 1, 2) self.grbl_box = QtWidgets.QVBoxLayout() self.grbl_box.setContentsMargins(0, 0, 0, 0) self.grbl_frame.setLayout(self.grbl_box) # ############################################################################################################# # ########################## GRBL TOOLBAR ##################################################################### # ############################################################################################################# self.al_toolbar = FCDetachableTab(protect=True) self.al_toolbar.setTabsClosable(False) self.al_toolbar.useOldIndex(True) self.al_toolbar.set_detachable(val=False) self.grbl_box.addWidget(self.al_toolbar) # GRBL Connect TAB self.gr_conn_tab = QtWidgets.QWidget() self.gr_conn_tab.setObjectName("connect_tab") self.gr_conn_tab_layout = QtWidgets.QVBoxLayout(self.gr_conn_tab) self.gr_conn_tab_layout.setContentsMargins(2, 2, 2, 2) # self.gr_conn_scroll_area = VerticalScrollArea() # self.gr_conn_tab_layout.addWidget(self.gr_conn_scroll_area) self.al_toolbar.addTab(self.gr_conn_tab, _("Connect")) # GRBL Control TAB self.gr_ctrl_tab = QtWidgets.QWidget() self.gr_ctrl_tab.setObjectName("connect_tab") self.gr_ctrl_tab_layout = QtWidgets.QVBoxLayout(self.gr_ctrl_tab) self.gr_ctrl_tab_layout.setContentsMargins(2, 2, 2, 2) # self.gr_ctrl_scroll_area = VerticalScrollArea() # self.gr_ctrl_tab_layout.addWidget(self.gr_ctrl_scroll_area) self.al_toolbar.addTab(self.gr_ctrl_tab, _("Control")) # GRBL Sender TAB self.gr_send_tab = QtWidgets.QWidget() self.gr_send_tab.setObjectName("connect_tab") self.gr_send_tab_layout = QtWidgets.QVBoxLayout(self.gr_send_tab) self.gr_send_tab_layout.setContentsMargins(2, 2, 2, 2) # self.gr_send_scroll_area = VerticalScrollArea() # self.gr_send_tab_layout.addWidget(self.gr_send_scroll_area) self.al_toolbar.addTab(self.gr_send_tab, _("Sender")) for idx in range(self.al_toolbar.count()): if self.al_toolbar.tabText(idx) == _("Connect"): self.al_toolbar.tabBar.setTabTextColor(idx, QtGui.QColor('red')) if self.al_toolbar.tabText(idx) == _("Control"): self.al_toolbar.tabBar.setTabEnabled(idx, False) if self.al_toolbar.tabText(idx) == _("Sender"): self.al_toolbar.tabBar.setTabEnabled(idx, False) # ############################################################################################################# # ############################################################################################################# # GRBL CONNECT # ############################################################################################################# self.connect_frame = FCFrame() self.gr_conn_tab_layout.addWidget(self.connect_frame) grbl_conn_grid = GLay(v_spacing=5, h_spacing=3, c_stretch=[0, 1, 0]) self.connect_frame.setLayout(grbl_conn_grid) # COM list self.com_list_label = FCLabel('%s:' % _("COM list")) self.com_list_label.setToolTip( _("Lists the available serial ports.") ) self.com_list_combo = FCComboBox() self.com_search_button = FCButton(_("Search")) self.com_search_button.setToolTip( _("Search for the available serial ports.") ) grbl_conn_grid.addWidget(self.com_list_label, 2, 0) grbl_conn_grid.addWidget(self.com_list_combo, 2, 1) grbl_conn_grid.addWidget(self.com_search_button, 2, 2) # BAUDRATES list self.baudrates_list_label = FCLabel('%s:' % _("Baud rates")) self.baudrates_list_label.setToolTip( _("Lists the available serial ports.") ) self.baudrates_list_combo = FCComboBox() cb_model = QtCore.QStringListModel() self.baudrates_list_combo.setModel(cb_model) self.baudrates_list_combo.addItems( ['9600', '19200', '38400', '57600', '115200', '230400', '460800', '500000', '576000', '921600', '1000000', '1152000', '1500000', '2000000']) self.baudrates_list_combo.setCurrentText('115200') grbl_conn_grid.addWidget(self.baudrates_list_label, 4, 0) grbl_conn_grid.addWidget(self.baudrates_list_combo, 4, 1) # New baudrate self.new_bd_label = FCLabel('%s:' % _("New")) self.new_bd_label.setToolTip( _("New, custom baudrate.") ) self.new_baudrate_entry = FCSpinner() self.new_baudrate_entry.set_range(40, 9999999) self.add_bd_button = FCButton(_("Add")) self.add_bd_button.setToolTip( _("Add the specified custom baudrate to the list.") ) grbl_conn_grid.addWidget(self.new_bd_label, 6, 0) grbl_conn_grid.addWidget(self.new_baudrate_entry, 6, 1) grbl_conn_grid.addWidget(self.add_bd_button, 6, 2) self.del_bd_button = FCButton(_("Delete selected baudrate")) grbl_conn_grid.addWidget(self.del_bd_button, 8, 0, 1, 3) ctrl_h_lay = QtWidgets.QHBoxLayout() self.controller_reset_button = FCButton(_("Reset")) self.controller_reset_button.setToolTip( _("Software reset of the controller.") ) self.controller_reset_button.setDisabled(True) ctrl_h_lay.addWidget(self.controller_reset_button) self.com_connect_button = FCButton() self.com_connect_button.setText(_("Disconnected")) self.com_connect_button.setToolTip( _("Connect to the selected port with the selected baud rate.") ) self.com_connect_button.setStyleSheet("QPushButton {background-color: red;}") ctrl_h_lay.addWidget(self.com_connect_button) grbl_conn_grid.addWidget(FCLabel(""), 9, 0, 1, 3) grbl_conn_grid.setRowStretch(9, 1) grbl_conn_grid.addLayout(ctrl_h_lay, 10, 0, 1, 3) # ############################################################################################################# # GRBL CONTROL # ############################################################################################################# self.ctrl_grbl_frame = FCFrame() self.gr_ctrl_tab_layout.addWidget(self.ctrl_grbl_frame) grbl_ctrl_grid = GLay(v_spacing=5, h_spacing=3, c_stretch=[0, 1, 0]) self.ctrl_grbl_frame.setLayout(grbl_ctrl_grid) self.ctrl_grbl_frame2 = FCFrame() self.gr_ctrl_tab_layout.addWidget(self.ctrl_grbl_frame2) grbl_ctrl2_grid = GLay(v_spacing=5, h_spacing=3) self.ctrl_grbl_frame2.setLayout(grbl_ctrl2_grid) self.gr_ctrl_tab_layout.addStretch(1) jog_title_label = FCLabel(_("Jog"), bold=True) zero_title_label = FCLabel(_("Zero Axes"), bold=True) # zero_title_label.setStyleSheet(""" # FCLabel # { # font-weight: bold; # } # """) grbl_ctrl_grid.addWidget(jog_title_label, 0, 0) grbl_ctrl_grid.addWidget(zero_title_label, 0, 2) self.jog_wdg = FCJog(self.app) self.jog_wdg.setStyleSheet(""" FCJog { border: 1px solid lightgray; border-radius: 5px; } """) self.zero_axs_wdg = FCZeroAxes(self.app) self.zero_axs_wdg.setStyleSheet(""" FCZeroAxes { border: 1px solid lightgray; border-radius: 5px } """) grbl_ctrl_grid.addWidget(self.jog_wdg, 2, 0) grbl_ctrl_grid.addWidget(self.zero_axs_wdg, 2, 2) self.pause_resume_button = RotatedToolButton() self.pause_resume_button.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) self.pause_resume_button.setText(_("Pause/Resume")) self.pause_resume_button.setCheckable(True) self.pause_resume_button.setStyleSheet(""" RotatedToolButton:checked { background-color: red; color: white; border: none; } """) pause_frame = QtWidgets.QFrame() pause_frame.setContentsMargins(0, 0, 0, 0) pause_frame.setSizePolicy(QtWidgets.QSizePolicy.Policy.Ignored, QtWidgets.QSizePolicy.Policy.Expanding) pause_h_lay = QtWidgets.QHBoxLayout() pause_h_lay.setContentsMargins(0, 0, 0, 0) pause_h_lay.addWidget(self.pause_resume_button) pause_frame.setLayout(pause_h_lay) grbl_ctrl_grid.addWidget(pause_frame, 2, 1) # JOG Step self.jog_step_label = FCLabel('%s:' % _("Step")) self.jog_step_label.setToolTip( _("Each jog action will move the axes with this value.") ) self.jog_step_entry = FCSliderWithDoubleSpinner() self.jog_step_entry.set_precision(self.decimals) self.jog_step_entry.setSingleStep(0.1) self.jog_step_entry.set_range(0, 500) grbl_ctrl2_grid.addWidget(self.jog_step_label, 0, 0) grbl_ctrl2_grid.addWidget(self.jog_step_entry, 0, 1) # JOG Feedrate self.jog_fr_label = FCLabel('%s:' % _("Feedrate")) self.jog_fr_label.setToolTip( _("Feedrate when jogging.") ) self.jog_fr_entry = FCSliderWithDoubleSpinner() self.jog_fr_entry.set_precision(self.decimals) self.jog_fr_entry.setSingleStep(10) self.jog_fr_entry.set_range(0, 10000) grbl_ctrl2_grid.addWidget(self.jog_fr_label, 1, 0) grbl_ctrl2_grid.addWidget(self.jog_fr_entry, 1, 1) # ############################################################################################################# # GRBL SENDER # ############################################################################################################# self.sender_frame = FCFrame() self.gr_send_tab_layout.addWidget(self.sender_frame) grbl_send_grid = GLay(v_spacing=5, h_spacing=3, c_stretch=[1, 0]) self.sender_frame.setLayout(grbl_send_grid) # Send CUSTOM COMMAND self.grbl_command_label = FCLabel('%s:' % _("Send Command")) self.grbl_command_label.setToolTip( _("Send a custom command to GRBL.") ) grbl_send_grid.addWidget(self.grbl_command_label, 2, 0, 1, 2) self.grbl_command_entry = FCEntry() self.grbl_command_entry.setPlaceholderText(_("Type GRBL command ...")) self.grbl_send_button = QtWidgets.QToolButton() self.grbl_send_button.setText(_("Send")) self.grbl_send_button.setToolTip( _("Send a custom command to GRBL.") ) grbl_send_grid.addWidget(self.grbl_command_entry, 4, 0) grbl_send_grid.addWidget(self.grbl_send_button, 4, 1) # Get Parameter self.grbl_get_param_label = FCLabel('%s:' % _("Get Config parameter")) self.grbl_get_param_label.setToolTip( _("A GRBL configuration parameter.") ) grbl_send_grid.addWidget(self.grbl_get_param_label, 6, 0, 1, 2) self.grbl_parameter_entry = FCEntry() self.grbl_parameter_entry.setPlaceholderText(_("Type GRBL parameter ...")) self.grbl_get_param_button = QtWidgets.QToolButton() self.grbl_get_param_button.setText(_("Get")) self.grbl_get_param_button.setToolTip( _("Get the value of a specified GRBL parameter.") ) grbl_send_grid.addWidget(self.grbl_parameter_entry, 8, 0) grbl_send_grid.addWidget(self.grbl_get_param_button, 8, 1) grbl_send_grid.setRowStretch(9, 1) # GET Report self.grbl_report_button = FCButton(_("Get Report")) self.grbl_report_button.setToolTip( _("Print in shell the GRBL report.") ) grbl_send_grid.addWidget(self.grbl_report_button, 10, 0, 1, 2) hm_lay = QtWidgets.QHBoxLayout() # GET HEIGHT MAP self.grbl_get_heightmap_button = FCButton(_("Apply AutoLevelling")) self.grbl_get_heightmap_button.setToolTip( _("Will send the probing GCode to the GRBL controller,\n" "wait for the Z probing data and then apply this data\n" "over the original GCode therefore doing autolevelling.") ) hm_lay.addWidget(self.grbl_get_heightmap_button, stretch=1) self.grbl_save_height_map_button = QtWidgets.QToolButton() self.grbl_save_height_map_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png')) self.grbl_save_height_map_button.setToolTip( _("Will save the GRBL height map.") ) hm_lay.addWidget(self.grbl_save_height_map_button, stretch=0, alignment=Qt.AlignmentFlag.AlignRight) grbl_send_grid.addLayout(hm_lay, 12, 0, 1, 2) self.grbl_frame.hide() # ############################################################################################################# height_lay = QtWidgets.QHBoxLayout() self.h_gcode_button = FCButton(_("Save Probing GCode")) self.h_gcode_button.setToolTip( _("Will save the probing GCode.") ) self.h_gcode_button.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) height_lay.addWidget(self.h_gcode_button) self.view_h_gcode_button = QtWidgets.QToolButton() self.view_h_gcode_button.setIcon(QtGui.QIcon(self.app.resource_location + '/edit_file32.png')) # self.view_h_gcode_button.setSizePolicy(QtWidgets.QSizePolicy.Policy.Ignored, # QtWidgets.QSizePolicy.Policy.Ignored) self.view_h_gcode_button.setToolTip( _("View/Edit the probing GCode.") ) # height_lay.addStretch() height_lay.addWidget(self.view_h_gcode_button) self.al_box.addLayout(height_lay) self.import_heights_button = FCButton(_("Import Height Map")) self.import_heights_button.setToolTip( _("Import the file that has the Z heights\n" "obtained through probing and then apply this data\n" "over the original GCode therefore\n" "doing autolevelling.") ) self.al_box.addWidget(self.import_heights_button) # self.h_gcode_button.hide() # self.import_heights_button.hide() # separator_line = QtWidgets.QFrame() # separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) # separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) # grid0.addWidget(separator_line, 35, 0, 1, 2) self.tools_box.addStretch(1) # ## Reset Tool self.reset_button = FCButton(_("Reset Tool"), bold=True) self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png')) self.reset_button.setToolTip( _("Will reset the tool parameters.") ) self.tools_box.addWidget(self.reset_button) # ############################ FINISHED GUI ################################### # ############################################################################# GLay.set_common_column_size([tool_grid, param_grid], 0) self.plot_probing_pts_cb.stateChanged.connect(self.on_plot_points_changed) self.avoid_exc_holes_cb.stateChanged.connect(self.on_avoid_exc_holes_changed) def confirmation_message(self, accepted, minval, maxval): if accepted is False: self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), self.decimals, minval, self.decimals, maxval), False) else: self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) def confirmation_message_int(self, accepted, minval, maxval): if accepted is False: self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % (_("Edited value is out of range"), minval, maxval), False) else: self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) def on_plot_points_changed(self, state): self.app.options["tools_al_plot_points"] = False if not state else True def on_avoid_exc_holes_changed(self, state): self.app.options["tools_al_avoid_exc_holes"] = False if not state else True