VERSION = '016.087' LCNCVER = '2.10' ''' qtplasmac_handler.py Copyright (C) 2020-2025 Phillip A Carter Copyright (C) 2020-2026 Gregory D Carl This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ''' import os import sys from shutil import copy as COPY from subprocess import Popen, PIPE from subprocess import run as RUN from subprocess import call as CALL from importlib import reload import time import tarfile import math import glob import linuxcnc import hal from qtpy import QtCore, QtWidgets, QtGui from qtpy.QtCore import * from qtpy.QtWidgets import * from qtpy.QtGui import * try: from qtpy.Qsci import QsciScintilla except ImportError: try: from PyQt5.Qsci import QsciScintilla except ImportError: from PyQt6.Qsci import QsciScintilla from qtvcp import logger from qtvcp.core import Status, Action, Info, Tool try: from qtpy.QtWebEngineWidgets import QWebEngineView as _QWebEngineView _WEBENGINE_AVAILABLE = True except Exception: _WEBENGINE_AVAILABLE = False from qtvcp.lib.gcodes import GCodes from qtvcp.lib.keybindings import Keylookup from qtvcp.lib.preferences import Access from qtvcp.lib.qtplasmac import tooltips as TOOLTIPS from qtvcp.lib.qtplasmac import set_offsets as OFFSETS from qtvcp.lib.qtplasmac import updater as UPDATER from qtvcp.lib.qtplasmac.interlock_rules import INTERLOCK_RULES from qtvcp.widgets.camview_widget import CamView as CAM from qtvcp.widgets.file_manager import FileManager as FILE_MAN from qtvcp.widgets.gcode_editor import GcodeEditor as EDITOR from qtvcp.widgets.gcode_editor import GcodeDisplay as DISPLAY from qtvcp.widgets.mdi_history import MDIHistory as MDI_HISTORY from qtvcp.widgets.mdi_line import MDILine as MDI_LINE from qtvcp.widgets.origin_offsetview import OriginOffsetView as OFFSETVIEW from qtvcp.widgets.origin_offsetview import MyTableModel as OFFSET_TABLE from qtvcp.widgets.screen_options import ScreenOptions as OPTIONS from qtvcp.widgets.simple_widgets import DoubleScale as DOUBLESCALE from qtvcp.widgets.status_label import StatusLabel as STATLABEL from qtvcp.widgets.stylesheeteditor import StyleSheetEditor as SSE from qtvcp.lib.aux_program_loader import Aux_program_loader from plasmac import run_from_line as RFL from rs274.glcanon import GlCanonDraw as DRAW from qt5_graphics import Lcnc_3dGraphics as DRO LOG = logger.getLogger(__name__) KEYBIND = Keylookup() STATUS = Status() INFO = Info() ACTION = Action() TOOL = Tool() AUX_PRGM = Aux_program_loader() INIPATH = os.environ.get('INI_FILE_NAME', '/dev/null') _translate = QCoreApplication.translate # a vertical line as a separator on the status bar class VLine(QFrame): def __init__(self): super(VLine, self).__init__() self.setFrameShape(self.VLine | self.Plain) # dummy class to raise an exception for style issues class ColorError(Exception): pass # click signal for some labels def click_signal(widget): class Filter(QObject): clicked = Signal() def eventFilter(self, obj, event): if obj == widget: if event.type() == QEvent.MouseButtonRelease: if obj.rect().contains(event.pos()): self.clicked.emit() return True return False filter = Filter(widget) widget.installEventFilter(filter) return filter.clicked # the main handler class HandlerClass: from qtvcp.lib.qtplasmac import conversational as CONV # when self.w.button_frame changes size def eventFilter(self, object, event): if event.type() == QtCore.QEvent.Resize: self.size_changed(object) return True def __init__(self, halcomp, widgets, paths): self.interlockRules = INTERLOCK_RULES.copy() self.state = { 'all_homed': False, 'consumable_change': False, 'consumable_changing': False, 'estop_cleared': False, 'file_bounds_error': False, 'file_opened': False, 'framing': False, 'gcode_loaded': False, 'gcodestack_is_mdi': False, 'interp_idle': True, 'interp_paused': False, 'interp_running': False, 'laser_button_state': True, 'machine_on': False, 'manual_cut_active': False, 'offsets_active': False, 'ohmic_probe_enable': False, 'ohmic_test': False, 'plasmac_idle': True, 'previewstack_is_edit': False, 'previewstack_is_camera': False, 'previewstack_is_user_manual': False, 'previewstack_is_offsets': False, 'previewstack_is_open': False, 'previewstack_is_preview': True, 'probe_bounds_error': False, 'probe_test': False, 'rfl_dialog': False, 'single_cut_dialog': False, 'torch_enable': False, 'torch_pulse': False, } self.firstRun = True self.h = halcomp self.w = widgets self.h.comp.setprefix('qtplasmac') self.PATHS = paths self.iniFile = INFO.INI self.foreColor = '#ffee06' # can we find M190 in the USER_M_PATH # if not we will attempt to find a valid USER_M_PATH path later self.mPath = self.iniFile.find('RS274NGC', 'USER_M_PATH').split(':') for path in self.mPath: if path.startswith('.'): path = os.path.join(self.PATHS.CONFIGPATH, path) if os.path.isfile(os.path.join(path, 'M190')): self.mPath = 'valid' break self.machineName = self.iniFile.getstring('EMC', 'MACHINE', fallback='') self.machineTitle = f'{self.machineName} - QtPlasmaC v{LCNCVER}-{VERSION}, powered by QtVCP and LinuxCNC' self.docsVer = 'devel' if 'pre' in linuxcnc.version else LCNCVER self.prefsFile = os.path.join(self.PATHS.CONFIGPATH, self.machineName + '.prefs') self.materialFile = os.path.join(self.PATHS.CONFIGPATH, self.machineName + '_material.cfg') self.unitsPerMm = 1 self.units = self.iniFile.find('TRAJ', 'LINEAR_UNITS') if self.units == 'inch': self.units = 'in' self.unitsPerMm = 0.03937 # open prefs file so we can use it for updates # it will not exist prior to V1.222.170 2022/03/08 if os.path.isfile(self.prefsFile): self.PREFS = Access(self.prefsFile) else: self.PREFS = None self.updateIni = {} self.updateData = [] self.update_check() self.PREFS = Access(self.prefsFile) self.MATS = Access(self.materialFile) self.STYLEEDITOR = SSE(widgets, paths) self.GCODES = GCodes(widgets) self.IMAGES = os.path.join(self.PATHS.IMAGEDIR, 'qtplasmac/images/') self.landscape = True if os.path.basename(self.PATHS.XML) == 'qtplasmac_9x16.ui': self.landscape = False self.upFile = os.path.join(self.PATHS.CONFIGPATH, 'user_periodic.py') self.umUrl = QUrl(f'https://linuxcnc.org/docs/{self.docsVer}/html/plasma/qtplasmac.html') KEYBIND.add_call('Key_F12', 'on_keycall_F12') KEYBIND.add_call('Key_F9', 'on_keycall_F9') KEYBIND.add_call('Key_Plus', 'on_keycall_PLUS') KEYBIND.add_call('Key_Minus', 'on_keycall_MINUS') KEYBIND.add_call('Key_R', 'on_keycall_RUN') KEYBIND.add_call('Key_Any', 'on_keycall_PAUSE') KEYBIND.add_call('Key_o', 'on_keycall_OPEN') KEYBIND.add_call('Key_l', 'on_keycall_LOAD') KEYBIND.add_call('Key_j', 'on_keycall_JOINT') KEYBIND.add_call('Key_1', 'on_keycall_NUMBER', 1) KEYBIND.add_call('Key_Exclam', 'on_keycall_NUMBER', 1) KEYBIND.add_call('Key_2', 'on_keycall_NUMBER', 2) KEYBIND.add_call('Key_At', 'on_keycall_NUMBER', 2) KEYBIND.add_call('Key_3', 'on_keycall_NUMBER', 3) KEYBIND.add_call('Key_NumberSign', 'on_keycall_NUMBER', 3) KEYBIND.add_call('Key_4', 'on_keycall_NUMBER', 4) KEYBIND.add_call('Key_Dollar', 'on_keycall_NUMBER', 4) KEYBIND.add_call('Key_5', 'on_keycall_NUMBER', 5) KEYBIND.add_call('Key_Percent', 'on_keycall_NUMBER', 5) KEYBIND.add_call('Key_6', 'on_keycall_NUMBER', 6) KEYBIND.add_call('Key_AsciiCircum', 'on_keycall_NUMBER', 6) KEYBIND.add_call('Key_7', 'on_keycall_NUMBER', 7) KEYBIND.add_call('Key_Ampersand', 'on_keycall_NUMBER', 7) KEYBIND.add_call('Key_8', 'on_keycall_NUMBER', 8) KEYBIND.add_call('Key_Asterisk', 'on_keycall_NUMBER', 8) KEYBIND.add_call('Key_9', 'on_keycall_NUMBER', 9) KEYBIND.add_call('Key_ParenLeft', 'on_keycall_NUMBER', 9) KEYBIND.add_call('Key_0', 'on_keycall_NUMBER', 0) KEYBIND.add_call('Key_ParenRight', 'on_keycall_NUMBER', 0) # there are no default keys for joint/axis 4 KEYBIND.add_call('Key_Comma', 'on_keycall_BPOS') KEYBIND.add_call('Key_Less', 'on_keycall_BPOS') KEYBIND.add_call('Key_Period', 'on_keycall_BNEG') KEYBIND.add_call('Key_Greater', 'on_keycall_BNEG') KEYBIND.add_call('Key_End', 'on_keycall_END') KEYBIND.add_call('Key_Delete', 'on_keycall_DELETE') KEYBIND.add_call('Alt+Key_Return', 'on_keycall_ALTRETURN') KEYBIND.add_call('Alt+Key_Enter', 'on_keycall_ALTRETURN') KEYBIND.add_call('Key_Return', 'on_keycall_RETURN') KEYBIND.add_call('Key_Enter', 'on_keycall_RETURN') KEYBIND.add_call('Key_QuoteLeft', 'on_keycall_QUOTELEFT') KEYBIND.add_call('Key_AsciiTilde', 'on_keycall_QUOTELEFT') self.axes = {'valid': [x.lower() for x in INFO.AVAILABLE_AXES]} for axis in 'abc': self.axes[axis] = [] for n in ['dro_0', 'dro_label_0', 'home_0', 'touch_0', 'jog_0_plus', 'jog_0_minus']: self.axes[axis].append(n.replace('0', axis)) self.systemList = ['G53', 'G54', 'G55', 'G56', 'G57', 'G58', 'G59', 'G59.1', 'G59.2', 'G59.3'] self.slowJogFactor = 10 self.jogFast = False self.jogSlow = False self.lastLoadedProgram = 'None' self.set_interlock_defaults() self.jogButtonList = ['jog_x_plus', 'jog_x_minus', 'jog_y_plus', 'jog_y_minus', 'jog_z_plus', 'jog_z_minus', 'jog_a_plus', 'jog_a_minus', 'jog_b_plus', 'jog_b_minus', 'jog_c_plus', 'jog_c_minus'] self.jogSyncList = [] self.xMin = self.iniFile.getreal('AXIS_X', 'MIN_LIMIT') self.xMax = self.iniFile.getreal('AXIS_X', 'MAX_LIMIT') self.yMin = self.iniFile.getreal('AXIS_Y', 'MIN_LIMIT') self.yMax = self.iniFile.getreal('AXIS_Y', 'MAX_LIMIT') self.zMin = self.iniFile.getreal('AXIS_Z', 'MIN_LIMIT') self.zMax = self.iniFile.getreal('AXIS_Z', 'MAX_LIMIT') self.xLen = self.xMax - self.xMin self.yLen = self.yMax - self.yMin self.thcFeedRate = self.iniFile.getreal('AXIS_Z', 'MAX_VELOCITY') * \ self.iniFile.getreal('AXIS_Z', 'OFFSET_AV_RATIO') * 60 self.offsetFeedRate = min(self.iniFile.getreal('AXIS_X', 'MAX_VELOCITY') * 30, self.iniFile.getreal('AXIS_Y', 'MAX_VELOCITY') * 30, self.iniFile.getreal('TRAJ', 'MAX_LINEAR_VELOCITY', fallback=100000)) self.maxHeight = self.zMax - self.zMin self.maxPidP = self.thcFeedRate / self.unitsPerMm * 0.1 self.tmpPath = '/tmp/qtplasmac/' if not os.path.isdir(self.tmpPath): os.mkdir(self.tmpPath) self.tmpMaterialGcode = f'{self.tmpPath}{self.machineName}_material.gcode' self.gcodeErrorFile = f'{self.tmpPath}gcode_errors.txt' self.materialUpdate = False self.autoChange = False self.pmx485Exists = False self.pmx485Connected = False self.pmx485CommsError = False self.pmx485FaultCode = 0.0 self.pmx485ArcTime = 0.0 self.pmx485LabelState = None self.currentX = self.currentY = 0 self.degreeSymbol = u"\u00b0" self.cameraOn = False self.fTmp = f'{self.tmpPath}temp.ngc' self.fNgc = f'{self.tmpPath}shape.ngc' self.fNgcBkp = f'{self.tmpPath}backup.ngc' self.fNgcSent = f'{self.tmpPath}sent_shape.ngc' self.filteredBkp = f'{self.tmpPath}filtered_bkp.ngc' self.oldConvButton = False self.convWidgetsLoaded = False self.programPrefix = self.iniFile.getstring('DISPLAY', 'PROGRAM_PREFIX', fallback=os.environ['LINUXCNC_NCFILES_DIR']) self.dialogError = False self.cutTypeText = '' self.heightOvr = 0.0 self.heightOvrScale = 0.1 self.old_ovr_counts = 0 self.startLine = 0 self.preRflFile = '' self.preClearFile = '' self.rflActive = False self.torchOn = False self.progRun = False self.rapidOn = False self.probeOn = False self.gcodeProps = {} self.framing = False self.fileBoundsError = False self.probeBoundsError = False self.singleCutDialog = False self.ohmicTest = False self.obLayout = '' self.notifyColor = 'normal' self.firstHoming = False self.droScale = 1 self.mdiError = False self.extLaserButton = False self.virtualMachine = False self.realTimeDelay = False self.preSingleCutMaterial = None self.preFileSaveMaterial = None self.laserDryRunCoords = None self.runType = {'type': 'end'} # plasmac states self.IDLE = 0 self.PROBE_HEIGHT = 1 self.PROBE_DOWN = 2 self.PROBE_UP = 3 self.ZERO_HEIGHT = 4 self.PIERCE_HEIGHT = 5 self.TORCH_ON = 6 self.ARC_OK = 7 self.PIERCE_DELAY = 8 self.PUDDLE_JUMP = 9 self.CUT_HEIGHT = 10 self.CUT_MODE_01 = 11 self.CUT_MODE_2 = 12 self.PAUSE_AT_END = 13 self.SAFE_HEIGHT = 14 self.MAX_HEIGHT = 15 self.END_CUT = 16 self.END_JOB = 17 self.TORCH_PULSE = 18 self.PAUSED_MOTION = 19 self.OHMIC_TEST = 20 self.PROBE_TEST = 21 self.SCRIBING = 22 self.CONS_CHNG_ON = 23 self.CONS_CHNG_OFF = 24 self.CUT_REC_ON = 25 self.CUT_REC_OFF = 26 self.DEBUG = 27 # main tab widget tab indexes self.MAIN = 0 self.CONVERSATIONAL = 1 self.PARAMETERS = 2 self.SETTINGS = 3 self.STATISTICS = 4 # map index to name for interlock rules self.tabNames = { self.MAIN: 'main_tab', self.CONVERSATIONAL: 'conversational_tab', self.PARAMETERS: 'parameters_tab', self.SETTINGS: 'settings_tab', self.STATISTICS: 'statistics_tab', } # preview stack indexes self.PREVIEW = 0 self.OPEN = 1 self.EDIT = 2 self.CAMERA = 3 self.OFFSETS = 4 self.USER_MANUAL = 5 # gcode stack indexes self.GCODE = 0 self.MDI = 1 # jog stack indexes self.JOG = 0 self.CUT_RECOVERY = 1 # tool indexes self.TORCH = 0 self.SCRIBE = 1 self.UNKNOWN = 2 self.tool = self.TORCH # called by qtvcp.py def initialized__(self): head = _translate('HandlerClass', 'QtPlasmaC GUI Message') if '.'.join(linuxcnc.version.split('.')[:2]) != LCNCVER: msg0 = _translate('HandlerClass', 'LinuxCNC version should be') msg1 = _translate('HandlerClass', 'The detected version is') msg2 = _translate('HandlerClass', 'QtPlasmac is closing') msg = f'{head}:\n{msg0} {LCNCVER}\n\n{msg1} {linuxcnc.version.split(".")[:2]}\n\n{msg2}\n' STATUS.emit('error', linuxcnc.OPERATOR_ERROR, msg) quit() # if USER_M_PATH is not valid try to find a valid USER_M_PATH in the possible default locations if self.mPath != 'valid' and not self.updateIni: msg0 = _translate('HandlerClass', 'cannot be found in the path') msg1 = _translate('HandlerClass', 'Please edit [RS274NGC]USER_M_PATH in the .ini file') msg2 = _translate('HandlerClass', 'QtPlasmac is closing') msg3 = _translate('HandlerClass', 'does exist in') for path in ['/usr/share/doc/linuxcnc/examples/nc_files/plasmac/m_files', self.PATHS.BASEDIR, self.PATHS.CONFIGPATH]: mPath = self.find_a_file('M190', path) if mPath: break if not mPath: msg3 = _translate('HandlerClass', 'does not exist in the default locations') msg = f'{head}:\nM190 {msg0}:\n{":".join(self.mPath)}\n\n{msg1}\n\nM190 {msg3}:\n{mPath[:-5]}\n\n{msg2}\n' STATUS.emit('error', linuxcnc.OPERATOR_ERROR, msg) quit() ucFile = os.path.join(self.PATHS.CONFIGPATH, 'qtplasmac_custom.py') if os.path.isfile(ucFile): exec(compile(open(ucFile, 'rb').read(), ucFile, 'exec')) # ensure we get all startup errors STATUS.connect('error', self.error_update) STATUS.connect('graphics-gcode-error', lambda o, e: self.error_update(o, linuxcnc.OPERATOR_ERROR, e)) STATUS.emit('update-machine-log', f'--- {self.machineTitle} ---', None) self.make_hal_pins() self.init_preferences() self.hide_widgets() self.init_widgets() # hijack the qtvcp shutdown to our own close event self.w.screen_options.QTVCP_INSTANCE_.closeEvent = self.closeEvent self.w.button_frame.installEventFilter(self.w) self.link_hal_pins() self.statistics_init() self.set_axes_and_joints() self.set_spinbox_parameters() self.load_plasma_parameters() self.set_mode() self.user_button_setup() self.load_material_file() self.offset_peripherals() self.set_probe_offset_pins() STATUS.connect('state-estop', lambda w: self.estop_state(True)) STATUS.connect('state-estop-reset', lambda w: self.estop_state(False)) STATUS.connect('state-on', lambda w: self.power_state(True)) STATUS.connect('state-off', lambda w: self.power_state(False)) STATUS.connect('hard-limits-tripped', self.hard_limit_tripped) STATUS.connect('user-system-changed', self.user_system_changed) STATUS.connect('file-loaded', self.file_loaded) STATUS.connect('homed', self.joint_homed) STATUS.connect('all-homed', self.joints_all_homed) STATUS.connect('not-all-homed', self.joint_unhomed) STATUS.connect('gcode-line-selected', lambda w, line: self.set_start_line(line)) STATUS.connect('graphics-line-selected', lambda w, line: self.set_start_line(line)) STATUS.connect('g-code-changed', self.gcodes_changed) STATUS.connect('m-code-changed', self.mcodes_changed) STATUS.connect('program-pause-changed', self.pause_changed) STATUS.connect('graphics-loading-progress', self.progress_changed) STATUS.connect('interp-paused', self.interp_paused) STATUS.connect('interp-idle', self.interp_idle) STATUS.connect('interp-reading', self.interp_reading) STATUS.connect('interp-waiting', self.interp_waiting) STATUS.connect('interp-run', self.interp_running) STATUS.connect('jograte-changed', self.jog_rate_changed) STATUS.connect('graphics-gcode-properties', lambda w, d: self.update_gcode_properties(d)) STATUS.connect('system_notify_button_pressed', self.system_notify_button_pressed) STATUS.connect('tool-in-spindle-changed', self.tool_changed) STATUS.connect('periodic', lambda w: self.update_periodic()) STATUS.connect('metric-mode-changed', self.metric_mode_changed) STATUS.connect('motion-type-changed', lambda w, data: self.motion_type_changed(data)) STATUS.connect('reload-display', lambda w: self.g10_reload()) self.startupTimer = QTimer() self.startupTimer.timeout.connect(self.startup_timeout) self.startupTimer.setSingleShot(True) self.shutdownTimer = QTimer() self.shutdownTimer.timeout.connect(self.shutdown_timeout) self.shutdownTime = 2000 self.laserTimer = QTimer() self.laserTimer.timeout.connect(self.laser_timeout) self.laserTimer.setSingleShot(True) self.ohmicLedTimer = QTimer() self.ohmicLedTimer.timeout.connect(self.ohmic_led_timeout) self.ohmicLedTimer.setSingleShot(True) self.watchDogTimer = QTimer() self.watchDogTimer.timeout.connect(self.watchdog_has_bit) self.watchDogTimer.setSingleShot(True) self.watchDogState = None self.set_color_styles() self.vm_check() # set hal pins only after initialized__ has begun # some locales won't set pins before this phase self.thcFeedRatePin.set(self.thcFeedRate) self.pmPort = None if self.PREFS.getpref('Port', '', str, 'POWERMAX'): self.pmPort = self.PREFS.getpref('Port', '', str, 'POWERMAX') if self.pmPort and self.pmx485_check(self.pmPort): self.pmx485_startup(self.pmPort) else: self.w.gas_pressure.hide() self.w.gas_pressure_label.hide() self.w.cut_mode.hide() self.w.cut_mode_label.hide() self.w.pmx485_frame.hide() self.w.pmx_stats_frame.hide() if self.w.mdihistory.rows: self.mdiLast = self.w.mdihistory.model.item(self.w.mdihistory.rows - 1).text() else: self.mdiLast = None self.w.mdihistory.MDILine.spindle_inhibit(True) self.w.mdihistory.MDILine.g92_inhibit(True) if self.updateIni: self.update_iniwrite() updateLog = os.path.join(self.PATHS.CONFIGPATH, 'update_log.txt') if self.updateData: restart = False msgType = linuxcnc.OPERATOR_TEXT msgText = '' with open(updateLog, 'a') as f: for update in self.updateData: if update[2]: f.write(f'{time.strftime("%y-%m-%d")} {update[2]}\n') msgText += f'{update[2]}\n' if update[0]: restart = True if update[1]: msgType = linuxcnc.OPERATOR_ERROR head = _translate('HandlerClass', 'QtPlasmaC Configuration Update') STATUS.emit('error', msgType, f'{head}:\n{msgText}\n') if restart: msg = _translate('HandlerClass', 'Due to configuration changes a restart is required') STATUS.emit('error', linuxcnc.OPERATOR_TEXT, f'{head}:\n{msg}\n') quit() if not os.path.isfile(updateLog): with open(updateLog, 'w') as f: f.write(f'{time.strftime("%y-%m-%d")} Initial V{LCNCVER}-{VERSION}\n') self.startupTimer.start(250) # turning off autorepeat should always be the last thing intialized__ does self.autorepeat_keys(False) # called by qtvcp.py, can override qtvcp settings or qtvcp allowed user options (via INI) def before_loop__(self): self.w.setWindowTitle(self.machineTitle) self.iconPath = 'share/icons/hicolor/scalable/apps/linuxcnc_alt/linuxcncicon_plasma.svg' appPath = os.path.realpath(os.path.dirname(sys.argv[0])) self.iconBase = '/usr' if appPath == '/usr/bin' else appPath.replace('/bin', '/debian/extras/usr') self.w.setWindowIcon(QIcon(os.path.join(self.iconBase, self.iconPath))) ######################################################################################################################### # CLASS PATCHING SECTION # # note that the widget must be initialized before it can be patched (Example: gcodegraphics needs patched later) ######################################################################################################################### # called by qtvcp.py def class_patch__(self): self.file_manager_patch() self.gcode_editor_patch() self.camview_patch() self.offset_table_patch() self.qt5_graphics_patch() self.screen_options_patch() # patched file manager functions def file_manager_patch(self): self.old_load = FILE_MAN.load FILE_MAN.load = self.new_load # remove temporary materials before loading a file def new_load(self, fname=None): try: if fname is None: self.w.filemanager._getPathActivated() return self.w.filemanager.recordBookKeeping() self.remove_temp_materials() ACTION.OPEN_PROGRAM(fname) STATUS.emit('update-machine-log', 'Loaded: ' + fname, 'TIME') except Exception as e: LOG.error(f'Load file error: {e}') STATUS.emit('error', linuxcnc.NML_ERROR, f'Load file error: {e}') # patched gcode editor functions def gcode_editor_patch(self): self.old_saveReturn = EDITOR.saveReturn EDITOR.saveReturn = self.new_saveReturn self.old_openReturn = EDITOR.openReturn EDITOR.openReturn = self.new_openReturn self.old_exitCall = EDITOR.exitCall EDITOR.exitCall = self.new_exitCall self.old_gcodeLexerCall = EDITOR.gcodeLexerCall EDITOR.gcodeLexerCall = self.new_gcodeLexerCall self.old_pythonLexerCall = EDITOR.pythonLexerCall EDITOR.pythonLexerCall = self.new_pythonLexerCall self.old_returnFromDialog = EDITOR.returnFromDialog EDITOR.returnFromDialog = self.new_returnFromDialog DISPLAY.load_preference = self.new_load_preference self.old_set_line_number = DISPLAY.set_line_number DISPLAY.set_line_number = self.new_set_line_number # save a non gcode file and don't load it into linuxcnc def new_saveReturn(self, filename): saved = ACTION.SAVE_PROGRAM(self.w.gcode_editor.editor.text(), filename) if saved is not None: self.w.gcode_editor.editor.setModified(False) if saved[-3:] in ['ngc', '.nc', 'tap']: self.preFileSaveMaterial = int(self.w.materials_box.currentText().split(': ', 1)[0]) if self.rflActive: self.clear_rfl() self.remove_temp_materials() ACTION.OPEN_PROGRAM(saved) # open a non gcode file and don't load it into linuxcnc def new_openReturn(self, filename): if filename[-3:] in ['ngc', '.nc', 'tap']: self.remove_temp_materials() ACTION.OPEN_PROGRAM(filename) else: self.w.gcode_editor.editor.load_text(filename) self.w.gcode_editor.editor.setModified(False) # modify the closing of the gcode editor def new_exitCall(self, index): proceed = self.editor_close_check() if not proceed: return self.w.preview_stack.setCurrentIndex(self.PREVIEW) if self.fileOpened and self.w.gcode_editor.editor.isModified(): self.w.gcode_editor.editor.setModified(False) self.file_reload_clicked() elif not self.fileOpened: self.w.gcode_editor.editor.new_text() self.w.gcode_editor.editor.setModified(False) self.view_t_pressed(self.w.gcodegraphics) self.w.gcode_editor.editMode() self.vkb_hide() ACTION.SET_MANUAL_MODE() # we don't use lexer colors def new_gcodeLexerCall(self): pass # we don't use lexer colors def new_pythonLexerCall(self): pass # don't allow rfl.ngc as a file name def new_returnFromDialog(self, w, message): if message.get('NAME') == self.w.gcode_editor.load_dialog_code: path = message.get('RETURN') code = bool(message.get('ID') == '%s__' % self.w.gcode_editor.objectName()) if path and code: self.w.gcode_editor.openReturn(path) elif message.get('NAME') == self.w.gcode_editor.save_dialog_code: path = message.get('RETURN') code = bool(message.get('ID') == '%s__' % self.w.gcode_editor.objectName()) if path and code: if not os.path.basename(path) in ['rfl', 'rfl.ngc']: self.w.gcode_editor.saveReturn(path) else: head = _translate('HandlerClass', 'Save Error') msg0 = _translate('HandlerClass', 'The file name') msg1 = _translate('HandlerClass', 'is not allowed') self.dialog_show_ok(QMessageBox.Warning, f'{head}', f'\n{msg0} "{os.path.basename(path)}" {msg1}\n\n') self.w.gcode_editor.getSaveFileName() return # load the qtplasmac preferences file rather than the qtvcp preferences file def new_load_preference(self, w): self.w.gcode_editor.editor.load_text(self.prefsFile) self.w.gcode_editor.editor.setCursorPosition(self.w.gcode_editor.editor.lines(), 0) # dont highlight lines selected from the editor in the preview window def new_set_line_number(self, line): if self.w.sender(): if self.w.sender().objectName() == 'gcode_editor_display': return else: STATUS.emit('gcode-line-selected', line+1) # patched camera functions def camview_patch(self): self.old_wheelEvent = CAM.wheelEvent CAM.wheelEvent = self.new_wheelEvent self.old_drawText = CAM.drawText CAM.drawText = self.new_drawText self.old_mousePressEvent = CAM.mousePressEvent CAM.mousePressEvent = self.new_mousePressEvent self.old_mouseDoubleClickEvent = CAM.mouseDoubleClickEvent CAM.mouseDoubleClickEvent = self.new_mouseDoubleClickEvent # format the angle display def new_drawText(self, event, qp): qp.setPen(self.w.camview.text_color) qp.setFont(self.w.camview.font) if self.w.camview.pix: qp.drawText(self.w.camview.rect(), QtCore.Qt.AlignTop, f'{self.w.camview.rotation:0.3f}{self.degreeSymbol}') else: qp.drawText(self.w.camview.rect(), QtCore.Qt.AlignCenter, self.w.camview.text) # limit scale and diameter, don't allow mouse rotation def new_wheelEvent(self, event): mouseState = qApp.mouseButtons() w = self.w.camview.size().width() h = self.w.camview.size().height() if event.angleDelta().y() < 0: if mouseState == QtCore.Qt.NoButton: self.w.camview.diameter -= 2 if mouseState == QtCore.Qt.LeftButton: self.w.camview.scale -= .1 else: if mouseState == QtCore.Qt.NoButton: self.w.camview.diameter += 2 if mouseState == QtCore.Qt.LeftButton: self.w.camview.scale += .1 if self.w.camview.diameter < 2: self.w.camview.diameter = 2 if self.w.camview.diameter > w - 5: self.w.camview.diameter = w - 5 if self.w.camview.diameter > h - 5: self.w.camview.diameter = h - 5 if self.w.camview.scale < 1: self.w.camview.scale = 1 if self.w.camview.scale > 5: self.w.camview.scale = 5 # inhibit mouse single clicks def new_mousePressEvent(self, event): pass # don't reset rotation with double click def new_mouseDoubleClickEvent(self, event): if event.button() & QtCore.Qt.LeftButton: self.w.camview.scale = 1 elif event.button() & QtCore.Qt.MiddleButton: self.w.camview.diameter = 20 # patched offset table functions def offset_table_patch(self): self.old_flags = OFFSET_TABLE.flags OFFSET_TABLE.flags = self.new_flags # we don't allow editing z axis or g92 offsets def new_flags(self, index): if not index.isValid(): return None if index.column() == 9 and index.row() in (0, 1, 2, 3): return Qt.ItemIsEnabled elif index.row() == 0: return Qt.ItemIsEnabled elif index.row() == 1 and not index.column() == 2: return Qt.NoItemFlags # prevent z axis offset editing elif index.column() == 2: return Qt.ItemIsEnabled # prevent g92 offset editing elif index.row() == 2: return Qt.ItemIsEnabled else: return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable # patched qt5_graphics functions def qt5_graphics_patch(self): self.old_dro_format = DRO.dro_format DRO.dro_format = self.new_dro_format # replace dro with current material def new_dro_format(self, s, spd, dtg, limit, homed, positions, axisdtg, g5x_offset, g92_offset, tlo_offset): text = self.get_overlay_text() return limit, homed, text, text # patched screen options functions def screen_options_patch(self): self.old_process_error = OPTIONS.process_error OPTIONS.process_error = self.new_process_error # we want custom notifications for jog errors def new_process_error(self, w, kind, text): self.realTimeDelay = False O = self.w.screen_options N = O.QTVCP_INSTANCE_._NOTICE if 'jog-inhibit' in text: if self.w.led_float_switch.hal_pin.get(): text = _translate('HandlerClass', 'Float Switch has disabled jogging') elif self.ohmicLedInPin.get(): text = _translate('HandlerClass', 'Ohmic Probe has disabled jogging') elif self.w.led_breakaway_switch.hal_pin.get(): text = _translate('HandlerClass', 'Breakaway Switch has disabled jogging') elif 'jog-stop' in text or 'jog-stop-immediate' in text: if self.w.led_float_switch.hal_pin.get(): text = _translate('HandlerClass', 'Float Switch has aborted active jogging') elif self.ohmicLedInPin.get(): text = _translate('HandlerClass', 'Ohmic Probe has aborted active jogging') elif self.w.led_breakaway_switch.hal_pin.get(): text = _translate('HandlerClass', 'Breakaway Switch has aborted active jogging') elif self.virtualMachine and 'unexpected realtime delay' in text.lower(): text = f'Error suppressed:\n"{text.strip()}"\nRealtime delays are expected in a virtual environment' self.realTimeDelay = True if O.desktop_notify: if 'on limit switch error' in text: N.update(O.notify_hard_limits, title='Machine Error:', message=text, msgs=O.notify_max_msgs) elif kind == linuxcnc.OPERATOR_ERROR and not self.realTimeDelay: N.update(O.notify_critical, icon='dialog-error', title='Operator Error:', message=text, msgs=O.notify_max_msgs) elif kind == linuxcnc.OPERATOR_TEXT: N.update(O.notify_critical, icon='dialog-information', title='Operator Text:', message=text, msgs=O.notify_max_msgs) elif kind == linuxcnc.OPERATOR_DISPLAY: N.update(O.notify_critical, icon='dialog-messages', title='Operator Display:', message=text, msgs=O.notify_max_msgs) elif kind == linuxcnc.NML_ERROR: N.update(O.notify_critical, title='Internal NML Error:', message=text, msgs=O.notify_max_msgs) elif kind == linuxcnc.NML_TEXT: N.update(O.notify_critical, title='Internal NML Text:', message=text, msgs=O.notify_max_msgs) elif kind == linuxcnc.NML_DISPLAY: N.update(O.notify_critical, title='Internal NML Display:', message=text, msgs=O.notify_max_msgs) elif kind == STATUS.TEMPORARY_MESSAGE: N.update(O.notify_normal, title='Operator Info:', message=text, status_timeout=0, timeout=2, msgs=O.notify_max_msgs) if O.play_sounds and O.mchnMsg_play_sound: STATUS.emit('play-sound', '%s' % O.mchnMsg_sound_type) if O.mchnMsg_speak_errors: if kind in (linuxcnc.OPERATOR_ERROR, linuxcnc.NML_ERROR): STATUS.emit('play-sound', 'SPEAK %s ' % text) if O.mchnMsg_speak_text: if kind in (linuxcnc.OPERATOR_TEXT, linuxcnc.NML_TEXT, linuxcnc.OPERATOR_DISPLAY, STATUS.TEMPORARY_MESSAGE): STATUS.emit('play-sound', 'SPEAK %s ' % text) STATUS.emit('update-machine-log', text, 'TIME') ######################################################################################################################### # SPECIAL FUNCTIONS SECTION # ######################################################################################################################### def make_hal_pins(self): self.consChangePin = self.h.newpin('consumable_changing', hal.HAL_BIT, hal.HAL_IN) self.convTabDisable = self.h.newpin('conv_disable', hal.HAL_BIT, hal.HAL_IN) self.cutTypePin = self.h.newpin('cut_type', hal.HAL_S32, hal.HAL_IN) self.developmentPin = self.h.newpin('development', hal.HAL_BIT, hal.HAL_IN) self.extAbortPin = self.h.newpin('ext_abort', hal.HAL_BIT, hal.HAL_IN) self.extAutoVoltsEnablePin = self.h.newpin('ext_auto_volts_enable', hal.HAL_BIT, hal.HAL_IN) self.extChangeConsPin = self.h.newpin('ext_consumables', hal.HAL_BIT, hal.HAL_IN) self.extCornerLockEnablePin = self.h.newpin('ext_cornerlock_enable', hal.HAL_BIT, hal.HAL_IN) self.extCutRecCancelPin = self.h.newpin('ext_cutrec_cancel', hal.HAL_BIT, hal.HAL_IN) self.extCutRecRevPin = self.h.newpin('ext_cutrec_rev', hal.HAL_BIT, hal.HAL_IN) self.extCutRecFwdPin = self.h.newpin('ext_cutrec_fwd', hal.HAL_BIT, hal.HAL_IN) self.extCutRecNPin = self.h.newpin('ext_cutrec_n', hal.HAL_BIT, hal.HAL_IN) self.extCutRecNEPin = self.h.newpin('ext_cutrec_ne', hal.HAL_BIT, hal.HAL_IN) self.extCutRecEPin = self.h.newpin('ext_cutrec_e', hal.HAL_BIT, hal.HAL_IN) self.extCutRecSEPin = self.h.newpin('ext_cutrec_se', hal.HAL_BIT, hal.HAL_IN) self.extCutRecSPin = self.h.newpin('ext_cutrec_s', hal.HAL_BIT, hal.HAL_IN) self.extCutRecSWPin = self.h.newpin('ext_cutrec_sw', hal.HAL_BIT, hal.HAL_IN) self.extCutRecWPin = self.h.newpin('ext_cutrec_w', hal.HAL_BIT, hal.HAL_IN) self.extCutRecNWPin = self.h.newpin('ext_cutrec_nw', hal.HAL_BIT, hal.HAL_IN) self.extFramingPin = self.h.newpin('ext_frame_job', hal.HAL_BIT, hal.HAL_IN) self.extHeightOvrCountsPin = self.h.newpin('ext_height_ovr_counts', hal.HAL_S32, hal.HAL_IN) self.extHeightOvrCountEnablePin = self.h.newpin('ext_height_ovr_count_enable', hal.HAL_BIT, hal.HAL_IN) self.extHeightOvrMinusPin = self.h.newpin('ext_height_ovr_minus', hal.HAL_BIT, hal.HAL_IN) self.extHeightOvrPlusPin = self.h.newpin('ext_height_ovr_plus', hal.HAL_BIT, hal.HAL_IN) self.extHeightOvrResetPin = self.h.newpin('ext_height_ovr_reset', hal.HAL_BIT, hal.HAL_IN) self.extHeightOvrScalePin = self.h.newpin('ext_height_ovr_scale', hal.HAL_FLOAT, hal.HAL_IN) self.extIgnoreArcOkPin = self.h.newpin('ext_ignore_arc_ok', hal.HAL_BIT, hal.HAL_IN) self.extJogSlowPin = self.h.newpin('ext_jog_slow', hal.HAL_BIT, hal.HAL_IN) # self.extKerfCrossEnablePin = self.h.newpin('ext_kerfcross_enable', hal.HAL_BIT, hal.HAL_IN) self.extLaserTouchOffPin = self.h.newpin('ext_laser_touchoff', hal.HAL_BIT, hal.HAL_IN) self.extLaserTogglePin = self.h.newpin('ext_laser_toggle', hal.HAL_BIT, hal.HAL_IN) self.extMeshModePin = self.h.newpin('ext_mesh_mode', hal.HAL_BIT, hal.HAL_IN) self.extOhmicPin = self.h.newpin('ext_ohmic', hal.HAL_BIT, hal.HAL_IN) self.extOhmicProbeEnablePin = self.h.newpin('ext_ohmic_probe_enable', hal.HAL_BIT, hal.HAL_IN) self.extPausePin = self.h.newpin('ext_pause', hal.HAL_BIT, hal.HAL_IN) self.extPauseOnlyPin = self.h.newpin('ext_pause_only', hal.HAL_BIT, hal.HAL_IN) self.extPowerPin = self.h.newpin('ext_power', hal.HAL_BIT, hal.HAL_IN) self.extProbePin = self.h.newpin('ext_probe', hal.HAL_BIT, hal.HAL_IN) self.extPulsePin = self.h.newpin('ext_pulse', hal.HAL_BIT, hal.HAL_IN) self.extResumePin = self.h.newpin('ext_resume', hal.HAL_BIT, hal.HAL_IN) self.extRunPausePin = self.h.newpin('ext_run_pause', hal.HAL_BIT, hal.HAL_IN) self.extRunPin = self.h.newpin('ext_run', hal.HAL_BIT, hal.HAL_IN) self.extThcEnablePin = self.h.newpin('ext_thc_enable', hal.HAL_BIT, hal.HAL_IN) self.extTorchEnablePin = self.h.newpin('ext_torch_enable', hal.HAL_BIT, hal.HAL_IN) self.extTouchOffPin = self.h.newpin('ext_touchoff', hal.HAL_BIT, hal.HAL_IN) self.extVoidLockEnablePin = self.h.newpin('ext_voidlock_enable', hal.HAL_BIT, hal.HAL_IN) self.gcodeScalePin = self.h.newpin('gcode_scale', hal.HAL_FLOAT, hal.HAL_OUT) self.heightOverridePin = self.h.newpin('height_override', hal.HAL_FLOAT, hal.HAL_OUT) self.jogInhibited = self.h.newpin('jog_inhibited', hal.HAL_BIT, hal.HAL_IN) self.laserOnPin = self.h.newpin('laser_on', hal.HAL_BIT, hal.HAL_OUT) self.offsetSetProbePin = self.h.newpin('offset_set_probe', hal.HAL_BIT, hal.HAL_OUT) self.offsetSetScribePin = self.h.newpin('offset_set_scribe', hal.HAL_BIT, hal.HAL_OUT) self.laserRecStatePin = self.h.newpin('laser_recovery_state', hal.HAL_S32, hal.HAL_IN) self.materialChangePin = self.h.newpin('material_change', hal.HAL_S32, hal.HAL_IN) self.materialChangeNumberPin = self.h.newpin('material_change_number', hal.HAL_S32, hal.HAL_IN) self.materialChangeTimeoutPin = self.h.newpin('material_change_timeout', hal.HAL_BIT, hal.HAL_IN) self.materialReloadPin = self.h.newpin('material_reload', hal.HAL_BIT, hal.HAL_IN) self.materialTempPin = self.h.newpin('material_temp', hal.HAL_S32, hal.HAL_IN) self.offsetsActivePin = self.h.newpin('offsets_active', hal.HAL_BIT, hal.HAL_IN) self.ohmicLedInPin = self.h.newpin('ohmic_led_in', hal.HAL_BIT, hal.HAL_IN) self.paramTabDisable = self.h.newpin('param_disable', hal.HAL_BIT, hal.HAL_IN) self.settingsTabDisable = self.h.newpin('settings_disable', hal.HAL_BIT, hal.HAL_IN) self.simStyleUpdate = self.h.newpin('sim_style_update', hal.HAL_BIT, hal.HAL_OUT) self.plasmacStatePin = self.h.newpin('plasmac_state', hal.HAL_S32, hal.HAL_IN) self.plasmacStopPin = self.h.newpin('plasmac_stop', hal.HAL_S32, hal.HAL_IN) self.pmx485CurrentPin = self.h.newpin('pmx485_current', hal.HAL_FLOAT, hal.HAL_IN) self.pmx485CurrentMaxPin = self.h.newpin('pmx485_current_max', hal.HAL_FLOAT, hal.HAL_IN) self.pmx485CurrentMinPin = self.h.newpin('pmx485_current_min', hal.HAL_FLOAT, hal.HAL_IN) self.pmx485FaultPin = self.h.newpin('pmx485_fault', hal.HAL_FLOAT, hal.HAL_IN) self.pmx485ModePin = self.h.newpin('pmx485_mode', hal.HAL_FLOAT, hal.HAL_IN) self.pmx485PressurePin = self.h.newpin('pmx485_pressure', hal.HAL_FLOAT, hal.HAL_IN) self.pmx485PressureMaxPin = self.h.newpin('pmx485_pressure_max', hal.HAL_FLOAT, hal.HAL_IN) self.pmx485PressureMinPin = self.h.newpin('pmx485_pressure_min', hal.HAL_FLOAT, hal.HAL_IN) self.pmx485StatusPin = self.h.newpin('pmx485_status', hal.HAL_BIT, hal.HAL_IN) self.pmx485ArcTimePin = self.h.newpin('pmx485_arc_time', hal.HAL_FLOAT, hal.HAL_IN) self.probeTestErrorPin = self.h.newpin('probe_test_error', hal.HAL_BIT, hal.HAL_IN) self.out0Pin = self.h.newpin('ext_out_0', hal.HAL_BIT, hal.HAL_OUT) self.out1Pin = self.h.newpin('ext_out_1', hal.HAL_BIT, hal.HAL_OUT) self.out2Pin = self.h.newpin('ext_out_2', hal.HAL_BIT, hal.HAL_OUT) self.sensorActive = self.h.newpin('sensor_active', hal.HAL_BIT, hal.HAL_IN) self.tabsAlwaysEnabled = self.h.newpin('tabs_always_enabled', hal.HAL_BIT, hal.HAL_IN) self.thcFeedRatePin = self.h.newpin('thc_feed_rate', hal.HAL_FLOAT, hal.HAL_OUT) self.velReduct = self.h.newpin('vel_reduct', hal.HAL_FLOAT, hal.HAL_IN) self.xOffsetPin = self.h.newpin('x_offset', hal.HAL_FLOAT, hal.HAL_IN) self.yOffsetPin = self.h.newpin('y_offset', hal.HAL_FLOAT, hal.HAL_IN) self.zHeightPin = self.h.newpin('z_height', hal.HAL_FLOAT, hal.HAL_IN) self.zOffsetPin = self.h.newpin('z_offset_counts', hal.HAL_S32, hal.HAL_IN) self.xMinPierceExtentPin = self.h.newpin('x_min_pierce_extent', hal.HAL_FLOAT, hal.HAL_IN) self.xMaxPierceExtentPin = self.h.newpin('x_max_pierce_extent', hal.HAL_FLOAT, hal.HAL_IN) self.yMinPierceExtentPin = self.h.newpin('y_min_pierce_extent', hal.HAL_FLOAT, hal.HAL_IN) self.yMaxPierceExtentPin = self.h.newpin('y_max_pierce_extent', hal.HAL_FLOAT, hal.HAL_IN) def link_hal_pins(self): # arc parameters CALL(['halcmd', 'net', 'plasmac:arc-fail-delay', 'qtplasmac.arc_fail_delay-f', 'plasmac.arc-fail-delay']) CALL(['halcmd', 'net', 'plasmac:arc-max-starts', 'qtplasmac.arc_max_starts-s', 'plasmac.arc-max-starts']) CALL(['halcmd', 'net', 'plasmac:restart-delay', 'qtplasmac.arc_restart_delay-f', 'plasmac.restart-delay']) CALL(['halcmd', 'net', 'plasmac:arc-voltage-scale', 'qtplasmac.arc_voltage_scale-f', 'plasmac.arc-voltage-scale']) CALL(['halcmd', 'net', 'plasmac:arc-voltage-offset', 'qtplasmac.arc_voltage_offset-f', 'plasmac.arc-voltage-offset']) CALL(['halcmd', 'net', 'plasmac:height-per-volt', 'qtplasmac.height_per_volt-f', 'plasmac.height-per-volt']) CALL(['halcmd', 'net', 'plasmac:arc-ok-high', 'qtplasmac.arc_ok_high-f', 'plasmac.arc-ok-high']) CALL(['halcmd', 'net', 'plasmac:arc-ok-low', 'qtplasmac.arc_ok_low-f', 'plasmac.arc-ok-low']) # thc parameters CALL(['halcmd', 'net', 'plasmac:thc-feed-rate', 'qtplasmac.thc_feed_rate', 'plasmac.thc-feed-rate']) CALL(['halcmd', 'net', 'plasmac:thc-auto', 'qtplasmac.thc_auto', 'plasmac.thc-auto']) CALL(['halcmd', 'net', 'plasmac:thc-delay', 'qtplasmac.thc_delay-f', 'plasmac.thc-delay']) CALL(['halcmd', 'net', 'plasmac:thc-sample-counts', 'qtplasmac.thc_sample_counts-s', 'plasmac.thc-sample-counts']) CALL(['halcmd', 'net', 'plasmac:thc-sample-threshold', 'qtplasmac.thc_sample_threshold-f', 'plasmac.thc-sample-threshold']) CALL(['halcmd', 'net', 'plasmac:thc-threshold', 'qtplasmac.thc_threshold-f', 'plasmac.thc-threshold']) CALL(['halcmd', 'net', 'plasmac:pid-p-gain', 'qtplasmac.pid_p_gain-f', 'plasmac.pid-p-gain']) CALL(['halcmd', 'net', 'plasmac:pid-i-gain', 'qtplasmac.pid_i_gain-f', 'plasmac.pid-i-gain']) CALL(['halcmd', 'net', 'plasmac:pid-d-gain', 'qtplasmac.pid_d_gain-f', 'plasmac.pid-d-gain']) CALL(['halcmd', 'net', 'plasmac:cornerlock-threshold', 'qtplasmac.cornerlock_threshold-f', 'plasmac.cornerlock-threshold']) CALL(['halcmd', 'net', 'plasmac:voidlock-slope', 'qtplasmac.voidlock_slope-s', 'plasmac.voidlock-slope']) # probe parameters CALL(['halcmd', 'net', 'plasmac:float-switch-travel', 'qtplasmac.float_switch_travel-f', 'plasmac.float-switch-travel']) CALL(['halcmd', 'net', 'plasmac:probe-feed-rate', 'qtplasmac.probe_feed_rate-f', 'plasmac.probe-feed-rate']) CALL(['halcmd', 'net', 'plasmac:probe-start-height', 'qtplasmac.probe_start_height-f', 'plasmac.probe-start-height']) CALL(['halcmd', 'net', 'plasmac:ohmic-probe-offset', 'qtplasmac.ohmic_probe_offset-f', 'plasmac.ohmic-probe-offset']) CALL(['halcmd', 'net', 'plasmac:ohmic-max-attempts', 'qtplasmac.ohmic_max_attempts-s', 'plasmac.ohmic-max-attempts']) CALL(['halcmd', 'net', 'plasmac:skip-ihs-distance', 'qtplasmac.skip_ihs_distance-f', 'plasmac.skip-ihs-distance']) CALL(['halcmd', 'net', 'plasmac:slat-height', 'qtplasmac.slat_height-f', 'plasmac.slat-height']) CALL(['halcmd', 'net', 'plasmac:offset-feed-rate', 'qtplasmac.offset_feed_rate-f', 'plasmac.offset-feed-rate']) # safety parameters CALL(['halcmd', 'net', 'plasmac:safe-height', 'qtplasmac.safe_height-f', 'plasmac.safe-height']) # scribe parameters CALL(['halcmd', 'net', 'plasmac:scribe-arm-delay', 'qtplasmac.scribe_arm_delay-f', 'plasmac.scribe-arm-delay']) CALL(['halcmd', 'net', 'plasmac:scribe-on-delay', 'qtplasmac.scribe_on_delay-f', 'plasmac.scribe-on-delay']) # spotting parameters CALL(['halcmd', 'net', 'plasmac:spotting-threshold', 'qtplasmac.spotting_threshold-f', 'plasmac.spotting-threshold']) CALL(['halcmd', 'net', 'plasmac:spotting-time', 'qtplasmac.spotting_time-f', 'plasmac.spotting-time']) # motion parameters CALL(['halcmd', 'net', 'plasmac:setup-feed-rate', 'qtplasmac.setup_feed_rate-f', 'plasmac.setup-feed-rate']) # material parameters CALL(['halcmd', 'net', 'plasmac:cut-feed-rate', 'qtplasmac.cut_feed_rate-f', 'plasmac.cut-feed-rate']) CALL(['halcmd', 'net', 'plasmac:cut-height', 'qtplasmac.cut_height-f', 'plasmac.cut-height']) CALL(['halcmd', 'net', 'plasmac:cut-volts', 'qtplasmac.cut_volts-f', 'plasmac.cut-volts']) CALL(['halcmd', 'net', 'plasmac:kerf-width', 'qtplasmac.kerf_width-f', 'plasmac.kerf-width']) CALL(['halcmd', 'net', 'plasmac:pause-at-end', 'qtplasmac.pause_at_end-f', 'plasmac.pause-at-end']) CALL(['halcmd', 'net', 'plasmac:pierce-delay', 'qtplasmac.pierce_delay-f', 'plasmac.pierce-delay']) CALL(['halcmd', 'net', 'plasmac:pierce-height', 'qtplasmac.pierce_height-f', 'plasmac.pierce-height']) CALL(['halcmd', 'net', 'plasmac:puddle-jump-delay', 'qtplasmac.puddle_jump_delay-f', 'plasmac.puddle-jump-delay']) CALL(['halcmd', 'net', 'plasmac:puddle-jump-height', 'qtplasmac.puddle_jump_height-f', 'plasmac.puddle-jump-height']) CALL(['halcmd', 'net', 'plasmac:material-thickness', 'qtplasmac.material_thickness-f', 'plasmac.material-thickness']) # monitor CALL(['halcmd', 'net', 'plasmac:arc_ok_out', 'plasmac.arc-ok-out', 'qtplasmac.led_arc_ok']) CALL(['halcmd', 'net', 'plasmac:arc_voltage_out', 'plasmac.arc-voltage-out', 'qtplasmac.arc_voltage']) CALL(['halcmd', 'net', 'plasmac:breakaway-switch-out', 'qtplasmac.led_breakaway_switch']) CALL(['halcmd', 'net', 'plasmac:cornerlock-is-locked', 'plasmac.cornerlock-is-locked', 'qtplasmac.led_corner_lock']) CALL(['halcmd', 'net', 'plasmac:float-switch-out', 'qtplasmac.led_float_switch']) CALL(['halcmd', 'net', 'plasmac:voidlock-is-locked', 'plasmac.voidlock-is-locked', 'qtplasmac.led_void_lock']) CALL(['halcmd', 'net', 'plasmac:led-up', 'plasmac.led-up', 'qtplasmac.led_thc_up']) CALL(['halcmd', 'net', 'plasmac:led-down', 'plasmac.led-down', 'qtplasmac.led_thc_down']) CALL(['halcmd', 'net', 'plasmac:ohmic-probe-out', 'qtplasmac.ohmic_led_in']) CALL(['halcmd', 'net', 'plasmac:thc-active', 'plasmac.thc-active', 'qtplasmac.led_thc_active']) CALL(['halcmd', 'net', 'plasmac:thc-enabled', 'plasmac.thc-enabled', 'qtplasmac.led_thc_enabled']) CALL(['halcmd', 'net', 'plasmac:torch-on', 'qtplasmac.led_torch_on']) # control CALL(['halcmd', 'net', 'plasmac:cornerlock-enable', 'qtplasmac.cornerlock_enable', 'plasmac.cornerlock-enable']) CALL(['halcmd', 'net', 'plasmac:voidlock-enable', 'qtplasmac.voidlock_enable', 'plasmac.voidlock-enable']) CALL(['halcmd', 'net', 'plasmac:mesh-enable', 'qtplasmac.mesh_enable', 'plasmac.mesh-enable']) CALL(['halcmd', 'net', 'plasmac:ignore-arc-ok-1', 'qtplasmac.ignore_arc_ok', 'plasmac.ignore-arc-ok-1']) CALL(['halcmd', 'net', 'plasmac:ohmic-probe-enable', 'qtplasmac.ohmic_probe_enable', 'plasmac.ohmic-probe-enable']) CALL(['halcmd', 'net', 'plasmac:thc-enable', 'qtplasmac.thc_enable', 'plasmac.thc-enable']) CALL(['halcmd', 'net', 'plasmac:use-auto-volts', 'qtplasmac.use_auto_volts', 'plasmac.use-auto-volts']) CALL(['halcmd', 'net', 'plasmac:torch-enable', 'qtplasmac.torch_enable', 'plasmac.torch-enable']) # offsets CALL(['halcmd', 'net', 'plasmac:x-offset-current', 'qtplasmac.x_offset']) CALL(['halcmd', 'net', 'plasmac:y-offset-current', 'qtplasmac.y_offset']) CALL(['halcmd', 'net', 'plasmac:offsets-active', 'qtplasmac.offsets_active']) # override CALL(['halcmd', 'net', 'plasmac:height-override', 'qtplasmac.height_override', 'plasmac.height-override']) # INI CALL(['halcmd', 'net', 'plasmac:axis-x-max-limit', 'ini.x.max_limit', 'plasmac.axis-x-max-limit']) CALL(['halcmd', 'net', 'plasmac:axis-x-min-limit', 'ini.x.min_limit', 'plasmac.axis-x-min-limit']) CALL(['halcmd', 'net', 'plasmac:axis-y-max-limit', 'ini.y.max_limit', 'plasmac.axis-y-max-limit']) CALL(['halcmd', 'net', 'plasmac:axis-y-min-limit', 'ini.y.min_limit', 'plasmac.axis-y-min-limit']) CALL(['halcmd', 'net', 'plasmac:axis-z-max-limit', 'ini.z.max_limit', 'plasmac.axis-z-max-limit']) CALL(['halcmd', 'net', 'plasmac:axis-z-min-limit', 'ini.z.min_limit', 'plasmac.axis-z-min-limit']) # misc CALL(['halcmd', 'net', 'plasmac:consumable-changing', 'plasmac.consumable-changing', 'qtplasmac.consumable_changing']) CALL(['halcmd', 'net', 'plasmac:gcode-scale', 'plasmac.gcode-scale', 'qtplasmac.gcode_scale']) CALL(['halcmd', 'net', 'plasmac:jog-inhibit', 'qtplasmac.jog_inhibited']) CALL(['halcmd', 'net', 'plasmac:laser-on', 'qtplasmac.laser_on']) CALL(['halcmd', 'net', 'plasmac:laser-recovery-state', 'plasmac.laser-recovery-state', 'qtplasmac.laser_recovery_state']) CALL(['halcmd', 'net', 'plasmac:probe-test-error', 'plasmac.probe-test-error', 'qtplasmac.probe_test_error']) CALL(['halcmd', 'net', 'plasmac:sensor_active', 'plasmac.sensor-active', 'qtplasmac.sensor_active']) CALL(['halcmd', 'net', 'plasmac:state', 'plasmac.state-out', 'qtplasmac.plasmac_state']) CALL(['halcmd', 'net', 'plasmac:stop', 'plasmac.stop-type-out', 'qtplasmac.plasmac_stop']) CALL(['halcmd', 'net', 'plasmac:z-height', 'plasmac.z-height', 'qtplasmac.z_height']) CALL(['halcmd', 'net', 'plasmac:z-offset-counts', 'qtplasmac.z_offset_counts']) CALL(['halcmd', 'net', 'plasmac:offset-set-probe', 'plasmac.offset-set-probe', 'qtplasmac.offset_set_probe']) CALL(['halcmd', 'net', 'plasmac:offset-set-scribe', 'plasmac.offset-set-scribe', 'qtplasmac.offset_set_scribe']) CALL(['halcmd', 'net', 'plasmac:adaptive-feed', 'qtplasmac.vel_reduct']) # *** add system hal pin changes here that may affect existing configs *** # *** these may be removed after auto updating is implemented *** if not hal.pin_has_writer('plasmac.feed-upm'): # if feed-upm is not yet connected in hal CALL(['halcmd', 'net', 'plasmac:feed-upm', 'motion.feed-upm', 'plasmac.feed-upm']) def init_preferences(self): self.mode = self.PREFS.getpref('Mode', 0, int, 'GUI_OPTIONS') self.autorepeat_skip = self.PREFS.getpref('Autorepeat all', False, bool, 'GUI_OPTIONS') self.flash_error = self.PREFS.getpref('Flash error', False, bool, 'GUI_OPTIONS') self.w.chk_keyboard_shortcuts.setChecked(self.PREFS.getpref('Use keyboard shortcuts', False, bool, 'GUI_OPTIONS')) self.w.chk_soft_keyboard.setChecked(self.PREFS.getpref('Use soft keyboard', False, bool, 'GUI_OPTIONS')) self.w.chk_overlay.setChecked(self.PREFS.getpref('Show materials', True, bool, 'GUI_OPTIONS')) self.w.chk_run_from_line.setChecked(self.PREFS.getpref('Run from line', True, bool, 'GUI_OPTIONS')) self.w.chk_tool_tips.setChecked(self.PREFS.getpref('Tool tips', True, bool, 'GUI_OPTIONS')) self.w.chk_exit_warning.setChecked(self.PREFS.getpref('Exit warning', True, bool, 'GUI_OPTIONS')) self.exitMessage = self.PREFS.getpref('Exit warning text', '', str, 'GUI_OPTIONS') self.w.cone_size.setValue(self.PREFS.getpref('Preview cone size', 0.5, float, 'GUI_OPTIONS')) self.w.grid_size.setValue(self.PREFS.getpref('Preview grid size', 0.0, float, 'GUI_OPTIONS')) self.w.table_zoom_scale.setValue(self.PREFS.getpref('T view zoom scale', 1.0, float, 'GUI_OPTIONS')) self.zPlusOverrideJog = self.PREFS.getpref('Override jog inhibit via Z+', False, bool, 'GUI_OPTIONS') self.w.color_foregrnd.setStyleSheet(f'background-color: {self.PREFS.getpref("Foreground", "#ffee06", str, "COLOR_OPTIONS")}') self.w.color_foregalt.setStyleSheet(f'background-color: {self.PREFS.getpref("Highlight", "#ffee06", str, "COLOR_OPTIONS")}') self.w.color_led.setStyleSheet(f'background-color: {self.PREFS.getpref("LED", "#ffee06", str, "COLOR_OPTIONS")}') self.w.color_backgrnd.setStyleSheet(f'background-color: {self.PREFS.getpref("Background", "#16160e", str, "COLOR_OPTIONS")}') self.w.color_backgalt.setStyleSheet(f'background-color: {self.PREFS.getpref("Background Alt", "#36362e", str, "COLOR_OPTIONS")}') self.w.color_frams.setStyleSheet(f'background-color: {self.PREFS.getpref("Frames", "#ffee06", str, "COLOR_OPTIONS")}') self.w.color_estop.setStyleSheet(f'background-color: {self.PREFS.getpref("Estop", "#ff0000", str, "COLOR_OPTIONS")}') self.w.color_disabled.setStyleSheet(f'background-color: {self.PREFS.getpref("Disabled", "#b0b0b0", str, "COLOR_OPTIONS")}') self.w.color_preview.setStyleSheet(f'background-color: {self.PREFS.getpref("Preview", "#000000", str, "COLOR_OPTIONS")}') self.lastLoadedProgram = self.w.PREFS_.getpref('RecentPath_0', 'None', str, 'BOOK_KEEPING') TOOLTIPS.tool_tips_changed(self, self.w) self.soft_keyboard() self.cone_size_changed(self.w.cone_size.value()) self.grid_size_changed(self.w.grid_size.value()) self.set_basic_colors() self.w.sd_text.setText(self.exitMessage) def hide_widgets(self): for b in ['RUN', 'PAUSE', 'ABORT']: if self.PREFS.getpref(f'Hide {b.lower()}', False, bool, 'GUI_OPTIONS'): self.w[b.lower()].hide() if self.landscape: self.w.machine_frame.setMaximumHeight(self.w.machine_frame.maximumHeight() - 44) self.w.machine_frame.setMinimumHeight(self.w.machine_frame.maximumHeight()) def init_widgets(self): droPos = self.PREFS.getpref('DRO position', 'bottom', str, 'GUI_OPTIONS') if droPos.lower() == 'top': # designer can change the layout name in the .ui file # this will find the name and move the dro to the top lay = self.w.dro_gcode_frame.children()[0].objectName() self.w[lay].removeWidget(self.w.dro_frame) self.w[lay].insertWidget(0, self.w.dro_frame) text = _translate('HandlerClass', 'CONTINUOUS') self.w.jogincrements.setItemText(0, text) self.w.main_tab_widget.setCurrentIndex(self.MAIN) self.w.preview_stack.setCurrentIndex(self.PREVIEW) self.prevPreviewIndex = self.PREVIEW self.w.gcode_stack.setCurrentIndex(self.GCODE) self.w.jog_stack.setCurrentIndex(self.JOG) self.w.thc_enable.setChecked(self.PREFS.getpref('THC enable', True, bool, 'ENABLE_OPTIONS')) self.w.cornerlock_enable.setChecked(self.PREFS.getpref('Corner lock enable', True, bool, 'ENABLE_OPTIONS')) self.w.voidlock_enable.setChecked(self.PREFS.getpref('Void lock enable', False, bool, 'ENABLE_OPTIONS')) self.w.use_auto_volts.setChecked(self.PREFS.getpref('Use auto volts', True, bool, 'ENABLE_OPTIONS')) self.w.ohmic_probe_enable.setChecked(self.PREFS.getpref('Ohmic probe enable', False, bool, 'ENABLE_OPTIONS')) self.w.thc_auto.setChecked(self.PREFS.getpref('THC auto', False, bool, 'ENABLE_OPTIONS')) self.thc_auto_changed(self.w.thc_auto.isChecked()) self.w.error_label = QLabel() self.w.tool_label = STATLABEL() self.w.gcodes_label = STATLABEL() self.w.mcodes_label = STATLABEL() self.w.lbl_tool = STATLABEL() self.w.lbl_gcodes = STATLABEL() self.w.lbl_mcodes = STATLABEL() self.w.statusbar.addPermanentWidget(self.w.error_label, stretch=1) self.w.error_label.setObjectName('error_label') self.w.statusbar.addPermanentWidget(VLine()) self.w.statusbar.addPermanentWidget(self.w.tool_label) self.w.tool_label.setObjectName('tool_label') self.w.tool_label.setText('TOOL: ') self.w.statusbar.addPermanentWidget(self.w.lbl_tool) self.w.lbl_tool.setObjectName('lbl_tool') self.w.statusbar.addPermanentWidget(VLine()) self.w.statusbar.addPermanentWidget(self.w.gcodes_label) self.w.gcodes_label.setObjectName('gcodes_label') self.w.gcodes_label.setText('G-CODES: ') self.w.statusbar.addPermanentWidget(self.w.lbl_gcodes) self.w.lbl_gcodes.setObjectName('lbl_gcodes') self.w.statusbar.addPermanentWidget(VLine()) self.w.statusbar.addPermanentWidget(self.w.mcodes_label) self.w.mcodes_label.setObjectName('mcodes_label') self.w.mcodes_label.setText('M-CODES: ') self.w.statusbar.addPermanentWidget(self.w.lbl_mcodes) self.w.lbl_mcodes.setObjectName('lbl_mcodes') text = _translate('HandlerClass', 'MOVE') self.w.cut_rec_move_label.setText(f'{text}\n{self.w.kerf_width.text()}') self.w.filemanager.windowLayout.setContentsMargins(4, 0, 4, 0) self.w.filemanager.windowLayout.setSpacing(4) # hide load button (redundant to select button) self.w.filemanager.loadButton.hide() # for copy/paste control if required self.w.filemanager.copy_control.hide() # add vertical and horizontal scroll bars to the materials QComboBoxs self.w.material_selector.view().setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.w.material_selector.view().setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.w.conv_material.view().setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.w.conv_material.view().setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.w.materials_box.view().setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.w.materials_box.view().setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.w.gcode_display.set_margin_metric(3) self.w.gcode_display.setBraceMatching(False) self.w.gcode_display.setCaretWidth(0) self.w.gcode_display.setCornerWidget(QLabel()) self.w.gcode_editor.topBox.setContentsMargins(4, 4, 4, 4) self.w.gcode_editor.bottomBox.setContentsMargins(4, 4, 4, 4) self.w.gcode_editor.set_margin_metric(3) self.w.gcode_editor.editor.setBraceMatching(False) self.w.gcode_editor.editor.setCaretWidth(4) self.w.gcode_editor.editor.setCornerWidget(QLabel()) self.w.gcode_editor.editMode() self.w.gcode_editor.pythonLexerAction.setVisible(False) self.w.gcode_editor.gCodeLexerAction.setVisible(False) self.w.gcode_editor.label.setText('') self.w.gcodegraphics.enable_dro = True self.w.gcodegraphics.set_alpha_mode(True) self.w.gcodegraphics.setShowOffsets(False) self.w.gcodegraphics._font = 'monospace 11' self.w.conv_preview.enable_dro = True self.w.conv_preview.set_cone_basesize(0.1) self.w.conv_preview.set_view('Z') self.w.conv_preview.show_tool = False self.w.conv_preview.show_limits = False self.w.conv_preview.show_small_origin = False self.w.conv_preview.set_alpha_mode(True) self.w.conv_preview.setShowOffsets(False) self.w.conv_preview._font = 'monospace 11' self.w.conv_preview.inhibit_selection = True self.w.conv_preview.update() self.w.conv_preview.setInhibitControls(True) self.w.estopButton = self.PREFS.getpref('Estop type', 0, int, 'GUI_OPTIONS') if self.w.estopButton == 0: self.w.estop.setEnabled(False) if self.w.estopButton == 1: self.w.estop.hide() self.w.camview.cross_color = QtCore.Qt.red self.w.camview.cross_pointer_color = QtCore.Qt.red self.w.camview.font = QFont('arial,helvetica', 16) self.flashState = False self.flashRate = 4 self.flasher = self.flashRate self.manualCut = False self.jogPreManCut = [False, INFO.DEFAULT_LINEAR_JOG_VEL, 0] self.probeTest = False self.torchPulse = False self.rflSelected = False self.fileOpened = False self.fileClear = False self.error_present = False self.laserButtonState = 'laser' self.camButtonState = 'markedge' self.overlayProgress = QProgressBar(self.w.gcode_display) self.overlayProgress.setOrientation(Qt.Vertical) self.overlayProgress.setInvertedAppearance(True) self.overlayProgress.setFormat('') self.overlayProgress.hide() self.camNum = self.PREFS.getpref('Camera port', 0, int, 'CAMERA_OFFSET') def get_overlay_text(self): text = [''] if not self.w.chk_overlay.isChecked(): return text scale = 1 if self.units == 'in' and self.w.dro_z.display_units_mm: scale = 25.4 elif self.units == 'mm' and not self.w.dro_z.display_units_mm: scale = 1 / 25.4 if not self.w.dro_z.display_units_mm: fr = 1 ou = 3 else: fr = 0 ou = 2 dp = '.' if '.' in self.w.pierce_height.text() else ',' if '.' in self.w.cut_feed_rate.text().replace(",", ".") and len(self.w.cut_feed_rate.text().replace(",", ".").split('.')[0]) > 3: text.append(f'FR: {float(self.w.cut_feed_rate.text().replace(",", ".").split(".")[0]) * scale:.{fr}f}'.replace(".", dp)) else: text.append(f'FR: {float(self.w.cut_feed_rate.text().replace(",", ".")) * scale:.{fr}f}'.replace(".", dp)) text.append(f'PH: {float(self.w.pierce_height.text().replace(",", ".")) * scale:.{ou}f}'.replace(".", dp)) text.append(f'PD: {self.w.pierce_delay.text()}') text.append(f'CH: {float(self.w.cut_height.text().replace(",", ".")) * scale:.{ou}f}'.replace(".", dp)) text.append(f'KW: {float(self.w.kerf_width.text().replace(",", ".")) * scale:.{ou}f}'.replace(".", dp)) if self.pmx485Exists: text.append(f'CA: {self.w.cut_amps.text()}') return text def offset_peripherals(self): self.camOffsetX = 0.0 self.camOffsetY = 0.0 self.laserOffsetX = 0.0 self.laserOffsetY = 0.0 self.probeOffsetX = 0.0 self.probeOffsetY = 0.0 self.probeDelay = 0.0 head = _translate('HandlerClass', 'Prefs File Error') # laser try: self.laserOffsetX = self.PREFS.getpref('X axis', 0.0, float, 'LASER_OFFSET') self.laserOffsetY = self.PREFS.getpref('Y axis', 0.0, float, 'LASER_OFFSET') except: self.w.laser.hide() msg0 = _translate('HandlerClass', 'Invalid entry for laser offset') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n') if not (self.laserOffsetX or self.laserOffsetY): self.w.laser.hide() # camera try: self.camOffsetX = self.PREFS.getpref('X axis', 0.0, float, 'CAMERA_OFFSET') self.camOffsetY = self.PREFS.getpref('Y axis', 0.0, float, 'CAMERA_OFFSET') except: self.w.camera.hide() msg0 = _translate('HandlerClass', 'Invalid entry for camera offset') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n') if self.camOffsetX or self.camOffsetY: self.w.camview.set_camnum(self.camNum) else: self.w.camera.hide() # probing try: self.probeOffsetX = self.PREFS.getpref('X axis', 0.0, float, 'OFFSET_PROBING') self.probeOffsetY = self.PREFS.getpref('Y axis', 0.0, float, 'OFFSET_PROBING') self.probeDelay = self.PREFS.getpref('Delay', 0.0, float, 'OFFSET_PROBING') except: self.w.offset_feed_rate.hide() self.w.offset_feed_rate_lbl.hide() msg0 = _translate('HandlerClass', 'Invalid entry for probe offset') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n') if not self.probeOffsetX and not self.probeOffsetY: self.w.offset_feed_rate.hide() self.w.offset_feed_rate_lbl.hide() # called by the modified closeEvent function in this handler def closing_cleanup__(self): # disconnect powermax self.w.pmx485_enable.setChecked(False) # close soft keyboard self.vkb_hide() # turn autorepeat back on for the OS self.autorepeat_keys(True) # save the log files self.save_logfile(5) # save preferences if not self.PREFS: return self.PREFS.putpref('Use keyboard shortcuts', self.w.chk_keyboard_shortcuts.isChecked(), bool, 'GUI_OPTIONS') self.PREFS.putpref('Use soft keyboard', self.w.chk_soft_keyboard.isChecked(), bool, 'GUI_OPTIONS') self.PREFS.putpref('Show materials', self.w.chk_overlay.isChecked(), bool, 'GUI_OPTIONS') self.PREFS.putpref('Run from line', self.w.chk_run_from_line.isChecked(), bool, 'GUI_OPTIONS') self.PREFS.putpref('Tool tips', self.w.chk_tool_tips.isChecked(), bool, 'GUI_OPTIONS') self.PREFS.putpref('Exit warning', self.w.chk_exit_warning.isChecked(), bool, 'GUI_OPTIONS') self.PREFS.putpref('Exit warning text', self.exitMessage, str, 'GUI_OPTIONS') self.PREFS.putpref('Preview cone size', self.w.cone_size.value(), float, 'GUI_OPTIONS') self.PREFS.putpref('Preview grid size', self.w.grid_size.value(), float, 'GUI_OPTIONS') self.PREFS.putpref('T view zoom scale', self.w.table_zoom_scale.value(), float, 'GUI_OPTIONS') self.PREFS.putpref('THC auto', self.w.thc_auto.isChecked(), bool, 'ENABLE_OPTIONS') self.PREFS.putpref('Override jog inhibit via Z+', self.zPlusOverrideJog, bool, 'GUI_OPTIONS') self.PREFS.putpref('THC enable', self.w.thc_enable.isChecked(), bool, 'ENABLE_OPTIONS') self.PREFS.putpref('Corner lock enable', self.w.cornerlock_enable.isChecked(), bool, 'ENABLE_OPTIONS') self.PREFS.putpref('Void lock enable', self.w.voidlock_enable.isChecked(), bool, 'ENABLE_OPTIONS') self.PREFS.putpref('Use auto volts', self.w.use_auto_volts.isChecked(), bool, 'ENABLE_OPTIONS') self.PREFS.putpref('Ohmic probe enable', self.w.ohmic_probe_enable.isChecked(), bool, 'ENABLE_OPTIONS') def save_logfile(self, numLogs): logPre = 'machine_log_' logFiles = [] logFiles = [f for f in os.listdir(self.PATHS.CONFIGPATH) if f.startswith(logPre)] logFiles.sort() if len(logFiles) > (numLogs - 1): for f in range(0, len(logFiles) - (numLogs - 1)): os.remove(logFiles[0]) logFiles = logFiles[1:] text = self.w.machinelog.getLogText() logName = f'{self.PATHS.CONFIGPATH}/{logPre}{time.strftime("%y-%m-%d_%H-%M-%S")}.txt' with open(logName, 'w') as f: f.write(text) # called by qt_makegui.py def processed_key_event__(self, receiver, event, is_pressed, key, code, shift, cntrl): # when typing in MDI, we don't want keybinding to call functions # so we catch and process the events directly. # We do want ESC, F1 and F2 to call keybinding functions though if code not in (Qt.Key_Escape, Qt.Key_F1, Qt.Key_F2): # search for the top widget of whatever widget received the event # then check if it's one we want the keypress events to go to flag = False allowTab = False mdiBlank = False receiver2 = receiver while receiver2 is not None and not flag: if isinstance(receiver2, QtWidgets.QDialog): flag = True break if isinstance(receiver2, QtWidgets.QListView): flag = True break if isinstance(receiver2, FILE_MAN): flag = True break if isinstance(receiver2, MDI_LINE): if self.w.mdihistory.MDILine.text().rstrip() == '': mdiBlank = True flag = True break if isinstance(receiver2, MDI_HISTORY): if self.w.mdihistory.MDILine.text().rstrip() == '': mdiBlank = True flag = True break if isinstance(receiver2, EDITOR): flag = True break if isinstance(receiver2, DOUBLESCALE): flag = True break if isinstance(receiver2, OFFSETVIEW): flag = True break if self.w.main_tab_widget.currentIndex() not in (self.MAIN, self.STATISTICS): allowTab = True flag = True break receiver2 = receiver2.parent() if flag: if is_pressed: if allowTab and (code == Qt.Key_Tab or code == Qt.Key_BackTab): self.keyPressEvent(event) else: receiver.keyPressEvent(event) if mdiBlank and (code == Qt.Key_Return or code == Qt.Key_Enter): self.keyPressEvent(event) event.accept() return True else: event.accept() return True if event.isAutoRepeat(): return True return KEYBIND.manage_function_calls(self, event, is_pressed, key, shift, cntrl) def size_changed(self, object): bHeight = self.w.file_edit.height() + 4 rows = int((object.height() - 24) / bHeight) if self.landscape: if rows == 4: self.buttons_hide(7, 20) elif rows == 5: self.buttons_hide(9, 20) self.buttons_show(7, 8) elif rows == 6: self.buttons_hide(11, 20) self.buttons_show(7, 10) elif rows == 7: self.buttons_hide(13, 20) self.buttons_show(7, 12) elif rows == 8: self.buttons_hide(15, 20) self.buttons_show(7, 14) elif rows == 9: self.buttons_hide(17, 20) self.buttons_show(7, 16) elif rows == 10: self.buttons_hide(19, 20) self.buttons_show(7, 18) else: self.buttons_show(7, 20) else: if rows == 15: self.buttons_hide(16, 20) self.buttons_show(9, 15) if rows == 16: self.buttons_hide(17, 20) self.buttons_show(9, 16) elif rows == 17: self.buttons_hide(18, 20) self.buttons_show(9, 17) elif rows == 18: self.buttons_hide(19, 20) self.buttons_show(9, 18) elif rows == 19: self.buttons_hide(19, 20) self.buttons_show(9, 19) else: self.buttons_show(9, 20) def buttons_hide(self, first, last): for b in range(first, last + 1): self.w[f'button_{b}'].hide() def buttons_show(self, first, last): for b in range(first, last + 1): self.w[f'button_{b}'].show() ######################################################################################################################### # CALLBACKS FROM STATUS # ######################################################################################################################### def estop_state(self, state): if state: self.w.power.setChecked(False) if not self.firstRun: log = _translate('HandlerClass', 'Emergency stop pressed') STATUS.emit('update-machine-log', log, 'TIME') else: if not self.firstRun: log = _translate('HandlerClass', 'Emergency stop cleared') STATUS.emit('update-machine-log', log, 'TIME') def power_state(self, state): if state: self.w.power.setChecked(True) if not self.firstRun: log = _translate('HandlerClass', 'GUI power on') STATUS.emit('update-machine-log', log, 'TIME') else: self.w.power.setChecked(False) if self.tpButton and self.torchPulse: self.torch_pulse_abort() if self.ptButton and hal.get_value('plasmac.probe-test'): self.probe_test_stop() log = _translate('HandlerClass', 'Probe test aborted due to GUI power off') STATUS.emit('update-machine-log', log, 'TIME') if not self.firstRun: log = _translate('HandlerClass', 'GUI power off') STATUS.emit('update-machine-log', log, 'TIME') self.refresh_button_states() def interp_idle(self, obj): hal.set_p('plasmac.consumable-change', '0') if self.single_cut_request: self.single_cut_request = False if self.oldFile and self.fileOpened: self.remove_temp_materials() ACTION.OPEN_PROGRAM(self.oldFile) else: self.fileOpened = True self.file_clear_clicked() if self.g91: ACTION.CALL_MDI_WAIT('G91') self.w.jog_stack.setCurrentIndex(self.JOG) if self.ccButton and not self.button_normal_check(self.ccButton): self.button_normal(self.ccButton) if self.laserDryRunCoords: oldX = STATUS.get_position()[0][0] - self.laserDryRunCoords[0] oldY = STATUS.get_position()[0][1] - self.laserDryRunCoords[1] units = '20' if self.gcodeProps['gcode_units'] == 'in' else '21' ACTION.CALL_MDI_WAIT(f'G{units} G10 L20 P0 X{oldX / self.boundsMultiplier} Y{oldY / self.boundsMultiplier}') self.laserDryRunCoords = None self.laserOnPin.set(0) hal.set_p('plasmac.dry-run', '0') self.w.gcodegraphics.logger.clear() if self.framing: self.framing = False self.laserOnPin.set(0) self.w.gcodegraphics.logger.clear() if self.w.preview_stack.currentIndex() != self.PREVIEW: self.preview_stack_changed() if self.jobRunning and obj: if self.w.torch_enable.isChecked(): self.statistics_save() else: self.jobRunning = False log = _translate('HandlerClass', 'Machine idle') STATUS.emit('update-machine-log', log, 'TIME') self.statistics_show() ACTION.SET_MANUAL_MODE() self.refresh_button_states() def interp_paused(self, obj): pass def interp_running(self, obj): if STATUS.is_auto_mode() and self.w.gcode_stack.currentIndex() != self.GCODE: self.w.gcode_stack.setCurrentIndex(self.GCODE) self.refresh_button_states() def interp_reading(self, obj): pass def interp_waiting(self, obj): pass def pause_changed(self, obj, state): if state: log = _translate('HandlerClass', 'Cycle paused') STATUS.emit('update-machine-log', log, 'TIME') else: self.w.jog_stack.setCurrentIndex(self.JOG) self.refresh_button_states() def jog_rate_changed(self, object, value): msg0 = _translate('HandlerClass', 'JOG') self.w.jogs_label.setText(f'{msg0}\n{STATUS.get_jograte():.0f}') def progress_changed(self, object, percent): if percent < 0: self.overlayProgress.setValue(0) self.overlayProgress.hide() self.w.gcode_display.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.w.gcode_display.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) else: self.overlayProgress.setValue(percent) self.overlayProgress.show() self.overlayProgress.setFixedHeight(self.w.gcode_display.geometry().height()) self.overlayProgress.move(self.w.gcode_display.geometry().width() - 20, 0) self.w.gcode_display.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.w.gcode_display.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) def user_system_changed(self, obj, data): userSys = self.systemList[int(data)] self.w.wcs_button.setText(f'WCS\n{userSys}') if ACTION.prefilter_path: self.file_reload_clicked() def file_loaded(self, obj, filename): if filename is not None: if 'qtplasmac_file_clear.ngc' in filename: self.fileClear = True self.overlayProgress.setValue(0) if not any(name in filename for name in ['qtplasmac_file_clear.ngc', 'single_cut.ngc']): self.lastLoadedProgram = filename if not self.cameraOn: self.preview_index_return(self.w.preview_stack.currentIndex()) self.w.file_open.setText(os.path.basename(filename)) if not self.single_cut_request: self.fileOpened = True text = _translate('HandlerClass', 'EDIT') self.w.edit_label.setText(f'{text}: {filename}') if self.w.gcode_stack.currentIndex() != self.GCODE: self.w.gcode_stack.setCurrentIndex(self.GCODE) self.w.gcodegraphics.logger.clear() self.w.gcodegraphics.clear_highlight() if self.preRflFile and self.preRflFile != ACTION.prefilter_path: self.rflActive = False self.startLine = 0 self.preRflFile = '' self.fileBoundsError, fileErrMsg = self.bounds_check_file() self.probeBoundsError, probeErrMsg = self.bounds_check_probe(False) errMsg = fileErrMsg + probeErrMsg if self.fileBoundsError or self.probeBoundsError: head = _translate('HandlerClass', 'Axis Limit Error') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{errMsg}\n') if self.single_cut_request: self.single_cut_request = False if self.oldFile and 'single_cut' not in self.oldFile: self.remove_temp_materials() ACTION.OPEN_PROGRAM(self.oldFile) else: # allows file clear to work on error self.fileOpened = True else: if self.single_cut_request: ACTION.RUN() if self.fileClear: self.fileClear = False self.fileOpened = False text = _translate('HandlerClass', 'OPEN') self.w.file_open.setText(text) text = _translate('HandlerClass', 'EDIT') self.w.edit_label.setText(f'{text}') self.w.gcode_editor.editor.new_text() self.w.gcode_editor.editor.setModified(False) self.w.gcode_display.new_text() self.w.gcode_display.set_margin_metric(3) self.w.gcode_editor.set_margin_metric(3) ACTION.SET_MANUAL_MODE() if not len(self.w.gcode_display.text()): self.w.gcode_display.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) if self.w.main_tab_widget.currentIndex() != self.MAIN: self.w.main_tab_widget.setCurrentIndex(self.MAIN) # forces the view to remain "table view" if T is checked when a file is loaded, or change to table view upon clicking CLEAR if self.w.view_t.isChecked() or 'qtplasmac_file_clear.ngc' in filename: self.view_t_pressed(self.w.gcodegraphics) if 'single_cut.ngc' not in filename: self.preSingleCutMaterial = None # remove unused temporary materials from comboboxes self.getMaterialBusy = True if int(self.w.materials_box.currentText().split(": ", 1)[0]) not in self.materialList: self.change_material(self.defaultMaterial) for idx in range(self.w.materials_box.count() - 1, -1, -1): matNum = int(self.w.materials_box.itemText(idx).split(': ', 1)[0]) if matNum < 1000000: break if matNum not in self.materialList: self.w.materials_box.removeItem(idx) self.w.material_selector.removeItem(idx) self.w.conv_material.removeItem(idx) self.getMaterialBusy = False self.refresh_button_states() def joints_all_homed(self, obj): self.interp_idle(None) if not self.firstHoming: ACTION.CALL_MDI_WAIT('T0 M6') ACTION.SET_MANUAL_MODE() self.firstHoming = True self.w.gcodegraphics.update() self.w.conv_preview.update() log = _translate('HandlerClass', 'Machine homed') STATUS.emit('update-machine-log', log, 'TIME') def joint_homed(self, obj, joint): dro = self.coordinates[int(joint)] self.w[f'dro_{dro}'].setProperty('homed', True) self.w[f'dro_{dro}'].setStyle(self.w[f'dro_{dro}'].style()) self.w[f'dro_label_{dro}'].setProperty('homed', True) self.w[f'dro_label_{dro}'].setStyle(self.w[f'dro_label_{dro}'].style()) self.w.update STATUS.emit('dro-reference-change-request', 1) self.w.gcodegraphics.logger.clear() def joint_unhomed(self, obj, joints): for joint in joints: dro = self.coordinates[int(joint)] self.w[f'dro_{dro}'].setProperty('homed', False) self.w[f'dro_{dro}'].setStyle(self.w[f'dro_{dro}'].style()) self.w[f'dro_label_{dro}'].setProperty('homed', False) self.w[f'dro_label_{dro}'].setStyle(self.w[f'dro_label_{dro}'].style()) if len(joints) < len(self.coordinates): self.w.home_all.setEnabled(True) self.w.update STATUS.emit('dro-reference-change-request', 1) self.interp_idle(None) self.w.gcodegraphics.update() self.w.conv_preview.update() def hard_limit_tripped(self, obj, tripped, list_of_tripped): self.w.chk_override_limits.setEnabled(tripped) if not tripped: self.w.chk_override_limits.setChecked(False) def jog_inhibited_changed(self, state): if state: self.w.chk_override_jog.setEnabled(state) self.w.releaseKeyboard() else: if not self.w.led_float_switch.hal_pin.get() and not self.ohmicLedInPin.get() and \ not self.w.led_breakaway_switch.hal_pin.get(): self.w.chk_override_jog.setChecked(False) hal.set_p('plasmac.override-jog', '0') def sensor_active_changed(self, state): if not state: self.w.chk_override_jog.setEnabled(False) self.w.chk_override_jog.setChecked(False) hal.set_p('plasmac.override-jog', str(state)) def z_offset_changed(self, height): currentState = self.plasmacStatePin.get() if currentState in [self.PROBE_HEIGHT, self.PROBE_DOWN, self.PROBE_UP, self.ZERO_HEIGHT, self.PIERCE_HEIGHT, self.PUDDLE_JUMP, self.CUT_HEIGHT, self.SAFE_HEIGHT, self.MAX_HEIGHT]: self.watchDogState = currentState self.watchDogTimer.start(3000) if STATUS.is_interp_paused() and not height and \ (hal.get_value('plasmac.stop-type-out') or hal.get_value('plasmac.cut-recovering')): self.w.set_cut_recovery() def override_jog_changed(self, state): if state: hal.set_p('plasmac.override-jog', '1') else: hal.set_p('plasmac.override-jog', '0') def tool_changed(self, obj, tool): if tool == 0: self.tool = self.TORCH text = _translate('HandlerClass', 'TORCH') self.w.lbl_tool.setText(text) elif tool == 1: self.tool = self.SCRIBE text = _translate('HandlerClass', 'SCRIBE') self.w.lbl_tool.setText(text) else: self.tool = self.UNKNOWN text = _translate('HandlerClass', 'UNKNOWN') self.w.lbl_tool.setText(text) def gcodes_changed(self, obj, cod): if self.units == 'in' and STATUS.is_metric_mode(): self.droScale = 25.4 self.gcodeScalePin.set(25.4) elif self.units == 'mm' and not STATUS.is_metric_mode(): self.droScale = 1 / 25.4 self.gcodeScalePin.set(1 / 25.4) else: self.droScale = 1 self.gcodeScalePin.set(1) self.w.lbl_gcodes.setText(f'{cod}') def mcodes_changed(self, obj, cod): self.w.lbl_mcodes.setText(f'{cod}') def metric_mode_changed(self, obj, state): self.w.gcodegraphics.update() def set_start_line(self, line): if self.fileOpened: if self.w.chk_run_from_line.isChecked(): if self.w.sender(): if self.w.sender().objectName() == 'gcode_editor_display': return if line > 1: msg0 = _translate('HandlerClass', 'SELECTED') self.w.run.setText(f'{msg0} {line}') self.runText = f'{msg0} {line}' self.rflSelected = True self.startLine = line - 1 elif self.rflActive: txt0 = _translate('HandlerClass', 'RUN FROM LINE') txt1 = _translate('HandlerClass', 'CYCLE START') self.runText = f'{txt0}\n{txt1}' self.rflSelected = False else: self.startLine = 0 self.rflSelected = False self.w.gcodegraphics.clear_highlight() if line < 1: self.w.gcode_display.setCursorPosition(0, 0) self.w.gcode_display.moveMarker(0) def update_gcode_properties(self, props): if props: self.gcodeProps = props if props['gcode_units'] == 'in': STATUS.emit('metric-mode-changed', False) else: STATUS.emit('metric-mode-changed', True) def error_update(self, obj, kind, error): if not self.realTimeDelay: if kind == linuxcnc.OPERATOR_ERROR: self.error_status(True) self.mdiError = True if kind == linuxcnc.NML_ERROR: self.error_status(True) ######################################################################################################################### # CALLBACKS FROM FORM # ######################################################################################################################### def ext_run(self, state): if self.w.run.isEnabled() and state: self.run_clicked() def ext_abort(self, state): if self.w.abort.isEnabled() and state: self.abort_pressed() def ext_pause(self, state): if self.w.pause_resume.isEnabled() and state: if STATUS.stat.paused: self.pause_resume_pressed() ACTION.PAUSE() def ext_pause_only(self, state): if self.w.pause_resume.isEnabled() and state: ACTION.PAUSE_MACHINE() def ext_resume(self, state): if self.w.pause_resume.isEnabled() and state: self.pause_resume_pressed() ACTION.RESUME() def ext_touch_off(self, state): if self.w.touch_xy.isEnabled() and state: self.touch_xy_clicked() def ext_laser_touch_off(self, state): if self.w.laser.isEnabled(): if state: self.extLaserButton = True self.laser_pressed() else: self.extLaserButton = False self.laser_clicked() def ext_laser_toggle(self, state): if self.w.laser.isEnabled() and state: self.laserOnPin.set(not self.laserOnPin.get()) def ext_jog_slow(self, state): if self.w.jog_slow.isEnabled() and state: self.jog_slow_pressed(True) def ext_run_pause(self, state): if self.w.run.isEnabled() and state: self.run_clicked() elif self.w.pause_resume.isEnabled() and state: if STATUS.stat.paused: self.pause_resume_pressed() ACTION.PAUSE() def power_button(self, action, state): if action == 'pressed': self.shutdownTimer.start(self.shutdownTime) elif action == 'released': self.shutdownTimer.stop() elif action == 'clicked': if STATUS.estop_is_clear(): ACTION.SET_MACHINE_STATE(not STATUS.machine_is_on()) else: self.w.power.setChecked(False) elif action == 'external' and self.w.power.isEnabled() and not self.firstRun: if state: self.shutdownTimer.start(self.shutdownTime) self.w.power.setDown(True) else: self.shutdownTimer.stop() self.w.power.setDown(False) self.w.power.click() def run_clicked(self): if self.startLine and self.rflSelected: self.rflActive = True self.refresh_button_states() self.rflSelected = False if self.developmentPin.get(): reload(RFL) self.runType = self.dialog_rfl_type() if self.runType['cancel']: if 'rfl.ngc' in self.lastLoadedProgram: self.set_start_line(-1) else: self.clear_rfl() return lastLine = 0 if self.runType['type'] == 'cut': count = 0 start = 0 start_found = False in_keep_z_motion = False with open(self.lastLoadedProgram, 'r') as inFile: for line in inFile: stripped = line.replace(' ', '') # track keep-z-motion state if '#=1' in stripped: in_keep_z_motion = True elif '#=0' in stripped: in_keep_z_motion = False # find start: last G00 XY before or first G00 XY after startLine if line[:3] == 'G00' and 'G53' not in line and ('X' in line or 'Y' in line): if count <= self.startLine or not start_found: start = count start_found = (count >= self.startLine) # find end: after both startLine and start if count > max(self.startLine, start): if (in_keep_z_motion and (('G53' in line and 'G00' in line and 'Z' in line) or '#=0' in stripped)) or \ (not in_keep_z_motion and line[:3] == 'M05'): break count += 1 lastLine = count self.startLine = start head = _translate('HandlerClass', 'Gcode Error') data = RFL.run_from_line_get(self.lastLoadedProgram, self.startLine, lastLine) # cannot do run from line within a subroutine or if using cutter compensation if data['error']: if data['compError']: msg0 = _translate('HandlerClass', 'Cannot run from line while') msg1 = _translate('HandlerClass', 'cutter compensation is active') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}\n') if data['subError']: msg0 = _translate('HandlerClass', 'Cannot do run from line') msg1 = _translate('HandlerClass', 'inside subroutine') msg2 = '' for sub in data['subError']: msg2 += f' {sub}' STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}{msg2}\n') self.clear_rfl() else: # are we in a cutting operation (needs leadin dialog) is_cutting = (data['codes']['spindle']['line'] and 'M03' in data['codes']['spindle']['line'] and '$0' in data['codes']['spindle']['line'] and not data.get('keep_z_motion_active', False)) # skip leadin dialog for "this cutpath" or non-cutting operations if self.runType['type'] == 'cut' or not is_cutting: # fake user input userInput = {'cancel': False, 'do': False, 'length': 0, 'angle': 0} # get user input else: userInput = self.dialog_run_from_line() # rfl cancel clicked if userInput['cancel']: if 'rfl.ngc' in self.lastLoadedProgram: self.set_start_line(-1) else: self.clear_rfl() return # rfl load clicked rflFile = f'{self.tmpPath}rfl.ngc' result = RFL.run_from_line_set(rflFile, data, userInput, self.unitsPerMm) # leadin cannot be used if result['error']: msg0 = _translate('HandlerClass', 'Unable to calculate a leadin for this cut') msg1 = _translate('HandlerClass', 'Program will run from selected line with no leadin applied') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}\n') # load rfl file if ACTION.prefilter_path or self.lastLoadedProgram != 'None': self.preRflFile = ACTION.prefilter_path or self.lastLoadedProgram ACTION.OPEN_PROGRAM(rflFile) ACTION.prefilter_path = self.preRflFile if self.runType['type'] == 'cut': log = _translate('HandlerClass', 'Run from line - this cutpath loaded') else: log = _translate('HandlerClass', 'Run from line - here to end loaded') log1 = _translate('HandlerClass', 'Start line') STATUS.emit('update-machine-log', f'{log} - {log1}: {(self.startLine + 1)}', 'TIME') elif self.cut_critical_toggle_check(): self.jobRunning = True ACTION.RUN(0) log = _translate('HandlerClass', 'Cycle started') if self.w.torch_enable.isChecked(): log1 = _translate('HandlerClass', 'Torch enabled') else: log1 = _translate('HandlerClass', 'Torch disabled') STATUS.emit('update-machine-log', f'{log} - {log1}', 'TIME') self.refresh_button_states() def abort_pressed(self): if self.manualCut: ACTION.SET_SPINDLE_STOP(0) self.manualCut = False self.set_mc_states(True) if self.mcButton: self.button_normal(self.mcButton) log = _translate('HandlerClass', 'Manual cut aborted') STATUS.emit('update-machine-log', log, 'TIME') return elif self.probeTest: self.probe_test_stop() log = _translate('HandlerClass', 'Probe test aborted') STATUS.emit('update-machine-log', log, 'TIME') return elif self.torchPulse: self.torch_pulse_abort() elif self.framing: ACTION.ABORT() self.framing = False ACTION.SET_MANUAL_MODE() self.laserOnPin.set(0) self.w.gcodegraphics.logger.clear() else: ACTION.ABORT() if hal.get_value('plasmac.cut-recovery'): hal.set_p('plasmac.cut-recovery', '0') self.laserOnPin.set(0) self.interp_idle(None) log = _translate('HandlerClass', 'Cycle aborted') STATUS.emit('update-machine-log', log, 'TIME') def pause_resume_pressed(self): if hal.get_value('plasmac.cut-recovering'): self.w.jog_stack.setCurrentIndex(self.JOG) self.laserOnPin.set(0) if STATUS.is_auto_paused(): log = _translate('HandlerClass', 'Cycle resumed') STATUS.emit('update-machine-log', log, 'TIME') def user_button_pressed(self, button): self.user_button_down(button) def user_button_released(self, button): self.user_button_up(button) def height_ovr_pressed(self, state, height): if state: if height: self.heightOvr += height * self.w.thc_threshold.value() else: self.heightOvr = 0 if self.heightOvr < -9: self.heightOvr = -9 if self.heightOvr > 9: self.heightOvr = 9 self.heightOverridePin.set(self.heightOvr) self.w.height_ovr_label.setText(f'{self.heightOvr:.2f}') def height_ovr_encoder(self, value): if (value != self.old_ovr_counts and self.extHeightOvrCountEnablePin.get()): self.heightOvr += (value-self.old_ovr_counts) * self.w.thc_threshold.value() * self.heightOvrScale if self.heightOvr < -9: self.heightOvr = -9 if self.heightOvr > 9: self.heightOvr = 9 self.heightOverridePin.set(self.heightOvr) self.w.height_ovr_label.setText(f'{self.heightOvr:.2f}') self.old_ovr_counts = value def height_ovr_scale_change(self, value): if value: self.heightOvrScale = value def touch_xy_clicked(self): self.touch_off_xy(0, 0) self.w.gcodegraphics.logger.clear() def setup_feed_rate_changed(self): if self.w.probe_feed_rate.value() > self.w.setup_feed_rate.value(): self.w.probe_feed_rate.setValue(self.w.setup_feed_rate.value()) self.w.probe_feed_rate.setMaximum(self.w.setup_feed_rate.value()) def save_plasma_clicked(self): self.save_plasma_parameters() def reload_plasma_clicked(self): self.load_plasma_parameters() def backup_clicked(self): self.save_logfile(6) bkpPath = f'{os.path.expanduser("~")}' bkpName = f'{self.machineName}_V{LCNCVER}-{VERSION}_{time.strftime("%y-%m-%d_%H-%M-%S")}.tar.gz' tmpFile = os.path.join(self.PATHS.CONFIGPATH, 'config_info.txt') lcncInfo = (Popen('linuxcnc_info -s', stdout=PIPE, stderr=PIPE, shell=True).communicate()[0]).decode('utf-8') network = (Popen('lspci | grep -i net', stdout=PIPE, stderr=PIPE, shell=True).communicate()[0]).decode('utf-8') with open(tmpFile, 'a') as outFile: outFile.write(f'locale:\n{os.getenv("LANG")}\n\n') if network: outFile.write(f'lspci | grep -i net:\n{network}\n') else: outFile.write('Unknown error with "lspci | grep -i net" command\n\n') if lcncInfo: outFile.write('linuxcnc_info:\n') try: with open('/tmp/linuxcnc_info.txt', 'r') as inFile: for line in inFile: outFile.write(line) except: outFile.write("Unknown error opening /tmp/linuxcnc_info.txt\n") else: outFile.write('Unknown error creating /tmp/linuxcnc_info.txt\n') with tarfile.open(f'{bkpPath}/{bkpName}', mode='w:gz', ) as archive: archive.add(f'{self.PATHS.CONFIGPATH}') try: os.remove(tmpFile) except: log = _translate('HandlerClass', 'Unknown error removing') STATUS.emit('update-machine-log', f'{log} {tmpFile}', 'TIME') head = _translate('HandlerClass', 'Backup Complete') msg0 = _translate('HandlerClass', 'A compressed backup of the machine configuration including the machine logs has been saved in your home directory as') msg1 = _translate('HandlerClass', 'It is safe to delete this file at any time') self.dialog_show_ok(QMessageBox.Information, head, f'\n{msg0}:\n{bkpName}\n\n{msg1}\n') def set_offsets_clicked(self): if self.developmentPin.get(): reload(OFFSETS) self.w.main_tab_widget.setCurrentIndex(self.MAIN) if STATUS.stat.rotation_xy: ACTION.CALL_MDI_WAIT('G10 L2 P0 R0') ACTION.SET_MANUAL_MODE() OFFSETS.dialog_show(self, self.w, self.PREFS, INIPATH, STATUS, ACTION, TOOL) def feed_label_pressed(self): self.w.feed_slider.setValue(100) def rapid_label_pressed(self): self.w.rapid_slider.setValue(100) def jogs_label_pressed(self): self.w.jog_slider.setValue(INFO.DEFAULT_LINEAR_JOG_VEL) def gui_button_jog(self, state, joint, direction): shift = False if state and joint == 'z' and direction == 1 and self.zPlusOverrideJog and self.w.chk_override_jog.isEnabled(): self.w.chk_override_jog.setChecked(True) if STATUS.is_joint_mode(): self.kb_jog(state, self.coordinates.index(joint), direction, shift) else: self.kb_jog(state, ['x', 'y', 'z', 'a', 'b', 'c'].index(joint), direction, shift) def view_t_pressed(self, widget): t = time.time() + 0.01 while time.time() < t: QApplication.processEvents() widget.set_view('Z') mid = DRAW.extents_info(widget)[0] mult = 1 if self.units == 'in' else 25.4 zoomScale = self.w.table_zoom_scale.value() * 2 xTableCenter = (self.xMin + self.xLen / 2) / mult - mid[0] yTableCenter = (self.yMin + self.yLen / 2) / mult - mid[1] xSize = self.xLen / mult / zoomScale ySize = self.yLen / mult / zoomScale widget.translate_modelview(-xTableCenter, -yTableCenter, 0) widget.set_eyepoint_from_extents(xSize, ySize) widget.perspective = False widget.lat = widget.lon = 0 widget.update() def view_p_pressed(self): self.w.gcodegraphics.set_view('P') def view_z_pressed(self, widget): widget.set_view('Z') def view_clear_pressed(self): self.w.gcodegraphics.logger.clear() def pan_left_pressed(self, widget): widget.recordMouse(0, 0) widget.translateOrRotate(-widget._view_incr, 0) def pan_right_pressed(self, widget): widget.recordMouse(0, 0) widget.translateOrRotate(widget._view_incr, 0) def pan_up_pressed(self, widget): widget.recordMouse(0, 0) widget.translateOrRotate(0, -widget._view_incr) def pan_down_pressed(self, widget): widget.recordMouse(0, 0) widget.translateOrRotate(0, widget._view_incr) def zoom_in_pressed(self, widget): widget.zoomin() def zoom_out_pressed(self, widget): widget.zoomout() def gcode_display_loaded(self): gcodeLines = len(str(self.w.gcode_display.lines())) self.w.gcode_display.set_margin_metric(gcodeLines) self.w.gcode_editor.set_margin_metric(gcodeLines) def file_clear_clicked(self): proceed = self.editor_close_check() if not proceed: return self.w.preview_stack.setCurrentIndex(self.PREVIEW) self.w.gcode_editor.editor.new_text() self.w.gcode_editor.editor.setModified(False) # clear error message list and error status if self.w.screen_options.desktop_notify: self.w.screen_options.QTVCP_INSTANCE_._NOTICE.alarmpage = [] self.w.screen_options.QTVCP_INSTANCE_._NOTICE.external_close() self.error_status(False) # return cut-type button to Normal if self.cutType: self.cutType = 0 self.cutTypePin.set(0) self.button_normal(self.ctButton) self.w[self.ctButton].setText(self.cutTypeText) if self.fileOpened: if self.rflActive: self.clear_rfl() clearFile = f'{self.tmpPath}qtplasmac_file_clear.ngc' with open(clearFile, 'w') as outFile: outFile.write('m2') if ACTION.prefilter_path: if 'single_cut' in ACTION.prefilter_path: self.preClearFile = self.oldFile else: if self.lastLoadedProgram != 'None': self.preClearFile = ACTION.prefilter_path or self.lastLoadedProgram self.w.materials_box.setCurrentIndex(self.materialList.index(self.defaultMaterial)) self.w.material_selector.setCurrentIndex(self.w.materials_box.currentIndex()) self.w.conv_material.setCurrentIndex(self.w.materials_box.currentIndex()) self.remove_temp_materials() ACTION.OPEN_PROGRAM(clearFile) ACTION.prefilter_path = self.preClearFile if self.tool != self.TORCH and STATUS.is_on_and_idle() and STATUS.is_all_homed(): ACTION.CALL_MDI_WAIT('T0 M6') ACTION.CALL_MDI_WAIT('G43 H0') ACTION.SET_MANUAL_MODE() log = _translate('HandlerClass', 'Program cleared') STATUS.emit('update-machine-log', log, 'TIME') else: self.view_t_pressed(self.w.gcodegraphics) def file_open_clicked(self): if self.w.preview_stack.currentIndex() != self.OPEN: self.w.preview_stack.setCurrentIndex(self.OPEN) else: self.preview_index_return(self.w.preview_stack.currentIndex()) def file_edit_clicked(self): if STATUS.stat.interp_state == linuxcnc.INTERP_IDLE and self.w.preview_stack.currentIndex() != self.EDIT: self.w.preview_stack.setCurrentIndex(self.EDIT) self.w.gcode_editor.editor.setFocus() if self.fileOpened and not self.w.gcode_editor.editor.isModified(): self.w.gcode_editor.editor.load_text(ACTION.prefilter_path) text = _translate('HandlerClass', 'EDIT') self.w.edit_label.setText(f'{text}: {ACTION.prefilter_path}') self.w.gcode_editor.editor.setModified(False) try: if os.path.getsize(self.gcodeErrorFile): with open(self.gcodeErrorFile, 'r') as inFile: self.w.gcode_editor.select_line(int(inFile.readline().rstrip()) - 1) inFile.seek(0) for line in inFile: self.w.gcode_editor.editor.userHandle = \ self.w.gcode_editor.editor.markerAdd(int(line) - 1, self.w.gcode_editor.editor.USER_MARKER_NUM) except: pass self.vkb_show() else: self.new_exitCall(self.PREVIEW) def mdi_show_clicked(self): if STATUS.is_on_and_idle() and STATUS.is_all_homed() and self.w.gcode_stack.currentIndex() != self.MDI: self.w.gcode_stack.setCurrentIndex(self.MDI) self.vkb_show() else: self.w.gcode_stack.setCurrentIndex(self.GCODE) ACTION.SET_MANUAL_MODE() self.vkb_hide() def file_cancel_clicked(self): self.preview_index_return(self.w.preview_stack.currentIndex()) def cone_size_changed(self, data): self.w.gcodegraphics.set_cone_basesize(data) def grid_size_changed(self, data): # grid size is in inches grid = data / self.unitsPerMm / 25.4 self.w.gcodegraphics.grid_size = grid def main_tab_changed(self, tab): # stop an active jog if user changed tabs while holding a jog key if STATUS.machine_is_on() and STATUS.is_all_homed() and STATUS.is_interp_idle(): self.w.releaseKeyboard() for joint in range(len(self.coordinates)): ACTION.JOG(joint, 0, 0, 0) t = time.time() + 0.01 while time.time() < t: QApplication.processEvents() if tab == self.MAIN: if self.w.preview_stack.currentIndex() == self.OPEN: self.vkb_show() else: self.vkb_hide() if self.w.gcode_stack.currentIndex() == self.GCODE and self.w.preview_stack.currentIndex() == self.PREVIEW: self.autorepeat_keys(False) elif tab == self.CONVERSATIONAL: self.w.conv_preview.logger.clear() self.w.conv_preview.set_current_view() if self.developmentPin.get(): reload(self.CONV) self.CONV.conv_setup(self, self.w) self.vkb_show(True) self.autorepeat_keys(True) elif tab == self.PARAMETERS: self.vkb_show(True) self.autorepeat_keys(True) elif tab == self.SETTINGS: self.vkb_show() self.autorepeat_keys(True) elif tab == self.STATISTICS: self.vkb_hide() self.w.machinelog.scrollToBottom() self.w.machinelog.hideCursor() self.error_status(False) def z_height_changed(self, value): self.w.dro_z.update_user(value * self.droScale) def offsets_active_changed(self, value): if not value: # set z dro to normal made self.w.dro_z.setProperty('Qreference_type', 1) def consumable_change_changed(self, value): if self.ccButton: if value: log = _translate('HandlerClass', 'Consumable change initiated') STATUS.emit('update-machine-log', log, 'TIME') else: self.button_normal(self.ccButton) log = _translate('HandlerClass', 'Consumable change completed') STATUS.emit('update-machine-log', log, 'TIME') self.refresh_button_states() def plasmac_state_changed(self, state): if (state > self.PROBE_UP or state == self.PROBE_TEST) and hal.get_value('axis.z.eoffset-counts'): # set z dro to offset mode self.w.dro_z.setProperty('Qreference_type', 10) if state == self.IDLE: self.refresh_button_states() if state in [self.PROBE_HEIGHT, self.PROBE_DOWN, self.PROBE_UP, self.ZERO_HEIGHT, self.PIERCE_HEIGHT, self.PUDDLE_JUMP, self.CUT_HEIGHT, self.SAFE_HEIGHT, self.MAX_HEIGHT]: self.watchDogState = state self.watchDogTimer.start(3000) else: self.watchDogTimer.stop() self.watchDogState = None def plasmac_stop_changed(self, state): if not state and not self.plasmacStatePin.get(): for pin in ['pierce-type', 'pierce-motion-delay', 'cut-height-delay', 'pierce-end-height', 'gouge-speed', 'gouge-speed-distance', 'creep-speed', 'creep-speed-distance']: hal.set_p(f'plasmac.{pin}', '0') def g10_reload(self): if self.fileOpened: self.file_reload_clicked() def file_reload_clicked(self): proceed = self.editor_close_check() if not proceed: return if self.rflActive: self.clear_rfl() if ACTION.prefilter_path or self.lastLoadedProgram != 'None': file = ACTION.prefilter_path or self.lastLoadedProgram if os.path.exists(file): self.overlayProgress.setValue(0) self.remove_temp_materials() ACTION.OPEN_PROGRAM(file) log = _translate('HandlerClass', 'Reloaded') STATUS.emit('update-machine-log', f'{log}: {file}', 'TIME') else: head = _translate('HandlerClass', 'File Error') msg0 = _translate('HandlerClass', 'does not exist') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{file} {msg0}\n') def jog_slow_pressed(self, external=False): if self.w.jog_slow.isChecked(): self.w.jog_slow.setText(_translate('HandlerClass', 'FAST')) self.w.jog_slider.setMaximum(self.w.jog_slider.maximum() * self.slowJogFactor) self.w.jog_slider.setValue(self.w.jog_slider.value() * self.slowJogFactor) self.w.jog_slider.setPageStep(100) self.previousJogSpeed = self.w.jog_slider.value() if external: self.w.jog_slow.setChecked(False) else: self.w.jog_slow.setText(_translate('HandlerClass', 'SLOW')) self.w.jog_slider.setValue(self.w.jog_slider.value() / self.slowJogFactor) self.w.jog_slider.setMaximum(self.w.jog_slider.maximum() / self.slowJogFactor) self.w.jog_slider.setPageStep(10) if external: self.w.jog_slow.setChecked(True) def chk_override_limits_changed(self, state): if state: ACTION.TOGGLE_LIMITS_OVERRIDE() def save_shutdown_message_clicked(self): self.PREFS.putpref('Exit warning text', self.w.sd_text.text(), str, 'GUI_OPTIONS') self.exitMessage = self.w.sd_text.text() def reload_shutdown_message_clicked(self): self.w.sd_text.setText(self.PREFS.getpref('Exit warning text', '', str, 'GUI_OPTIONS')) def save_user_button_clicked(self): self.set_interlock_defaults() for n in range(1, 21): self.PREFS.putpref(f'{n} Name', self.w[f'ub_name_{n}'].text(), str, 'BUTTONS') self.PREFS.putpref(f'{n} Code', self.w[f'ub_code_{n}'].text(), str, 'BUTTONS') self.interlockRules = INTERLOCK_RULES.copy() self.user_button_setup() self.preview_stack_changed() self.refresh_button_states() def reload_user_button_clicked(self): for n in range(1, 21): self.w[f'ub_name_{n}'].clear() self.w[f'ub_code_{n}'].clear() self.interlockRules = INTERLOCK_RULES.copy() self.user_button_setup() self.refresh_button_states() def web_back_pressed(self): self.w.webview.back() def web_forward_pressed(self): self.w.webview.forward() def web_reload_pressed(self): self.w.webview.load(self.umUrl) ######################################################################################################################### # GENERAL FUNCTIONS # ######################################################################################################################### # called by ScreenOptions, this function overrides ScreenOption's closeEvent def closeEvent(self, event): O = self.w.screen_options if self.w.chk_exit_warning.isChecked() or not STATUS.is_interp_idle(): icon = QMessageBox.Question if STATUS.is_interp_idle() else QMessageBox.Critical head = _translate('HandlerClass', 'Shutdown') if not STATUS.is_interp_idle(): msg0 = _translate('HandlerClass', 'Current operation is not complete') msg0 += '!\n\n' else: msg0 = '' if self.exitMessage: exitLines = self.exitMessage.split('\\') for line in exitLines: msg0 += f'{line}\n' msg0 += '\n' msg0 += _translate('HandlerClass', 'Do you want to shutdown QtPlasmaC') if self.dialog_show_yesno(icon, head, f'\n{msg0}?\n'): if O.PREFS_ and O.play_sounds and O.shutdown_play_sound: STATUS.emit('play-sound', O.shutdown_exit_sound_type) O.QTVCP_INSTANCE_.settings.sync() O.QTVCP_INSTANCE_.shutdown() O.QTVCP_INSTANCE_.panel_.shutdown() STATUS.shutdown() event.accept() else: event.ignore() else: if O.PREFS_ and O.play_sounds and O.shutdown_play_sound: STATUS.emit('play-sound', O.shutdown_exit_sound_type) O.QTVCP_INSTANCE_.settings.sync() O.QTVCP_INSTANCE_.shutdown() O.QTVCP_INSTANCE_.panel_.shutdown() STATUS.shutdown() def update_check(self): # newest update must be added last in this function # if any writing to the INI file is required then that needs # to be done later in the update_iniwrite function halfiles = self.iniFile.findall('HAL', 'HALFILE') qtvcpPrefsFile = os.path.join(self.PATHS.CONFIGPATH, 'qtvcp.prefs') self.restart = False # use qtplasmac_comp.hal for component connections (pre V1.221.154 2022/01/18) if halfiles and not [f for f in halfiles if 'plasmac.tcl' in f] and not \ [f for f in halfiles if 'qtplasmac_comp.hal' in f]: restart, error, text = UPDATER.add_component_hal_file(self.PATHS.CONFIGPATH, halfiles) self.updateData.append([restart, error, text]) if error: return self.updateIni[154] = True # split out qtplasmac specific prefs into a separate file (pre V1.222.170 2022/03/08) if not os.path.isfile(self.prefsFile): old = os.path.join(self.PATHS.CONFIGPATH, 'qtplasmac.prefs') if os.path.isfile(old): restart, error, text = UPDATER.split_prefs_file(old, qtvcpPrefsFile, self.prefsFile) self.updateData.append([restart, error, text]) if error: return self.PREFS = Access(self.prefsFile) # move conversational prefs from qtvcp.prefs to .prefs (pre V1.222.187 2022/05/03) if os.path.isfile(qtvcpPrefsFile) and os.path.isfile(self.prefsFile): with open(qtvcpPrefsFile, 'r') as inFile: data = inFile.readlines() if [line for line in data if '[CONVERSATIONAL]' in line]: restart, error, text = UPDATER.move_prefs(qtvcpPrefsFile, self.prefsFile) self.updateData.append([restart, error, text]) if error: return # change RS274 startup parameters from a subroutine (pre V1.224.207 2022/06/22) startupCode = self.iniFile.find('RS274NGC', 'RS274NGC_STARTUP_CODE') if 'metric_startup' in startupCode or 'imperial_startup' in startupCode: self.updateIni[207] = True # remove the qtplasmac link from the config directory (pre V1.225.208 2022/06/29) if os.path.islink(os.path.join(self.PATHS.CONFIGPATH, 'qtplasmac')): # stage 1: set up for unlinking on the next run of qtplasmac if 'code.py' in self.iniFile.find('FILTER', 'ngc'): self.updateIni[208] = True # stage 2: remove the qtplasmac link else: os.unlink(os.path.join(self.PATHS.CONFIGPATH, 'qtplasmac')) # move qtplasmac options from INI file to prefs file pre V1.227.219 2022/07/14) if not self.PREFS.has_section('BUTTONS'): update = False for n in range(1, 21): if self.iniFile.find('QTPLASMAC', f'BUTTON_{n}_NAME'): update = True break if update: restart, error, text = UPDATER.move_options_to_prefs_file(self.iniFile, self.PREFS) self.updateData.append([restart, error, text]) if error: return self.updateIni[219] = True # move port info from [GUI_OPTIONS] section (if it was moved via V1.227.219 update) to [POWERMAX] section if self.PREFS.has_option('GUI_OPTIONS', 'Port'): restart, error, text = UPDATER.move_port(self.PREFS) self.updateData.append([restart, error, text]) if error: return # move default material from prefs file to material 0 in materials file (pre V1.236.278 2023/07/07) if self.PREFS.has_section('DEFAULT MATERIAL'): restart, error, text = UPDATER.move_default_material(self.PREFS, self.materialFile, self.unitsPerMm) self.updateData.append([restart, error, text]) if error: return # change runcritical to cutcritical in .prefs file (pre V2.10-001.015 2023/12/23) if os.path.isfile(self.prefsFile): with open(self.prefsFile, 'r') as inFile: text = inFile.read() if 'runcritical' in text: restart, error, text = UPDATER.rename_runcritical(self.prefsFile) self.updateData.append([restart, error, text]) if error: return # set user_m_path to include the nc_files directory (pre V2.10-001.017 2024/01/23) mPathIni = self.iniFile.find('RS274NGC', 'USER_M_PATH') if 'nc_files/plasmac/m_files' not in mPathIni: if '/usr' in self.PATHS.BASEDIR: mPath = '/usr/share/doc/linuxcnc/examples/nc_files/plasmac/m_files' # simPath = os.path.join(self.PATHS.BASEDIR, 'share/doc/linuxcnc/examples/sample-configs/sim/qtplasmac') # we need elevated privileges to remove a file from here so forget it... # we may revisit this. simPath = None else: mPath = os.path.realpath(os.path.join(self.PATHS.BASEDIR, 'nc_files/plasmac/m_files')) simPath = os.path.join(self.PATHS.BASEDIR, 'configs/sim/qtplasmac') restart, error, text = UPDATER.insert_user_m_path(self.PATHS.CONFIGPATH, simPath) if error: return self.updateIni['001-017'] = mPath # add M52P1 to the [CONVERSATIONAL] Preamble and Postamble (pre V2.10-009.070 2025/08/07) if any(self.PREFS.has_option('CONVERSATIONAL', option) and 'M52P1' not in self.PREFS.get('CONVERSATIONAL', option) for option in ('Preamble', 'Postamble')): restart, error, text = UPDATER.add_adaptive(self.PREFS) self.updateData.append([restart, error, text]) if error: return # add newlines between codes [CONVERSATIONAL] Preamble and Postamble (pre V2.10-009.071 2025/08/09) if any(self.PREFS.has_option('CONVERSATIONAL', option) and ' ' in self.PREFS.get('CONVERSATIONAL', option) for option in ('Preamble', 'Postamble')): restart, error, text = UPDATER.add_newlines(self.PREFS) self.updateData.append([restart, error, text]) if error: return def update_iniwrite(self): # this is for updates that write to the INI file if 154 in self.updateIni: restart, error, text = UPDATER.add_component_hal_file_iniwrite(INIPATH) if restart: self.restart = True self.updateData.append([self.restart, error, text]) if error: return if 207 in self.updateIni: restart, error, text = UPDATER.rs274ngc_startup_code_iniwrite(INIPATH) if restart: self.restart = True self.updateData.append([self.restart, error, text]) if error: return if 208 in self.updateIni: restart, error, text = UPDATER.remove_qtplasmac_link_iniwrite(INIPATH) if restart: self.restart = True self.updateData.append([self.restart, error, text]) if error: return if 219 in self.updateIni: restart, error, text = UPDATER.move_options_to_prefs_file_iniwrite(INIPATH) if restart: self.restart = True self.updateData.append([self.restart, error, text]) if error: return if '001-017' in self.updateIni: restart, error, text = UPDATER.insert_user_m_path_iniwrite(INIPATH, self.updateIni['001-017']) if restart: self.restart = True self.updateData.append([self.restart, error, text]) if error: return def find_a_file(self, name, path): ''' find a file "name" in the path "path" ''' for root, dirs, files in os.walk(path): # variable is not currently used del dirs if name in files: return os.path.join(root, name) def motion_type_changed(self, value): if value == 0 and STATUS.is_mdi_mode(): ACTION.SET_MANUAL_MODE() def set_interlock_defaults(self): self.ccButton, self.otButton, self.ptButton, self.tpButton = '', '', '', '' self.ctButton, self.scButton, self.frButton, self.mcButton = '', '', '', '' self.ovButton, self.llButton, self.tlButton, self.umButton, self.jtButton = '', '', [], '', '' self.halTogglePins = {} self.halPulsePins = {} self.dualCodeButtons = {} def preview_index_return(self, index): if self.w.gcode_editor.editor.isModified(): self.new_exitCall(index) else: if index == self.PREVIEW: pass elif index == self.OPEN: self.vkb_hide() ACTION.SET_MANUAL_MODE() elif index == self.EDIT: pass elif index == self.CAMERA: self.button_normal('camera') self.cameraOn = False self.vkb_hide() ACTION.SET_MANUAL_MODE() elif index == self.OFFSETS: pass elif index == self.USER_MANUAL: pass self.w.preview_stack.setCurrentIndex(self.PREVIEW) def editor_close_check(self): if self.w.gcode_editor.editor.isModified(): head = _translate('HandlerClass', 'Unsaved Editor Changes') msg0 = _translate('HandlerClass', 'Unsaved changes will be lost') msg1 = _translate('HandlerClass', 'Do you want to proceed') if not self.dialog_show_yesno(QMessageBox.Question, head, f'\n{msg0}\n\n{msg1}?\n'): self.w.preview_stack.setCurrentIndex(self.EDIT) self.w.gcode_editor.editor.setFocus() return False return True def system_notify_button_pressed(self, object, button, state): if button in ['background', 'clearAll', 'close', 'lastFive'] and state: self.error_status(False) def error_status(self, state): self.error_present = state if state: text = _translate('HandlerClass', 'ERROR SENT TO MACHINE LOG') else: text = '' self.w.error_label.setText(f'{text}') def touch_off_xy(self, x, y): if STATUS.is_on_and_idle() and STATUS.is_all_homed(): ACTION.CALL_MDI_WAIT(f'G10 L20 P0 X{x} Y{y}') if self.fileOpened: self.file_reload_clicked() ACTION.SET_MANUAL_MODE() def bounds_check_file(self): msg = _translate('HandlerClass', 'G-code') self.boundsMultiplier = 1 if 'gcode_units' in self.gcodeProps: if self.units == 'in' and self.gcodeProps['gcode_units'] == 'mm': self.boundsMultiplier = 0.03937 elif self.units == 'mm' and self.gcodeProps['gcode_units'] == 'in': self.boundsMultiplier = 25.4 xMin = float(self.gcodeProps['x'].split()[0]) * self.boundsMultiplier xMax = float(self.gcodeProps['x'].split()[2]) * self.boundsMultiplier yMin = float(self.gcodeProps['y'].split()[0]) * self.boundsMultiplier yMax = float(self.gcodeProps['y'].split()[2]) * self.boundsMultiplier errMsg = self.bounds_compare(xMin, xMax, yMin, yMax, msg) return (True, errMsg) if errMsg else (False, '') def bounds_check_probe(self, static): if not (self.probeOffsetX or self.probeOffsetY) and not self.cutTypePin.get(): return False, '' msg = _translate('HandlerClass', 'Move to probe coordinate') xPierceOffset = yPierceOffset = xMinP = xMaxP = yMinP = yMaxP = 0 if static: xMinP = xMaxP = STATUS.get_position()[0][0] + self.probeOffsetX yMinP = yMaxP = STATUS.get_position()[0][1] + self.probeOffsetY elif any((self.xMinPierceExtentPin.get(), self.xMaxPierceExtentPin.get(), self.yMinPierceExtentPin.get(), self.yMaxPierceExtentPin.get())): if self.cutTypePin.get(): xPierceOffset = self.w.x_pierce_offset.value() yPierceOffset = self.w.y_pierce_offset.value() if self.probeOffsetX or (xPierceOffset and self.cutTypePin.get()): xMinP = self.xMinPierceExtentPin.get() * self.boundsMultiplier + self.probeOffsetX + STATUS.stat.g5x_offset[0] + xPierceOffset xMaxP = self.xMaxPierceExtentPin.get() * self.boundsMultiplier + self.probeOffsetX + STATUS.stat.g5x_offset[0] + xPierceOffset if self.probeOffsetY or (yPierceOffset and self.cutTypePin.get()): yMinP = self.yMinPierceExtentPin.get() * self.boundsMultiplier + self.probeOffsetY + STATUS.stat.g5x_offset[1] + yPierceOffset yMaxP = self.yMaxPierceExtentPin.get() * self.boundsMultiplier + self.probeOffsetY + STATUS.stat.g5x_offset[1] + yPierceOffset errMsg = self.bounds_compare(xMinP, xMaxP, yMinP, yMaxP, msg) return (True, errMsg) if errMsg else (False, '') def bounds_check_framing(self, xOffset=0, yOffset=0, laser=False): msg = _translate('HandlerClass', 'Framing move') msg1 = '' if laser: msg1 = _translate('HandlerClass', 'due to laser offset') xStart = STATUS.stat.g5x_offset[0] + xOffset yStart = STATUS.stat.g5x_offset[1] + yOffset xMin = float(self.gcodeProps['x_zero_rxy'].split()[0]) * self.boundsMultiplier + xOffset xMax = float(self.gcodeProps['x_zero_rxy'].split()[2]) * self.boundsMultiplier + xOffset yMin = float(self.gcodeProps['y_zero_rxy'].split()[0]) * self.boundsMultiplier + yOffset yMax = float(self.gcodeProps['y_zero_rxy'].split()[2]) * self.boundsMultiplier + yOffset coordinates = [[xStart, yStart], [xMin, yMin], [xMin, yMax], [xMax, yMax], [xMax, yMin]] framePoints, xMin, yMin, xMax, yMax = self.rotate_frame(coordinates) errMsg = self.bounds_compare(xMin, xMax, yMin, yMax, msg, msg1) return (errMsg, framePoints) def bounds_compare(self, xMin, xMax, yMin, yMax, msg, msg1=''): errMsg = '' epsilon = 1e-4 txtxMin = _translate('HandlerClass', 'exceeds the X minimum limit by') txtxMax = _translate('HandlerClass', 'exceeds the X maximum limit by') txtyMin = _translate('HandlerClass', 'exceeds the Y minimum limit by') txtyMax = _translate('HandlerClass', 'exceeds the Y maximum limit by') lessThan = _translate('HandlerClass', 'less than') if xMin < self.xMin: errMsg += f'{msg} {txtxMin} {lessThan} {epsilon} {self.units} {msg1}\n' if (self.xMin - xMin) < epsilon \ else f'{msg} {txtxMin} {self.xMin - xMin:0.4f} {self.units} {msg1}\n' if xMax > self.xMax: errMsg += f'{msg} {txtxMax} {lessThan} {epsilon} {self.units} {msg1}\n' if (xMax - self.xMax) < epsilon \ else f'{msg} {txtxMax} {xMax - self.xMax:0.4f} {self.units} {msg1}\n' if yMin < self.yMin: errMsg += f'{msg} {txtyMin} {lessThan} {epsilon} {self.units} {msg1}\n' if (self.yMin - yMin) < epsilon \ else f'{msg} {txtyMin} {self.yMin - yMin:0.4f} {self.units} {msg1}\n' if yMax > self.yMax: errMsg += f'{msg} {txtyMax} {lessThan} {epsilon} {self.units} {msg1}\n' if (yMax - self.yMax) < epsilon \ else f'{msg} {txtyMax} {yMax - self.yMax:0.4f} {self.units} {msg1}\n' return errMsg def rotate_frame(self, coordinates): angle = math.radians(STATUS.stat.rotation_xy) cos = math.cos(angle) sin = math.sin(angle) framePoints = [coordinates[0]] ox = framePoints[0][0] oy = framePoints[0][1] for x, y in coordinates[1:]: tox = x - ox toy = y - oy rx = (tox * cos) - (toy * sin) + ox ry = (tox * sin) + (toy * cos) + oy framePoints.append([rx, ry]) xMin = min(framePoints[1:])[0] xMax = max(framePoints[1:])[0] yMin = min(framePoints[1:])[1] yMax = max(framePoints[1:])[1] return framePoints, xMin, yMin, xMax, yMax def save_plasma_parameters(self): self.PREFS.putpref('Arc OK High', self.w.arc_ok_high.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Arc OK Low', self.w.arc_ok_low.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Arc Maximum Starts', self.w.arc_max_starts.value(), int, 'PLASMA_PARAMETERS') self.PREFS.putpref('Arc Fail Timeout', self.w.arc_fail_delay.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Arc Voltage Offset', self.w.arc_voltage_offset.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Arc Voltage Scale', self.w.arc_voltage_scale.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Velocity Anti Dive Threshold', self.w.cornerlock_threshold.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Float Switch Travel', self.w.float_switch_travel.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Height Per Volt', self.w.height_per_volt.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Void Sense Slope', self.w.voidlock_slope.value(), int, 'PLASMA_PARAMETERS') self.PREFS.putpref('Offset Feed Rate', self.w.offset_feed_rate.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Ohmic Maximum Attempts', self.w.ohmic_max_attempts.value(), int, 'PLASMA_PARAMETERS') self.PREFS.putpref('Ohmic Probe Offset', self.w.ohmic_probe_offset.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Pid P Gain', self.w.pid_p_gain.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Pid D Gain', self.w.pid_d_gain.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Pid I Gain', self.w.pid_i_gain.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Probe Feed Rate', self.w.probe_feed_rate.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Probe Start Height', self.w.probe_start_height.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Slat Height', self.w.slat_height.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Arc Restart Delay', self.w.arc_restart_delay.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Safe Height', self.w.safe_height.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Scribe Arming Delay', self.w.scribe_arm_delay.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Scribe On Delay', self.w.scribe_on_delay.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Setup Feed Rate', self.w.setup_feed_rate.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Skip IHS Distance', self.w.skip_ihs_distance.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Spotting Threshold', self.w.spotting_threshold.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Spotting Time', self.w.spotting_time.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('THC Delay', self.w.thc_delay.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('THC Sample Counts', self.w.thc_sample_counts.value(), int, 'PLASMA_PARAMETERS') self.PREFS.putpref('THC Sample Threshold', self.w.thc_sample_threshold.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('THC Threshold', self.w.thc_threshold.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('X Pierce Offset', self.w.x_pierce_offset.value(), float, 'PLASMA_PARAMETERS') self.PREFS.putpref('Y Pierce Offset', self.w.y_pierce_offset.value(), float, 'PLASMA_PARAMETERS') def load_plasma_parameters(self): self.w.arc_fail_delay.setValue(self.PREFS.getpref('Arc Fail Timeout', 3.0, float, 'PLASMA_PARAMETERS')) self.w.arc_ok_high.setValue(self.PREFS.getpref('Arc OK High', 250.0, float, 'PLASMA_PARAMETERS')) self.w.arc_ok_low.setValue(self.PREFS.getpref('Arc OK Low', 60.0, float, 'PLASMA_PARAMETERS')) self.w.arc_max_starts.setValue(self.PREFS.getpref('Arc Maximum Starts', 3, int, 'PLASMA_PARAMETERS')) self.w.arc_voltage_offset.setValue(self.PREFS.getpref('Arc Voltage Offset', 0.0, float, 'PLASMA_PARAMETERS')) self.w.arc_voltage_scale.setValue(self.PREFS.getpref('Arc Voltage Scale', 1.0, float, 'PLASMA_PARAMETERS')) self.w.cornerlock_threshold.setValue(self.PREFS.getpref('Velocity Anti Dive Threshold', 90.0, float, 'PLASMA_PARAMETERS')) self.w.float_switch_travel.setValue(self.PREFS.getpref('Float Switch Travel', round(1.5 * self.unitsPerMm, 2), float, 'PLASMA_PARAMETERS')) self.w.height_per_volt.setValue(self.PREFS.getpref('Height Per Volt', round(0.1 * self.unitsPerMm, 3), float, 'PLASMA_PARAMETERS')) self.w.offset_feed_rate.setValue(self.PREFS.getpref('Offset Feed Rate', self.offsetFeedRate * 0.8, float, 'PLASMA_PARAMETERS')) self.w.ohmic_max_attempts.setValue(self.PREFS.getpref('Ohmic Maximum Attempts', 0, int, 'PLASMA_PARAMETERS')) self.w.ohmic_probe_offset.setValue(self.PREFS.getpref('Ohmic Probe Offset', 0.0, float, 'PLASMA_PARAMETERS')) self.w.pid_p_gain.setValue(self.PREFS.getpref('Pid P Gain', 10.0, float, 'PLASMA_PARAMETERS')) self.w.pid_d_gain.setValue(self.PREFS.getpref('Pid D Gain', 0.0, float, 'PLASMA_PARAMETERS')) self.w.pid_i_gain.setValue(self.PREFS.getpref('Pid I Gain', 0.0, float, 'PLASMA_PARAMETERS')) self.w.probe_feed_rate.setValue(self.PREFS.getpref('Probe Feed Rate', round(300.0 * self.unitsPerMm, 0), float, 'PLASMA_PARAMETERS')) self.w.probe_start_height.setValue(self.PREFS.getpref('Probe Start Height', round(25.0 * self.unitsPerMm, 0), float, 'PLASMA_PARAMETERS')) self.w.slat_height.setValue(self.PREFS.getpref('Slat Height', 0.0, float, 'PLASMA_PARAMETERS')) self.w.arc_restart_delay.setValue(self.PREFS.getpref('Arc Restart Delay', 1.0, float, 'PLASMA_PARAMETERS')) self.w.safe_height.setValue(self.PREFS.getpref('Safe Height', round(25.0 * self.unitsPerMm, 0), float, 'PLASMA_PARAMETERS')) self.w.setup_feed_rate.setValue(self.PREFS.getpref('Setup Feed Rate', self.thcFeedRate * 0.8, float, 'PLASMA_PARAMETERS')) self.w.scribe_arm_delay.setValue(self.PREFS.getpref('Scribe Arming Delay', 0.0, float, 'PLASMA_PARAMETERS')) self.w.scribe_on_delay.setValue(self.PREFS.getpref('Scribe On Delay', 0.0, float, 'PLASMA_PARAMETERS')) self.w.skip_ihs_distance.setValue(self.PREFS.getpref('Skip IHS Distance', 0.0, float, 'PLASMA_PARAMETERS')) self.w.spotting_threshold.setValue(self.PREFS.getpref('Spotting Threshold', 1.0, float, 'PLASMA_PARAMETERS')) self.w.spotting_time.setValue(self.PREFS.getpref('Spotting Time', 0.0, float, 'PLASMA_PARAMETERS')) self.w.thc_delay.setValue(self.PREFS.getpref('THC Delay', 0.5, float, 'PLASMA_PARAMETERS')) self.w.thc_sample_counts.setValue(self.PREFS.getpref('THC Sample Counts', 50, int, 'PLASMA_PARAMETERS')) self.w.thc_sample_threshold.setValue(self.PREFS.getpref('THC Sample Threshold', 1.0, float, 'PLASMA_PARAMETERS')) self.w.thc_threshold.setValue(self.PREFS.getpref('THC Threshold', 1.0, float, 'PLASMA_PARAMETERS')) self.w.voidlock_slope.setValue(self.PREFS.getpref('Void Sense Slope', 500, int, 'PLASMA_PARAMETERS')) self.w.x_pierce_offset.setValue(self.PREFS.getpref('X Pierce Offset', round(1.6 * self.unitsPerMm, 2), float, 'PLASMA_PARAMETERS')) self.w.y_pierce_offset.setValue(self.PREFS.getpref('Y Pierce Offset', 0.0, float, 'PLASMA_PARAMETERS')) def set_signal_connections(self): self.w.power.pressed.connect(lambda: self.power_button("pressed", True)) self.w.power.released.connect(lambda: self.power_button("released", False)) self.w.power.clicked.connect(lambda: self.power_button("clicked", None)) self.w.run.clicked.connect(self.run_clicked) self.w.pause_resume.pressed.connect(self.pause_resume_pressed) self.w.abort.pressed.connect(self.abort_pressed) self.w.file_reload.clicked.connect(self.file_reload_clicked) self.w.jog_slow.pressed.connect(self.jog_slow_pressed) self.w.chk_soft_keyboard.stateChanged.connect(self.soft_keyboard) self.w.chk_override_limits.stateChanged.connect(self.chk_override_limits_changed) self.w.chk_overlay.stateChanged.connect(self.overlay_update) self.w.chk_tool_tips.stateChanged.connect(lambda: TOOLTIPS.tool_tips_changed(self, self.w)) self.w.torch_enable.stateChanged.connect(lambda w: self.torch_enable_changed(w)) self.w.ohmic_probe_enable.stateChanged.connect(self.refresh_button_states) self.w.thc_auto.stateChanged.connect(lambda w: self.thc_auto_changed(w)) self.w.cone_size.valueChanged.connect(self.cone_size_changed) self.w.grid_size.valueChanged.connect(self.grid_size_changed) self.w.gcode_display.linesChanged.connect(self.gcode_display_loaded) self.w.gcode_editor.percentDone.connect(lambda w: self.progress_changed(None, w)) self.w.file_clear.clicked.connect(self.file_clear_clicked) self.w.file_open.clicked.connect(self.file_open_clicked) self.w.file_edit.clicked.connect(self.file_edit_clicked) self.w.mdi_show.clicked.connect(self.mdi_show_clicked) self.w.file_cancel.clicked.connect(self.file_cancel_clicked) self.w.color_foregrnd.clicked.connect(lambda: self.openColorDialog(self.w.color_foregrnd)) self.w.color_foregalt.clicked.connect(lambda: self.openColorDialog(self.w.color_foregalt)) self.w.color_led.clicked.connect(lambda: self.openColorDialog(self.w.color_led)) self.w.color_backgrnd.clicked.connect(lambda: self.openColorDialog(self.w.color_backgrnd)) self.w.color_backgalt.clicked.connect(lambda: self.openColorDialog(self.w.color_backgalt)) self.w.color_frams.clicked.connect(lambda: self.openColorDialog(self.w.color_frams)) self.w.color_estop.clicked.connect(lambda: self.openColorDialog(self.w.color_estop)) self.w.color_disabled.clicked.connect(lambda: self.openColorDialog(self.w.color_disabled)) self.w.color_preview.clicked.connect(lambda: self.openColorDialog(self.w.color_preview)) self.w.save_plasma.clicked.connect(self.save_plasma_clicked) self.w.reload_plasma.clicked.connect(self.reload_plasma_clicked) self.w.backup.clicked.connect(self.backup_clicked) self.w.set_offsets.clicked.connect(self.set_offsets_clicked) self.w.save_material.clicked.connect(self.save_materials_clicked) self.w.reload_material.clicked.connect(self.reload_materials_clicked) self.w.new_material.clicked.connect(lambda: self.new_material_clicked(0, 0)) self.w.delete_material.clicked.connect(self.delete_material_clicked) self.w.setup_feed_rate.valueChanged.connect(self.setup_feed_rate_changed) self.w.touch_xy.clicked.connect(self.touch_xy_clicked) self.w.materials_box.currentIndexChanged.connect(lambda w: self.material_changed(w)) self.w.material_selector.currentIndexChanged.connect(lambda w: self.selector_changed(w)) self.w.conv_material.currentIndexChanged.connect(lambda w: self.conv_material_changed(w)) self.w.default_material.currentIndexChanged.connect(lambda w: self.default_material_changed(w)) self.w.sd_save.clicked.connect(self.save_shutdown_message_clicked) self.w.sd_reload.clicked.connect(self.reload_shutdown_message_clicked) self.w.ub_save.clicked.connect(self.save_user_button_clicked) self.w.ub_reload.clicked.connect(self.reload_user_button_clicked) self.materialChangePin.value_changed.connect(lambda w: self.material_change_pin_changed(w)) self.materialChangeNumberPin.value_changed.connect(lambda w: self.material_change_number_pin_changed(w)) self.materialChangeTimeoutPin.value_changed.connect(lambda w: self.material_change_timeout_pin_changed(w)) self.materialReloadPin.value_changed.connect(lambda w: self.material_reload_pin_changed(w)) self.materialTempPin.value_changed.connect(lambda w: self.material_temp_pin_changed(w)) self.w.height_lower.pressed.connect(lambda: self.height_ovr_pressed(1, -1)) self.w.height_raise.pressed.connect(lambda: self.height_ovr_pressed(1, 1)) self.w.height_reset.pressed.connect(lambda: self.height_ovr_pressed(1, 0)) self.w.button_1.pressed.connect(lambda: self.user_button_pressed(1)) self.w.button_1.released.connect(lambda: self.user_button_released(1)) self.w.button_2.pressed.connect(lambda: self.user_button_pressed(2)) self.w.button_2.released.connect(lambda: self.user_button_released(2)) self.w.button_3.pressed.connect(lambda: self.user_button_pressed(3)) self.w.button_3.released.connect(lambda: self.user_button_released(3)) self.w.button_4.pressed.connect(lambda: self.user_button_pressed(4)) self.w.button_4.released.connect(lambda: self.user_button_released(4)) self.w.button_5.pressed.connect(lambda: self.user_button_pressed(5)) self.w.button_5.released.connect(lambda: self.user_button_released(5)) self.w.button_6.pressed.connect(lambda: self.user_button_pressed(6)) self.w.button_6.released.connect(lambda: self.user_button_released(6)) self.w.button_7.pressed.connect(lambda: self.user_button_pressed(7)) self.w.button_7.released.connect(lambda: self.user_button_released(7)) self.w.button_8.pressed.connect(lambda: self.user_button_pressed(8)) self.w.button_8.released.connect(lambda: self.user_button_released(8)) self.w.button_9.pressed.connect(lambda: self.user_button_pressed(9)) self.w.button_9.released.connect(lambda: self.user_button_released(9)) self.w.button_10.pressed.connect(lambda: self.user_button_pressed(10)) self.w.button_10.released.connect(lambda: self.user_button_released(10)) self.w.button_11.pressed.connect(lambda: self.user_button_pressed(11)) self.w.button_11.released.connect(lambda: self.user_button_released(11)) self.w.button_12.pressed.connect(lambda: self.user_button_pressed(12)) self.w.button_12.released.connect(lambda: self.user_button_released(12)) self.w.button_13.pressed.connect(lambda: self.user_button_pressed(13)) self.w.button_13.released.connect(lambda: self.user_button_released(13)) self.w.button_14.pressed.connect(lambda: self.user_button_pressed(14)) self.w.button_14.released.connect(lambda: self.user_button_released(14)) self.w.button_15.pressed.connect(lambda: self.user_button_pressed(15)) self.w.button_15.released.connect(lambda: self.user_button_released(15)) self.w.button_16.pressed.connect(lambda: self.user_button_pressed(16)) self.w.button_16.released.connect(lambda: self.user_button_released(16)) self.w.button_17.pressed.connect(lambda: self.user_button_pressed(17)) self.w.button_17.released.connect(lambda: self.user_button_released(17)) self.w.button_18.pressed.connect(lambda: self.user_button_pressed(18)) self.w.button_18.released.connect(lambda: self.user_button_released(18)) self.w.button_19.pressed.connect(lambda: self.user_button_pressed(19)) self.w.button_19.released.connect(lambda: self.user_button_released(19)) self.w.button_20.pressed.connect(lambda: self.user_button_pressed(20)) self.w.button_20.released.connect(lambda: self.user_button_released(20)) self.w.cut_rec_speed.valueChanged.connect(lambda w: self.cutrec_speed_changed(w)) self.w.kerf_width.valueChanged.connect(lambda w: self.cutrec_move_changed(w)) self.w.jog_x_plus.pressed.connect(lambda: self.gui_button_jog(1, 'x', 1)) self.w.jog_x_plus.released.connect(lambda: self.gui_button_jog(0, 'x', 1)) self.w.jog_x_minus.pressed.connect(lambda: self.gui_button_jog(1, 'x', -1)) self.w.jog_x_minus.released.connect(lambda: self.gui_button_jog(0, 'x', -1)) self.w.jog_y_plus.pressed.connect(lambda: self.gui_button_jog(1, 'y', 1)) self.w.jog_y_plus.released.connect(lambda: self.gui_button_jog(0, 'y', 1)) self.w.jog_y_minus.pressed.connect(lambda: self.gui_button_jog(1, 'y', -1)) self.w.jog_y_minus.released.connect(lambda: self.gui_button_jog(0, 'y', -1)) self.w.jog_z_plus.pressed.connect(lambda: self.gui_button_jog(1, 'z', 1)) self.w.jog_z_plus.released.connect(lambda: self.gui_button_jog(0, 'z', 1)) self.w.jog_z_minus.pressed.connect(lambda: self.gui_button_jog(1, 'z', -1)) self.w.jog_z_minus.released.connect(lambda: self.gui_button_jog(0, 'z', -1)) self.w.jog_a_plus.pressed.connect(lambda: self.gui_button_jog(1, 'a', 1)) self.w.jog_a_plus.released.connect(lambda: self.gui_button_jog(0, 'a', 1)) self.w.jog_a_minus.pressed.connect(lambda: self.gui_button_jog(1, 'a', -1)) self.w.jog_a_minus.released.connect(lambda: self.gui_button_jog(0, 'a', -1)) self.w.jog_b_plus.pressed.connect(lambda: self.gui_button_jog(1, 'b', 1)) self.w.jog_b_plus.released.connect(lambda: self.gui_button_jog(0, 'b', 1)) self.w.jog_b_minus.pressed.connect(lambda: self.gui_button_jog(1, 'b', -1)) self.w.jog_b_minus.released.connect(lambda: self.gui_button_jog(0, 'b', -1)) self.w.jog_c_plus.pressed.connect(lambda: self.gui_button_jog(1, 'c', 1)) self.w.jog_c_plus.released.connect(lambda: self.gui_button_jog(0, 'c', 1)) self.w.jog_c_minus.pressed.connect(lambda: self.gui_button_jog(1, 'c', -1)) self.w.jog_c_minus.released.connect(lambda: self.gui_button_jog(0, 'c', -1)) self.w.cut_rec_fwd.pressed.connect(lambda: self.cutrec_motion(1)) self.w.cut_rec_fwd.released.connect(lambda: self.cutrec_motion(0)) self.w.cut_rec_rev.pressed.connect(lambda: self.cutrec_motion(-1)) self.w.cut_rec_rev.released.connect(lambda: self.cutrec_motion(0)) self.w.cut_rec_cancel.pressed.connect(lambda: self.cutrec_cancel_pressed(1)) self.w.cut_rec_n.pressed.connect(lambda: self.cutrec_move(1, 0, 1)) self.w.cut_rec_ne.pressed.connect(lambda: self.cutrec_move(1, 1, 1)) self.w.cut_rec_e.pressed.connect(lambda: self.cutrec_move(1, 1, 0)) self.w.cut_rec_se.pressed.connect(lambda: self.cutrec_move(1, 1, -1)) self.w.cut_rec_s.pressed.connect(lambda: self.cutrec_move(1, 0, -1)) self.w.cut_rec_sw.pressed.connect(lambda: self.cutrec_move(1, -1, -1)) self.w.cut_rec_w.pressed.connect(lambda: self.cutrec_move(1, -1, 0)) self.w.cut_rec_nw.pressed.connect(lambda: self.cutrec_move(1, -1, 1)) self.xOffsetPin.value_changed.connect(lambda v: self.cutrec_offset_changed(v, self.yOffsetPin.get())) self.yOffsetPin.value_changed.connect(lambda v: self.cutrec_offset_changed(self.xOffsetPin.get(), v)) self.offsetsActivePin.value_changed.connect(lambda v: self.offsets_active_changed(v)) self.consChangePin.value_changed.connect(lambda v: self.consumable_change_changed(v)) self.w.cam_mark.clicked.connect(self.cam_mark_clicked) self.w.cam_goto.clicked.connect(self.cam_goto_clicked) self.w.cam_zoom_plus.pressed.connect(self.cam_zoom_plus_pressed) self.w.cam_zoom_minus.pressed.connect(self.cam_zoom_minus_pressed) self.w.cam_dia_plus.pressed.connect(self.cam_dia_plus_pressed) self.w.cam_dia_minus.pressed.connect(self.cam_dia_minus_pressed) self.w.view_p.pressed.connect(self.view_p_pressed) self.w.view_z.pressed.connect(lambda: self.view_z_pressed(self.w.gcodegraphics)) self.w.view_t.pressed.connect(lambda: self.view_t_pressed(self.w.gcodegraphics)) self.w.view_clear.pressed.connect(self.view_clear_pressed) self.w.pan_left.pressed.connect(lambda: self.pan_left_pressed(self.w.gcodegraphics)) self.w.pan_right.pressed.connect(lambda: self.pan_right_pressed(self.w.gcodegraphics)) self.w.pan_up.pressed.connect(lambda: self.pan_up_pressed(self.w.gcodegraphics)) self.w.pan_down.pressed.connect(lambda: self.pan_down_pressed(self.w.gcodegraphics)) self.w.zoom_in.pressed.connect(lambda: self.zoom_in_pressed(self.w.gcodegraphics)) self.w.zoom_out.pressed.connect(lambda: self.zoom_out_pressed(self.w.gcodegraphics)) self.w.conv_view_z.pressed.connect(lambda: self.view_z_pressed(self.w.conv_preview)) self.w.conv_view_t.pressed.connect(lambda: self.view_t_pressed(self.w.conv_preview)) self.w.conv_pan_left.pressed.connect(lambda: self.pan_left_pressed(self.w.conv_preview)) self.w.conv_pan_right.pressed.connect(lambda: self.pan_right_pressed(self.w.conv_preview)) self.w.conv_pan_up.pressed.connect(lambda: self.pan_up_pressed(self.w.conv_preview)) self.w.conv_pan_down.pressed.connect(lambda: self.pan_down_pressed(self.w.conv_preview)) self.w.conv_zoom_in.pressed.connect(lambda: self.zoom_in_pressed(self.w.conv_preview)) self.w.conv_zoom_out.pressed.connect(lambda: self.zoom_out_pressed(self.w.conv_preview)) self.w.camera.pressed.connect(self.camera_pressed) self.w.laser.pressed.connect(self.laser_pressed) self.w.laser.clicked.connect(self.laser_clicked) self.w.main_tab_widget.currentChanged.connect(lambda w: self.main_tab_changed(w)) self.zHeightPin.value_changed.connect(lambda v: self.z_height_changed(v)) self.plasmacStatePin.value_changed.connect(lambda v: self.plasmac_state_changed(v)) self.plasmacStopPin.value_changed.connect(lambda v: self.plasmac_stop_changed(v)) self.w.feed_label.pressed.connect(self.feed_label_pressed) self.w.rapid_label.pressed.connect(self.rapid_label_pressed) self.w.jogs_label.pressed.connect(self.jogs_label_pressed) self.paramTabDisable.value_changed.connect(lambda v: self.w.main_tab_widget.setTabVisible(self.PARAMETERS, not v)) self.settingsTabDisable.value_changed.connect(lambda v: self.w.main_tab_widget.setTabVisible(self.SETTINGS, not v)) self.convTabDisable.value_changed.connect(lambda v: self.w.main_tab_widget.setTabVisible(self.CONVERSATIONAL, not v)) self.w.cut_time_reset.pressed.connect(lambda: self.statistic_reset('cut_time', 'Cut time')) self.w.probe_time_reset.pressed.connect(lambda: self.statistic_reset('probe_time', 'Probe time')) self.w.paused_time_reset.pressed.connect(lambda: self.statistic_reset('paused_time', 'Paused time')) self.w.run_time_reset.pressed.connect(lambda: self.statistic_reset('run_time', 'Program run time')) self.w.torch_time_reset.pressed.connect(lambda: self.statistic_reset('torch_time', 'Torch on time')) self.w.rapid_time_reset.pressed.connect(lambda: self.statistic_reset('rapid_time', 'Rapid time')) self.w.cut_length_reset.pressed.connect(lambda: self.statistic_reset('cut_length', 'Cut length')) self.w.pierce_reset.pressed.connect(lambda: self.statistic_reset('pierce_count', 'Pierce count')) self.w.all_reset.pressed.connect(self.statistics_reset) self.extPowerPin.value_changed.connect(lambda v: self.power_button("external", v)) self.extRunPin.value_changed.connect(lambda v: self.ext_run(v)) self.extPausePin.value_changed.connect(lambda v: self.ext_pause(v)) self.extPauseOnlyPin.value_changed.connect(lambda v: self.ext_pause_only(v)) self.extResumePin.value_changed.connect(lambda v: self.ext_resume(v)) self.extAbortPin.value_changed.connect(lambda v: self.ext_abort(v)) self.extTouchOffPin.value_changed.connect(lambda v: self.ext_touch_off(v)) self.extLaserTouchOffPin.value_changed.connect(lambda v: self.ext_laser_touch_off(v)) self.extLaserTogglePin.value_changed.connect(lambda v: self.ext_laser_toggle(v)) self.extRunPausePin.value_changed.connect(lambda v: self.ext_run_pause(v)) self.extHeightOvrPlusPin.value_changed.connect(lambda v: self.height_ovr_pressed(v, 1)) self.extHeightOvrMinusPin.value_changed.connect(lambda v: self.height_ovr_pressed(v, -1)) self.extHeightOvrResetPin.value_changed.connect(lambda v: self.height_ovr_pressed(v, 0)) self.extHeightOvrCountsPin.value_changed.connect(lambda v: self.height_ovr_encoder(v)) self.extHeightOvrScalePin.value_changed.connect(lambda v: self.height_ovr_scale_change(v)) self.extChangeConsPin.value_changed.connect(lambda v: self.ext_change_consumables(v)) self.extCutRecRevPin.value_changed.connect(lambda v: self.cutrec_motion(-v)) self.extCutRecFwdPin.value_changed.connect(lambda v: self.cutrec_motion(v)) self.extCutRecNPin.value_changed.connect(lambda v: self.cutrec_move(v, 0, 1)) self.extCutRecNEPin.value_changed.connect(lambda v: self.cutrec_move(v, 1, 1)) self.extCutRecEPin.value_changed.connect(lambda v: self.cutrec_move(v, 1, 0)) self.extCutRecSEPin.value_changed.connect(lambda v: self.cutrec_move(v, 1, -1)) self.extCutRecSPin.value_changed.connect(lambda v: self.cutrec_move(v, 0, -1)) self.extCutRecSWPin.value_changed.connect(lambda v: self.cutrec_move(v, -1, -1)) self.extCutRecWPin.value_changed.connect(lambda v: self.cutrec_move(v, -1, 0)) self.extCutRecNWPin.value_changed.connect(lambda v: self.cutrec_move(v, -1, 1)) self.extCutRecCancelPin.value_changed.connect(lambda v: self.cutrec_cancel_pressed(v)) self.extTorchEnablePin.value_changed.connect(lambda v: self.ext_torch_enable_changed(v)) self.extThcEnablePin.value_changed.connect(lambda v: self.ext_thc_enable_changed(v)) self.extCornerLockEnablePin.value_changed.connect(lambda v: self.ext_corner_lock_enable_changed(v)) self.extVoidLockEnablePin.value_changed.connect(lambda v: self.ext_void_lock_enable_changed(v)) self.extIgnoreArcOkPin.value_changed.connect(lambda v: self.ext_ignore_arc_ok_changed(v)) self.extMeshModePin.value_changed.connect(lambda v: self.ext_mesh_mode_changed(v)) self.extOhmicProbeEnablePin.value_changed.connect(lambda v: self.ext_ohmic_probe_enable_changed(v)) self.extAutoVoltsEnablePin.value_changed.connect(lambda v: self.ext_auto_volts_enable_changed(v)) self.extJogSlowPin.value_changed.connect(self.ext_jog_slow) self.extProbePin.value_changed.connect(lambda v: self.ext_probe_test(v)) self.extPulsePin.value_changed.connect(lambda v: self.ext_torch_pulse(v)) self.extOhmicPin.value_changed.connect(lambda v: self.ext_ohmic_test(v)) self.extFramingPin.value_changed.connect(lambda v: self.ext_frame_job(v)) self.probeTestErrorPin.value_changed.connect(lambda v: self.probe_test_error(v)) self.w.preview_stack.currentChanged.connect(self.preview_stack_changed) self.w.gcode_stack.currentChanged.connect(self.gcode_stack_changed) click_signal(self.w.material_label).connect(self.show_material_selector) click_signal(self.w.velocity_label).connect(self.show_material_selector) click_signal(self.w.velocity_show).connect(self.show_material_selector) self.w.conv_line.pressed.connect(lambda: self.conv_call('line')) self.w.conv_circle.pressed.connect(lambda: self.conv_call('circle')) self.w.conv_ellipse.pressed.connect(lambda: self.conv_call('ellipse')) self.w.conv_triangle.pressed.connect(lambda: self.conv_call('triangle')) self.w.conv_rectangle.pressed.connect(lambda: self.conv_call('rectangle')) self.w.conv_polygon.pressed.connect(lambda: self.conv_call('polygon')) self.w.conv_bolt.pressed.connect(lambda: self.conv_call('bolt')) self.w.conv_slot.pressed.connect(lambda: self.conv_call('slot')) self.w.conv_star.pressed.connect(lambda: self.conv_call('star')) self.w.conv_gusset.pressed.connect(lambda: self.conv_call('gusset')) self.w.conv_sector.pressed.connect(lambda: self.conv_call('sector')) self.w.conv_block.pressed.connect(lambda: self.conv_call('block')) self.w.conv_new.pressed.connect(lambda: self.conv_call('new')) self.w.conv_save.pressed.connect(lambda: self.conv_call('save')) self.w.conv_settings.pressed.connect(lambda: self.conv_call('settings')) self.w.conv_send.pressed.connect(lambda: self.conv_call('send')) self.w.chk_override_jog.stateChanged.connect(self.override_jog_changed) self.jogInhibited.value_changed.connect(lambda v: self.jog_inhibited_changed(v)) self.sensorActive.value_changed.connect(lambda v: self.sensor_active_changed(v)) self.zOffsetPin.value_changed.connect(lambda v: self.z_offset_changed(v)) self.laserRecStatePin.value_changed.connect(lambda v: self.laser_recovery_state_changed(v)) self.ohmicLedInPin.value_changed.connect(lambda v: self.ohmic_sensed(v)) self.w.webview_back.pressed.connect(self.web_back_pressed) self.w.webview_forward.pressed.connect(self.web_forward_pressed) self.w.webview_reload.pressed.connect(self.web_reload_pressed) self.velReduct.value_changed.connect(lambda: self.vel_reduct()) def conv_call(self, operation): if self.developmentPin.get(): reload(self.CONV) if operation == 'block': self.CONV.conv_block_pressed(self, self.w) elif operation == 'new': self.CONV.conv_new_pressed(self, self.w, 'button') elif operation == 'save': self.CONV.conv_save_pressed(self, self.w) elif operation == 'settings': self.CONV.conv_settings_pressed(self, self.w) elif operation == 'send': self.CONV.conv_send_pressed(self, self.w) else: self.CONV.conv_shape_request(self, self.w, f'conv_{operation}') def set_axes_and_joints(self): self.coordinates = 'xyz' # backup in case we cannot find valid coordinates kinematics = self.iniFile.find('KINS', 'KINEMATICS').lower().split() or None if not kinematics: head = _translate('HandlerClass', 'INI File Error') msg0 = _translate('HandlerClass', 'Error in [KINS]KINEMATICS in the INI file') msg1 = _translate('HandlerClass', 'reverting to default coordinates of xyz') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}\n') else: coords = [s for s in kinematics if 'coordinates' in s] if coords: self.coordinates = coords[0].split('=')[1].strip() else: coords = self.iniFile.find('KINS', 'COORDINATES').lower() or None if coords: self.coordinates = coords # hide axes a, b, and c if not being used for axis in 'abc': if axis not in self.axes['valid']: for i in self.axes[axis]: self.w[i].hide() # setup home buttons for axis in self.axes['valid']: self.w[f'home_{axis}'].set_joint(self.coordinates.index(axis)) self.w[f'home_{axis}'].set_joint_number(self.coordinates.index(axis)) for joint in range(len(self.coordinates)): # check if home all button required if not self.iniFile.find(f'JOINT_{joint}', 'HOME_SEQUENCE'): self.w.home_all.hide() # check if not joggable before homing elif self.iniFile.find(f'JOINT_{joint}', 'HOME_SEQUENCE').startswith('-'): if f'jog_{self.coordinates[joint]}_plus' not in self.jogSyncList: self.jogSyncList.append(f'jog_{self.coordinates[joint]}_plus') self.jogSyncList.append(f'jog_{self.coordinates[joint]}_minus') self.jogButtonList.remove(f'jog_{self.coordinates[joint]}_plus') self.jogButtonList.remove(f'jog_{self.coordinates[joint]}_minus') # remove 'all_homed' requirement for buttons in self.jogButtonList for jogButton in self.jogButtonList: andRules = self.interlockRules[jogButton].get('and', {}) if 'all_homed' in andRules: del andRules['all_homed'] def set_mode(self): block1 = ['arc_ok_high', 'arc_ok_high_lbl', 'arc_ok_low', 'arc_ok_low_lbl'] block2 = ['arc_voltage_scale', 'arc_voltage_scale_lbl', 'arc_voltage_offset', 'arc_voltage_offset_lbl', 'voidlock_frm', 'height_per_volt', 'height_per_volt_lbl', 'thc_delay', 'thc_delay_lbl', 'thc_sample_counts', 'thc_sample_counts_lbl', 'thc_sample_threshold', 'thc_sample_threshold_lbl', 'thc_threshold', 'thc_threshold_lbl', 'pid_i_gain', 'pid_i_gain_lbl', 'pid_d_gain', 'pid_d_gain_lbl', 'use_auto_volts', 'use_auto_volts_lbl', 'led_thc_active', 'led_thc_active_lbl', 'arc_voltage', 'arc_override_frm', 'voidlock_slope', 'voidlock_slope_lbl', 'thc_auto', 'thc_auto_lbl'] if self.mode == 1: hal.set_p('plasmac.mode', '1') for widget in block1: self.w[widget].hide() elif self.mode == 2: hal.set_p('plasmac.mode', '2') for widget in block1 + block2: self.w[widget].hide() self.w.pid_p_gain_lbl.setText(_translate('HandlerClass', 'Speed %')) def set_spinbox_parameters(self): if self.units == 'in': self.w.setup_feed_rate.setRange(4.0, int(self.thcFeedRate)) self.w.setup_feed_rate.setDecimals(1) self.w.setup_feed_rate.setSingleStep(0.1) self.w.safe_height.setRange(0, int(self.maxHeight)) self.w.safe_height.setDecimals(3) self.w.safe_height.setSingleStep(0.01) self.w.probe_feed_rate.setRange(4.0, int(self.thcFeedRate)) self.w.probe_feed_rate.setDecimals(1) self.w.probe_feed_rate.setSingleStep(0.1) self.w.probe_start_height.setRange(0, int(self.maxHeight)) self.w.probe_start_height.setDecimals(3) self.w.probe_start_height.setSingleStep(0.01) self.w.offset_feed_rate.setRange(4.0, int(self.offsetFeedRate)) self.w.offset_feed_rate.setDecimals(1) self.w.offset_feed_rate.setSingleStep(0.1) self.w.float_switch_travel.setRange(-1.0, 1.0) self.w.float_switch_travel.setDecimals(3) self.w.float_switch_travel.setSingleStep(0.001) self.w.height_per_volt.setRange(0.001, 0.01) self.w.height_per_volt.setDecimals(3) self.w.height_per_volt.setSingleStep(0.001) self.w.ohmic_probe_offset.setRange(-1.0, 1.0) self.w.ohmic_probe_offset.setDecimals(3) self.w.ohmic_probe_offset.setSingleStep(0.001) self.w.skip_ihs_distance.setRange(0.0, 40.0) self.w.skip_ihs_distance.setDecimals(1) self.w.skip_ihs_distance.setSingleStep(0.1) self.w.kerf_width.setRange(0.0, 1.0) self.w.kerf_width.setDecimals(3) self.w.kerf_width.setSingleStep(0.001) self.w.cut_feed_rate.setRange(0.0, 999.0) self.w.cut_feed_rate.setDecimals(1) self.w.cut_feed_rate.setSingleStep(0.1) self.w.cut_height.setRange(0.0, 1.0) self.w.cut_height.setDecimals(3) self.w.cut_height.setSingleStep(0.001) self.w.pierce_height.setRange(0.0, 1.0) self.w.pierce_height.setDecimals(3) self.w.pierce_height.setSingleStep(0.001) self.w.x_pierce_offset.setDecimals(3) self.w.x_pierce_offset.setRange(-0.2, 0.2) self.w.x_pierce_offset.setSingleStep(0.01) self.w.y_pierce_offset.setDecimals(3) self.w.y_pierce_offset.setRange(-0.2, 0.2) self.w.y_pierce_offset.setSingleStep(0.01) self.w.material_thickness.setRange(0.0, int(self.maxHeight)) self.w.material_thickness.setDecimals(3) self.w.material_thickness.setSingleStep(0.001) self.w.slat_height.setRange(0.0, int(self.maxHeight)) self.w.slat_height.setDecimals(3) self.w.slat_height.setSingleStep(0.001) else: self.w.setup_feed_rate.setMaximum(int(self.thcFeedRate)) self.w.safe_height.setMaximum(int(self.maxHeight)) self.w.probe_feed_rate.setMaximum(int(self.thcFeedRate)) self.w.probe_start_height.setMaximum(int(self.maxHeight)) self.w.offset_feed_rate.setMaximum(int(self.offsetFeedRate)) self.w.material_thickness.setMaximum(int(self.maxHeight)) self.w.slat_height.setMaximum(int(self.maxHeight)) def set_probe_offset_pins(self): hal.set_p('plasmac.offset-probe-x', f'{self.probeOffsetX}') hal.set_p('plasmac.offset-probe-y', f'{self.probeOffsetY}') hal.set_p('plasmac.offset-probe-delay', f'{self.probeDelay}') def kb_jog(self, state, joint, direction, shift=False, linear=True): if linear: distance = STATUS.get_jog_increment() rate = STATUS.get_jograte()/60 else: distance = STATUS.get_jog_increment_angular() rate = STATUS.get_jograte_angular()/60 if state: if not STATUS.is_man_mode() or not STATUS.machine_is_on() or \ (self.offsetsActivePin.get() and not self.manualCut): return if (shift or self.jogFast) and not self.manualCut: rate = INFO.MAX_LINEAR_JOG_VEL elif self.jogSlow and not self.w.jog_slow.isChecked(): rate = STATUS.get_jograte()/60/self.slowJogFactor ACTION.JOG(joint, direction, rate, distance) self.w.grabKeyboard() else: self.w.releaseKeyboard() if not STATUS.get_jog_increment(): ACTION.JOG(joint, 0, 0, 0) def keyboard_shortcuts(self): if self.w.chk_keyboard_shortcuts.isChecked(): return True else: return False def soft_keyboard(self): if self.w.chk_soft_keyboard.isChecked(): inputType = 'CALCULATOR' self.w.originoffsetview.setProperty('dialog_code_string', 'CALCULATOR') self.w.originoffsetview.setProperty('text_dialog_code_string', 'KEYBOARD') self.w.gcode_display.SendScintilla(QsciScintilla.SCI_SETEXTRAASCENT, 4) self.w.gcode_display.SendScintilla(QsciScintilla.SCI_SETEXTRADESCENT, 4) self.w.gcode_editor.editor.SendScintilla(QsciScintilla.SCI_SETEXTRAASCENT, 4) self.w.gcode_editor.editor.SendScintilla(QsciScintilla.SCI_SETEXTRADESCENT, 4) self.vkb_check() if self.w.main_tab_widget.currentIndex() == self.PARAMETERS: self.vkb_show(True) elif self.w.main_tab_widget.currentIndex() == self.SETTINGS: self.vkb_show() self.w.chk_keyboard_shortcuts.setChecked(False) self.w.chk_keyboard_shortcuts.setEnabled(False) else: inputType = 'ENTRY' self.w.originoffsetview.setProperty('dialog_code_string', '') self.w.originoffsetview.setProperty('text_dialog_code_string', '') self.w.gcode_display.SendScintilla(QsciScintilla.SCI_SETEXTRAASCENT, 1) self.w.gcode_display.SendScintilla(QsciScintilla.SCI_SETEXTRADESCENT, 1) self.w.gcode_editor.editor.SendScintilla(QsciScintilla.SCI_SETEXTRAASCENT, 1) self.w.gcode_editor.editor.SendScintilla(QsciScintilla.SCI_SETEXTRADESCENT, 1) self.vkb_hide() self.w.chk_keyboard_shortcuts.setEnabled(True) for axis in 'xyzabc': button = f'touch_{axis}' self.w[button].dialog_code = inputType def overlay_update(self, state): self.w.gcodegraphics.update() self.w.conv_preview.update() def dialog_show_ok(self, icon, title, error, bText=_translate('HandlerClass', 'OK')): msg = QMessageBox(self.w) buttonY = msg.addButton(QMessageBox.Yes) buttonY.setText(bText) buttonY.setIcon(QIcon()) msg.setIcon(icon) msg.setWindowTitle(title) msg.setText(error) msg.setWindowFlag(Qt.Popup) msg.exec_() self.dialogError = False return msg def dialog_show_yesno(self, icon, title, error, bY=_translate('HandlerClass', 'YES'), bN=_translate('HandlerClass', 'NO')): msg = QMessageBox(self.w) buttonY = msg.addButton(QMessageBox.Yes) buttonY.setText(bY) buttonY.setIcon(QIcon()) buttonN = msg.addButton(QMessageBox.No) buttonN.setText(bN) buttonN.setIcon(QIcon()) msg.setIcon(icon) msg.setWindowTitle(title) msg.setText(error) msg.setWindowFlag(Qt.WindowStaysOnTopHint) if 'shutdown' in error else msg.setWindowFlag(Qt.Popup) choice = msg.exec_() if choice == QMessageBox.Yes: return True else: return False # virtkb: 0=none, 1=alpha~close, 2=num~close, 3=alpha~num, 4=num~num, 5=alpha~alpha, 6=num~alpha def dialog_input(self, virtkb, title, text, btn1, btn2, delay=None): dialog = QInputDialog(self.w) dialog.setWindowTitle(title) dialog.setLabelText(f'{text}') if btn1: dialog.setOkButtonText(btn1) if btn2: dialog.setCancelButtonText(btn2) if delay is not None: dialog.setTextValue(f'{delay:0.2f}') for button in dialog.findChildren(QPushButton): button.setIcon(QIcon()) if virtkb in (1, 3, 5): self.vkb_show(False) elif virtkb in (2, 4, 6): self.vkb_show(True) valid = dialog.exec_() if virtkb < 3: self.vkb_hide() elif virtkb in (3, 4): self.vkb_show(True) elif virtkb in (5, 6): self.vkb_show(False) out = dialog.textValue() return valid, out def dialog_run_from_line(self): rFl = QDialog(self.w) rFl.setWindowTitle(_translate('HandlerClass', 'Run From Line')) lbl1 = QLabel(_translate('HandlerClass', 'USE LEADIN:')) lbl2 = QLabel(_translate('HandlerClass', 'LEADIN LENGTH:')) lbl3 = QLabel(_translate('HandlerClass', 'LEADIN ANGLE:')) lbl4 = QLabel('') leadinDo = QCheckBox() leadinLength = QDoubleSpinBox() leadinAngle = QDoubleSpinBox() buttons = QDialogButtonBox.Ok | QDialogButtonBox.Cancel buttonBox = QDialogButtonBox(buttons) buttonBox.accepted.connect(rFl.accept) buttonBox.rejected.connect(rFl.reject) buttonBox.button(QDialogButtonBox.Ok).setText(_translate('HandlerClass', 'LOAD')) buttonBox.button(QDialogButtonBox.Ok).setIcon(QIcon()) buttonBox.button(QDialogButtonBox.Cancel).setText(_translate('HandlerClass', 'CANCEL')) buttonBox.button(QDialogButtonBox.Cancel).setIcon(QIcon()) layout = QGridLayout() layout.addWidget(lbl1, 0, 0) layout.addWidget(lbl2, 1, 0) layout.addWidget(lbl3, 2, 0) layout.addWidget(lbl4, 3, 0) layout.addWidget(leadinDo, 0, 1) layout.addWidget(leadinLength, 1, 1) layout.addWidget(leadinAngle, 2, 1) layout.addWidget(buttonBox, 4, 0, 1, 2) rFl.setLayout(layout) lbl1.setAlignment(Qt.AlignRight | Qt.AlignBottom) lbl2.setAlignment(Qt.AlignRight | Qt.AlignBottom) lbl3.setAlignment(Qt.AlignRight | Qt.AlignBottom) if self.units == 'in': leadinLength.setDecimals(2) leadinLength.setSingleStep(0.05) leadinLength.setSuffix(' inch') leadinLength.setMinimum(0.05) else: leadinLength.setDecimals(0) leadinLength.setSingleStep(1) leadinLength.setSuffix(' mm') leadinLength.setMinimum(1) leadinAngle.setDecimals(0) leadinAngle.setSingleStep(1) leadinAngle.setSuffix(' deg') leadinAngle.setRange(-359, 359) leadinAngle.setWrapping(True) self.vkb_show(True) result = rFl.exec_() self.vkb_hide() # load clicked if result: return {'cancel': False, 'do': leadinDo.isChecked(), 'length': leadinLength.value(), 'angle': leadinAngle.value()} # cancel clicked else: return {'cancel': True} def dialog_rfl_type(self): rflT = QDialog(self.w) rflT.setWindowTitle(_translate('HandlerClass', 'Run From Line')) run = QRadioButton(_translate('HandlerClass', 'HERE TO END')) cut = QRadioButton(_translate('HandlerClass', 'THIS CUTPATH')) lbl = QLabel('') buttons = QDialogButtonBox.Ok | QDialogButtonBox.Cancel buttonBox = QDialogButtonBox(buttons) buttonBox.accepted.connect(rflT.accept) buttonBox.rejected.connect(rflT.reject) buttonBox.button(QDialogButtonBox.Ok).setText(_translate('HandlerClass', 'OK')) buttonBox.button(QDialogButtonBox.Ok).setIcon(QIcon()) buttonBox.button(QDialogButtonBox.Cancel).setText(_translate('HandlerClass', 'CANCEL')) buttonBox.button(QDialogButtonBox.Cancel).setIcon(QIcon()) layout = QGridLayout() layout.addWidget(run, 0, 0) layout.addWidget(cut, 1, 0) layout.addWidget(lbl, 2, 0) layout.addWidget(buttonBox, 3, 0) rflT.setLayout(layout) run.setChecked(True) self.vkb_show(True) result = rflT.exec_() self.vkb_hide() # ok clicked if result: if cut.isChecked(): type_ = 'cut' else: type_ = 'end' return {'cancel': False, 'type': type_} # cancel clicked else: return {'cancel': True, 'type': 'end'} def show_cut_critical_dialog(self, rcButtonList): checkStyle = 'QCheckBox::indicator { margin-left: 8px; margin-right: 8px; }\n \ QCheckBox { font-size: 11pt; }' ccr = QDialog(self.w) ccr.setWindowTitle(_translate('HandlerClass', 'Untoggled Cut Critical Buttons')) icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxWarning) iconLabel = QLabel() iconLabel.setPixmap(icon.pixmap(32, 32)) msg0 = _translate('HandlerClass', 'The following buttons have not been toggled') msg1 = _translate('HandlerClass', 'Select items to be toggled when CONTINUE is clicked') lbl0 = QLabel(f'\n{msg0}:\n') lbl1 = QLabel('') lbl2 = QLabel(f'\n{msg1}\n') lbl2.setStyleSheet("padding-left: 1px;") buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttonBox.accepted.connect(ccr.accept) buttonBox.rejected.connect(ccr.reject) buttonBox.button(QDialogButtonBox.Ok).setText(_translate('HandlerClass', 'CONTINUE')) buttonBox.button(QDialogButtonBox.Ok).setIcon(QIcon()) buttonBox.button(QDialogButtonBox.Ok).setMinimumWidth(120) buttonBox.button(QDialogButtonBox.Cancel).setText(_translate('HandlerClass', 'CANCEL')) buttonBox.button(QDialogButtonBox.Cancel).setIcon(QIcon()) hLayout = QHBoxLayout() hLayout.addWidget(iconLabel) hLayout.addWidget(lbl0) hLayout.addStretch() vLayout = QVBoxLayout() vLayout.addLayout(hLayout) checkBoxes = [] for bText in rcButtonList: checkBox = QCheckBox(bText) checkBox.setStyleSheet(checkStyle) vLayout.addWidget(checkBox) checkBoxes.append(checkBox) if len(rcButtonList) > 1: toggleAll = QCheckBox(_translate('HandlerClass', 'TOGGLE ALL')) toggleAll.setStyleSheet(checkStyle) vLayout.addWidget(lbl1) vLayout.addWidget(toggleAll) toggleAll.stateChanged.connect(lambda state: [checkBox.setChecked(state == Qt.Checked) for checkBox in checkBoxes]) vLayout.addWidget(lbl2) vLayout.addWidget(buttonBox) ccr.setLayout(vLayout) result = ccr.exec_() checkList = [checkBox.text() for checkBox in checkBoxes if checkBox.isChecked()] return result, checkList def invert_pin_state(self, halpin): if 'qtplasmac.ext_out_' in halpin: pin = f'out{halpin.split("out_")[1]}Pin' self[pin].set(not hal.get_value(halpin)) else: hal.set_p(halpin, str(not hal.get_value(halpin))) self.set_button_color() def set_button_color(self): for halpin in self.halTogglePins: if hal.get_value(halpin): if self.button_normal_check(self.halTogglePins[halpin][0]): self.button_active(self.halTogglePins[halpin][0]) text = 3 else: if not self.button_normal_check(self.halTogglePins[halpin][0]): self.button_normal(self.halTogglePins[halpin][0]) text = 2 if self.halTogglePins[halpin][3]: toggleText = self.halTogglePins[halpin][text].replace('\\', '\n') self.w[self.halTogglePins[halpin][0]].setText(f'{toggleText}') for halpin in self.halPulsePins: if hal.get_value(halpin): if self.button_normal_check(self.halPulsePins[halpin][0]): self.button_active(self.halPulsePins[halpin][0]) else: if not self.button_normal_check(self.halPulsePins[halpin][0]): self.button_normal(self.halPulsePins[halpin][0]) if self.tlButton: for button in self.tlButton: if self.laserOnPin.get(): if self.button_normal_check(button): self.button_active(button) else: if not self.button_normal_check(button): self.button_normal(button) def cut_critical_toggle_check(self): # self.halTogglePins format is: button name, run critical flag, button text, alt button text checkDict = { self.halTogglePins[halpin][2].replace('\n', ' '): halpin for halpin in self.halTogglePins if self.halTogglePins[halpin][1] and not hal.get_value(halpin) } if checkDict and self.w.torch_enable.isChecked(): result, checked = self.show_cut_critical_dialog(list(checkDict.keys())) if result: for bText in checked: halpin = checkDict[bText] self.user_button_down(int(self.halTogglePins[halpin][0].replace('button_',''))) return result return True def preview_stack_changed(self): index = self.w.preview_stack.currentIndex() if index == self.PREVIEW: self.autorepeat_keys(False) elif index == self.OPEN: self.button_active(self.w.file_open.objectName()) self.autorepeat_keys(True) self.vkb_hide() self.w.filemanager.table.setFocus() elif index == self.EDIT: self.button_active(self.w.file_edit.objectName()) text0 = _translate('HandlerClass', 'EDIT') text1 = _translate('HandlerClass', 'CLOSE') self.w.file_edit.setText(f'{text0}\n{text1}') self.autorepeat_keys(True) elif index == self.CAMERA: self.button_active('camera') self.cameraOn = True elif index == self.OFFSETS: self.button_active(self.ovButton) for row in range(self.w.originoffsetview.tablemodel.rowCount(self.w.originoffsetview)): self.w.originoffsetview.resizeRowToContents(row) for column in range(self.w.originoffsetview.tablemodel.rowCount(self.w.originoffsetview)-1): self.w.originoffsetview.resizeColumnToContents(column) elif index == self.USER_MANUAL: self.button_active(self.umButton) self.autorepeat_keys(True) if index != self.OPEN or index == self.PREVIEW: self.button_normal(self.w.file_open.objectName()) if index != self.EDIT or index == self.PREVIEW: self.button_normal(self.w.file_edit.objectName()) self.w.file_edit.setText(_translate('HandlerClass', 'EDIT')) if index != self.CAMERA or index == self.PREVIEW: self.button_normal('camera') if index != self.OFFSETS or index == self.PREVIEW: if self.ovButton: self.button_normal(self.ovButton) if index != self.USER_MANUAL or index == self.PREVIEW: if self.umButton: self.button_normal(self.umButton) self.refresh_button_states() def gcode_stack_changed(self): if self.w.gcode_stack.currentIndex() == self.MDI: self.button_active(self.w.mdi_show.objectName()) text0 = _translate('HandlerClass', 'MDI') text1 = _translate('HandlerClass', 'CLOSE') self.w.mdi_show.setText(f'{text0}\n{text1}') self.w.mdihistory.reload() self.w.mdihistory.MDILine.setFocus() self.autorepeat_keys(True) else: self.button_normal(self.w.mdi_show.objectName()) self.w.mdi_show.setText(_translate('HandlerClass', 'MDI')) if self.w.preview_stack.currentIndex() != self.EDIT: self.autorepeat_keys(False) self.refresh_button_states() def set_mc_states(self, state): if self.manualCut: self.jogPreManCut[0] = self.w.jog_slow.isChecked() self.jogPreManCut[1] = self.w.jog_slider.value() self.jogPreManCut[2] = self.w.jogincrements.currentIndex() if self.w.jog_slow.isChecked(): self.jog_slow_pressed(True) self.w.jog_slider.setValue(self.w.cut_feed_rate.value()) self.w.jogincrements.setCurrentIndex(0) else: if self.jogPreManCut[0]: self.jog_slow_pressed(True) self.w.jog_slider.setValue(self.jogPreManCut[1]) self.w.jogincrements.setCurrentIndex(self.jogPreManCut[2]) self.refresh_button_states() def show_material_selector(self): if STATUS.is_interp_idle(): self.w.material_selector.showPopup() def autorepeat_keys(self, state): if not self.autorepeat_skip: if state: ACTION.ENABLE_AUTOREPEAT_KEYS(' ') else: ACTION.DISABLE_AUTOREPEAT_KEYS(' ') def clear_rfl(self): self.rflActive = False self.startLine = 0 self.preRflFile = '' self.w.gcodegraphics.clear_highlight() def ohmic_sensed(self, state): if state: hal.set_p('qtplasmac.led_ohmic_probe', '1') self.ohmicLedTimer.start(150) def vm_check(self): try: response = (Popen('cat /sys/class/dmi/id/product_name', stdout=PIPE, stderr=PIPE, shell=True).communicate()[0]).decode('utf-8') if 'virtual' in response.lower() or 'vmware' in response.lower(): self.virtualMachine = True STATUS.emit('update-machine-log', f'"{response.strip()}" Virtual Machine detected', 'TIME') except: pass def toggle_joint_mode(self): if not STATUS.is_all_homed(): return if hal.get_value('halui.mode.is-teleop'): teleop = False elif hal.get_value('halui.mode.is-joint'): teleop = True ACTION.cmd.teleop_enable(teleop) for axis in self.axes['valid']: self.w[f'touch_{axis}'].setEnabled(teleop) self.w[f'dro_{axis}'].setEnabled(teleop) self.w[f'dro_label_{axis}'].setProperty('homed', teleop) self.w[f'dro_label_{axis}'].setStyle(self.w[f'dro_label_{axis}'].style()) time.sleep(0.1) self.w.gcodegraphics.update() self.w.conv_preview.update() # called when qtplasmac.vel-reduct pin value changes, and by update_periodic every 100mS when not idle to ensure # the label does not get stuck due to timing discrepancies between the GUI and the component poll rates def vel_reduct(self): factor = self.velReduct.get() self.w.velocity_label.setText('VEL:' if factor == 1.0 else f'VEL@{factor * 100:.0f}%:') ######################################################################################################################### # TIMER FUNCTIONS # ######################################################################################################################### def shutdown_timeout(self): self.w.close() def startup_timeout(self): if STATUS.stat.estop: self.estop_state(True) self.w.gcode_display.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.view_t_pressed(self.w.gcodegraphics) self.set_signal_connections() if self.firstRun is True: self.firstRun = False def watchdog_has_bit(self): states = { self.PROBE_HEIGHT: _translate('HandlerClass', 'moving to probe height'), self.PROBE_DOWN: _translate('HandlerClass', 'probing down'), self.PROBE_UP: _translate('HandlerClass', 'probing up'), self.ZERO_HEIGHT: _translate('HandlerClass', 'moving to zero height'), self.PIERCE_HEIGHT: _translate('HandlerClass', 'moving to pierce height'), self.PUDDLE_JUMP: _translate('HandlerClass', 'moving to puddle jump height'), self.CUT_HEIGHT: _translate('HandlerClass', 'moving to cut height'), self.SAFE_HEIGHT: _translate('HandlerClass', 'moving to safe height'), self.MAX_HEIGHT: _translate('HandlerClass', 'moving to maximum height') } head = _translate('HandlerClass', 'Z Axis Motion Timeout') msg0 = _translate('HandlerClass', 'Z axis did not reach the destination while') msg1 = _translate('HandlerClass', 'program is paused') msg2 = _translate('HandlerClass', 'check settings on the PARAMETERS tab') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} {states[self.watchDogState]}\n{msg1}\n{msg2}\n') ACTION.PAUSE() def update_periodic(self): if not STATUS.is_interp_idle(): self.vel_reduct() self.set_button_color() if self.flasher: self.flasher -= 1 else: self.flasher = self.flashRate self.flashState = not self.flashState self.flasher_timeout() if not self.firstRun and self.pmPort and not hal.component_exists('pmx485'): if self.pmx485_check(self.pmPort, True): self.w.gas_pressure.show() self.w.gas_pressure_label.show() self.w.cut_mode.show() self.w.cut_mode_label.show() self.w.pmx485_frame.show() self.w.pmx_stats_frame.show() self.pmx485_startup(self.pmPort) if not self.firstRun and os.path.isfile(self.upFile): exec(open(self.upFile).read()) def flasher_timeout(self): if STATUS.is_auto_paused(): if self.flashState: self.w.pause_resume.setText(_translate('HandlerClass', 'CYCLE RESUME')) else: self.w.pause_resume.setText('') elif self.w.jog_stack.currentIndex() == self.JOG: self.w.pause_resume.setText(_translate('HandlerClass', 'CYCLE PAUSE')) text = _translate('HandlerClass', 'FEED') if self.w.feed_slider.value() != 100: if self.flashState: self.w.feed_label.setText(f'{text}\n{STATUS.stat.feedrate * 100:.0f}%') else: self.w.feed_label.setText(' \n ') else: self.w.feed_label.setText(f'{text}\n{STATUS.stat.feedrate * 100:.0f}%') text = _translate('HandlerClass', 'RAPID') if self.w.rapid_slider.value() != 100: if self.flashState: self.w.rapid_label.setText(f'{text}\n{STATUS.stat.rapidrate * 100:.0f}%') else: self.w.rapid_label.setText(' \n ') else: self.w.rapid_label.setText(f'{text}\n{STATUS.stat.rapidrate * 100:.0f}%') text = _translate('HandlerClass', 'JOG') if self.manualCut: if self.flashState: self.w.run.setText(_translate('HandlerClass', 'MANUAL CUT')) self.w.jogs_label.setText(f'{text}\n{STATUS.get_jograte():.0f}') else: self.w.run.setText('') self.w.jogs_label.setText(' \n ') if self.heightOvr > 0.01 or self.heightOvr < -0.01: if self.flashState: self.w.height_ovr_label.setText(f'{self.heightOvr:.2f}') else: self.w.height_ovr_label.setText('') else: self.w.height_ovr_label.setText(f'{self.heightOvr:.2f}') if self.flash_error and self.error_present: if self.flashState: self.w.error_label.setText(_translate('HandlerClass', 'ERROR SENT TO MACHINE LOG')) else: self.w.error_label.setText('') if self.startLine > 0: if not self.w.run.text().startswith(_translate('HandlerClass', 'RUN')): if self.flashState: self.w.run.setText(self.runText) else: self.w.run.setText('') elif not self.manualCut: self.w.run.setText(_translate('HandlerClass', 'CYCLE START')) if not self.w.pmx485_enable.isChecked(): self.w.pmx485_label.setText('') self.pmx485LabelState = None elif self.pmx485CommsError: if self.flashState: self.w.pmx485_label.setText(_translate('HandlerClass', 'COMMS ERROR')) self.pmx485LabelState = None else: self.w.pmx485_label.setText('') self.pmx485LabelState = None elif not self.pmx485LabelState: if self.flashState: self.w.pmx485_label.setText(f'Fault Code: {self.pmx485FaultCode}') self.pmx485LabelState = None else: self.w.pmx485_label.setText('') self.pmx485LabelState = None def probe_timeout(self): if self.probeTime > 1: self.probeTime -= 1 self.probeTimer.start(1000) self.w[self.ptButton].setText(f'{self.probeTime}') else: self.probe_test_stop() log = _translate('HandlerClass', 'Probe test completed') STATUS.emit('update-machine-log', log, 'TIME') def torch_timeout(self): self.tpRemaining = max(0.0, self.tpRemaining - 0.1) self.w[self.tpButton].setText(f'{self.tpRemaining:.1f}') if self.tpRemaining > 0: return self.tpTimer.stop() if not hal.get_value('plasmac.torch-pulse-hold') and not self.extPulsePin.get(): self.torch_pulse_states(True) log = _translate('HandlerClass', 'Torch pulse completed') STATUS.emit('update-machine-log', log, 'TIME') else: text0 = _translate('HandlerClass', 'TORCH') text1 = _translate('HandlerClass', 'ON') self.w[self.tpButton].setText(f'{text0}\n{text1}') def pulse_timer_timeout(self): # halPulsePins format is: button name, pulse time, button text, remaining time, button number active = False try: for halpin in self.halPulsePins: if self.halPulsePins[halpin][3] > 0.05: active = True if self.halPulsePins[halpin][1] == self.halPulsePins[halpin][3]: self.invert_pin_state(halpin) self.halPulsePins[halpin][3] -= 0.1 self.w[self.halPulsePins[halpin][0]].setText(f'{self.halPulsePins[halpin][3]:0.1f}') elif self.w[self.halPulsePins[halpin][0]].text() != self.halPulsePins[halpin][2]: self.invert_pin_state(halpin) self.halPulsePins[halpin][3] = 0 self.w[self.halPulsePins[halpin][0]].setText(f'{self.halPulsePins[halpin][2]}') except Exception as err: self.halPulsePins[halpin][3] = 0 bNum = self.halPulsePins[halpin][4] head = _translate('HandlerClass', 'HAL Pin Error') msg0 = _translate('HandlerClass', 'Invalid code for user button') msg1 = _translate('HandlerClass', 'Failed to pulse HAL pin') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n"{halpin}" {err}\n') if not active: self.pulseTimer.stop() def laser_timeout(self): if self.w.laser.isDown() or self.extLaserButton: if self.w.run.isEnabled() and self.laserButtonState in ['reset', 'laser']: framingError = self.bounds_check_framing(self.laserOffsetX, self.laserOffsetY, True)[0] if framingError: head = _translate('HandlerClass', 'Axis Limit Error') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{framingError}\n') self.laserButtonState = 'reset' self.button_press_timeout('laser') return newX = STATUS.get_position()[0][0] - STATUS.stat.g5x_offset[0] - self.laserOffsetX newY = STATUS.get_position()[0][1] - STATUS.stat.g5x_offset[1] - self.laserOffsetY units = '20' if self.gcodeProps['gcode_units'] == 'in' else '21' ACTION.CALL_MDI_WAIT(f'G{units} G10 L20 P0 X{newX / self.boundsMultiplier} Y{newY / self.boundsMultiplier}') self.laserDryRunCoords = [STATUS.stat.g5x_offset[0], STATUS.stat.g5x_offset[1]] self.laserOnPin.set(1) hal.set_p('plasmac.dry-run', '1') self.run_clicked() return else: self.laserButtonState = 'reset' self.button_press_timeout('laser') self.laserOnPin.set(0) self.laserButtonState = 'reset' self.w.laser.setText(_translate('HandlerClass', 'LASER')) def ohmic_led_timeout(self): if not self.ohmicLedInPin.get(): hal.set_p('qtplasmac.led_ohmic_probe', '0') else: self.ohmicLedTimer.start(50) ######################################################################################################################### # USER BUTTON FUNCTIONS # ######################################################################################################################### def user_button_setup(self): self.iniButtonCodes = ['Codes'] iniButtonCodes = ['Codes'] self.probePressed = False self.probeTime = 0 self.probeTimer = QTimer() self.probeTimer.setSingleShot(True) self.probeTimer.timeout.connect(self.probe_timeout) self.tpRemaining = 0.0 self.tpTimer = QTimer() self.tpTimer.setSingleShot(False) self.tpTimer.setInterval(100) self.tpTimer.timeout.connect(self.torch_timeout) self.pulseTime = 0 self.pulseTimer = QTimer() self.pulseTimer.timeout.connect(self.pulse_timer_timeout) self.cutType = 0 self.single_cut_request = False self.oldFile = None singleCodes = ['change-consumables', 'cut-type', 'framing', 'manual-cut', 'offsets-view', 'ohmic-test', 'probe-test', 'single-cut', 'torch-pulse', 'user-manual', 'latest-file', 'toggle-joint'] head = _translate('HandlerClass', 'User Button Error') for bNum in range(1, 21): self.w[f'button_{bNum}'].setCheckable(False) bName = self.PREFS.getpref(f'{bNum} Name', '', str, 'BUTTONS') or None bCode = self.PREFS.getpref(f'{bNum} Code', '', str, 'BUTTONS') or None if bName or bCode: self.w[f'ub_name_{bNum}'].setText(bName) self.w[f'ub_code_{bNum}'].setText(bCode) if (bCode and not bName) or (not bCode and bName): msg0 = _translate('HandlerClass', 'are both required') msg1 = _translate('HandlerClass', 'only one has been specified for') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\nCODE + NAME {msg0}\n{msg1} BUTTON_{bNum}\n') self.w[f'button_{bNum}'].setText('') self.iniButtonCodes.append('') continue if not bCode: self.w[f'button_{bNum}'].setText('') self.iniButtonCodes.append('') continue code = bCode.lower().strip().split()[0] if code in singleCodes and code in iniButtonCodes: msg1 = _translate('HandlerClass', 'Duplicate code entry for') msg2 = _translate('HandlerClass', 'Using first instance only of') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg1} BUTTON_{iniButtonCodes.index(code)} + BUTTON_{bNum}\n{msg2} {bCode.split()[0]}\n') self.w[f'button_{bNum}'].setText('') self.iniButtonCodes.append('') continue self.iniButtonCodes.append(bCode) iniButtonCodes.append(code) msg0 = _translate('HandlerClass', 'Invalid code for user button') bNames = bName.split('\\') bLabel = bNames[0] if len(bNames) > 1: for name in range(1, len(bNames)): bLabel += f'\n{bNames[name]}' self.w[f'button_{bNum}'].setText(bLabel) # toggle-laser can be anywhere in the button code if 'toggle-laser' in bCode: self.tlButton.append(f'button_{bNum}') continue # button code is required to start with the following codes if code == 'change-consumables': self.ccParm = bCode.replace('change-consumables', '').replace(' ', '').lower() or None if self.ccParm is not None and ('x' in self.ccParm or 'y' in self.ccParm): self.ccButton = f'button_{bNum}' self.interlockRules[self.ccButton] = self.interlockRules['change-consumables_template'].copy() else: msg1 = _translate('HandlerClass', 'Check button code for invalid or missing arguments') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n') continue elif code == 'probe-test': if len(bCode.split()) < 3: if bCode.lower().replace('probe-test', '').strip(): try: self.ptTime = round(float(bCode.lower().replace('probe-test', '').strip())) except: msg1 = _translate('HandlerClass', 'Check button code for invalid seconds argument') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n') continue else: self.ptTime = 10 self.ptButton = f'button_{bNum}' self.interlockRules[self.ptButton] = self.interlockRules['probe-test_template'].copy() self.probeText = self.w[self.ptButton].text() else: msg1 = _translate('HandlerClass', 'Check button code for extra arguments') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n') continue elif code == 'torch-pulse': if len(bCode.split()) < 3: if bCode.lower().replace('torch-pulse', '').strip(): try: self.tpTime = round(float(bCode.lower().replace('torch-pulse', '').strip()), 1) except: msg1 = _translate('HandlerClass', 'Check button code for invalid seconds argument') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n') continue self.tpTime = 3.0 if self.tpTime > 3.0 else self.tpTime else: self.tpTime = 1.0 self.tpButton = f'button_{bNum}' self.interlockRules[self.tpButton] = self.interlockRules['torch-pulse_template'].copy() self.tpText = self.w[self.tpButton].text() else: msg1 = _translate('HandlerClass', 'Check button code for extra arguments') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n') continue elif code == 'ohmic-test': self.otButton = f'button_{bNum}' self.interlockRules[self.otButton] = self.interlockRules['ohmic-test_template'].copy() elif code == 'framing': frButton = True self.defaultZ = True self.frFeed = 0 bCode = bCode.lower().replace('framing', '').strip() if 'usecurrentzheight' in bCode: bCode = bCode.lower().replace('usecurrentzheight', '').strip() self.defaultZ = False if len(bCode): if bCode[0] == 'f': try: self.frFeed = float(bCode.replace('f', '')) except: msg1 = _translate('HandlerClass', 'Check button code for invalid feed argument') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n') frButton = False continue else: msg1 = _translate('HandlerClass', 'Check button code for invalid arguments') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n') continue if frButton: self.frButton = f'button_{bNum}' self.interlockRules[self.frButton] = self.interlockRules['framing_template'].copy() elif code == 'cut-type': self.ctButton = f'button_{bNum}' self.interlockRules[self.ctButton] = self.interlockRules['cut-type_template'].copy() elif code == 'single-cut': self.scButton = f'button_{bNum}' self.interlockRules[self.scButton] = self.interlockRules['single-cut_template'].copy() elif code == 'manual-cut': self.mcButton = f'button_{bNum}' self.interlockRules[self.mcButton] = self.interlockRules['manual-cut_template'].copy() elif code == 'load': self.interlockRules[f'button_{bNum}'] = self.interlockRules['load_template'].copy() elif code == 'toggle-halpin': head = _translate('HandlerClass', 'HAL Pin Error') altLabel = None if ';;' in bCode: altLabel = bCode[bCode.index(';;') + 2:].strip() bCode = bCode[:bCode.index(';;')].strip() if len(bCode.split()) == 3 and 'cutcritical' in bCode.lower(): critical = True elif len(bCode.split()) == 2: critical = False else: head = _translate('HandlerClass', 'User Button Error') msg1 = _translate('HandlerClass', 'Check button code for invalid arguments') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n') continue halpin = bCode.lower().split('toggle-halpin')[1].split(' ')[1].strip() excludedHalPins = ('plasmac.torch-pulse-start', 'plasmac.ohmic-test', 'plasmac.probe-test', 'plasmac.consumable-change') if halpin in excludedHalPins: msg1 = _translate('HandlerClass', 'HAL pin') msg2 = _translate('HandlerClass', 'must be toggled') msg3 = _translate('HandlerClass', 'using standard button code') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1} "{halpin}" {msg2}\n{msg3}\n') continue else: try: hal.get_value(halpin) self.interlockRules[f'button_{bNum}'] = self.interlockRules['toggle-halpin_template'].copy() except: msg1 = _translate('HandlerClass', 'HAL pin') msg2 = _translate('HandlerClass', 'does not exist') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1} "{halpin}" {msg2}\n') continue # halTogglePins format is: button name, run critical flag, button text, alt button text self.halTogglePins[halpin] = [f'button_{bNum}', critical, bLabel, altLabel] elif code == 'pulse-halpin': if len(bCode.split()) < 4: try: code, halpin, delay = bCode.lower().strip().split() except: try: code, halpin = bCode.lower().strip().split() delay = '1.0' except: head = _translate('HandlerClass', 'User Button Error') msg1 = _translate('HandlerClass', 'Check button code for invalid arguments') code = halpin = delay = '' STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n') continue excludedHalPins = ('plasmac.torch-pulse-start', 'plasmac.ohmic-test', 'plasmac.probe-test', 'plasmac.consumable-change') head = _translate('HandlerClass', 'HAL Pin Error') if halpin in excludedHalPins: msg1 = _translate('HandlerClass', 'HAL pin') msg2 = _translate('HandlerClass', 'must be pulsed') msg3 = _translate('HandlerClass', 'using standard button code') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1} "{halpin}" {msg2}\n{msg3}\n') continue else: try: hal.get_value(halpin) self.interlockRules[f'button_{bNum}'] = self.interlockRules['pulse-halpin_template'].copy() except: msg1 = _translate('HandlerClass', 'HAL pin') msg2 = _translate('HandlerClass', 'does not exist') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1} "{halpin}" {msg2}\n') continue # halPulsePins format is: button name, pulse time, button text, remaining time, button number try: self.halPulsePins[halpin] = [f'button_{bNum}', float(delay), bLabel, 0.0, bNum] except: head = _translate('HandlerClass', 'User Button Error') msg1 = _translate('HandlerClass', 'Check button code for invalid seconds argument') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n') continue else: head = _translate('HandlerClass', 'User Button Error') msg1 = _translate('HandlerClass', 'Check button code for invalid arguments') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n') continue elif code == 'offsets-view': self.ovButton = f'button_{bNum}' self.interlockRules[self.ovButton] = self.interlockRules['offsets-view_template'].copy() elif code == 'latest-file': self.llButton = f'button_{bNum}' self.interlockRules[self.llButton] = self.interlockRules['latest-file_template'].copy() elif code == 'user-manual': self.umButton = f'button_{bNum}' self.interlockRules[self.umButton] = self.interlockRules['user-manual_template'].copy() if _WEBENGINE_AVAILABLE: self.w.webview.page().loadFinished.connect(self.style_user_manual) self.w.webview.page().setBackgroundColor(QColor(self.backColor)) else: head = _translate('HandlerClass', 'User Button Warning') msg1 = _translate('HandlerClass', 'QtWebEngine dependency missing for user button') msg2 = _translate('HandlerClass', 'User Manual styling will not match GUI') msg3 = _translate('HandlerClass', 'Fix using "sudo apt install python3-pyqt5.qtwebengine"') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg1} #{bNum}\n{msg2}\n{msg3}\n') self.w.webview.load(self.umUrl) elif code == 'toggle-joint': self.jtButton = f'button_{bNum}' self.interlockRules[self.jtButton] = self.interlockRules['toggle-joint_template'].copy() else: if 'dual-code' in bCode: # incoming code is: "dual-code" ;; code1 ;; label1 ;; code2 ;; checked (optional = true) data = bCode.split(';;') if len(data) not in [4, 5]: head = _translate('HandlerClass', 'User Button Error') msg1 = _translate('HandlerClass', 'Check button code for invalid arguments') code = halpin = delay = '' STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n') continue else: if len(data) == 5 and data[4].strip().lower() == 'true': self.w[f'button_{bNum}'].setCheckable(True) checked = True else: checked = True self.dualCodeButtons[bNum] = [data[1], data[2], data[3], bLabel, checked] # dualCodeButtons format is: code1 ;; label1 ;; code2 ;; label2 ;; checked commands = f'{data[1]}\\{data[3]}' else: commands = bCode for command in commands.split('\\'): command = command.strip() if command and command[0].lower() in 'xyzabcgmfsto' and command.replace(' ', '')[1] in '0123456789<': if f'button_{bNum}' not in self.interlockRules: self.interlockRules[f'button_{bNum}'] = self.interlockRules['dual-code_template'].copy() elif command and command[0] == '%': cmd = command.lstrip('%').lstrip(' ').split(' ', 1)[0] if cmd[-3:] == '.py': reply = os.path.exists(os.path.expanduser(cmd)) else: reply = Popen(f'which {cmd}', stdout=PIPE, stderr=PIPE, shell=True).communicate()[0] if not reply: head = _translate('HandlerClass', 'External Code Error') msg1 = _translate('HandlerClass', 'External command') msg2 = _translate('HandlerClass', 'does not exist') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1} "{cmd}" {msg2}\n') else: self.interlockRules[f'button_{bNum}'] = self.interlockRules['always-on_template'].copy() else: head = _translate('HandlerClass', 'Code Error') msg1 = self.w[f'button_{bNum}'].text().replace('\n', ' ') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}: "{command}"\n') self.remove_button(f'button_{bNum}') break for button_name in self.tlButton: self.interlockRules[button_name] = self.interlockRules['toggle-laser_template'].copy() template_keys = [key for key in self.interlockRules.keys() if key.endswith('_template')] for template_key in template_keys: self.interlockRules.pop(template_key) def user_button_down(self, bNum): bCode = self.iniButtonCodes[bNum] if not bCode: return # toggle-laser is the only code that can appear anywhere in the button code if 'toggle-laser' in bCode.lower(): self.laserOnPin.set(not self.laserOnPin.get()) for command in bCode.split('\\'): command = command.strip() if command != 'toggle-laser': self.user_button_command(bNum, command) ACTION.SET_MANUAL_MODE() elif bCode.lower().startswith('change-consumables') and 'e-halpin' not in bCode.lower(): self.change_consumables(True) elif bCode.lower().startswith('probe-test') and 'e-halpin' not in bCode.lower(): self.probe_test(True) elif bCode.lower().startswith('torch-pulse') and 'e-halpin' not in bCode.lower(): self.torch_pulse(True) elif bCode.lower().startswith('ohmic-test') and 'e-halpin' not in bCode.lower(): self.ohmic_test(True) elif bCode.lower().startswith('framing'): self.frame_job(True) elif bCode.lower().startswith('cut-type'): self.w.gcodegraphics.logger.clear() self.cutType ^= 1 if self.cutType: self.cutTypePin.set(1) self.button_active(self.ctButton) self.cutTypeText = self.w[self.ctButton].text() self.w[self.ctButton].setText(_translate('HandlerClass', 'PIERCE\nONLY')) else: self.cutTypePin.set(0) self.button_normal(self.ctButton) self.w[self.ctButton].setText(self.cutTypeText) self.overlayProgress.setValue(0) if self.fileOpened: self.file_reload_clicked() elif bCode.lower().startswith('load'): lFile = f'{self.programPrefix}/{bCode.split("load", 1)[1].strip()}' self.overlayProgress.setValue(0) self.remove_temp_materials() ACTION.OPEN_PROGRAM(lFile) elif bCode.lower().startswith('toggle-halpin'): halpin = bCode.lower().split('toggle-halpin')[1].split(' ')[1].strip() try: if halpin in self.halPulsePins and self.halPulsePins[halpin][3] > 0.05: self.halPulsePins[halpin][3] = 0.0 else: self.invert_pin_state(halpin) except Exception as err: head = _translate('HandlerClass', 'HAL Pin Error') msg0 = _translate('HandlerClass', 'Invalid code for user button') msg1 = _translate('HandlerClass', 'Failed to toggle HAL pin') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head,}:\n{msg0} #{bNum}\n{msg1}\n"{halpin}" {err}\n') elif bCode.lower().startswith('pulse-halpin'): head = _translate('HandlerClass', 'HAL Pin Error') msg1 = _translate('HandlerClass', 'Failed to pulse HAL pin') halpin = bCode.lower().strip().split()[1] # halPulsePins format is: button name, pulse time, button text, remaining time, button number try: if self.halPulsePins[halpin][3] > 0.05: self.halPulsePins[halpin][3] = 0.0 else: self.w[self.halPulsePins[halpin][0]].setText(f'{self.halPulsePins[halpin][2]}') self.halPulsePins[halpin][3] = self.halPulsePins[halpin][1] if not self.pulseTimer.isActive(): self.pulseTimer.start(100) except: msg0 = _translate('HandlerClass', 'Invalid code for user button') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1} "{halpin}"\n') elif bCode.lower().startswith('single-cut'): self.single_cut() elif bCode.lower().startswith('manual-cut'): self.manual_cut() elif bCode.lower().startswith('offsets-view'): if self.w.preview_stack.currentIndex() != self.OFFSETS: self.w.preview_stack.setCurrentIndex(self.OFFSETS) else: self.preview_index_return(self.w.preview_stack.currentIndex()) elif bCode.lower().startswith('latest-file'): try: if len(bCode.split()) == 2: lastDir = bCode.split()[1] else: lastDir = self.w.PREFS_.getpref('last_loaded_directory', '', str, 'BOOK_KEEPING') files = glob.glob(f'{lastDir}/*.ngc') latest = max(files, key=os.path.getctime) self.overlayProgress.setValue(0) self.remove_temp_materials() ACTION.OPEN_PROGRAM(latest) except: head = _translate('HandlerClass', 'File Error') msg0 = _translate('HandlerClass', 'Cannot open latest file from user button') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n') elif bCode.lower().startswith('user-manual'): if self.w.preview_stack.currentIndex() != self.USER_MANUAL: self.prevPreviewIndex = self.w.preview_stack.currentIndex() self.w.preview_stack.setCurrentIndex(self.USER_MANUAL) else: self.w.preview_stack.setCurrentIndex(self.prevPreviewIndex) self.prevPreviewIndex = self.USER_MANUAL elif bCode.lower().startswith('toggle-joint'): self.toggle_joint_mode() else: self.reloadRequired = False if bCode.lower().startswith('dual-code'): # dualCodeButtons format is: code1 ;; label1 ;; code2 ;; label2 ;; checked if self.w[f'button_{bNum}'].text() == self.dualCodeButtons[bNum][3]: bCode = self.dualCodeButtons[bNum][0] self.w[f'button_{bNum}'].setText(self.dualCodeButtons[bNum][1]) self.w[f'button_{bNum}'].setChecked(True) else: bCode = self.dualCodeButtons[bNum][2] self.w[f'button_{bNum}'].setText(self.dualCodeButtons[bNum][3]) self.w[f'button_{bNum}'].setChecked(False) for command in bCode.split('\\'): command = command.strip() self.user_button_command(bNum, command) if command[0] == "%": continue if command.lower().replace(' ', '').startswith('g10l20') and self.fileOpened: self.reloadRequired = True if self.reloadRequired: self.file_reload_clicked() else: self.w.gcodegraphics.logger.clear() ACTION.SET_MANUAL_MODE() # for G-code commands and external commands def user_button_command(self, bNum, command): if command and command[0].lower() in 'xyzabcgmfsto' and command.replace(' ', '')[1] in '0123456789<': if '{' in command: newCommand = subCommand = '' for char in command: if char == '{': subCommand = ':' elif char == '}': if len(subCommand.split()) > 1: section, option = subCommand.replace(':', '').split(' ', 1) else: head = _translate('HandlerClass', 'Code Error') msg0 = _translate('HandlerClass', 'Value Error in user button') msg1 = self.w[f'button_{bNum}'].text().replace('\n', ' ') errorCode = f'{subCommand.replace(":", "{")}' + '}' msg2 = _translate('HandlerClass', 'Requires a valid section and option pair') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum} ({msg1}):\n"{errorCode}"\n{msg2}\n') return if self.PREFS.has_option(section, option): newCommand += str(self.PREFS.get(section, option)) elif self.iniFile.find(section, option): newCommand += self.iniFile.find(section, option) else: head = _translate('HandlerClass', 'Code Error') msg0 = _translate('HandlerClass', 'Invalid code in user button') msg1 = self.w[f'button_{bNum}'].text().replace('\n', ' ') errorCode = f'{subCommand.replace(":", "{")}' + '}' msg2 = _translate('HandlerClass', 'Section') msg3 = _translate('HandlerClass', 'or option') msg4 = _translate('HandlerClass', 'does not exist in') msg5 = _translate('HandlerClass', 'Section and option pairs are case sensative') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum} ({msg1}):\n"{errorCode}"\n{msg2}: "[{section}]" {msg3}: "{option}" {msg4}:\n{self.prefsFile}\n{msg5}\n') return subCommand = '' elif subCommand.startswith(':'): subCommand += char else: newCommand += char command = newCommand ACTION.CALL_MDI(command) while not STATUS.is_interp_idle(): self.w.gcodegraphics.update() QApplication.processEvents() elif command and command[0] == '%': command = command.lstrip('%').lstrip() if command[-3:] == '.py': cmd = 'python3 ' + command Popen(cmd, stdin=None, stdout=PIPE, stderr=PIPE, shell=True) else: command += '&' Popen(command, stdout=PIPE, stderr=PIPE, shell=True) else: head = _translate('HandlerClass', 'Code Error') msg0 = _translate('HandlerClass', 'Invalid code for user button') msg1 = self.w[f'button_{bNum}'].text().replace('\n', ' ') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}: "{command}"\n') def user_button_up(self, bNum): commands = self.iniButtonCodes[bNum] if not commands: return elif 'torch-pulse' in commands.lower() and 'e-halpin' not in commands.lower(): self.torch_pulse(False) if 'ohmic-test' in commands.lower() and 'e-halpin' not in commands.lower(): self.ohmic_test(False) def torch_enable_changed(self, state): if self.torchPulse: self.torch_pulse_abort() self.refresh_button_states() def ext_torch_enable_changed(self, state): if state: self.w.torch_enable.toggle() def ext_thc_enable_changed(self, state): if state: self.w.thc_enable.toggle() def ext_corner_lock_enable_changed(self, state): if state: self.w.cornerlock_enable.toggle() def ext_void_lock_enable_changed(self, state): if state: self.w.voidlock_enable.toggle() def ext_ignore_arc_ok_changed(self, state): if state: self.w.ignore_arc_ok.toggle() def ext_mesh_mode_changed(self, state): if state: self.w.mesh_enable.toggle() def ext_ohmic_probe_enable_changed(self, state): if state: self.w.ohmic_probe_enable.toggle() def ext_auto_volts_enable_changed(self, state): if state: self.w.use_auto_volts.toggle() def thc_auto_changed(self, state): if state: self.w.thc_delay.hide() self.w.thc_delay_lbl.hide() self.w.thc_sample_counts.show() self.w.thc_sample_counts_lbl.show() self.w.thc_sample_threshold.show() self.w.thc_sample_threshold_lbl.show() else: self.w.thc_delay.show() self.w.thc_delay_lbl.show() self.w.thc_sample_counts.hide() self.w.thc_sample_counts_lbl.hide() self.w.thc_sample_threshold.hide() self.w.thc_sample_threshold_lbl.hide() def consumable_change_setup(self): self.ccXpos = self.ccYpos = self.ccFeed = None X = Y = F = '' ccAxis = [X, Y, F] ccName = ['x', 'y', 'f'] for loop in range(3): count = 0 if ccName[loop] in self.ccParm: while 1: if not self.ccParm[count]: break if self.ccParm[count] == ccName[loop]: count += 1 break count += 1 while 1: if count == len(self.ccParm): break if self.ccParm[count].isdigit() or self.ccParm[count] in '.-': ccAxis[loop] += self.ccParm[count] else: break count += 1 if ccName[loop] == 'x' and ccAxis[loop]: self.ccXpos = float(ccAxis[loop]) elif ccName[loop] == 'y' and ccAxis[loop]: self.ccYpos = float(ccAxis[loop]) elif ccName[loop] == 'f' and ccAxis[loop]: self.ccFeed = float(ccAxis[loop]) if not self.ccFeed or self.ccFeed < 1: msg0 = _translate('HandlerClass', 'Invalid feed rate for consumable change') msg1 = _translate('HandlerClass', 'Defaulting to materials cut feed rate') STATUS.emit('update-machine-log', f'{msg0}, {msg1}', 'TIME') self.ccFeed = float(self.w.cut_feed_rate.text().replace(',', '.')) def ext_change_consumables(self, state): if self.ccButton and self.w[self.ccButton].isEnabled(): self.change_consumables(state) def change_consumables(self, state): if hal.get_value('axis.x.eoffset-counts') or hal.get_value('axis.y.eoffset-counts'): hal.set_p('plasmac.consumable-change', '0') hal.set_p('plasmac.x-offset', '0') hal.set_p('plasmac.y-offset', '0') self.button_normal(self.ccButton) else: self.consumable_change_setup() if not self.ccXpos: self.ccXpos = STATUS.get_position()[0][0] if self.ccXpos < round(self.xMin, 6) + (10 * self.unitsPerMm): self.ccXpos = round(self.xMin, 6) + (10 * self.unitsPerMm) elif self.ccXpos > round(self.xMax, 6) - (10 * self.unitsPerMm): self.ccXpos = round(self.xMax, 6) - (10 * self.unitsPerMm) if not self.ccYpos: self.ccYpos = STATUS.get_position()[0][1] if self.ccYpos < round(self.yMin, 6) + (10 * self.unitsPerMm): self.ccYpos = round(self.yMin, 6) + (10 * self.unitsPerMm) elif self.ccYpos > round(self.yMax, 6) - (10 * self.unitsPerMm): self.ccYpos = round(self.yMax, 6) - (10 * self.unitsPerMm) hal.set_p('plasmac.xy-feed-rate', str(float(self.ccFeed))) hal.set_p('plasmac.x-offset', f'{(self.ccXpos - STATUS.get_position()[0][0]) / hal.get_value("plasmac.offset-scale"):.0f}') hal.set_p('plasmac.y-offset', f'{(self.ccYpos - STATUS.get_position()[0][1]) / hal.get_value("plasmac.offset-scale"):.0f}') hal.set_p('plasmac.consumable-change', '1') self.button_active(self.ccButton) self.refresh_button_states() def ext_probe_test(self, state): if self.ptButton and self.w[self.ptButton].isEnabled(): self.probe_test(state) def probe_test(self, state): if state: if self.probeTimer.remainingTime() <= 0 and not self.offsetsActivePin.get(): probeError, errMsg = self.bounds_check_probe(True) if probeError: head = _translate('HandlerClass', 'Axis Limit Error') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{errMsg}\n') return self.probeTime = self.ptTime self.probeTimer.start(1000) self.probeTest = True hal.set_p('plasmac.probe-test', '1') self.w[self.ptButton].setText(f'{self.probeTime}') self.button_active(self.ptButton) log = _translate('HandlerClass', 'Probe test started') STATUS.emit('update-machine-log', log, 'TIME') else: self.probe_test_stop() log = _translate('HandlerClass', 'Probe test aborted') STATUS.emit('update-machine-log', log, 'TIME') self.refresh_button_states() def probe_test_stop(self): self.probeTest = False self.probeTimer.stop() self.probeTime = 0 hal.set_p('plasmac.probe-test', '0') self.w[self.ptButton].setText(self.probeText) self.button_normal(self.ptButton) self.refresh_button_states() def probe_test_error(self, state): if state: self.probe_test_stop() log = _translate('HandlerClass', 'Probe test error detected') STATUS.emit('update-machine-log', log, 'TIME') def ext_torch_pulse(self, state): if self.tpButton and self.w[self.tpButton].isEnabled(): self.torch_pulse(state) def torch_pulse(self, state): if state: self.torch_pulse_pressed() else: self.torch_pulse_released() def torch_pulse_pressed(self): # second press while active aborts if self.tpRemaining > 0: self.torch_pulse_abort() return if hal.get_value('plasmac.torch-on'): return self.tpRemaining = self.tpTime self.torchPulse = True hal.set_p('plasmac.torch-pulse-time', str(self.tpRemaining)) hal.set_p('plasmac.torch-pulse-start', '1') hal.set_p('plasmac.torch-pulse-hold', '1') self.tpTimer.start() self.w[self.tpButton].setText(f'{self.tpRemaining}') self.button_active(self.tpButton) self.torch_pulse_states(False) log = _translate('HandlerClass', 'Torch pulse started') STATUS.emit('update-machine-log', log, 'TIME') def torch_pulse_released(self): hal.set_p('plasmac.torch-pulse-start', '0') hal.set_p('plasmac.torch-pulse-hold', '0') if self.tpRemaining == 0: self.torch_pulse_states(True) log = _translate('HandlerClass', 'Torch pulse hold released') STATUS.emit('update-machine-log', log, 'TIME') def torch_pulse_abort(self): hal.set_p('plasmac.torch-pulse-abort', '1') QTimer.singleShot(50, lambda: hal.set_p('plasmac.torch-pulse-abort', '0')) hal.set_p('plasmac.torch-pulse-start', '0') hal.set_p('plasmac.torch-pulse-hold', '0') self.tpTimer.stop() self.tpRemaining = 0.0 self.torch_pulse_states(True) log = _translate('HandlerClass', 'Torch pulse aborted') STATUS.emit('update-machine-log', log, 'TIME') def torch_pulse_states(self, state): if state: self.w[self.tpButton].setText(self.tpText) self.button_normal(self.tpButton) self.torchPulse = False self.refresh_button_states() def ext_ohmic_test(self, state): if self.otButton and self.w[self.otButton].isEnabled(): self.ohmic_test(state) def ohmic_test(self, state): self.ohmicTest = state hal.set_p('plasmac.ohmic-test', str(state)) self.refresh_button_states() def ext_frame_job(self, state): if self.frButton and self.w[self.frButton].isEnabled(): self.frame_job(state) def frame_job(self, state): if self.gcodeProps and state: response = False if self.w.laser.isEnabled(): framingError, framePoints = self.bounds_check_framing(self.laserOffsetX, self.laserOffsetY, True) if framingError: head = _translate('HandlerClass', 'Axis Limit Error') framingError += _translate('HandlerClass', '\n\nFrame the job using the torch instead?\n') response = self.dialog_show_yesno(QMessageBox.Warning, f'{head}', f'\n{framingError}') if response: framePoints = self.bounds_check_framing()[1] else: return else: self.laserOnPin.set(1) else: framePoints = self.bounds_check_framing()[1] if not self.frFeed: feed = float(self.w.cut_feed_rate.text().replace(',', '.')) else: feed = self.frFeed zHeight = self.zMax - (hal.get_value('plasmac.max-offset') * self.unitsPerMm) if STATUS.is_on_and_idle() and STATUS.is_all_homed(): self.framing = True previousMode = '' if self.units == 'in' and STATUS.is_metric_mode(): previousMode = 'G21' ACTION.CALL_MDI('G20') elif self.units == 'mm' and not STATUS.is_metric_mode(): previousMode = 'G20' ACTION.CALL_MDI('G21') ACTION.CALL_MDI_WAIT(f'G64 P{0.25 * self.unitsPerMm:0.3f}') if self.defaultZ: ACTION.CALL_MDI(f'G53 G0 Z{zHeight:0.4f}') ACTION.CALL_MDI(f'G53 G0 X{framePoints[1][0]:0.2f} Y{framePoints[1][1]:0.2f}') ACTION.CALL_MDI(f'G53 G1 X{framePoints[2][0]:0.2f} Y{framePoints[2][1]:0.2f} F{feed:0.0f}') ACTION.CALL_MDI(f'G53 G1 X{framePoints[3][0]:0.2f} Y{framePoints[3][1]:0.2f}') ACTION.CALL_MDI(f'G53 G1 X{framePoints[4][0]:0.2f} Y{framePoints[4][1]:0.2f}') ACTION.CALL_MDI(f'G53 G1 X{framePoints[1][0]:0.2f} Y{framePoints[1][1]:0.2f}') ACTION.CALL_MDI('G0 X0 Y0') ACTION.CALL_MDI(previousMode) self.refresh_button_states() def single_cut(self): self.singleCutDialog = True self.refresh_button_states() sC = QDialog(self.w) sC.setWindowTitle(_translate('HandlerClass', 'Single Cut')) l1 = QLabel(_translate('HandlerClass', 'X LENGTH:')) xLength = QDoubleSpinBox() xLength.setAlignment(Qt.AlignRight) xLength.setMinimum(-9999) xLength.setMaximum(9999) xLength.setDecimals(1) l2 = QLabel(_translate('HandlerClass', 'Y LENGTH:')) yLength = QDoubleSpinBox() yLength.setAlignment(Qt.AlignRight) yLength.setMinimum(-9999) yLength.setMaximum(9999) yLength.setDecimals(1) l3 = QLabel('') buttons = QDialogButtonBox.Ok | QDialogButtonBox.Cancel buttonBox = QDialogButtonBox(buttons) buttonBox.accepted.connect(sC.accept) buttonBox.rejected.connect(sC.reject) buttonBox.button(QDialogButtonBox.Ok).setText(_translate('HandlerClass', 'CUT')) buttonBox.button(QDialogButtonBox.Ok).setIcon(QIcon()) buttonBox.button(QDialogButtonBox.Cancel).setText(_translate('HandlerClass', 'CANCEL')) buttonBox.button(QDialogButtonBox.Cancel).setIcon(QIcon()) layout = QVBoxLayout() layout.addWidget(l1) layout.addWidget(xLength) layout.addWidget(l2) layout.addWidget(yLength) layout.addWidget(l3) layout.addWidget(buttonBox) sC.setLayout(layout) xLength.setValue(self.PREFS.getpref('X length', 0.0, float, 'SINGLE CUT')) yLength.setValue(self.PREFS.getpref('Y length', 0.0, float, 'SINGLE CUT')) self.vkb_show(True) result = sC.exec_() self.singleCutDialog = False self.refresh_button_states() self.vkb_hide() if not result or not self.cut_critical_toggle_check(): return self.PREFS.putpref('X length', xLength.value(), float, 'SINGLE CUT') self.PREFS.putpref('Y length', yLength.value(), float, 'SINGLE CUT') self.oldFile = ACTION.prefilter_path if ACTION.prefilter_path else None self.g91 = True if 910 in STATUS.stat.gcodes else False xEnd = STATUS.get_position()[0][0] + xLength.value() yEnd = STATUS.get_position()[0][1] + yLength.value() newFile = f'{self.tmpPath}single_cut.ngc' matNum = int(self.w.materials_box.currentText().split(': ', 1)[0]) with open(newFile, 'w') as f: f.write('G90\n') f.write(f'M190 P{matNum}\n') f.write('F#<_hal[plasmac.cut-feed-rate]>\n') f.write(f'G53 G0 X{STATUS.get_position()[0][0]:0.6f} Y{STATUS.get_position()[0][1]:0.6f}\n') f.write('M3 $0 S1\n') f.write(f'G53 G1 X{xEnd:0.6f} Y{yEnd:0.6f}\n') f.write('M5 $0\n') f.write('M2\n') self.single_cut_request = True if self.fileOpened: self.preSingleCutMaterial = matNum ACTION.OPEN_PROGRAM(newFile) def manual_cut(self): if self.manualCut: self.abort_pressed() elif STATUS.machine_is_on() and STATUS.is_all_homed() and STATUS.is_interp_idle() and self.cut_critical_toggle_check(): probeError, errMsg = self.bounds_check_probe(True) if probeError: head = _translate('HandlerClass', 'Axis Limit Error') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{errMsg}\n') return self.manualCut = True self.set_mc_states(False) if self.mcButton: self.button_active(self.mcButton) ACTION.SET_SPINDLE_ROTATION(1, 1, 0) log = _translate('HandlerClass', 'Manual cut started') STATUS.emit('update-machine-log', log, 'TIME') def button_active(self, button): self.w[button].setStyleSheet(f'QPushButton {{ color: {self.backColor}; background: {self.fore1Color} }} \ QPushButton:pressed {{ color: {self.backColor}; background: {self.fore1Color} }} \ QPushButton:disabled {{ color: {self.disabledColor}}}') def button_normal(self, button): if button == 'file_open': self.w[button].setStyleSheet(f'QPushButton {{ color: {self.backColor}; background: {self.foreColor} }} \ QPushButton:pressed {{ color: {self.foreColor}; background: {self.backColor} }} \ QPushButton:disabled {{ color: {self.backColor}; background: {self.disabledColor} }}') else: self.w[button].setStyleSheet(f'QPushButton {{ color: {self.foreColor}; background: {self.backColor} }} \ QPushButton:pressed {{ color: {self.backColor}; background: {self.fore1Color} }} \ QPushButton:disabled {{ color: {self.disabledColor} }}') def button_press_timeout(self, button): self.w[button].setStyleSheet(f'QPushButton:pressed {{ color: {self.foreColor}; background: {self.backColor} }}') def button_normal_check(self, button): '''Returns True if the button is in the normal state (background color of the button matches the background color of the GUI)''' return self.w[button].palette().color(QtGui.QPalette.Background) \ == self.w.color_backgrnd.palette().color(QPalette.Background) def can_enable(self, widgetName): rules = self.interlockRules.get(widgetName, {}) overrideRules = rules.get('override', {}) if overrideRules: for condition, value in overrideRules.items(): if condition not in self.state: LOG.error(f"'{condition}' does not exist") return False if self.state.get(condition) == value: return True allRules = rules.get('all', {}) for condition, value in allRules.items(): if condition not in self.state: LOG.error(f"'{condition}' does not exist") return False if self.state.get(condition) != value: return False anyRules = rules.get('any', {}) if anyRules: match = False for condition, value in anyRules.items(): if condition not in self.state: LOG.error(f"'{condition}' does not exist") return False if self.state.get(condition) == value: match = True break if not match: return False return True def sync_conditions(self): self.state['all_homed'] = STATUS.is_all_homed() self.state['consumable_change'] = hal.get_value('plasmac.consumable-change') self.state['consumable_changing'] = self.consChangePin.get() self.state['estop_cleared'] = STATUS.estop_is_clear() self.state['file_bounds_error'] = self.fileBoundsError self.state['file_opened'] = self.fileOpened self.state['framing'] = self.framing self.state['gcode_loaded'] = self.w.gcode_display.lines() > 1 self.state['gcodestack_is_mdi'] = self.w.gcode_stack.currentIndex() == self.MDI self.state['interp_idle'] = STATUS.is_interp_idle() self.state['interp_paused'] = STATUS.is_interp_paused() self.state['interp_running'] = STATUS.is_interp_running() self.state['laser_button_state'] = self.laserButtonState == 'laser' self.state['machine_on'] = STATUS.machine_is_on() self.state['manual_cut_active'] = self.manualCut self.state['offsets_active'] = self.offsetsActivePin.get() self.state['ohmic_probe_enable'] = self.w.ohmic_probe_enable.isChecked() self.state['ohmic_test'] = self.ohmicTest self.state['plasmac_idle'] = self.plasmacStatePin.get() == self.IDLE self.state['previewstack_is_camera'] = self.w.preview_stack.currentIndex() == self.CAMERA self.state['previewstack_is_edit'] = self.w.preview_stack.currentIndex() == self.EDIT self.state['previewstack_is_offsets'] = self.w.preview_stack.currentIndex() == self.OFFSETS self.state['previewstack_is_open'] = self.w.preview_stack.currentIndex() == self.OPEN self.state['previewstack_is_preview'] = self.w.preview_stack.currentIndex() == self.PREVIEW self.state['previewstack_is_user_manual'] = self.w.preview_stack.currentIndex() == self.USER_MANUAL self.state['probe_bounds_error'] = self.probeBoundsError self.state['probe_test'] = hal.get_value('plasmac.probe-test') self.state['rfl_dialog'] = self.rflActive self.state['single_cut_dialog'] = self.singleCutDialog self.state['torch_enable'] = self.w.torch_enable.isChecked() self.state['torch_pulse'] = self.torchPulse def refresh_button_states(self): self.sync_conditions() for widgetName in self.interlockRules.keys(): try: widget = getattr(self.w, widgetName, None) if widget is not None: shouldEnable = self.can_enable(widgetName) widget.setEnabled(shouldEnable) except Exception as e: LOG.error(f"Error updating button '{widgetName}': {e}") # Update tab states if self.tabsAlwaysEnabled.get(): return for tabIndex, tabName in self.tabNames.items(): try: if tabName in self.interlockRules: shouldEnable = self.can_enable(tabName) self.w.main_tab_widget.setTabEnabled(tabIndex, shouldEnable) except Exception as e: LOG.error(f"Error updating state for tab '{tabName}': {e}") def remove_button(self, widgetName): if widgetName in self.interlockRules: del self.interlockRules[widgetName] ######################################################################################################################### # ONBOARD VIRTUAL KEYBOARD FUNCTIONS # ######################################################################################################################### def vkb_check(self): if self.w.chk_soft_keyboard.isChecked() and not os.path.isfile('/usr/bin/onboard'): head = _translate('HandlerClass', 'Virtual Keyboard Error') msg0 = _translate('HandlerClass', '"onboard" virtual keyboard is not installed') msg1 = _translate('HandlerClass', 'some keyboard functions are not available') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}\n') return try: cmd = 'gsettings get org.onboard.window.landscape width' self.obWidth = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True).communicate()[0].decode().strip() cmd = 'gsettings get org.onboard.window.landscape height' self.obHeight = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True).communicate()[0].decode().strip() cmd = 'gsettings get org.onboard layout' layout = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True).communicate()[0].decode().strip() if '/numpad' in layout or '/keyboard' in layout: self.obLayout = 'compact' else: self.obLayout = layout except: self.obWidth = '700' self.obHeight = '300' self.obLayout = 'compact' def vkb_show(self, numpad=False): if self.firstRun: return if os.path.isfile('/usr/bin/onboard'): if self.w.chk_soft_keyboard.isChecked(): w = '240' if numpad else '740' h = '240' l = 'numpad' if numpad else 'keyboard' self.vkb_setup(w, h, os.path.join(self.PATHS.IMAGEDIR, 'qtplasmac', l)) cmd = 'dbus-send' cmd += ' --type=method_call' cmd += ' --dest=org.onboard.Onboard' cmd += ' /org/onboard/Onboard/Keyboard' cmd += ' org.onboard.Onboard.Keyboard.Show' Popen(cmd, stdout=PIPE, shell=True) def vkb_hide(self, custom=False): if os.path.isfile('/usr/bin/onboard') and self.obLayout: cmd = 'dbus-send' cmd += ' --type=method_call' cmd += ' --dest=org.onboard.Onboard' cmd += ' /org/onboard/Onboard/Keyboard' cmd += ' org.onboard.Onboard.Keyboard.Hide' Popen(cmd, stdout=PIPE, shell=True) if not custom: self.vkb_setup(self.obWidth, self.obHeight, self.obLayout) def vkb_setup(self, w, h, l): if os.path.isfile('/usr/bin/onboard'): Popen(f'gsettings set org.onboard.window.landscape width {int(w)-1}', stdout=PIPE, shell=True) Popen(f'gsettings set org.onboard.window.landscape height {int(h)-1}', stdout=PIPE, shell=True) Popen(f'gsettings set org.onboard layout {l}', stdout=PIPE, shell=True) Popen(f'gsettings set org.onboard.window.landscape width {int(w)}', stdout=PIPE, shell=True) Popen(f'gsettings set org.onboard.window.landscape height {int(h)}', stdout=PIPE, shell=True) ######################################################################################################################### # MATERIAL HANDLING FUNCTIONS # ######################################################################################################################### def save_materials_clicked(self): matNum = self.materialChangeNumberPin.get() if matNum == -1: matNum = self.defaultMaterial index = self.w.materials_box.currentIndex() self.save_material_file(matNum, index) def reload_materials_clicked(self): self.materialUpdate = True index = self.w.materials_box.currentIndex() self.load_material_file(True) self.w.materials_box.setCurrentIndex(index) self.materialUpdate = False self.materialReloadPin.set(0) def new_material_clicked(self, repeat, value): head = _translate('HandlerClass', 'Add Material') msg1 = _translate('HandlerClass', 'Enter New Material Number') msgs = msg1 btn1 = _translate('HandlerClass', 'ADD') btn2 = _translate('HandlerClass', 'CANCEL') virtkb = 4 while 1: valid, matNum = self.dialog_input(virtkb, head, f'{msgs}:', btn1, btn2) if not valid: return try: matNum = int(matNum) except: if not matNum: msg0 = _translate('HandlerClass', 'A material number is required') msgs = f'{msg0}.\n\n{msg1}' else: msg0 = _translate('HandlerClass', 'is not a valid number') msgs = f'{matNum} {msg0}.\n\n{msg1}' continue if matNum in self.materialNumList: msg0 = _translate('HandlerClass', 'Material') msg2 = _translate('HandlerClass', 'is in use') msgs = f'{msg0} #{matNum} {msg2}.\n\n{msg1}' continue elif matNum >= 1000000: msg0 = _translate('HandlerClass', 'Material numbers need to be less than 1000000') msgs = f'{msg0}.\n\n{msg1}' continue break msg1 = _translate('HandlerClass', 'Enter New Material Name') msgs = msg1 virtkb = 3 while 1: valid, matNam = self.dialog_input(virtkb, head, f'{msgs}:', btn1, btn2) if not valid: return if not matNam: msg0 = _translate('HandlerClass', 'Material name is required') msgs = f'{msg0}.\n\n{msg1}' continue break mat = [matNum, matNam] mat[2:] = self.materialDict[self.materialList[self.w.materials_box.currentIndex()]][1:] self.write_one_material(mat) self.materialUpdate = True self.load_material_file(True) self.w.materials_box.setCurrentIndex(self.materialList.index(matNum)) self.materialUpdate = False def delete_material_clicked(self): head = _translate('HandlerClass', 'Delete Material') msg1 = _translate('HandlerClass', 'Enter Material Number To Delete') btn1 = _translate('HandlerClass', 'DELETE') btn2 = _translate('HandlerClass', 'CANCEL') msgs = msg1 virtkb = 4 while 1: valid, matNum = self.dialog_input(virtkb, head, f'{msgs}:', btn1, btn2) if not valid: return try: matNum = int(matNum) except: if not matNum: msg0 = _translate('HandlerClass', 'A material number is required') msgs = f'{msg0}.\n\n{msg1}' else: msg0 = _translate('HandlerClass', 'is not a valid number') msgs = f'{matNum} {msg0}.\n\n{msg1}' continue if matNum == self.defaultMaterial: msg0 = _translate('HandlerClass', 'Default material cannot be deleted') msgs = f'{msg0}.\n\n{msg1}' continue elif matNum >= 1000000 and matNum in self.materialList: msg0 = _translate('HandlerClass', 'Temporary material') msg3 = _translate('HandlerClass', 'cannot be deleted') msgs = f'{msg0} #{matNum} {msg3}.\n\n{msg1}' continue elif matNum not in self.materialNumList: msg0 = _translate('HandlerClass', 'Material') msg3 = _translate('HandlerClass', 'does not exist') msgs = f'{msg0} #{matNum} {msg3}.\n\n{msg1}' continue break head = _translate('HandlerClass', 'Delete Material') msg0 = _translate('HandlerClass', 'Do you really want to delete material') if not self.dialog_show_yesno(QMessageBox.Question, f'{head}', f'\n{msg0} #{matNum}?\n'): return self.MATS.remove_section(f'MATERIAL_NUMBER_{matNum}') self.MATS.write(open(self.MATS.fn, 'w')) self.materialUpdate = True self.load_material_file(True) self.materialUpdate = False def remove_temp_materials(self): mats = [m for m in self.materialList if m >= 1000000] if mats: for mat in mats: self.materialDict.pop(mat) self.materialList.remove(mat) def selector_changed(self, index): if index == -1 or self.getMaterialBusy: return if self.w.material_selector.currentIndex() != self.w.materials_box.currentIndex(): self.w.materials_box.setCurrentIndex(index) self.w.conv_material.setCurrentIndex(index) def conv_material_changed(self, index): if index == -1 or self.getMaterialBusy: return if self.w.conv_material.currentIndex() != self.w.materials_box.currentIndex(): self.w.materials_box.setCurrentIndex(index) self.w.material_selector.setCurrentIndex(index) def material_changed(self, index): if index == -1 or self.getMaterialBusy: return if self.w.materials_box.currentText(): if self.getMaterialBusy: self.materialChangePin.set(-1) self.autoChange = False return matNum = int(self.w.materials_box.currentText().split(': ', 1)[0]) if matNum >= 1000000: self.w.save_material.setEnabled(False) else: self.w.save_material.setEnabled(True) if self.autoChange: hal.set_p('motion.digital-in-03', '0') if self.preSingleCutMaterial is None and self.preFileSaveMaterial is None: self.change_material(matNum) self.materialChangePin.set(2) hal.set_p('motion.digital-in-03', '1') else: self.change_material(matNum) self.w.material_selector.setCurrentIndex(self.w.materials_box.currentIndex()) self.w.conv_material.setCurrentIndex(self.w.materials_box.currentIndex()) self.autoChange = False self.overlay_update(None) def material_change_pin_changed(self, halpin): if halpin == 0: hal.set_p('motion.digital-in-03', '0') elif halpin == 3: hal.set_p('motion.digital-in-03', '1') self.materialChangePin.set(0) def material_change_number_pin_changed(self, halpin): if halpin == -1: halpin = self.defaultMaterial if self.getMaterialBusy or halpin == int(self.w.materials_box.currentText().split(': ', 1)[0]): return if self.materialChangePin.get() == 1: self.autoChange = True if not self.material_exists(halpin): self.autoChange = False return if self.preSingleCutMaterial is not None: self.materialChangeNumberPin.set(self.preSingleCutMaterial) self.preSingleCutMaterial = None elif self.preFileSaveMaterial is not None: self.materialChangeNumberPin.set(self.preFileSaveMaterial) self.preFileSaveMaterial = None else: self.w.materials_box.setCurrentIndex(self.materialList.index(halpin)) def material_change_timeout_pin_changed(self, halpin): head = _translate('HandlerClass', 'Materials Error') if halpin: # should we stop or pause the program if a timeout occurs??? matNum = int(self.w.materials_box.currentText().split(': ', 1)[0]) msg0 = _translate('HandlerClass', 'Material change timeout occurred for material') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:{msg0} #{matNum}\n') self.materialChangeNumberPin.set(matNum) self.materialChangeTimeoutPin.set(0) hal.set_p('motion.digital-in-03', '0') def material_reload_pin_changed(self, halpin): if halpin: self.reload_materials_clicked() def material_temp_pin_changed(self, halpin): if halpin: mat = [halpin, f'Temporary {halpin}', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] with open(self.tmpMaterialGcode, 'r') as f_in: for line in f_in: if line.startswith('NAME'): mat[1] = line.split('=')[1].strip() if line.startswith('KERF_WIDTH'): mat[2] = float(line.split('=')[1].strip()) elif line.startswith('PIERCE_HEIGHT'): mat[3] = float(line.split('=')[1].strip()) elif line.startswith('PIERCE_DELAY'): mat[4] = float(line.split('=')[1].strip()) elif line.startswith('PUDDLE_JUMP_HEIGHT'): mat[5] = float(line.split('=')[1].strip()) elif line.startswith('PUDDLE_JUMP_DELAY'): mat[6] = float(line.split('=')[1].strip()) elif line.startswith('CUT_HEIGHT'): mat[7] = float(line.split('=')[1].strip()) elif line.startswith('CUT_SPEED'): mat[8] = float(line.split('=')[1].strip()) elif line.startswith('CUT_AMPS'): mat[9] = float(line.split('=')[1].strip()) elif line.startswith('CUT_VOLTS'): mat[10] = float(line.split('=')[1].strip()) elif line.startswith('PAUSE_AT_END'): mat[11] = float(line.split('=')[1].strip()) elif line.startswith('GAS_PRESSURE'): mat[12] = float(line.split('=')[1].strip()) elif line.startswith('CUT_MODE'): mat[13] = float(line.split('=')[1].strip()) elif line.startswith('THICKNESS'): mat[14] = float(line.split('=')[1].strip()) self.write_materials_to_dict(mat) if halpin not in self.materialList: self.materialList.append(halpin) exists = False for n in range(self.w.materials_box.count()): if self.w.materials_box.itemText(n) .startswith(str(halpin)): self.w.materials_box.setItemText(n, f'{halpin:05d}: {self.materialDict[halpin][0]}') self.w.material_selector.setItemText(n, f'{halpin:05d}: {self.materialDict[halpin][0]}') self.w.conv_material.setItemText(n, f'{halpin:05d}: {self.materialDict[halpin][0]}') exists = True if not exists: self.w.materials_box.addItem(f'{halpin:05d}: {self.materialDict[halpin][0]}') self.w.material_selector.addItem(f'{halpin:05d}: {self.materialDict[halpin][0]}') self.w.conv_material.addItem(f'{halpin:05d}: {self.materialDict[halpin][0]}') if halpin == int(self.w.materials_box.currentText().split(': ', 1)[0]): self.change_material(halpin) self.materialTempPin.set(0) def write_materials_to_dict(self, matNum): self.materialDict[matNum[0]] = matNum[1:] def display_materials(self): self.materialList = [] self.w.materials_box.clear() self.w.material_selector.clear() self.w.conv_material.clear() for key in sorted(self.materialDict): self.w.materials_box.addItem(f'{key:05d}: {self.materialDict[key][0]}') self.w.material_selector.addItem(f'{key:05d}: {self.materialDict[key][0]}') self.w.conv_material.addItem(f'{key:05d}: {self.materialDict[key][0]}') self.materialList.append(key) def change_material(self, matNum): self.materialName = self.materialDict[matNum][0] self.w.kerf_width.setValue(self.materialDict[matNum][1]) self.w.pierce_height.setValue(self.materialDict[matNum][2]) self.w.pierce_delay.setValue(self.materialDict[matNum][3]) self.w.puddle_jump_height.setValue(self.materialDict[matNum][4]) self.w.puddle_jump_delay.setValue(self.materialDict[matNum][5]) self.w.cut_height.setValue(self.materialDict[matNum][6]) self.w.cut_feed_rate.setValue(self.materialDict[matNum][7]) self.w.cut_amps.setValue(self.materialDict[matNum][8]) self.w.cut_volts.setValue(self.materialDict[matNum][9]) self.w.pause_at_end.setValue(self.materialDict[matNum][10]) self.w.gas_pressure.setValue(self.materialDict[matNum][11]) self.w.cut_mode.setValue(self.materialDict[matNum][12]) self.w.material_thickness.setValue(self.materialDict[matNum][13]) self.materialChangeNumberPin.set(matNum) def save_material_file(self, matNum, index): mat = [matNum] mat.append(self.w.materials_box.currentText().split(': ', 1)[1].strip()) mat.append(self.w.kerf_width.value()) mat.append(self.w.pierce_height.value()) mat.append(self.w.pierce_delay.value()) mat.append(self.w.puddle_jump_height.value()) mat.append(self.w.puddle_jump_delay.value()) mat.append(self.w.cut_height.value()) mat.append(self.w.cut_feed_rate.value()) mat.append(self.w.cut_amps.value()) mat.append(self.w.cut_volts.value()) mat.append(self.w.pause_at_end.value()) mat.append(self.w.gas_pressure.value()) mat.append(self.w.cut_mode.value()) mat.append(self.w.material_thickness.value()) self.write_one_material(mat) self.materialUpdate = True self.load_material_file(True) self.w.materials_box.setCurrentIndex(index) self.materialUpdate = False self.set_saved_material() def set_saved_material(self): mat = [int(self.w.materials_box.currentText().split(': ', 1)[0])] mat.append(self.materialName) mat.append(self.w.kerf_width.value()) mat.append(self.w.pierce_height.value()) mat.append(self.w.pierce_delay.value()) mat.append(self.w.puddle_jump_height.value()) mat.append(self.w.puddle_jump_delay.value()) mat.append(self.w.cut_height.value()) mat.append(self.w.cut_feed_rate.value()) mat.append(self.w.cut_amps.value()) mat.append(self.w.cut_volts.value()) mat.append(self.w.pause_at_end.value()) mat.append(self.w.gas_pressure.value()) mat.append(self.w.cut_mode.value()) mat.append(self.w.material_thickness.value()) self.write_materials_to_dict(mat) def load_material_file(self, keepTemp=False): self.getMaterialBusy = True self.MATS = Access(self.materialFile) # don't remove temporary materials unless required if keepTemp: pop = [key for key in self.materialDict if key < 1000000] for e in pop: self.materialDict.pop(e) else: self.materialDict = {} self.materialNumList = [] # create a basic default material if no materials exist if not self.MATS.sections(): if self.units == 'mm': mat = [0, 'Basic default Material', 1, 3, .1, 0, 0, 1, 1000, 45, 100, 0, 0, 1, 0] else: mat = [0, 'Basic default Material', .04, .12, .1, 0, 0, .04, 40, 45, 100, 0, 0, 1, 0] self.write_one_material(mat) head = _translate('HandlerClass', 'Materials Error') # read all the materials into the materials dict for section in self.MATS.sections(): matNum = int(section.rsplit('_', 1)[1]) if matNum >= 1000000: msg0 = _translate('HandlerClass', 'Material number') msg1 = _translate('HandlerClass', 'is invalid') msg2 = _translate('HandlerClass', 'Material numbers need to be less than 1000000') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} "{matNum}" {msg1}\n{msg2}\n') continue mat = self.read_one_material(section) self.materialNumList.append(matNum) self.write_materials_to_dict(mat) self.display_materials() self.defaultMaterial = self.PREFS.getpref('Default material', self.materialNumList[0], int, 'GUI_OPTIONS') if not self.material_exists(self.defaultMaterial): self.defaultMaterial = self.materialList[0] self.PREFS.putpref('Default material', self.defaultMaterial, int, 'GUI_OPTIONS') self.change_material(self.defaultMaterial) self.w.materials_box.setCurrentIndex(self.materialList.index(self.defaultMaterial)) self.w.material_selector.setCurrentIndex(self.w.materials_box.currentIndex()) self.w.conv_material.setCurrentIndex(self.w.materials_box.currentIndex()) self.set_default_material() self.getMaterialBusy = False def material_exists(self, matNum): if int(matNum) in self.materialList: return True else: if self.autoChange: self.materialChangePin.set(-1) self.materialChangeNumberPin.set(int(self.w.materials_box.currentText().split(': ', 1)[0])) head = _translate('HandlerClass', 'Materials Error') msg0 = _translate('HandlerClass', 'Material #') msg1 = _translate('HandlerClass', 'not in material list') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{int(matNum)} {msg1}\n') return False def read_one_material(self, section): mat = [int(section.rsplit('_', 1)[1])] mat.append(self.MATS.getpref('NAME', 'Material', str, section)) mat.append(self.MATS.getpref('KERF_WIDTH', 1.0 / self.unitsPerMm, float, section)) mat.append(self.MATS.getpref('PIERCE_HEIGHT', 3.0 / self.unitsPerMm, float, section)) mat.append(self.MATS.getpref('PIERCE_DELAY', 0.2, float, section)) mat.append(self.MATS.getpref('PUDDLE_JUMP_HEIGHT', 0.0, float, section)) mat.append(self.MATS.getpref('PUDDLE_JUMP_DELAY', 0.0, float, section)) mat.append(self.MATS.getpref('CUT_HEIGHT', 1.0 / self.unitsPerMm, float, section)) mat.append(self.MATS.getpref('CUT_SPEED', 2000.0 / self.unitsPerMm, float, section)) mat.append(self.MATS.getpref('CUT_AMPS', 45.0, float, section)) mat.append(self.MATS.getpref('CUT_VOLTS', 100, float, section)) mat.append(self.MATS.getpref('PAUSE_AT_END', 0.0, float, section)) mat.append(self.MATS.getpref('GAS_PRESSURE', 0.0, float, section)) mat.append(self.MATS.getpref('CUT_MODE', 1.0, float, section)) mat.append(self.MATS.getpref('THICKNESS', 0.0, float, section)) return mat def write_one_material(self, mat): section = f'MATERIAL_NUMBER_{mat[0]}' self.MATS.putpref('NAME', mat[1], str, section) self.MATS.putpref('KERF_WIDTH', mat[2], float, section) self.MATS.putpref('PIERCE_HEIGHT', mat[3], float, section) self.MATS.putpref('PIERCE_DELAY', mat[4], float, section) self.MATS.putpref('PUDDLE_JUMP_HEIGHT', mat[5], float, section) self.MATS.putpref('PUDDLE_JUMP_DELAY', mat[6], float, section) self.MATS.putpref('CUT_HEIGHT', mat[7], float, section) self.MATS.putpref('CUT_SPEED', mat[8], float, section) self.MATS.putpref('CUT_AMPS', mat[9], float, section) self.MATS.putpref('CUT_VOLTS', mat[10], float, section) self.MATS.putpref('PAUSE_AT_END', mat[11], float, section) self.MATS.putpref('GAS_PRESSURE', mat[12], float, section) self.MATS.putpref('CUT_MODE', mat[13], float, section) self.MATS.putpref('THICKNESS', mat[14], float, section) def set_default_material(self): self.getMaterialBusy = True self.w.default_material.clear() for n in self.materialNumList: self.w.default_material.addItem(str(n)) self.getMaterialBusy = False self.w.default_material.setCurrentIndex(self.materialList.index(self.defaultMaterial)) def default_material_changed(self, index): if self.getMaterialBusy: return self.defaultMaterial = self.materialList[index] self.change_material(self.defaultMaterial) self.w.materials_box.setCurrentIndex(self.materialList.index(self.defaultMaterial)) self.PREFS.putpref('Default material', self.defaultMaterial, int, 'GUI_OPTIONS') ######################################################################################################################### # CAMERA AND LASER FUNCTIONS # ######################################################################################################################### def camera_pressed(self): self.w.camview.rotation = STATUS.stat.rotation_xy if self.w.preview_stack.currentIndex() != self.CAMERA: self.w.preview_stack.setCurrentIndex(self.CAMERA) else: self.preview_index_return(self.w.preview_stack.currentIndex()) def laser_recovery_state_changed(self, value): hal.set_p('plasmac.laser-recovery-start', '0') def laser_clicked(self): if STATUS.is_interp_paused(): return if self.laserButtonState == 'reset': self.laserButtonState = 'laser' self.button_normal('laser') self.refresh_button_states() return xPos = STATUS.get_position()[0][0] - self.laserOffsetX yPos = STATUS.get_position()[0][1] - self.laserOffsetY if xPos < self.xMin or xPos > self.xMax or yPos < self.yMin or yPos > self.yMax: head = _translate('HandlerClass', 'Laser Error') msg0 = _translate('HandlerClass', 'Laser is outside the machine boundary') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n') return if self.laserButtonState == 'laser': self.w.laser.setText(_translate('HandlerClass', 'MARK\nEDGE')) self.laserButtonState = 'markedge' self.button_active('laser') self.laserOnPin.set(1) self.refresh_button_states() return elif self.laserButtonState == 'setorigin': self.button_normal('laser') self.laserOnPin.set(0) self.laserButtonState = self.sheet_align(self.laserButtonState, self.w.laser, self.laserOffsetX, self.laserOffsetY) self.refresh_button_states() def laser_pressed(self): if STATUS.is_interp_paused() and not self.laserRecStatePin.get(): xPos = STATUS.get_position()[0][0] + self.laserOffsetX yPos = STATUS.get_position()[0][1] + self.laserOffsetY if xPos < self.xMin or xPos > self.xMax or yPos < self.yMin or yPos > self.yMax: head = _translate('HandlerClass', 'Laser Error') msg0 = _translate('HandlerClass', 'Torch cannot move outside the machine boundary') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n') return hal.set_p('plasmac.laser-x-offset', f'{int(self.laserOffsetX / self.oScale)}') hal.set_p('plasmac.laser-y-offset', f'{int(self.laserOffsetY / self.oScale)}') hal.set_p('plasmac.laser-recovery-start', '1') hal.set_p('plasmac.cut-recovery', '1') self.laserOnPin.set(1) return self.laserTimer.start(750) def sheet_align(self, button_state, button, offsetX, offsetY): if button_state == 'markedge': zAngle = self.w.camview.rotation = 0 ACTION.CALL_MDI_WAIT('G10 L2 P0 R0', 3) ACTION.SET_MANUAL_MODE() self.w.gcodegraphics.logger.clear() self.w.cam_goto.setEnabled(False) button.setText(_translate('HandlerClass', 'SET\nORIGIN')) button_state = 'setorigin' self.currentX = STATUS.get_position()[0][0] self.currentY = STATUS.get_position()[0][1] else: if button == self.w.cam_mark: button.setText(_translate('HandlerClass', 'MARK\nEDGE')) button_state = 'markedge' else: button.setText(_translate('HandlerClass', 'LASER')) button_state = 'laser' xDiff = STATUS.get_position()[0][0] - self.currentX yDiff = STATUS.get_position()[0][1] - self.currentY if xDiff and yDiff: zAngle = math.degrees(math.atan(yDiff / xDiff)) if xDiff > 0: zAngle += 180 elif yDiff > 0: zAngle += 360 if abs(xDiff) < abs(yDiff): zAngle -= 90 elif xDiff: if xDiff > 0: zAngle = 180 else: zAngle = 0 elif yDiff: if yDiff > 0: zAngle = 180 else: zAngle = 0 else: zAngle = 0 self.w.camview.rotation = zAngle ACTION.CALL_MDI_WAIT(f'G10 L20 P0 X{offsetX} Y{offsetY}') ACTION.CALL_MDI_WAIT(f'G10 L2 P0 R{zAngle}') ACTION.CALL_MDI('G0 X0 Y0') while not STATUS.is_interp_idle(): self.w.gcodegraphics.update() if self.fileOpened: self.file_reload_clicked() self.w.gcodegraphics.logger.clear() self.w.cam_goto.setEnabled(True) ACTION.SET_MANUAL_MODE() return button_state def cam_mark_clicked(self): xPos = STATUS.get_position()[0][0] - self.camOffsetX yPos = STATUS.get_position()[0][1] - self.camOffsetY if xPos < self.xMin or xPos > self.xMax or yPos < self.yMin or yPos > self.yMax: head = _translate('HandlerClass', 'Camera Error') msg0 = _translate('HandlerClass', 'Camera is outside the machine boundary') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n') return self.camButtonState = self.sheet_align(self.camButtonState, self.w.cam_mark, self.camOffsetX, self.camOffsetY) def cam_goto_clicked(self): ACTION.CALL_MDI_WAIT('G0 X0 Y0') while not STATUS.is_interp_idle(): self.w.gcodegraphics.update() ACTION.SET_MANUAL_MODE() def cam_zoom_plus_pressed(self): if self.w.camview.scale >= 5: return self.w.camview.scale += 0.1 def cam_zoom_minus_pressed(self): if self.w.camview.scale <= 1: return self.w.camview.scale -= 0.1 def cam_dia_plus_pressed(self): if self.w.camview.size().height() < self.w.camview.size().width(): size = self.w.camview.size().height() else: size = self.w.camview.size().width() if self.w.camview.diameter >= size - 5: return self.w.camview.diameter += 2 def cam_dia_minus_pressed(self): if self.w.camview.diameter <= 2: return self.w.camview.diameter -= 2 ######################################################################################################################### # STATISTICS FUNCTIONS # ######################################################################################################################### def statistics_show(self): for stat in ['cut', 'paused', 'probe', 'run', 'torch', 'rapid']: self.display_hms(f'{stat}_time', hal.get_value(f'plasmac.{stat}-time')) self.w.cut_length.setText(f'{hal.get_value("plasmac.cut-length") / self.statsDivisor:0.2f}') self.w.pierce_count.setText(f'{hal.get_value("plasmac.pierce-count"):d}') self.statistics_load() def statistics_save(self, reset=False): self.PREFS.putpref('Cut time', f'{self.statsSaved["cut"] + hal.get_value("plasmac.cut-time"):0.2f}', float, 'STATISTICS') self.PREFS.putpref('Paused time', f'{self.statsSaved["paused"] + hal.get_value("plasmac.paused-time"):0.2f}', float, 'STATISTICS') self.PREFS.putpref('Probe time', f'{self.statsSaved["probe"] + hal.get_value("plasmac.probe-time"):0.2f}', float, 'STATISTICS') self.PREFS.putpref('Program run time', f'{self.statsSaved["run"] + hal.get_value("plasmac.run-time"):0.2f}', float, 'STATISTICS') self.PREFS.putpref('Torch on time', f'{self.statsSaved["torch"] + hal.get_value("plasmac.torch-time"):0.2f}', float, 'STATISTICS') self.PREFS.putpref('Rapid time', f'{self.statsSaved["rapid"] + hal.get_value("plasmac.rapid-time"):0.2f}', float, 'STATISTICS') self.PREFS.putpref('Cut length', f'{self.statsSaved["length"] + hal.get_value("plasmac.cut-length"):0.2f}', float, 'STATISTICS') self.PREFS.putpref('Pierce count', f'{self.statsSaved["pierce"] + hal.get_value("plasmac.pierce-count"):d}', int, 'STATISTICS') self.statistics_load() self.jobRunning = False def statistics_load(self): self.statsSaved['cut'] = self.PREFS.getpref('Cut time', 0.0, float, 'STATISTICS') self.statsSaved['paused'] = self.PREFS.getpref('Paused time', 0.0, float, 'STATISTICS') self.statsSaved['probe'] = self.PREFS.getpref('Probe time', 0.0, float, 'STATISTICS') self.statsSaved['run'] = self.PREFS.getpref('Program run time', 0.0, float, 'STATISTICS') self.statsSaved['torch'] = self.PREFS.getpref('Torch on time', 0.0, float, 'STATISTICS') self.statsSaved['rapid'] = self.PREFS.getpref('Rapid time', 0.0, float, 'STATISTICS') self.statsSaved['length'] = self.PREFS.getpref('Cut length', 0.0, float, 'STATISTICS') self.statsSaved['pierce'] = self.PREFS.getpref('Pierce count', 0, int, 'STATISTICS') for stat in ['cut', 'paused', 'probe', 'run', 'torch', 'rapid']: self.display_hms(f'{stat}_time_t', self.statsSaved[f'{stat}']) self.w.cut_length_t.setText(f'{self.statsSaved["length"] / self.statsDivisor:0.2f}') self.w.pierce_count_t.setText(f'{self.statsSaved["pierce"]:d}') def display_hms(self, widget, time): m, s = divmod(time, 60) h, m = divmod(m, 60) self.w[widget].setText(f'{h:.0f}:{m:02.0f}:{s:02.0f}') def statistic_reset(self, stat, statT): if stat in ['cut_time', 'paused_time', 'probe_time', 'run_time', 'torch_time', 'rapid_time']: self.display_hms(f'{stat}', 0) elif stat == 'cut_length': self.w.cut_length.setText('0.00') elif stat == 'pierce_count': self.w.pierce_count.setText('0') self.PREFS.putpref(statT, 0.0, float, 'STATISTICS') self.statistics_load() def statistics_reset(self): for stat in ['cut', 'paused', 'probe', 'run', 'torch', 'rapid']: self.display_hms(f'{stat}_time', 0) self.w.cut_length.setText('0.00') self.w.pierce_count.setText('0') for stat in ['Cut time', 'Paused time', 'Probe time', 'Program run time', 'Torch on time', 'Rapid time', 'Cut length']: self.PREFS.putpref(stat, 0.0, float, 'STATISTICS') self.PREFS.putpref('Pierce count', 0, int, 'STATISTICS') self.statistics_load() def statistics_init(self): if self.unitsPerMm == 1: self.statsDivisor = 1000 unit = _translate('HandlerClass', 'Metres') else: self.statsDivisor = 1 unit = _translate('HandlerClass', 'Inches') msg0 = _translate('HandlerClass', 'CUT LENGTH') self.w.cut_length_label.setText(f'{msg0} ({unit})') self.statsSaved = {'cut': 0, 'length': 0, 'pierce': 0, 'paused': 0, 'probe': 0, 'rapid': 0, 'run': 0, 'torch': 0} self.jobRunning = False self.statistics_load() ######################################################################################################################### # POWERMAX COMMUNICATIONS FUNCTIONS # ######################################################################################################################### def pmx485_check(self, port, periodic=False): try: import serial import serial.tools.list_ports as PORTS head = _translate('HandlerClass', 'Port Error') msg1 = _translate('HandlerClass', 'Powermax communications are disabled') ports = [] for p in PORTS.comports(): ports.append(p[0]) if port in ports: try: sPort = serial.Serial(port, 19200) sPort.close() except Exception as err: if not periodic: STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{err}\n{msg1}\n') return False else: if not periodic: msg0 = _translate('HandlerClass', 'cannot be found') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{port} {msg0}\n{msg1}\n') return False except: if not periodic: head = _translate('HandlerClass', 'Module Error') msg0 = _translate('HandlerClass', 'python3-serial cannot be found') msg1 = _translate('HandlerClass', 'Install python3-serial or linuxcnc-dev') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}\n') return False return True def pmx485_startup(self, port): self.pmx485CommsError = False self.w.pmx485Status = False self.meshMode = False self.w.pmx485_enable.stateChanged.connect(lambda w: self.pmx485_enable_changed(self.w.pmx485_enable.isChecked())) self.pmx485StatusPin.value_changed.connect(lambda w: self.pmx485_status_changed(w)) self.pmx485ModePin.value_changed.connect(self.pmx485_mode_changed) self.pmx485FaultPin.value_changed.connect(lambda w: self.pmx485_fault_changed(w)) self.pmx485ArcTimePin.value_changed.connect(lambda w: self.pmx485_arc_time_changed(w)) self.w.gas_pressure.valueChanged.connect(self.pmx485_pressure_changed) self.w.mesh_enable.stateChanged.connect(lambda w: self.pmx485_mesh_enable_changed(self.w.mesh_enable.isChecked())) self.pmx485CommsTimer = QTimer() self.pmx485CommsTimer.timeout.connect(self.pmx485_timeout) self.pmx485RetryTimer = QTimer() self.pmx485RetryTimer.timeout.connect(lambda: self.pmx485_enable_changed(True)) self.oldCutMode = self.w.cut_mode.value() self.pressure = self.w.gas_pressure.value() if self.pmx485_load(port): return self.pmx485Exists = True self.pmx485_setup() self.w.pmx485_enable.setChecked(True) def pmx485_load(self, port): head = _translate('HandlerClass', 'Comms Error') msg0 = _translate('HandlerClass', 'PMX485 component is not able to be loaded,') msg1 = _translate('HandlerClass', 'Powermax communications are not available') err = f'{head}:\n{msg0}\n{msg1}\n' count = 0 while not hal.component_exists('pmx485'): if count >= 3: STATUS.emit('error', linuxcnc.OPERATOR_ERROR, err) return 1 RUN(['halcmd', 'loadusr', '-Wn', 'pmx485', 'pmx485', f'{port}']) count += 1 return 0 def pmx485_setup(self): self.pins485Comp = ['pmx485.enable', 'pmx485.status', 'pmx485.fault', 'pmx485.mode_set', 'pmx485.mode', 'pmx485.current_set', 'pmx485.current', 'pmx485.current_min', 'pmx485.current_max', 'pmx485.pressure_set', 'pmx485.pressure', 'pmx485.pressure_min', 'pmx485.pressure_max', 'pmx485.arcTime'] pinsSelf = ['pmx485_enable', 'pmx485_status', 'pmx485_fault', 'cut_mode-f', 'pmx485_mode', 'cut_amps-f', 'pmx485_current', 'pmx485_current_min', 'pmx485_current_max', 'gas_pressure-f', 'pmx485_pressure', 'pmx485_pressure_min', 'pmx485_pressure_max', 'pmx485_arc_time'] pinType = [hal.HAL_BIT, hal.HAL_BIT, hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT] for pin in self.pins485Comp: hal.new_sig(f'plasmac:{pin.replace("pmx485.", "pmx485_")}', pinType[self.pins485Comp.index(pin)]) hal.connect(pin, f'plasmac:{pin.replace("pmx485.", "pmx485_")}') hal.connect(f'qtplasmac.{pinsSelf[self.pins485Comp.index(pin)]}', f'plasmac:{pin.replace("pmx485.", "pmx485_")}') self.pmx485_mesh_enable_changed(self.w.mesh_enable.isChecked()) self.w.cut_amps.setToolTip(_translate('HandlerClass', 'Powermax cutting current')) def pmx485_enable_changed(self, state): if state: self.pmx485CommsError = False self.pmx485RetryTimer.stop() if self.pmx485_load(self.PREFS.getpref('Port', '', str, 'POWERMAX')): return # if pins not connected then connect them if not hal.pin_has_writer('pmx485.enable'): self.pmx485_setup() # ensure valid parameters before trying to connect if self.w.cut_mode.value() == 0 or self.w.cut_amps.value() == 0: head = _translate('HandlerClass', 'Materials Error') msg0 = _translate('HandlerClass', 'Invalid Cut Mode or Cut Amps,') msg1 = _translate('HandlerClass', 'cannot connect to Powermax') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}\n') self.w.pmx485_enable.setChecked(False) return # good to go else: self.w.pmx485_label.setText(_translate('HandlerClass', 'CONNECTING')) self.pmx485LabelState = 'CONNECT' self.pmx485CommsTimer.start(3000) else: self.pmx485Connected = False self.pmx485CommsError = False self.w.pmx485_label.setText('') self.pmx485LabelState = None self.w.pmx485_label.setToolTip(_translate('HandlerClass', 'Status of PMX485 communications')) self.pmx485CommsTimer.stop() self.pmx485RetryTimer.stop() def pmx485_mode_changed(self, widget): if self.pmx485Connected: self.w.gas_pressure.setValue(0) def pmx485_pressure_changed(self, pressure): if self.pmx485Connected: if pressure < self.pressure: if pressure < 0: self.w.gas_pressure.setValue(self.gas_maximum) elif self.w.gas_pressure.value() < self.gas_minimum: self.w.gas_pressure.setValue(0) elif pressure > self.pressure: if pressure > self.gas_maximum: self.w.gas_pressure.setValue(0) elif pressure < self.gas_minimum: self.w.gas_pressure.setValue(self.gas_minimum) self.pressure = self.w.gas_pressure.value() def pmx485_min_max_changed(self): if not self.pmx485Connected: return self.w.cut_amps.setMinimum(self.pmx485CurrentMinPin.get()) self.w.cut_amps.setMaximum(self.pmx485CurrentMaxPin.get()) self.gas_minimum = self.pmx485PressureMinPin.get() self.gas_maximum = self.pmx485PressureMaxPin.get() self.w.gas_pressure.setMinimum(-1) self.w.gas_pressure.setMaximum(self.gas_maximum + 1) if self.gas_maximum > 15: self.w.gas_pressure.setSuffix(' psi') self.w.gas_pressure.setDecimals(0) self.w.gas_pressure.setSingleStep(1) else: self.w.gas_pressure.setSuffix(' bar') self.w.gas_pressure.setDecimals(1) self.w.gas_pressure.setSingleStep(0.1) def pmx485_status_changed(self, state): if state != self.pmx485Connected: if state: self.pmx485CommsError = False self.w.pmx485_label.setText(_translate('HandlerClass', 'CONNECTED')) self.pmx485LabelState = 'CONNECT' self.pmx485Connected = True self.pmx485_min_max_changed() if self.pmx485ArcTimePin.get(): self.pmx485_arc_time_changed(self.pmx485ArcTimePin.get()) if self.pmx485FaultPin.get(): self.pmx485_fault_changed(self.pmx485FaultPin.get()) self.pmx485CommsTimer.stop() self.pmx485RetryTimer.stop() else: self.w.pmx485_label.setText(_translate('HandlerClass', 'COMMS ERROR')) self.pmx485LabelState = None self.pmx485CommsError = True self.pmx485Connected = False self.pmx485RetryTimer.start(3000) def pmx485_arc_time_changed(self, time): if self.pmx485Connected: self.pmx485ArcTime = self.pmx485ArcTimePin.get() self.w.pmx_arc_time_label.setText(_translate('HandlerClass', 'ARC ON TIME')) self.display_hms('pmx_arc_time_t', self.pmx485ArcTime) def pmx485_fault_changed(self, fault): if self.pmx485Connected: faultRaw = f'{fault:04.0f}' self.pmx485FaultCode = f'{faultRaw[0]}-{faultRaw[1:3]}-{faultRaw[3]}' head = _translate('HandlerClass', 'Powermax Error') code = _translate('HandlerClass', 'Fault Code') text = _translate('HandlerClass', 'Powermax error') if faultRaw == '0000': self.w.pmx485_label.setText(_translate('HandlerClass', 'CONNECTED')) self.pmx485LabelState = 'CONNECT' self.w.pmx485_label.setToolTip(_translate('HandlerClass', 'Status of PMX485 communications')) elif faultRaw in self.pmx485FaultName.keys(): if faultRaw == '0210' and self.w.pmx485.current_max.value() > 110: faultMsg = self.pmx485FaultName[faultRaw][1] elif faultRaw == '0210': faultMsg = self.pmx485FaultName[faultRaw][0] else: faultMsg = self.pmx485FaultName[faultRaw] self.w.pmx485_label.setText(f'{code}: {self.pmx485FaultCode}') self.pmx485LabelState = None self.w.pmx485_label.setStatusTip(f'{text} ({self.pmx485FaultCode}) {faultMsg}') msg0 = _translate('HandlerClass', 'CODE') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}: {self.pmx485FaultCode}\n{faultMsg}\n') else: self.w.pmx485_label.setText(f'{code}: {faultRaw}') self.pmx485LabelState = None msg0 = _translate('HandlerClass', 'Unknown Powermax fault code') self.w.pmx485_label.setStatusTip(f'{msg0} ({faultRaw})') STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}: {faultRaw}\n') def pmx485_mesh_enable_changed(self, state): if state and not self.meshMode: self.oldCutMode = self.w.cut_mode.value() self.w.cut_mode.setValue(2) self.w.cut_mode.setEnabled(False) self.meshMode = True elif not hal.get_value('plasmac.mesh-enable') and self.meshMode: self.w.cut_mode.setValue(self.oldCutMode) self.w.cut_mode.setEnabled(True) self.meshMode = False def pmx485_timeout(self): self.pmx485CommsTimer.stop() self.w.pmx485_label.setText(_translate('HandlerClass', 'COMMS ERROR')) self.pmx485LabelState = None self.pmx485CommsError = True self.pmx485Connected = False self.pmx485RetryTimer.start(3000) pmx485FaultName = { '0110': 'Remote controller mode invalid', '0111': 'Remote controller current invalid', '0112': 'Remote controller pressure invalid', '0120': 'Low input gas pressure', '0121': 'Output gas pressure low', '0122': 'Output gas pressure high', '0123': 'Output gas pressure unstable', '0130': 'AC input power unstable', '0199': 'Power board hardware protection', '0200': 'Low gas pressure', '0210': ('Gas flow lost while cutting', 'Excessive arc voltage'), '0220': 'No gas input', '0300': 'Torch stuck open', '0301': 'Torch stuck closed', '0320': 'End of consumable life', '0400': 'PFC/Boost IGBT module under temperature', '0401': 'PFC/Boost IGBT module over temperature', '0402': 'Inverter IGBT module under temperature', '0403': 'Inverter IGBT module over temperature', '0500': 'Retaining cap off', '0510': 'Start/trigger signal on at power up', '0520': 'Torch not connected', '0600': 'AC input voltage phase loss', '0601': 'AC input voltage too low', '0602': 'AC input voltage too high', '0610': 'AC input unstable', '0980': 'Internal communication failure', '0990': 'System hardware fault', '1000': 'Digital signal processor fault', '1100': 'A/D converter fault', '1200': 'I/O fault', '2000': 'A/D converter value out of range', '2010': 'Auxiliary switch disconnected', '2100': 'Inverter module temp sensor open', '2101': 'Inverter module temp sensor shorted', '2110': 'Pressure sensor is open', '2111': 'Pressure sensor is shorted', '2200': 'DSP does not recognize the torch', '3000': 'Bus voltage fault', '3100': 'Fan speed fault', '3101': 'Fan fault', '3110': 'PFC module temperature sensor open', '3111': 'PFC module temperature sensor shorted', '3112': 'PFC module temperature sensor circuit fault', '3200': 'Fill valve', '3201': 'Dump valve', '3202': 'Valve ID', '3203': 'Electronic regulator is disconnected', '3410': 'Drive fault', '3420': '5 or 24 VDC fault', '3421': '18 VDC fault', '3430': 'Inverter capacitors unbalanced', '3441': 'PFC over current', '3511': 'Inverter saturation fault', '3520': 'Inverter shoot-through fault', '3600': 'Power board fault', '3700': 'Internal serial communications fault', } ######################################################################################################################### # CUT RECOVERY FUNCTIONS # ######################################################################################################################### def set_cut_recovery(self): if hal.get_value('plasmac.cut-recovering'): self.w.jog_stack.setCurrentIndex(self.CUT_RECOVERY) return self.w.jog_stack.setCurrentIndex(self.CUT_RECOVERY) self.cutrec_speed_changed(self.w.cut_rec_speed.value()) hal.set_p('plasmac.cut-recovery', '0') self.laserOnPin.set(0) self.xOrig = hal.get_value('axis.x.eoffset-counts') self.yOrig = hal.get_value('axis.y.eoffset-counts') self.zOrig = hal.get_value('axis.z.eoffset-counts') self.oScale = hal.get_value('plasmac.offset-scale') def cutrec_speed_changed(self, speed): text = _translate('HandlerClass', 'FEED') if STATUS.is_metric_mode(): self.w.cut_rec_feed.setText(f'{text}\n{self.w.cut_feed_rate.value() * speed * 0.01:0.0f}') else: self.w.cut_rec_feed.setText(f'{text}\n{self.w.cut_feed_rate.value() * speed * 0.01:0.1f}') def cutrec_move_changed(self, distance): text = _translate('HandlerClass', 'MOVE') self.w.cut_rec_move_label.setText(f'{text}\n{distance}') def cutrec_motion(self, direction): if self.w.cut_rec_fwd.isEnabled() and self.w.cut_rec_rev.isEnabled(): speed = float(self.w.cut_rec_speed.value()) * 0.01 * direction hal.set_p('plasmac.paused-motion-speed', str(speed)) hal.set_p('plasmac.cut-recovery', '1') def cutrec_move(self, state, x, y): if not STATUS.is_interp_paused(): return if state: maxMove = 10 if self.units == 'in': maxMove = 0.4 laser = self.laserRecStatePin.get() > 0 distX = hal.get_value('qtplasmac.kerf_width-f') * x distY = hal.get_value('qtplasmac.kerf_width-f') * y xNew = hal.get_value('plasmac.axis-x-position') + hal.get_value('axis.x.eoffset') - (self.laserOffsetX * laser) + distX yNew = hal.get_value('plasmac.axis-y-position') + hal.get_value('axis.y.eoffset') - (self.laserOffsetY * laser) + distY if xNew > self.xMax or xNew < self.xMin or yNew > self.yMax or yNew < self.yMin: return xTotal = hal.get_value('axis.x.eoffset') - (self.laserOffsetX * laser) + distX yTotal = hal.get_value('axis.y.eoffset') - (self.laserOffsetY * laser) + distY if xTotal > maxMove or xTotal < -maxMove or yTotal > maxMove or yTotal < -maxMove: return moveX = int(distX / self.oScale) moveY = int(distY / self.oScale) hal.set_p('plasmac.x-offset', f'{hal.get_value("plasmac.x-offset") + moveX}') hal.set_p('plasmac.y-offset', f'{hal.get_value("plasmac.y-offset") + moveY}') hal.set_p('plasmac.cut-recovery', '1') def cutrec_offset_changed(self, xOffset, yOffset): if hal.get_value('plasmac.consumable-changing'): return if xOffset > 0.001 * self.unitsPerMm or xOffset < -0.001 * self.unitsPerMm or \ yOffset > 0.001 * self.unitsPerMm or yOffset < -0.001 * self.unitsPerMm: self.refresh_button_states() elif not self.laserRecStatePin.get(): hal.set_p('plasmac.cut-recovery', '0') hal.set_p('plasmac.x-offset', '0') hal.set_p('plasmac.y-offset', '0') self.laserOnPin.set(0) def cutrec_cancel_pressed(self, state): if state: if hal.get_value('plasmac.cut-recovery'): hal.set_p('plasmac.cut-recovery', '0') self.laserOnPin.set(0) ######################################################################################################################### # STYLING FUNCTIONS # ######################################################################################################################### def openColorDialog(self, widget): initColor = QColor(widget.palette().color(QPalette.Background)) options = QColorDialog.DontUseNativeDialog options |= QColorDialog.ShowAlphaChannel color = QColorDialog.getColor(initColor, options=options) if color.isValid(): widget.setStyleSheet(f'background-color: {color.name()}') buttons = ['foregrnd', 'foregalt', 'led', 'backgrnd', 'backgalt', 'frams', 'estop', 'disabled', 'preview'] labels = ['Foreground', 'Highlight', 'LED', 'Background', 'Background Alt', 'Frames', 'Estop', 'Disabled', 'Preview'] button = widget.objectName() label = labels[buttons.index(button.split('_')[1])] self.PREFS.putpref(label, color.name(), str, 'COLOR_OPTIONS') self.set_basic_colors() self.set_color_styles() self.preview_stack_changed() if self.umButton and _WEBENGINE_AVAILABLE: self.w.webview.page().loadFinished.connect(self.style_user_manual) self.w.webview.page().setBackgroundColor(QColor(self.backColor)) self.w.webview.reload() self.simStyleUpdate.set(not self.simStyleUpdate.get()) def set_basic_colors(self): self.foreColor = self.PREFS.getpref('Foreground', '#ffee06', str, 'COLOR_OPTIONS') self.fore1Color = self.PREFS.getpref('Highlight', '#ffee06', str, 'COLOR_OPTIONS') self.backColor = self.PREFS.getpref('Background', '#16160e', str, 'COLOR_OPTIONS') self.back1Color = self.PREFS.getpref('Background Alt', '#26261e', str, 'COLOR_OPTIONS') self.disabledColor = self.PREFS.getpref('Disabled', '#b0b0b0', str, 'COLOR_OPTIONS') self.estopColor = self.PREFS.getpref('Estop', '#ff0000', str, 'COLOR_OPTIONS') def set_color_styles(self): self.styleSheetFile = os.path.join(self.PATHS.CONFIGPATH, 'qtplasmac.qss') ssFile = self.PREFS.getpref('Custom style', 'None', str, 'GUI_OPTIONS') # if custom stylesheet try to use it if ssFile != 'None': COPY(ssFile, self.styleSheetFile) self.custom_stylesheet() # otherwise use the standard stylesheet else: self.standard_stylesheet() # apply the new stylesheet self.w.setStyleSheet('') with open(self.styleSheetFile, 'r') as set_style: self.w.setStyleSheet(set_style.read()) # style some buttons buttons = ['jog_x_minus', 'jog_x_plus', 'jog_y_minus', 'jog_y_plus', 'jog_z_minus', 'jog_z_plus', 'jog_a_minus', 'jog_a_plus', 'jog_b_minus', 'jog_b_plus', 'jog_c_minus', 'jog_c_plus', 'cut_rec_n', 'cut_rec_ne', 'cut_rec_e', 'cut_rec_se', 'cut_rec_s', 'cut_rec_sw', 'cut_rec_w', 'cut_rec_nw', 'conv_line', 'conv_circle', 'conv_ellipse', 'conv_triangle', 'conv_rectangle', 'conv_polygon', 'conv_bolt', 'conv_slot', 'conv_star', 'conv_gusset', 'conv_sector', 'conv_block'] conv_images = ['conv_line_point', 'conv_line_angle', 'conv_line_3p', 'conv_line_2pr', 'conv_arc_angle', 'conv_bolt_l', 'conv_circle_l', 'conv_ellipse_l', 'conv_gusset_l', 'conv_polygon_l', 'conv_rectangle_l', 'conv_sector_l', 'conv_slot_l', 'conv_star_l', 'conv_triangle_l'] for button in buttons: self.color_item(button, self.foreColor, 'button') self.w[button].setStyleSheet(f'QPushButton {{ background: {self.backColor} }} \ QPushButton:pressed {{ background: {self.backColor} }}') for conv_image in conv_images: self.color_item(conv_image, self.foreColor, 'image') # some gcode display/editor colors cannot use .qss file # gcode display current gcode line self.w.gcode_display.setMarkerBackgroundColor(QColor(self.back1Color)) # gcode display active line self.w.gcode_display.setCaretLineBackgroundColor(QColor(self.back1Color)) # gcode editor current gcode line self.w.gcode_editor.editor.setMarkerBackgroundColor(QColor(self.back1Color)) self.w.gcode_editor.editor.setCaretForegroundColor(QColor(self.fore1Color)) # gcode editor active line self.w.gcode_editor.editor.setCaretLineBackgroundColor(QColor(self.backColor)) def standard_stylesheet(self): baseStyleFile = os.path.join(self.PATHS.SCREENDIR, self.PATHS.BASEPATH, 'qtplasmac.style') customStyleFile = os.path.join(self.PATHS.CONFIGPATH, 'qtplasmac_custom.qss') # Read in the base stylesheet file with open(baseStyleFile, 'r') as inFile: lines = inFile.readlines() # Append the base file with changes if custom stylesheet is found if os.path.isfile(customStyleFile): with open(customStyleFile, 'r') as inFile: lines += inFile.readlines() elementColorMap = { 'backalt': self.w.color_backgalt.styleSheet().split(':')[1].strip(), 'backgnd': self.w.color_backgrnd.styleSheet().split(':')[1].strip(), 'e-stop': self.w.color_estop.styleSheet().split(':')[1].strip(), 'foregnd': self.w.color_foregrnd.styleSheet().split(':')[1].strip(), 'frames': self.w.color_frams.styleSheet().split(':')[1].strip(), 'highlight': self.w.color_foregalt.styleSheet().split(':')[1].strip(), 'inactive': self.w.color_disabled.styleSheet().split(':')[1].strip(), 'l-e-d': self.w.color_led.styleSheet().split(':')[1].strip(), 'prevu': self.w.color_preview.styleSheet().split(':')[1].strip() } with open(self.styleSheetFile, 'w') as outFile: for line in lines: for element, color in elementColorMap.items(): if element in line: line = line.replace(element, color) outFile.write(line) def custom_stylesheet(self): head = _translate('HandlerClass', 'Stylesheet Error') try: # set basic colors from stylesheet header colors = [0, 0, 0, 0, 0] with open(self.styleSheetFile, 'r') as inFile: for line in inFile: if line.startswith('color1'): colors[0] += 1 self.foreColor = QColor(line.split('=')[1].strip()).name() elif line.startswith('color2'): colors[1] += 1 self.backColor = QColor(line.split('=')[1].strip()).name() elif line.startswith('color3'): colors[2] += 1 self.fore1Color = QColor(line.split('=')[1].strip()).name() elif line.startswith('color4'): colors[3] += 1 self.back1Color = QColor(line.split('=')[1].strip()).name() elif line.startswith('color5'): colors[4] += 1 self.disabledColor = QColor(line.split('=')[1].strip()).name() if line.startswith('*'): break if colors != [1, 1, 1, 1, 1]: raise ColorError() # hide color buttons for button in ['color_foregrnd', 'color_foregrnd_lbl', 'color_foregalt', 'color_foregalt_lbl', 'color_backgrnd', 'color_backgrnd_lbl', 'color_backgalt', 'color_backgalt_lbl', 'color_frams', 'color_frams_lbl', 'color_estop', 'color_estop_lbl', 'color_disabled', 'color_disabled_lbl', 'color_preview', 'color_preview_lbl', 'color_led', 'color_led_lbl']: self.w[button].hide() for button in ['camera', 'laser', self.ctButton, self.tpButton, self.ptButton, self.ccButton]: if button: self.button_normal(button) except ColorError: msg0 = _translate('HandlerClass', 'Invalid number of colors defined') msg1 = _translate('HandlerClass', 'in custom stylesheet header') msg2 = _translate('HandlerClass', 'Reverting to standard stylesheet') self.dialog_show_ok(QMessageBox.Warning, f'{head}', f'\n{msg0}\n{msg1}\n\n{msg2}\n') self.standard_stylesheet() except: msg0 = _translate('HandlerClass', 'Cannot open custom stylesheet') msg1 = _translate('HandlerClass', 'Reverting to standard stylesheet') self.dialog_show_ok(QMessageBox.Warning, f'{head}', f'\n{msg0}\n\n{msg1}\n') self.standard_stylesheet() def color_item(self, item, color, type): image_path = f'{self.IMAGES}{item}.svg' self.image = QPixmap(image_path) colorChange = QPainter(self.image) colorChange.setCompositionMode(QPainter.CompositionMode_SourceIn) colorChange.fillRect(self.image.rect(), QColor(color)) colorChange.end() if type == 'button': self.w[item].setIcon(QIcon(self.image)) elif type == 'image': self[item] = QPixmap(self.image) def style_user_manual(self): # There is a brief delay between the "loadFinished" signal and the versioning site's readiness for CSS changes delayTime = 500 if 'qtplasmac/versions.html' in self.w.webview.url().toString() else 0 customStyling = f""" setTimeout(function() {{ var style = document.createElement('style'); style.innerHTML = ` /* Apply background color to elements */ .caption, a, blockquote, body, figcaption, caption, code, div, div.content, h1, h2, h3, h4, h5, h6, table, td, th, pre, ol, ul {{ background-color: {self.backColor} !important; }} /* Apply foreground color to elements */ body, blockquote, caption, div, li, td, p {{ color: {self.foreColor} !important; }} /* Apply highlight color to elements */ .caption, a, code, div.title, dt, em, figcaption, h1, h2, h3, h4, h5, h6, span, strong, th, tt, ul {{ color: {self.fore1Color} !important; }} /* Change table borders color and fix sizing */ table {{ border: 2px solid {self.foreColor} !important; border-collapse: collapse !important; }} /* Change table divider color and fix sizing */ td, th {{ border: 1px solid {self.foreColor} !important; }} /* Remove borders from these elements, or things look odd after the other styling */ hr, div, div.content {{ border: none !important; }} /* Apply highlight color to header underline */ h1, h2, h3, h4, h5, h6 {{ border-bottom: solid {self.fore1Color} !important; }} /* Some images have a transparent background, this makes them visible */ img {{ background-color: white !important; }} /* Apply alternate background color to highlighted sections (on section link click from TOC) */ :target {{ background: {self.back1Color} !important; }} /* The following change scroll bar to match GUI styling */ ::-webkit-scrollbar {{ width: 20px; height: 20px; }} ::-webkit-scrollbar-thumb {{ background: {self.foreColor} !important; border-radius: 4px; min-height: 40px !important; min-width: 40px !important; }} ::-webkit-scrollbar-track {{ background: {self.back1Color} !important; border-radius: 4px; }} ::-webkit-scrollbar-corner {{ background: {self.backColor} !important; }} `; document.head.appendChild(style); }}, {delayTime}); """ self.w.webview.page().runJavaScript(customStyling) ######################################################################################################################### # KEY BINDING CALLS # ######################################################################################################################### def key_is_valid(self, event, state): return self.keyboard_shortcuts() and state and not event.isAutoRepeat() def jog_is_valid(self, key, event): return self.keyboard_shortcuts() and not event.isAutoRepeat() and self.w.main_tab_widget.currentIndex() == self.MAIN and self.w[f'jog_{key}'].isEnabled() def on_keycall_ESTOP(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and self.w.estopButton == 2: ACTION.SET_ESTOP_STATE(STATUS.estop_is_clear()) def on_keycall_POWER(self, event, state, shift, cntrl): if self.key_is_valid(event, state): ACTION.SET_MACHINE_STATE(not STATUS.machine_is_on()) def on_keycall_ABORT(self, event, state, shift, cntrl): if self.key_is_valid(event, state): self.abort_pressed() def on_keycall_HOME(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and cntrl and not shift and self.w.main_tab_widget.currentIndex() == self.MAIN and STATUS.is_on_and_idle() and self.w.home_all.isEnabled(): if STATUS.is_all_homed(): ACTION.SET_MACHINE_UNHOMED(-1) else: ACTION.SET_MACHINE_HOMING(-1) def on_keycall_RUN(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and cntrl and not shift and self.w.main_tab_widget.currentIndex() == self.MAIN: if self.w.run.isEnabled(): self.run_clicked() elif self.w.pause_resume.isEnabled() and STATUS.stat.paused: self.pause_resume_pressed() ACTION.RESUME() def on_keycall_PAUSE(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and not shift and self.w.main_tab_widget.currentIndex() == self.MAIN: if cntrl: if self.w.screen_options.desktop_notify: self.w.screen_options.QTVCP_INSTANCE_._NOTICE.external_close() self.error_status(False) elif self.w.pause_resume.isEnabled() and not STATUS.stat.paused: ACTION.PAUSE() def on_keycall_OPEN(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN and \ self.w.file_open.isEnabled(): self.file_open_clicked() def on_keycall_LOAD(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN and \ self.w.file_reload.isEnabled(): self.file_reload_clicked() def on_keycall_JOINT(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN: self.toggle_joint_mode() def on_keycall_F12(self, event, state, shift, cntrl): if self.key_is_valid(event, state): self.STYLEEDITOR.load_dialog() def on_keycall_F9(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN and \ not self.probeTest and not self.torchPulse and not self.framing and \ (STATUS.is_interp_idle() or (self.manualCut and STATUS.is_interp_paused())): self.manual_cut() def on_keycall_XPOS(self, event, state, shift, cntrl): if self.jog_is_valid('x_plus', event): if STATUS.is_joint_mode(): self.kb_jog(state, self.coordinates.index('x'), 1, shift) else: self.kb_jog(state, 0, 1, shift) def on_keycall_XNEG(self, event, state, shift, cntrl): if self.jog_is_valid('x_minus', event): if STATUS.is_joint_mode(): self.kb_jog(state, self.coordinates.index('x'), -1, shift) else: self.kb_jog(state, 0, -1, shift) def on_keycall_YPOS(self, event, state, shift, cntrl): if self.jog_is_valid('y_plus', event): if STATUS.is_joint_mode(): self.kb_jog(state, self.coordinates.index('y'), 1, shift) else: self.kb_jog(state, 1, 1, shift) def on_keycall_YNEG(self, event, state, shift, cntrl): if self.jog_is_valid('y_minus', event): if STATUS.is_joint_mode(): self.kb_jog(state, self.coordinates.index('y'), -1, shift) else: self.kb_jog(state, 1, -1, shift) def on_keycall_ZPOS(self, event, state, shift, cntrl): if self.jog_is_valid('z_plus', event): if self.zPlusOverrideJog and self.w.chk_override_jog.isEnabled(): self.w.chk_override_jog.setChecked(True) if STATUS.is_joint_mode(): self.kb_jog(state, self.coordinates.index('z'), 1, shift) else: self.kb_jog(state, 2, 1, shift) def on_keycall_ZNEG(self, event, state, shift, cntrl): if self.jog_is_valid('z_minus', event): if STATUS.is_joint_mode(): self.kb_jog(state, self.coordinates.index('z'), -1, shift) else: self.kb_jog(state, 2, -1, shift) def on_keycall_APOS(self, event, state, shift, cntrl): if self.jog_is_valid('a_plus', event): if STATUS.is_joint_mode(): self.kb_jog(state, self.coordinates.index('a'), 1, shift) else: self.kb_jog(state, 3, 1, shift) def on_keycall_ANEG(self, event, state, shift, cntrl): if self.jog_is_valid('a_minus', event): if STATUS.is_joint_mode(): self.kb_jog(state, self.coordinates.index('a'), -1, shift) else: self.kb_jog(state, 3, -1, shift) def on_keycall_BPOS(self, event, state, shift, cntrl): if self.jog_is_valid('b_plus', event): if STATUS.is_joint_mode(): self.kb_jog(state, self.coordinates.index('b'), 1, shift) else: self.kb_jog(state, 4, 1, shift) def on_keycall_BNEG(self, event, state, shift, cntrl): if self.jog_is_valid('b_minus', event): if STATUS.is_joint_mode(): self.kb_jog(state, self.coordinates.index('b'), -1, shift) else: self.kb_jog(state, 4, -1, shift) # FIXME - keys for these jogs not defined yet # def on_keycall_CPOS(self, event, state, shift, cntrl): # if self.jog_is_valid('c_plus', event): # if STATUS.is_joint_mode(): # self.kb_jog(state, self.coordinates.index('c'), 1, shift) # else: # self.kb_jog(state, 5, 1, shift) # def on_keycall_CNEG(self, event, state, shift, cntrl): # if self.jog_is_valid('c_minus', event): # if STATUS.is_joint_mode(): # self.kb_jog(state, self.coordinates.index('c'), -1, shift) # else: # self.kb_jog(state, 5, -1, shift) def on_keycall_PLUS(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN and self.jogSlow and self.w.jog_slider.isEnabled(): return if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN: self.jogFast = True else: self.jogFast = False def on_keycall_MINUS(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN and self.jogFast and self.w.jog_slider.isEnabled(): return if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN: self.jogSlow = True else: self.jogSlow = False def on_keycall_NUMBER(self, event, state, shift, cntrl, number): if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN: if shift and cntrl: pass elif shift and not cntrl: if number: self.w.rapid_slider.setValue(10 * number) else: self.w.rapid_slider.setValue(100) elif cntrl and not shift: if number: self.w.feed_slider.setValue(10 * number) else: self.w.feed_slider.setValue(100) else: if number and self.w.jog_slider.isEnabled(): if self.w.jog_slow.isChecked(): self.w.jog_slider.setValue(INFO.DEFAULT_LINEAR_JOG_VEL * 0.10 * number / self.slowJogFactor) else: self.w.jog_slider.setValue(INFO.DEFAULT_LINEAR_JOG_VEL * 0.10 * number) elif self.w.jog_slider.isEnabled(): if self.w.jog_slow.isChecked(): self.w.jog_slider.setValue(INFO.DEFAULT_LINEAR_JOG_VEL / self.slowJogFactor) else: self.w.jog_slider.setValue(INFO.DEFAULT_LINEAR_JOG_VEL) def on_keycall_END(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN and self.w.touch_xy.isEnabled(): self.touch_xy_clicked() def on_keycall_DELETE(self, event, state, shift, cntrl): if self.keyboard_shortcuts() and self.w.main_tab_widget.currentIndex() == self.MAIN and self.w.laser.isEnabled(): if state and not event.isAutoRepeat(): self.extLaserButton = True self.laser_pressed() else: self.extLaserButton = False self.laser_clicked() def on_keycall_ALTRETURN(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and not cntrl and not shift and self.w.main_tab_widget.currentIndex() == self.MAIN and self.w.mdi_show.isEnabled(): self.mdi_show_clicked() def on_keycall_RETURN(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and not cntrl and not shift and self.w.main_tab_widget.currentIndex() == self.MAIN and self.w.gcode_stack.currentIndex() == self.MDI and self.w.mdi_show.isEnabled(): self.mdi_show_clicked() def on_keycall_QUOTELEFT(self, event, state, shift, cntrl): if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN: if shift and cntrl: pass elif cntrl and not shift: pass elif shift and not cntrl: self.w.rapid_slider.setValue(0) else: self.w.jog_slider.setValue(0) ######################################################################################################################### # required class boiler code # ######################################################################################################################### def __getitem__(self, item): return getattr(self, item) def __setitem__(self, item, value): return setattr(self, item, value) ######################################################################################################################### # required handler boiler code # ######################################################################################################################### def get_handlers(halcomp, widgets, paths): return [HandlerClass(halcomp, widgets, paths)]