#!/usr/bin/env python3 # Qtvcp versa probe # # Copyright (c) 2018 Chris Morley # # 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. # # a probe screen based on Versa probe screen import sys import os import hal import json import re from qtpy import QtGui, QtCore, QtWidgets, uic from qtpy.QtCore import QProcess, QEvent, Qt, Property from qtpy.QtWidgets import QDialogButtonBox, QAbstractSlider from qtvcp.widgets.widget_baseclass import _HalWidgetBase from qtvcp.core import Status, Action, Info, Path from qtvcp.widgets.dialogMixin import GeometryMixin from qtvcp import logger # Instantiate the libraries with global reference # STATUS gives us status messages from linuxcnc # LOG is for running code logging STATUS = Status() ACTION = Action() INFO = Info() PATH = Path() LOG = logger.getLogger(__name__) # Force the log level for this module #LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL current_dir = os.path.dirname(__file__) SUBPROGRAM = os.path.abspath(os.path.join(current_dir, 'probe_subprog.py')) # can use/favours local image and help files HELP = PATH.find_widget_path() ICONPATH = os.path.join(PATH.find_image_path(), 'probe_icons') DEFAULT = 0 WARNING = 1 CRITICAL = 2 class VersaProbeParent(QtWidgets.QWidget, _HalWidgetBase): def __init__(self, parent=None): super(VersaProbeParent, self).__init__(parent) self.proc = None self.tool_diameter = None self.tool_number = None self.probe_number = -1 self._nextIndex = 0 self._cmd = None self._runImmediately = True STATUS.connect('tool-info-changed', lambda w, data: self._tool_info(data)) if INFO.MACHINE_IS_METRIC: self.valid = QtGui.QRegularExpressionValidator(QtCore.QRegularExpression(r'^((\d{1,4}(\.\d{1,3})?)|(\.\d{1,3}))$')) else: self.valid = QtGui.QRegularExpressionValidator(QtCore.QRegularExpression(r'^((\d{1,3}(\.\d{1,4})?)|(\.\d{1,4}))$')) self.setMinimumSize(600, 420) # Load the widgets UI file will use local file if available: self.filename = PATH.find_widget_path('versa_probe.ui') try: self.instance = uic.loadUi(self.filename, self) except AttributeError as e: LOG.critical(e) self.dialog_code = 'CALCULATOR' self.hilightStyle = "border: 2px solid red;" #create parameter dictionary self.send_dict = {} # these parameters are sent to the subprogram # the is also the order of the next widget when dialog 'next' button is pressed self.parm_list = ['probe_diam', 'rapid_vel', 'search_vel', 'probe_vel', 'xy_clearance', 'z_clearance', 'side_edge_length', 'max_travel', 'max_z_travel', 'latch_return_dist', 'tool_probe_height', 'tool_block_height', 'adj_x', 'adj_y', 'adj_z', 'adj_angle',] self.status_list = ['xm', 'xc', 'xp', 'ym', 'yc', 'yp', 'lx', 'ly', 'z', 'd', 'a','th','bh'] for i in self.parm_list: self['input_' + i].setValidator(self.valid) # button connections self.btn_help.clicked.connect(self.help_clicked) self.inside_buttonGroup.buttonClicked.connect(self.probe_btn_clicked) self.outside_buttonGroup.buttonClicked.connect(self.probe_btn_clicked) self.skew_buttonGroup.buttonClicked.connect(self.probe_btn_clicked) self.length_buttonGroup.buttonClicked.connect(self.probe_btn_clicked) self.tool_buttonGroup.buttonClicked.connect(self.probe_btn_clicked) self.pbtn_set_x.released.connect(self.pbtn_set_x_released) self.pbtn_set_y.released.connect(self.pbtn_set_y_released) self.pbtn_set_z.released.connect(self.pbtn_set_z_released) self.pbtn_set_angle.released.connect(self.pbtn_set_angle_released) self.buildToolTip(self.input_search_vel, 'Search Velocity', 'search_vel') self.buildToolTip(self.input_probe_vel, 'Probe Velocity', 'probe_vel') self.buildToolTip(self.input_z_clearance, 'Z Clearence Distance', 'Zclearance') self.buildToolTip(self.input_max_travel, 'Maximum Probe Search Distance', 'rapid') self.buildToolTip(self.input_latch_return_dist, 'Return After Latch Distance', 'rapid_return') self.buildToolTip(self.input_probe_diam,'Probe Diameter','probe_diam') self.buildToolTip(self.input_xy_clearance, 'XY Clearence Distance', 'XYclearance') self.buildToolTip(self.input_side_edge_length, 'Edge length', 'edge_length') #self.buildToolTip(self.input_tool_probe_height, '', '') #self.buildToolTip(self.input_tool_block_height, '', '') #self.buildToolTip(self.input_adj_x, '', '') #self.buildToolTip(self.input_adj_y, '', '') #self.buildToolTip(self.input_adj_z, '', '') #self.buildToolTip(self.input_adj_angle, '', '') self.buildToolTip(self.input_rapid_vel, 'Rapid Velocity', 'rapid_vel') # catch focusIn event to pop calculator dialog def eventFilter(self, obj, event): if event.type() == QEvent.FocusIn: if isinstance(obj, QtWidgets.QLineEdit): # only if mouse selected if event.reason () == 0: self._nextIndex = self.parm_list.index(obj.objectName().replace('input_','')) self.popEntry(obj) if self.dialog_code == 'CALCULATOR': obj.clearFocus() event.accept() return True return super(VersaProbeParent, self).eventFilter(obj, event) # keep track of tool number and diameter # update the probe loaded HAL pin # can be used to inhibit the spindle def _tool_info(self, data): if data.id != -1: self.tool_diameter = data.diameter self.tool_number = data.id if self.probe_number == self.tool_number: self.probe_loaded.set(True) else: self.probe_loaded.set(False) return self.tool_diameter = None self.tool_number = None self.probe_loaded.set(False) def _hal_init(self): def homed_on_test(): return (STATUS.machine_is_on() and (STATUS.is_all_homed() or INFO.NO_HOME_REQUIRED)) # get current style of the line input self._oldstyle = self.input_search_vel.styleSheet() # have to call hal_init on widgets in this widget ourselves # qtvcp doesn't see them otherwise oldname = self.HAL_GCOMP_.comp.getprefix() self.HAL_GCOMP_.comp.setprefix('qtversaprobe') self.pbtn_use_tool_measurement.setProperty('pin_name','enable') self.pbtn_use_tool_measurement.hal_init() self.HAL_GCOMP_.comp.setprefix(oldname) self.allow_auto_skew.hal_init() self.allow_auto_zero.hal_init() self.statuslabel_motiontype.hal_init() self.statelabel_machineUnits.hal_init() self.statelabel_machineUnits_2.hal_init() self.help = HelpDialog(self.QTVCP_INSTANCE_) self.help.hal_init(HAL_NAME='_versa_help') # connect to STATUS STATUS.connect('state-off', lambda w: self.setEnabled(False)) STATUS.connect('state-estop', lambda w: self.setEnabled(False)) STATUS.connect('interp-idle', lambda w: self.setEnabled(homed_on_test())) STATUS.connect('all-homed', lambda w: self.setEnabled(homed_on_test())) # STATUS.connect('error', self.send_error) STATUS.connect('periodic', lambda w: self.check_probe()) STATUS.connect('general',self.return_value) # install event filters on all the lineedits # so we can call up a dialog when lineedit get focus self.input_search_vel.installEventFilter(self) self.input_probe_vel.installEventFilter(self) self.input_z_clearance.installEventFilter(self) self.input_max_travel.installEventFilter(self) self.input_max_z_travel.installEventFilter(self) self.input_latch_return_dist.installEventFilter(self) self.input_probe_diam.installEventFilter(self) self.input_xy_clearance.installEventFilter(self) self.input_side_edge_length.installEventFilter(self) self.input_tool_probe_height.installEventFilter(self) self.input_tool_block_height.installEventFilter(self) self.input_adj_x.installEventFilter(self) self.input_adj_y.installEventFilter(self) self.input_adj_z.installEventFilter(self) self.input_adj_angle.installEventFilter(self) self.input_rapid_vel.installEventFilter(self) self.set_checkableButtons(not self._runImmediately) if self.PREFS_: self.probe_number = self.PREFS_.getpref('ps_probe_tool', -1, int, 'VERSA_PROBE_OPTIONS') self.input_search_vel.setText(str(self.PREFS_.getpref( "ps_searchvel", 300.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_probe_vel.setText(str(self.PREFS_.getpref( "ps_probevel", 10.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_z_clearance.setText(str(self.PREFS_.getpref( "ps_z_clearance", 3.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_max_travel.setText(str(self.PREFS_.getpref( "ps_probe_max", 1.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_max_z_travel.setText(str(self.PREFS_.getpref( "ps_probe_max_z_travel", 1.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_latch_return_dist.setText(str(self.PREFS_.getpref( "ps_probe_latch", 0.5, float, 'VERSA_PROBE_OPTIONS')) ) self.input_probe_diam.setText(str(self.PREFS_.getpref( "ps_probe_diam", 2.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_xy_clearance.setText(str(self.PREFS_.getpref( "ps_xy_clearance", 5.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_side_edge_length.setText(str(self.PREFS_.getpref( "ps_side_edge_length", 5.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_tool_probe_height.setText(str(self.PREFS_.getpref( "ps_probe_height", 20.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_tool_block_height.setText(str(self.PREFS_.getpref( "ps_block_height", 20.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_adj_x.setText(str(self.PREFS_.getpref( "ps_offs_x", 0.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_adj_y.setText(str(self.PREFS_.getpref( "ps_offs_y", 0.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_adj_z.setText(str(self.PREFS_.getpref( "ps_offs_z", 0.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_adj_angle.setText(str(self.PREFS_.getpref( "ps_offs_angle", 0.0, float, 'VERSA_PROBE_OPTIONS')) ) self.input_rapid_vel.setText(str(self.PREFS_.getpref( "ps_probe_rapid_vel", 60.0, float, 'VERSA_PROBE_OPTIONS')) ) self.pbtn_use_tool_measurement.setChecked((self.PREFS_.getpref( "use_tool_measurement", True, bool, 'VERSA_PROBE_OPTIONS')) ) self.allow_auto_zero.setChecked((self.PREFS_.getpref( "use_auto_zero", True, bool, 'VERSA_PROBE_OPTIONS')) ) self.allow_auto_skew.setChecked((self.PREFS_.getpref( "use_auto_skew", True, bool, 'VERSA_PROBE_OPTIONS')) ) self.z_max_clear = INFO.get_safe_float("VERSA_TOOLSETTER", "Z_MAX_CLEAR") self.ts_x = INFO.get_safe_float('VERSA_TOOLSETTER','X') self.ts_y = INFO.get_safe_float('VERSA_TOOLSETTER','Y') self.ts_z = INFO.get_safe_float('VERSA_TOOLSETTER','Z') self.ts_max = INFO.get_safe_float('VERSA_TOOLSETTER','MAXPROBE') self.ts_diam = INFO.get_safe_float('VERSA_TOOLSETTER','DIAMETER') # make pins available for tool measure remaps oldname = self.HAL_GCOMP_.comp.getprefix() self.HAL_GCOMP_.comp.setprefix('qtversaprobe') self.pin_svel = self.HAL_GCOMP_.newpin("searchvel", hal.HAL_FLOAT, hal.HAL_OUT) self.pin_svel.set(float(self.input_search_vel.text())) self.pin_pvel = self.HAL_GCOMP_.newpin("probevel", hal.HAL_FLOAT, hal.HAL_OUT) self.pin_pvel.set(float(self.input_probe_vel.text())) self.pin_pheight = self.HAL_GCOMP_.newpin("probeheight", hal.HAL_FLOAT, hal.HAL_OUT) self.pin_pheight.set(float(self.input_tool_probe_height.text())) self.pin_bheight = self.HAL_GCOMP_.newpin("blockheight", hal.HAL_FLOAT, hal.HAL_OUT) self.pin_bheight.set(float(self.input_tool_block_height.text())) self.pin_latch_rtn = self.HAL_GCOMP_.newpin("backoffdist", hal.HAL_FLOAT, hal.HAL_OUT) self.pin_latch_rtn.set(float(self.input_latch_return_dist.text())) self.probe_loaded = self.HAL_GCOMP_.newpin("probe-loaded", hal.HAL_BIT, hal.HAL_OUT) self.HAL_GCOMP_.comp.setprefix(oldname) # install callbacks to update HAL pins self.input_search_vel.textChanged.connect(self.update_search_vel_pin) self.input_probe_vel.textChanged.connect(self.update_probe_vel_pin) self.input_tool_probe_height.textChanged.connect(self.update_probe_height_pin) self.input_tool_block_height.textChanged.connect(self.update_block_height_pin) self.input_latch_return_dist.textChanged.connect(self.update_latch_return_dist_pin) # when qtvcp closes this gets called def _hal_cleanup(self): if self.PREFS_: LOG.debug('Saving Versa probe data to preference file.') self.PREFS_.putpref('ps_probe_tool', self.probe_number, int, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_searchvel", float(self.input_search_vel.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_probevel", float(self.input_probe_vel.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_z_clearance", float(self.input_z_clearance.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_probe_max", float(self.input_max_travel.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_probe_max_z_travel", float(self.input_max_z_travel.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_probe_latch", float(self.input_latch_return_dist.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_probe_diam", float(self.input_probe_diam.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_xy_clearance", float(self.input_xy_clearance.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_side_edge_length", float(self.input_side_edge_length.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_probe_height", float(self.input_tool_probe_height.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_block_height", float(self.input_tool_block_height.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_offs_x", float(self.input_adj_x.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_offs_y", float(self.input_adj_y.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_offs_z", float(self.input_adj_z.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_offs_angle", float(self.input_adj_angle.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "ps_probe_rapid_vel", float(self.input_rapid_vel.text()), float, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "use_tool_measurement", bool(self.pbtn_use_tool_measurement.isChecked()), bool, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "use_auto_zero", bool(self.allow_auto_zero.isChecked()), bool, 'VERSA_PROBE_OPTIONS') self.PREFS_.putpref( "use_auto_skew", bool(self.allow_auto_skew.isChecked()), bool, 'VERSA_PROBE_OPTIONS') # process the STATUS return message # set the line edit to the value if not cancelled def return_value(self, w, message): num = message['RETURN'] code = bool(message.get('ID') == '%s__'% self.objectName()) name = bool(message.get('NAME') == self.dialog_code) next = message.get('NEXT', False) back = message.get('BACK', False) if code and name: obj = message.get('OBJECT') if num is not None: LOG.debug('message return:{}'.format (message)) obj.setText(str(num)) # clear high lighting obj.setStyleSheet(self._oldstyle) # request for next input widget from parm_list if next: self._nextIndex += 1 if self._nextIndex == len(self.parm_list): self._nextIndex = 0 newobj = self['input_{}'.format(self.parm_list[self._nextIndex])] # update the dialog self.popEntry(newobj,True) elif back: self._nextIndex -= 1 if self._nextIndex == -1: self._nextIndex = len(self.parm_list)-1 newobj = self['input_{}'.format(self.parm_list[self._nextIndex])] # update the dialog self.popEntry(newobj,True) def popEntry(self, obj, next=False): obj.setStyleSheet(self.hilightStyle) mess = {'NAME':self.dialog_code, 'ID':'%s__' % self.objectName(), 'OVERLAY':False, 'OBJECT':obj, 'TITLE':'Set Entry for {}'.format(obj.objectName().upper()), 'GEONAME':'_{}'.format(self.dialog_code), 'NEXT':next, 'WIDGETCYCLE': True } STATUS.emit('dialog-request', mess) LOG.debug('message sent:{}'.format (mess)) def buildToolTip(self,obj, text, icon): path = os.path.join(ICONPATH, icon) obj.setToolTip('''

{}

''' .format(text, path)) ############################################# # process control ############################################# def start_process(self): self.proc = QProcess() self.proc.setReadChannel(QProcess.StandardOutput) self.proc.started.connect(self.process_started) self.proc.readyReadStandardOutput.connect(self.read_stdout) self.proc.readyReadStandardError.connect(self.read_stderror) self.proc.finished.connect(self.process_finished) self.proc.start('python3 {}'.format(SUBPROGRAM)) def start_probe(self, cmd): if self.proc is not None: LOG.info("Probe Routine processor is busy") return ACTION.RECORD_CURRENT_MODE() self.start_process() string_to_send = cmd + '$' + json.dumps(self.send_dict) + '\n' #print("String to send ", string_to_send) STATUS.block_error_polling() self.proc.writeData(bytes(string_to_send, 'utf-8')) def process_started(self): LOG.info("Versa_Probe started with PID {}\n".format(self.proc.processId())) def read_stdout(self): qba = self.proc.readAllStandardOutput() line = qba.data() self.parse_input(line) def read_stderror(self): qba = self.proc.readAllStandardError() line = qba.data() self.parse_input(line) def process_finished(self, exitCode, exitStatus): LOG.info(("Probe Process finished - exitCode {} exitStatus {}".format(exitCode, exitStatus))) self.proc = None STATUS.unblock_error_polling() ACTION.RESTORE_RECORDED_MODE() def parse_input(self, line): line = line.decode("utf-8") if "ERROR INFO" in line: ACTION.SET_ERROR_MESSAGE(line) elif "ERROR" in line: #print(line) STATUS.unblock_error_polling() ACTION.SET_ERROR_MESSAGE('Versa Probe process finished in error') elif "PROBE_ROUTINES" in line: if LOG.getEffectiveLevel() < logger.INFO: print(line) elif "INFO" in line: pass elif "COMPLETE" in line: STATUS.unblock_error_polling() LOG.info("Versa Probing routine completed without errors") return_data = line.rstrip().split('$') data = json.loads(return_data[1]) self.show_results(data) elif "HISTORY" in line: if not self.set_statusbar(line, DEFAULT): STATUS.emit('update-machine-log', line, 'TIME') elif "DEBUG" in line: pass else: LOG.error("Error parsing return data from sub_processor. Line={}".format(line)) ##################################################### # button callbacks ##################################################### def help_clicked(self): self.pop_help() def probe_btn_clicked(self, button): cmd = button #print("Button clicked ", cmd.property('probe'),self._cmd) # run probe when buttons are pressed if self._runImmediately: self.set_statusbar('Versa Probe: Start Cycle: {}'.format(cmd.toolTip(),DEFAULT)) self.get_parms() self.start_probe(cmd.property('probe')) # select probe when buttons pressed, run when start_cycle function called else: # Auto exclusive doesn't allow unchecking all buttons # We force it here if cmd == self._cmd: button.group().setExclusive(False) button.blockSignals(True) button.setChecked(False) button.blockSignals(False) button.group().setExclusive(True) self._cmd = None return self.set_statusbar('Versa probe: Selected: {}'.format(cmd.toolTip()),DEFAULT,noLog=True) self._cmd = cmd # called externally to run selected routine # if not in _runImmediately mode def cycle_start(self): if self._runImmediately: self.set_statusbar('Versa Probe set to run buttons immediately',WARNING) return if self._cmd is None: self.set_statusbar('No Versa Probe probe selected',WARNING) return self.set_statusbar('Versa Probe: Start Cycle: {}'.format(self._cmd.toolTip(),DEFAULT)) self.get_parms() self.start_probe(self._cmd.property('probe')) ###### set origin offset ###################### def pbtn_set_x_released(self): ACTION.SET_AXIS_ORIGIN('X', float(self.input_adj_x.text())) def pbtn_set_y_released(self): ACTION.SET_AXIS_ORIGIN('Y', float(self.input_adj_y.text())) def pbtn_set_z_released(self): ACTION.SET_AXIS_ORIGIN('Z', float(self.input_adj_z.text())) def pbtn_set_angle_released(self): self.status_a = "%.3f" % float(self.input_adj_angle.text()) s="G10 L2 P0" if self.allow_auto_zero.isChecked(): s += " X%.4f"% float(self.input_adj_x.text()) s += " Y%.4f"% float(self.input_adj_y.text()) else : a = STATUS.get_probed_position_with_offsets() s += " X%.4f"%a[0] s += " Y%.4f"%a[1] s += " R%.4f"% float(self.input_adj_angle.text()) ACTION.CALL_MDI_WAIT(s, 30) # selects probe routine group of buttons def input_next(self): next = self.stackedWidget_probe_type.currentIndex() +1 if next == self.stackedWidget_probe_type.count(): next = 0 self.stackedWidget_probe_type.setCurrentIndex(next) if self._runImmediately: return # Auto exclusive doesn't allow unchecking all buttons # We force it here if not self._cmd is None: button = self._cmd button.group().setExclusive(False) button.blockSignals(True) button.setChecked(False) button.blockSignals(False) button.group().setExclusive(True) self._cmd = None ##################################################### # Entry callbacks ##################################################### def update_search_vel_pin(self, text): try: self.pin_svel.set(float(text)) except: pass def update_probe_vel_pin(self, text): try: self.pin_pvel.set(float(text)) except: pass def update_probe_height_pin(self, text): try: self.pin_pheight.set(float(text)) except: pass def update_block_height_pin(self, text): value = float(text) #if value == self.pin_bheight.get(): return origin = INFO.INI.getreal("AXIS_Z", "MIN_LIMIT") + value ACTION.CALL_MDI_WAIT( "G10 L2 P0 Z%s" % origin ) try: self.pin_bheight.set(value) except: pass def update_latch_return_dist_pin(self, text): try: self.pin_latch_rtn.set(float(text)) except: pass ##################################################### # Helper functions ##################################################### # return false if failed so other ways of reporting can be used. # there might not be a statusbar in main screen. def set_statusbar(self, msg, priority = DEFAULT, noLog = False): try: self.QTVCP_INSTANCE_.add_status(msg, priority, noLog) except: return False return True def get_parms(self): self.send_dict = {key: self['input_' + key].text() for key in (self.parm_list)} for key in ['allow_auto_zero', 'allow_auto_skew']: val = '1' if self[key].isChecked() else '0' self.send_dict.update( {key: val} ) # come from INI for key in ['ts_diam','z_max_clear','ts_x','ts_y','ts_z','ts_max','tool_diameter','tool_number']: val = str(self[key]) if val == 'NONE': val = None self.send_dict.update( {key: val} ) def check_probe(self): try: self.led_probe_function_chk.setState(hal.get_value('motion.probe-input')) except: pass def show_results(self, line): for key in self.status_list: if key in('th','bh'): if key == 'bh' and line[key] != 'None': self.input_tool_block_height.setText(line[key]) elif line[key] != 'None': self.input_tool_probe_height.setText(line[key]) elif line[key] != 'None': self['status_' + key].setText(line[key]) else: self['status_' + key].setText('') def pop_help(self): self.help.showDialog() # run-immediately buttons are momentary # pre-select buttons are checkable # focus policy changes for 'cycle start button steering' # with pre-select option def set_checkableButtons(self, state): if state: policy = Qt.ClickFocus else: policy = Qt.NoFocus for i in self.outside_buttonGroup.buttons(): i.setCheckable(state) for i in self.inside_buttonGroup.buttons(): i.setCheckable(state) for i in self.skew_buttonGroup.buttons(): i.setCheckable(state) for i in self.length_buttonGroup.buttons(): i.setCheckable(state) for i in self.tool_buttonGroup.buttons(): i.setCheckable(state) ######################################## # required boiler code ######################################## def __getitem__(self, item): return getattr(self, item) def __setitem__(self, item, value): return setattr(self, item, value) ######################################################################### # This is how designer can interact with our widget properties. # designer will show the Property properties in the editor # it will use the get set and reset calls to do those actions ######################################################################### def set_dialog_code(self, data): self.dialog_code = data def get_dialog_code(self): return self.dialog_code def reset_dialog_code(self): self.dialog_code = 'CALCULATOR' dialogCodeString = Property(str, get_dialog_code, set_dialog_code, reset_dialog_code) def set_runImmediately(self, data): self._runImmediately = data self.set_checkableButtons(not data) def get_runImmediately(self): return self._runImmediately def reset_runImmediately(self): self._runImmediately = True # toggle run on button push or run on function call runImmediately = Property(bool, get_runImmediately, set_runImmediately, reset_runImmediately) class HelpDialog(QtWidgets.QDialog, GeometryMixin): def __init__(self, parent=None): super(HelpDialog, self).__init__(parent) self._title = 'Versa Help' self.setWindowFlags(self.windowFlags() | Qt.Tool | Qt.Dialog | Qt.WindowStaysOnTopHint | Qt.WindowSystemMenuHint) self.currentHelpPage=-1 self.setMinimumWidth(600) self.setMinimumHeight(600) self.helpPages = ['versa_usage.html','versa_usage1.html','versa_usage2.html', 'versa_usage3.html','versa_usage4.html','versa_usage5.html', 'versa_usage6.html','versa_usage7.html','versa_usage8.html'] def _hal_init(self): self.buildWidget() self.set_default_geometry() self.read_preference_geometry('VersaHelpDialog-geometry') def buildWidget(self): l = QtWidgets.QVBoxLayout() t = QtWidgets.QTextEdit('Versa Probe Help') t.setReadOnly(True) l.addWidget(t) buttons = QDialogButtonBox() closebutton = QtWidgets.QPushButton() closebutton.setIconSize(QtCore.QSize(38, 38)) closebutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/standardbutton-cancel-128.png')) closebutton.clicked.connect(lambda : self.close()) nextbutton = QtWidgets.QPushButton() nextbutton.setIconSize(QtCore.QSize(38, 38)) nextbutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/right-32.png')) nextbutton.clicked.connect(lambda : self.next(t,True)) previousbutton = QtWidgets.QPushButton() previousbutton.setIconSize(QtCore.QSize(38, 38)) previousbutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/left-32.png')) previousbutton.clicked.connect(lambda : self.next(t,False)) self.pageStepUpbutton = QtWidgets.QPushButton() self.pageStepUpbutton.setIconSize(QtCore.QSize(38, 38)) self.pageStepUpbutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/up-32.png')) self.pageStepUpbutton.clicked.connect(lambda : self.pageStep(t,False)) self.pageStepDwnbutton = QtWidgets.QPushButton() self.pageStepDwnbutton.setIconSize(QtCore.QSize(38, 38)) self.pageStepDwnbutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/down-32.png')) self.pageStepDwnbutton.clicked.connect(lambda : self.pageStep(t,True)) bBox = QDialogButtonBox(buttons) bBox.addButton(self.pageStepUpbutton, QDialogButtonBox.ActionRole) bBox.addButton(self.pageStepDwnbutton, QDialogButtonBox.ActionRole) bBox.addButton(previousbutton, QDialogButtonBox.ActionRole) bBox.addButton(nextbutton, QDialogButtonBox.ActionRole) bBox.addButton(closebutton, QDialogButtonBox.DestructiveRole) bBox.rejected.connect(self.reject) l.addWidget(bBox) self.setLayout(l) # Load the first help page lazily (on first show): pre-scaling its # images at construction can hang or crash qtvcp headless. self._helpText = t self._helpLoaded = False def _set_scaled_html(self, t, html): # QTextEdit scales raster images with a nearest-neighbour filter, which # makes the help diagrams look jagged. Pre-scale each raster to # its requested size with a smooth filter and register it as a document # resource so Qt draws it 1:1. SVG images are left untouched so Qt's # vector renderer draws them crisply at any size. doc = t.document() for tag in re.findall(r']*>', html): match = re.search(r'src="([^"]+)"', tag) if match is None or match.group(1).lower().endswith('.svg'): continue src = match.group(1) image = QtGui.QImage(src) if image.isNull(): continue width = re.search(r'width="(\d+)"', tag) height = re.search(r'height="(\d+)"', tag) if width is not None and int(width.group(1)) != image.width(): image = image.scaledToWidth(int(width.group(1)), QtCore.Qt.SmoothTransformation) elif height is not None and int(height.group(1)) != image.height(): image = image.scaledToHeight(int(height.group(1)), QtCore.Qt.SmoothTransformation) doc.addResource(QtGui.QTextDocument.ImageResource, QtCore.QUrl(src), image) t.setHtml(html) def next(self,t,direction=None): if direction is None: self.currentHelpPage = 0 elif direction: self.currentHelpPage +=1 if self.currentHelpPage > len(self.helpPages)-1: self.currentHelpPage = len(self.helpPages)-1 else: self.currentHelpPage -=1 if self.currentHelpPage < 0: self.currentHelpPage = 0 try: pagePath = os.path.join(HELP, self.helpPages[self.currentHelpPage]) file = QtCore.QFile(pagePath) file.open(QtCore.QFile.ReadOnly) html = file.readAll() html = str(html, encoding='utf8') html = html.replace("../images/","{}/".format(PATH.IMAGEDIR)) self._set_scaled_html(t, html) if t.verticalScrollBar().isVisible(): t.verticalScrollBar().setPageStep(20) self.pageStepDwnbutton.show() self.pageStepUpbutton.show() else: self.pageStepDwnbutton.hide() self.pageStepUpbutton.hide() except Exception as e: t.setText('Versa Probe Help file Unavailable:\n\n{}'.format(e)) if direction is None: return self.show() def pageStep(self, t, state): if state: t.verticalScrollBar().triggerAction (QAbstractSlider.SliderPageStepAdd) else: t.verticalScrollBar().triggerAction (QAbstractSlider.SliderPageStepSub) # accept button applies presets and if line number given starts linuxcnc def close(self): self.record_geometry() super(HelpDialog, self).close() def showDialog(self): if not self._helpLoaded: try: self.next(self._helpText) except Exception as e: self._helpText.setText('Versa Probe Help file Unavailable:\n\n{}'.format(e)) self._helpLoaded = True self.setWindowTitle(self._title); self.set_geometry() retval = self.exec() LOG.debug('Value of pressed button: {}'.format(retval)) # look for a custom version of Versa Probe module = PATH.find_custom_widget_path('versa_probe.py','VersaProbeCustom') if not module: module = VersaProbeParent class VersaProbe(module): def __init__(self, parent=None): super(VersaProbe, self).__init__(parent) #################################### # Testing #################################### if __name__ == "__main__": from qtpy.QtWidgets import * from qtpy.QtCore import * from qtpy.QtGui import * app = QtWidgets.QApplication(sys.argv) w = VersaProbeParent() w.setObjectName('versaprobeParent') w.show() sys.exit( app.exec() )