# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File by: Marius Adrian Stanciu (c) #
# Date: 11/12/2020 #
# License: MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.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
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 voronoi import Voronoi
from voronoi 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
# 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']
# #############################################################################################################
# ####################################### Signals ###########################################################
# #############################################################################################################
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("ToolFollow()")
log.debug("ToolLevelling().run() was launched ...")
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:
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.defaults['units'].upper()
# 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:
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.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.ScrollBarAlwaysOff)
h_header = self.ui.al_probe_points_table.horizontalHeader()
h_header.setMinimumSectionSize(10)
h_header.setDefaultSectionSize(70)
h_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
h_header.resizeSection(0, 20)
h_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
h_header.setSectionResizeMode(2, QtWidgets.QHeaderView.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')
target_obj = self.app.collection.get_by_name(self.ui.object_combo.get_value())
if target_obj and target_obj.is_segmented_gcode is True:
self.ui.al_frame.setDisabled(False)
self.ui.al_mode_radio.set_value(target_obj.options['tools_al_mode'])
self.on_controller_change()
self.on_mode_radio(val=target_obj.options['tools_al_mode'])
self.on_method_radio(val=target_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 None or target_obj.is_segmented_gcode is False:
self.ui.al_frame.setDisabled(True)
else:
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")
def on_object_selection_changed(self, current, previous):
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.ItemIsEnabled)
coords_item.setFlags(QtCore.Qt.ItemIsEnabled)
height_item.setFlags(QtCore.Qt.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.Fixed)
h_header.resizeSection(0, 20)
h_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
h_header.setSectionResizeMode(2, QtWidgets.QHeaderView.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.ControlModifier
pass
elif mod.lower() == 'alt':
# modifiers = QtCore.Qt.AltModifier
pass
elif mod.lower() == 'shift':
# modifiers = QtCore.Qt.ShiftModifier
pass
else:
# modifiers = QtCore.Qt.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_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_G or key == 'G':
self.app.ui.grid_snap_btn.trigger()
# Jump to coords
if key == QtCore.Qt.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):
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()
# 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
if not self.probing_gcode_text or self.probing_gcode_text == '':
# 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)
@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(
_(
"In BEGINNER mode many parameters\n"
"are hidden from the user in this mode.\n"
"ADVANCED mode will make available all parameters.\n\n"
"To change the application LEVEL, go to:\n"
"Edit -> Preferences -> General and check:\n"
"'APP. LEVEL' radio button."
)
)
# self.level.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.level.setCheckable(True)
self.title_box.addWidget(self.level)
self.obj_combo_label = FCLabel('%s:' % _("CNCjob"))
self.obj_combo_label.setToolTip(
_("Source object.")
)
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.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.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 = QtWidgets.QGridLayout()
grid0.setColumnStretch(0, 0)
grid0.setColumnStretch(1, 1)
self.al_box.addLayout(grid0)
self.al_title = FCLabel('%s' % _("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.AlignRight)
grid0.addLayout(hor_lay, 0, 0, 1, 2)
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')])
grid0.addWidget(self.al_probe_points_table, 1, 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.")
)
grid0.addWidget(self.plot_probing_pts_cb, 3, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 5, 0, 1, 2)
# #############################################################################################################
# ############### Probe GCode Generation ######################################################################
# #############################################################################################################
self.probe_gc_label = FCLabel('%s:' % _("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.")
)
grid0.addWidget(self.probe_gc_label, 7, 0, 1, 2)
# 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)
grid0.addWidget(self.ptravelz_label, 9, 0)
grid0.addWidget(self.ptravelz_entry, 9, 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)
grid0.addWidget(self.pdepth_label, 11, 0)
grid0.addWidget(self.pdepth_entry, 11, 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)
grid0.addWidget(self.feedrate_probe_label, 13, 0)
grid0.addWidget(self.feedrate_probe_entry, 13, 1)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 15, 0, 1, 2)
# AUTOLEVELL MODE
al_mode_lbl = FCLabel('%s:' % _("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'}
])
grid0.addWidget(al_mode_lbl, 16, 0)
grid0.addWidget(self.al_mode_radio, 16, 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')
grid0.addWidget(self.al_method_lbl, 17, 0)
grid0.addWidget(self.al_method_radio, 17, 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.")
)
grid0.addWidget(self.al_columns_label, 19, 0)
grid0.addWidget(self.al_columns_entry, 19, 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.")
)
grid0.addWidget(self.al_rows_label, 21, 0)
grid0.addWidget(self.al_rows_entry, 21, 1)
self.al_add_button = FCButton(_("Add Probe Points"))
grid0.addWidget(self.al_add_button, 23, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 25, 0, 1, 2)
self.al_controller_label = FCLabel('%s:' % _("Controller"))
self.al_controller_label.setToolTip(
_("The kind of controller for which to generate\n"
"height map gcode.")
)
self.al_controller_combo = FCComboBox()
self.al_controller_combo.addItems(["MACH3", "MACH4", "LinuxCNC", "GRBL"])
grid0.addWidget(self.al_controller_label, 27, 0)
grid0.addWidget(self.al_controller_combo, 27, 1)
# #############################################################################################################
# ########################## GRBL frame #######################################################################
# #############################################################################################################
self.grbl_frame = QtWidgets.QFrame()
self.grbl_frame.setContentsMargins(0, 0, 0, 0)
grid0.addWidget(self.grbl_frame, 29, 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
# #############################################################################################################
grbl_conn_grid = QtWidgets.QGridLayout()
grbl_conn_grid.setColumnStretch(0, 0)
grbl_conn_grid.setColumnStretch(1, 1)
grbl_conn_grid.setColumnStretch(2, 0)
self.gr_conn_tab_layout.addLayout(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
# #############################################################################################################
grbl_ctrl_grid = QtWidgets.QGridLayout()
grbl_ctrl_grid.setColumnStretch(0, 0)
grbl_ctrl_grid.setColumnStretch(1, 1)
grbl_ctrl_grid.setColumnStretch(2, 0)
self.gr_ctrl_tab_layout.addLayout(grbl_ctrl_grid)
grbl_ctrl2_grid = QtWidgets.QGridLayout()
grbl_ctrl2_grid.setColumnStretch(0, 0)
grbl_ctrl2_grid.setColumnStretch(1, 1)
self.gr_ctrl_tab_layout.addLayout(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.Minimum, QtWidgets.QSizePolicy.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.Ignored, QtWidgets.QSizePolicy.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
# #############################################################################################################
grbl_send_grid = QtWidgets.QGridLayout()
grbl_send_grid.setColumnStretch(0, 1)
grbl_send_grid.setColumnStretch(1, 0)
self.gr_send_tab_layout.addLayout(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.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.Expanding, QtWidgets.QSizePolicy.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.Ignored, QtWidgets.QSizePolicy.Ignored)
self.view_h_gcode_button.setToolTip(
_("View/Edit the probing GCode.")
)
# height_lay.addStretch()
height_lay.addWidget(self.view_h_gcode_button)
grid0.addLayout(height_lay, 31, 0, 1, 2)
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.")
)
grid0.addWidget(self.import_heights_button, 33, 0, 1, 2)
self.h_gcode_button.hide()
self.import_heights_button.hide()
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.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)