#!/usr/bin/env python3 # Qtvcp probe subprogram # # 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. # This subprogram is used by both versa_probe and basic_probe widgets import sys import time import json from qtpy.QtCore import QObject from qtvcp.core import Status, Action, Info from qtvcp.widgets.probe_routines import ProbeRoutines # Instantiate the libraries with global reference # STATUS gives us status messages from linuxcnc STATUS = Status() ACTION = Action() INFO = Info() class ProbeSubprog(QObject, ProbeRoutines): def __init__(self): QObject.__init__(self) ProbeRoutines.__init__(self) if INFO.MACHINE_IS_METRIC: self._format_template = "%3.3f" else: self._format_template = "%2.4f" self.send_dict = {} self.error_status = None # list of parameters received from main probe program # excluding booleans, these are handled separately self.parm_list = ['probe_diam', 'latch_return_dist', 'max_travel', 'max_z_travel', 'search_vel', 'probe_vel', 'rapid_vel', 'side_edge_length', 'xy_clearance', 'adj_x', 'adj_y', 'adj_z', 'adj_angle', 'x_hint_bp', 'y_hint_bp', 'x_hint_rv', 'y_hint_rv', 'diameter_hint', 'z_clearance', 'extra_depth', 'cal_x_width', 'cal_y_width', 'cal_diameter', 'calibration_offset', 'ts_diam', 'z_max_clear', 'ts_x', 'ts_y', 'ts_z', 'ts_max', 'tool_diameter', 'tool_number', 'tool_probe_height', 'tool_block_height'] # data structure to hold parameters # common self.data_probe_diam = 1.0 self.data_latch_return_dist = 1.0 self.data_search_vel = 10.0 self.data_probe_vel = 10.0 self.data_rapid_vel = 10.0 self.data_max_travel = 10.0 self.data_max_z_travel = 10.0 self.data_side_edge_length = 1.0 self.data_xy_clearance = 1.0 self.data_z_clearance = 1.0 self.data_extra_depth = 0.0 self.allow_auto_zero = False self.allow_auto_skew = False # VersaProbe exclusive self.data_adj_x = 0.0 self.data_adj_y = 0.0 self.data_adj_z = 0.0 self.data_adj_angle = 0.0 self.data_ts_diam = None self.data_z_max_clear = None self.data_ts_x= None self.data_ts_y= None self.data_ts_z= None self.data_ts_max = None self.data_tool_diameter = None self.data_tool_number = None self.data_tool_probe_height = None self.data_tool_block_height = None # BasicProbe exclusive self.data_x_hint_bp = 0.0 self.data_y_hint_bp = 0.0 self.data_x_hint_rv = 0.0 self.data_y_hint_rv = 0.0 self.data_diameter_hint = 0.0 self.data_cal_x_width = 0.0 self.data_cal_y_width = 0.0 self.data_cal_diameter = 0.0 self.data_calibration_offset = 0.0 self.cal_avg_error = False self.cal_x_error = False self.cal_y_error = False # list of results to be transferred to main program self.status_list = ['xm', 'xc', 'xp', 'ym', 'yc', 'yp', 'lx', 'ly', 'z', 'd', 'a', 'delta','th','bh'] # data structure to hold result values self.status_xm = None self.status_xc = None self.status_xp = None self.status_ym = None self.status_yc = None self.status_yp = None self.status_lx = None self.status_ly = None self.status_z = None self.status_d = None self.status_a = None self.status_delta = None self.status_th = None self.status_bh = None self.history_log = "" self.process() def process(self): while 1: try: line = sys.stdin.readline() except KeyboardInterrupt: break if line: cmd = line line = None try: error = self.process_command(cmd) # block polling here 0 main program should start polling in their end STATUS.block_error_polling() # error = 1 means success, # error = None means ignore, # anything else is an error - a returned string is an error message if error is not None: if error != 1: if type(error) == str: sys.stdout.write("ERROR INFO Probe routine: {}\n".format(error)) else: sys.stdout.write("ERROR Probe routine returned with error\n") else: self.collect_status() sys.stdout.write("COMPLETE$" + json.dumps(self.send_dict) + "\n") sys.stdout.flush() sys.stdout.flush() # print history if self.history_log != "": time.sleep(0.1) sys.stdout.write("HISTORY {}\n".format(self.history_log)) self.history_log = "" sys.stdout.flush() except Exception as e: sys.stdout.write("ERROR Command Error: {}\n".format(e)) sys.stdout.flush() break # check that the command is actually a method in our class else # this message isn't for us - ignore it def process_command(self, cmd): cmd = cmd.rstrip().split('$') if cmd[0] in dir(self): if not STATUS.is_on_and_idle(): return None pre = self.prechecks() if pre is not None: return pre parms = json.loads(cmd[1]) self.update_data(parms) # start polling errors here - parent program should have blocked their polling STATUS.unblock_error_polling() error = self[cmd[0]]() if (error != 1 or type(error)== str) and STATUS.is_on_and_idle(): ACTION.CALL_MDI("G90") self.postreset() return error else: return 'Command function {} not in probe routines'.format(cmd[0]) def update_data(self, parms): for key in parms: if key in self.parm_list: try: self['data_' + key] = float(parms[key].encode('utf-8')) except: pass self.allow_auto_zero = True if parms['allow_auto_zero'] == '1' else False self.allow_auto_skew = True if parms['allow_auto_skew'] == '1' else False try: self.cal_avg_error = True if parms['cal_avg_error'] == '1' else False self.cal_x_error = True if parms['cal_x_error'] == '1' else False self.cal_y_error = True if parms['cal_y_error'] == '1' else False except: pass # adjust z_clearance data #self.data_z_clearance += self.data_extra_depth # clear all previous probe results for i in (self.status_list): self['status_' + i] = None def collect_status(self): try: tmpl = lambda s: self._format_template % s for key in self.status_list: if self['status_' + key] is None: data = 'None' else: data = tmpl(self['status_' + key]) self.send_dict.update( {key: data} ) except Exception as e: print('ERROR ',e) return #for key in self.status_list: # data = "{:3.3f}".format(self['status_' + key]) # self.send_dict.update( {key: data} ) #return # need to be in the right mode - entries are in machine units def prechecks(self): # This is a work around. If a user sets the spindle running in MDI # but turn it off with a manual button, then when M72 will turn the # spindle back on! So we explicitly set M5 here. ACTION.CALL_MDI('M5') # record current G,S,M codes ACTION.CALL_MDI('M70') # set proper mode based on what machine is based if INFO.MACHINE_IS_METRIC and STATUS.is_metric_mode(): return None if not INFO.MACHINE_IS_METRIC and not STATUS.is_metric_mode(): return None if INFO.MACHINE_IS_METRIC: ACTION.CALL_MDI('g21') else: ACTION.CALL_MDI('g20') return None # return to previous motion modes def postreset(self): ACTION.CALL_MDI('M72') ######################################## # required boiler code ######################################## def __getitem__(self, item): return getattr(self, item) def __setitem__(self, item, value): return setattr(self, item, value) #################################### # Testing #################################### if __name__ == "__main__": w = ProbeSubprog()