- in Corner Markers Plugin I added a new feature, ability to insert existing markers into external objects. Finished the work for external Gerbers and work in progress for the external Geometry
2387 lines
98 KiB
Python
2387 lines
98 KiB
Python
# ##########################################################
|
|
# FlatCAM: 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 appObjects.FlatCAMObj import ObjectDeleted
|
|
from appTool import AppTool
|
|
from appGUI.VisPyVisuals import *
|
|
from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
|
|
from appGUI.GUIElements import RadioSet, FCButton, FCComboBox, FCLabel, FCFileSaveDialog, FCCheckBox, FCTable, \
|
|
FCDoubleSpinner, FCSpinner, FCDetachableTab, FCZeroAxes, FCJog, FCSliderWithDoubleSpinner, RotatedToolButton, \
|
|
FCEntry, VerticalScrollArea, FCGridLayout, FCFrame, FCComboBox2
|
|
from appEditors.AppTextEditor import AppTextEditor
|
|
|
|
from camlib import CNCjob
|
|
|
|
from copy import deepcopy
|
|
import time
|
|
import serial
|
|
import glob
|
|
import random
|
|
import sys
|
|
from io import StringIO
|
|
from datetime import datetime
|
|
|
|
import numpy as np
|
|
|
|
from shapely.ops import unary_union
|
|
from shapely.geometry import Point, MultiPoint, box, MultiPolygon
|
|
import shapely.affinity as affinity
|
|
|
|
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
|
|
|
|
import logging
|
|
import gettext
|
|
import appTranslation as fcTranslate
|
|
import builtins
|
|
|
|
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.defaults["cncjob_steps_per_circle"])
|
|
|
|
# #############################################################################
|
|
# ######################### Tool GUI ##########################################
|
|
# #############################################################################
|
|
self.ui = LevelUI(layout=self.layout, app=self.app)
|
|
self.pluginName = self.ui.pluginName
|
|
self.connect_signals_at_init()
|
|
|
|
# updated in the self.set_tool_ui()
|
|
self.form_fields = {}
|
|
|
|
self.first_click = False
|
|
self.cursor_pos = None
|
|
self.mouse_is_dragging = False
|
|
|
|
# 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.defaults['global_selection_shape']
|
|
|
|
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("ToolFollow()")
|
|
|
|
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])
|
|
|
|
AppTool.run(self)
|
|
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.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.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.options['name']
|
|
else:
|
|
name = ''
|
|
|
|
# Shapes container for the Voronoi cells in Autolevelling
|
|
if self.app.is_legacy is False:
|
|
self.probing_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
|
|
else:
|
|
self.probing_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=name + "_probing_shapes")
|
|
|
|
self.form_fields.update({
|
|
"tools_al_travelz": 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_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.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.setDisabled(True)
|
|
self.ui.al_method_radio.set_value('v')
|
|
|
|
if loaded_obj and loaded_obj.is_segmented_gcode is True and loaded_obj.options["type"] == 'Geometry':
|
|
self.ui.al_frame.setDisabled(False)
|
|
self.ui.al_mode_radio.set_value(loaded_obj.options['tools_al_mode'])
|
|
self.on_controller_change()
|
|
|
|
self.on_mode_radio(val=loaded_obj.options['tools_al_mode'])
|
|
self.on_method_radio(val=loaded_obj.options['tools_al_method'])
|
|
else:
|
|
self.ui.al_frame.setDisabled(True)
|
|
|
|
# Show/Hide Advanced Options
|
|
app_mode = self.app.defaults["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()
|
|
|
|
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.options["type"] == 'Geometry':
|
|
|
|
self.ui.al_frame.setDisabled(False)
|
|
|
|
# Shapes container for the Voronoi cells in Autolevelling
|
|
if self.app.is_legacy is False:
|
|
self.probing_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
|
|
else:
|
|
self.probing_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=obj_name + "_probing_shapes")
|
|
return
|
|
|
|
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.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()
|
|
|
|
xmin, ymin, xmax, ymax = self.solid_geo.bounds
|
|
|
|
if self.ui.al_mode_radio.get_value() == 'grid':
|
|
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)
|
|
)
|
|
# do not add the point if is already added
|
|
if formatted_point not in points:
|
|
points.append(formatted_point)
|
|
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_2(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)
|
|
|
|
else:
|
|
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 = 0.3 if self.units == 'MM' else 0.012
|
|
fprobe_pt_buff = f_probe_pt.buffer(radius)
|
|
|
|
self.app.inform.emit(_("Click on canvas to add a Probe Point..."))
|
|
self.app.defaults['global_selection_shape'] = False
|
|
|
|
if self.app.is_legacy is False:
|
|
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.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()
|
|
|
|
# voronoi diagram
|
|
if al_method == 'v':
|
|
# create the geometry
|
|
radius = 0.1 if self.units == 'MM' else 0.004
|
|
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':
|
|
radius = 0.1 if self.units == 'MM' else 0.004
|
|
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.is_legacy is False:
|
|
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.is_legacy is False:
|
|
# 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] = affinity.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:
|
|
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(list(env.exterior.coords))
|
|
|
|
new_pts = [[pt.x, pt.y] for pt in pts]
|
|
|
|
# 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.points:
|
|
p_coords = [(coord.x, coord.y) for coord in p.get_coordinates()]
|
|
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
|
|
|
|
# To be called after clicking on the plot.
|
|
def on_mouse_click_release(self, event):
|
|
|
|
if self.app.is_legacy is False:
|
|
event_pos = event.pos
|
|
# event_is_dragging = event.is_dragging
|
|
right_button = 2
|
|
else:
|
|
event_pos = (event.xdata, event.ydata)
|
|
# event_is_dragging = self.app.plotcanvas.is_dragging
|
|
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:
|
|
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
|
|
|
|
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
|
|
|
|
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 = 0.3 if self.units == 'MM' else 0.012
|
|
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.is_legacy is False:
|
|
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)
|
|
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)
|
|
|
|
# signal that the mouse events are disconnected from local methods
|
|
self.mouse_events_connected = False
|
|
|
|
# restore selection
|
|
self.app.defaults['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_2(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 type(event) is int:
|
|
key = event
|
|
# events from the GUI are of type QKeyEvent
|
|
elif type(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.is_legacy is False:
|
|
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)
|
|
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.defaults['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_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.al_method_radio.set_value('v')
|
|
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.defaults['tools_al_method'])
|
|
|
|
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 genrated (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.defaults["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(datetime.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.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.defaults['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.defaults['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.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 autolevel 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.defaults['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.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)
|
|
title_label.setStyleSheet("""
|
|
QLabel
|
|
{
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
}
|
|
""")
|
|
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('<span style="color:darkorange;"><b>%s</b></span>' % _("Source Object"))
|
|
self.obj_combo_label.setToolTip(
|
|
_("CNCJob source object to be levelled.")
|
|
)
|
|
|
|
self.tools_box.addWidget(self.obj_combo_label)
|
|
|
|
# #############################################################################################################
|
|
# ################################ The object to be Autolevelled ##############################################
|
|
# #############################################################################################################
|
|
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)
|
|
|
|
grid0 = FCGridLayout(v_spacing=5, h_spacing=3)
|
|
self.al_box.addLayout(grid0)
|
|
|
|
self.al_title = FCLabel('<b>%s</b>' % _("Probe Points Table"))
|
|
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.tools_box.addWidget(tt_frame)
|
|
|
|
# Grid Layout
|
|
tool_grid = FCGridLayout(v_spacing=5, h_spacing=3, c_stretch=[0, 0])
|
|
tt_frame.setLayout(tool_grid)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
# #############################################################################################################
|
|
# ############### Probe GCode Generation ######################################################################
|
|
# #############################################################################################################
|
|
self.probe_gc_label = FCLabel('<span style="color:blue;"><b>%s</b></span>' % _("Parameters"))
|
|
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.tools_box.addWidget(self.probe_gc_label)
|
|
|
|
tp_frame = FCFrame()
|
|
self.tools_box.addWidget(tp_frame)
|
|
|
|
# Grid Layout
|
|
param_grid = FCGridLayout(v_spacing=5, h_spacing=3)
|
|
tp_frame.setLayout(param_grid)
|
|
|
|
# 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, 0, 0)
|
|
param_grid.addWidget(self.ptravelz_entry, 0, 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, 2, 0)
|
|
param_grid.addWidget(self.pdepth_entry, 2, 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, 4, 0)
|
|
param_grid.addWidget(self.feedrate_probe_entry, 4, 1)
|
|
|
|
separator_line = QtWidgets.QFrame()
|
|
separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
|
|
separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
|
|
param_grid.addWidget(separator_line, 6, 0, 1, 2)
|
|
|
|
# AUTOLEVELL MODE
|
|
al_mode_lbl = FCLabel('<b>%s</b>:' % _("Mode"))
|
|
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, 8, 0)
|
|
param_grid.addWidget(self.al_mode_radio, 8, 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, 10, 0)
|
|
param_grid.addWidget(self.al_method_radio, 10, 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, 12, 0)
|
|
param_grid.addWidget(self.al_columns_entry, 12, 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, 14, 0)
|
|
param_grid.addWidget(self.al_rows_entry, 14, 1)
|
|
|
|
self.al_add_button = FCButton(_("Add Probe Points"))
|
|
self.tools_box.addWidget(self.al_add_button)
|
|
|
|
# #############################################################################################################
|
|
# Controller Frame
|
|
# #############################################################################################################
|
|
self.al_controller_label = FCLabel('<span style="color:red;"><b>%s</b></span>' % _("Controller"))
|
|
self.al_controller_label.setToolTip(
|
|
_("The kind of controller for which to generate\n"
|
|
"height map gcode.")
|
|
)
|
|
self.tools_box.addWidget(self.al_controller_label)
|
|
|
|
self.c_frame = FCFrame()
|
|
self.tools_box.addWidget( self.c_frame)
|
|
|
|
ctrl_grid = FCGridLayout(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 = FCGridLayout(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()
|
|
cbmodel = QtCore.QStringListModel()
|
|
self.baudrates_list_combo.setModel(cbmodel)
|
|
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_hlay = 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_hlay.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_hlay.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_hlay, 10, 0, 1, 3)
|
|
|
|
# #############################################################################################################
|
|
# GRBL CONTROL
|
|
# #############################################################################################################
|
|
self.ctrl_grbl_frame = FCFrame()
|
|
self.gr_ctrl_tab_layout.addWidget(self.ctrl_grbl_frame)
|
|
grbl_ctrl_grid = FCGridLayout(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 = FCGridLayout(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"))
|
|
jog_title_label.setStyleSheet("""
|
|
FCLabel
|
|
{
|
|
font-weight: bold;
|
|
}
|
|
""")
|
|
|
|
zero_title_label = FCLabel(_("Zero Axes"))
|
|
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_hlay = QtWidgets.QHBoxLayout()
|
|
pause_hlay.setContentsMargins(0, 0, 0, 0)
|
|
|
|
pause_hlay.addWidget(self.pause_resume_button)
|
|
pause_frame.setLayout(pause_hlay)
|
|
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 = FCGridLayout(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.tools_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.tools_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"))
|
|
self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
|
|
self.reset_button.setToolTip(
|
|
_("Will reset the tool parameters.")
|
|
)
|
|
self.reset_button.setStyleSheet("""
|
|
QPushButton
|
|
{
|
|
font-weight: bold;
|
|
}
|
|
""")
|
|
self.tools_box.addWidget(self.reset_button)
|
|
# ############################ FINISHED GUI ###################################
|
|
# #############################################################################
|
|
|
|
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)
|