import os import linuxcnc import collections # Set up logging from . import logger # log instance holder variable LOG = None class _IStat(object): def __init__(self, ini=None): # only initialize once for all instances if self.__class__._instanceNum >= 1: return self.__class__._instanceNum += 1 global LOG LOG = logger.getLogger(__name__) # Force the log level for this module only #LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL inipath = os.environ.get('INI_FILE_NAME', '/dev/null') self.LINUXCNC_IS_RUNNING = bool(inipath != '/dev/null') if not self.LINUXCNC_IS_RUNNING: # Reset the log level for this module # Linuxcnc isn't running so we expect INI errors LOG.setLevel(logger.CRITICAL) self.ENVIRO_INI_PATH = inipath # if no INI was given use the one from the environment if ini is None: ini = inipath self.INIPATH = ini or '/dev/null' LOG.debug('INI Path: {}'.format(self.INIPATH)) self.INI = linuxcnc.ini(self.INIPATH) # Are we using RIP or installed version? self.RIP_FLAG = bool(os.environ.get('LINUXCNC_RIP_FLAG', False)) self.BASE = self.get_base() self.LINUXCNC_VERSION = self.get_linuxcnc_version() self.MACRO_PATH_LIST = [] self.SUB_PATH_LIST = [] self.USER_M_PATH_LIST = [] self.TITLE = "" self.ICON = "" # this is updated by parent caller on startup self.IS_SCREEN = False self.update() def update(self): ct = self.INI.getreal('DISPLAY', 'CYCLE_TIME', fallback=100) # possibly in seconds or ms if ct < 1: self.CYCLE_TIME = int(ct * 1000) else: self.CYCLE_TIME = int(ct) self.GRAPHICS_CYCLE_TIME = self.INI.getint('DISPLAY', 'GRAPHICS_CYCLE_TIME', fallback=100) # in seconds self.HALPIN_CYCLE_TIME = self.INI.getint('DISPLAY', 'HALPIN_CYCLE_TIME', fallback=100) # in seconds self.MDI_HISTORY_PATH = self.INI.getstring('DISPLAY', 'MDI_HISTORY_FILE', fallback='~/.axis_mdi_history') self.QTVCP_LOG_HISTORY_PATH = self.INI.getstring('DISPLAY', 'LOG_FILE', fallback='~/qtvcp.log') self.MACHINE_LOG_HISTORY_PATH = self.INI.getstring('DISPLAY', 'MACHINE_LOG_PATH', fallback='~/.machine_log_history') self.PREFERENCE_PATH = self.INI.getstring("DISPLAY", "PREFERENCE_FILE_PATH") # or None self.PROGRAM_PREFIX = self.get_error_safe_setting("DISPLAY", "PROGRAM_PREFIX", '~/linuxcnc/nc_files') temp = os.path.expanduser(self.PROGRAM_PREFIX) if not os.path.exists(temp): LOG.warning(f'Path not valid in INI File [DISPLAY] PROGRAM_PREFIX section: {temp}') temp = self.INI.getstring("DISPLAY", "USER_COMMAND_FILE") if not temp is None: self.USER_COMMAND_FILE = os.path.expanduser(temp) else: self.USER_COMMAND_FILE = None self.STARTUP_CODES = self.INI.getstring('RS274NGC', 'RS274NGC_STARTUP_CODE') # or None self.SUB_PATH = self.INI.getstring("RS274NGC", "SUBROUTINE_PATH") # or None if self.SUB_PATH is not None: for mpath in (self.SUB_PATH.split(':')): temp = os.path.expanduser(mpath) if not os.path.exists(temp): LOG.warning(f'Path not valid in INI File [RS274NGC] SUBROUTINE_PATH section: {temp}') self.SUB_PATH_LIST.append(mpath) if 'macro' in mpath: path = mpath self.MACRO_PATH_LIST.append(mpath) self.MACRO_PATH = mpath or None else: self.MACRO_PATH = None self.USER_M_PATH = self.INI.getstring("RS274NGC", "USER_M_PATH") # or None if self.USER_M_PATH is not None: for mpath in (self.USER_M_PATH.split(':')): self.USER_M_PATH_LIST.append(mpath) self.NGC_SUB_PATH = self.INI.getstring("DISPLAY","NGCGUI_SUBFILE_PATH") # or None if not self.NGC_SUB_PATH is None: self.NGC_SUB_PATH = os.path.expanduser(self.NGC_SUB_PATH) self.NGC_SUB = self.INI.findall("DISPLAY", "NGCGUI_SUBFILE") or None self.MACHINE_IS_LATHE = self.INI.getbool("DISPLAY", "LATHE", fallback=False) try: self.MACHINE_IS_QTPLASMAC = 'qtplasmac' in self.INI.getstring("DISPLAY", "DISPLAY") except: self.MACHINE_IS_QTPLASMAC = False extensions = self.INI.findall("FILTER", "PROGRAM_EXTENSION") self.PROGRAM_FILTERS = ([e.split(None, 1) for e in extensions]) or None self.PROGRAM_FILTERS_EXTENSIONS = self.get_filters_extensions() self.VALID_PROGRAM_EXTENSIONS = self.get_all_valid_extensions() self.PARAMETER_FILE = self.INI.getstring("RS274NGC", "PARAMETER_FILE") # or None if self.PARAMETER_FILE is None and self.LINUXCNC_IS_RUNNING: LOG.critical('Missing PARAMETER_FILE setting in RS274NGC section') try: # check the INI file if UNITS are set to mm" # first check the global settings units = self.INI.getstring("TRAJ", "LINEAR_UNITS") if units is None: if self.LINUXCNC_IS_RUNNING: LOG.critical('Missing LINEAR_UNITS in TRAJ, guessing units for machine from JOINT 0') # else then guess; The joint 0 is usually X axis units = self.INI.getstring("JOINT_0", "UNITS") if units is None: if self.LINUXCNC_IS_RUNNING: LOG.critical('Missing UNITS in JOINT_0, assuming metric based machine') units = 'metric' except: units = "metric" finally: units = units.lower() # set up the conversion arrays based on what units we discovered if units == "mm" or units == "metric" or units == "1.0": self.MACHINE_IS_METRIC = True self.MACHINE_UNIT_CONVERSION = 1.0 / 25.4 self.MACHINE_UNIT_CONVERSION_9 = [1.0 / 25.4] * 3 + [1] * 3 + [1.0 / 25.4] * 3 self.MACHINE_UNIT_CONVERSION_10 = [1.0 / 25.4] * 3 + [1] * 3 + [1.0 / 25.4] * 3 + [1] LOG.debug('Machine is METRIC based. unit Conversion constant={}'.format(self.MACHINE_UNIT_CONVERSION)) else: self.MACHINE_IS_METRIC = False self.MACHINE_UNIT_CONVERSION = 25.4 self.MACHINE_UNIT_CONVERSION_9 = [25.4] * 3 + [1] * 3 + [25.4] * 3 self.MACHINE_UNIT_CONVERSION_10 = [25.4] * 3 + [1] * 3 + [25.4] * 3 + [1] LOG.debug('Machine is IMPERIAL based. unit Conversion constant={}'.format(self.MACHINE_UNIT_CONVERSION)) axes = self.INI.getstring("TRAJ", "COORDINATES") try: self.trajcoordinates = self.INI.getstring("TRAJ", "COORDINATES").lower().replace(" ","") except: self.trajcoordinates ='xyz' self.AVAILABLE_AXES = [] self.AVAILABLE_JOINTS = [] self.GET_NAME_FROM_JOINT = {} self.GET_JOG_FROM_NAME = {} if axes is not None: # i.e. LCNC is running, not just in Qt Designer axes = axes.replace(" ", "") self.TRAJCO = axes.lower() LOG.debug('TRAJ COORDINATES: {}'.format(axes)) temp = [] for num, letter in enumerate(axes): temp.append(letter) # list of available axes if letter not in self.AVAILABLE_AXES: self.AVAILABLE_AXES.append(letter.upper()) # map of axis designation from joint number # This allows calling joints x2 or y2 etc count = collections.Counter(temp) if count[letter] > 1: c = letter + str(count[letter]) else: c = letter self.GET_NAME_FROM_JOINT[num] = c # map of axis designation to joint-to-jog when in axis mode. # so then you can jog either joint of an axis to move the axis if count[letter] > 1: self.GET_JOG_FROM_NAME[c] = self.GET_JOG_FROM_NAME[letter] else: self.GET_JOG_FROM_NAME[c] = num # list of available joint numbers self.AVAILABLE_JOINTS.append(num) # AXIS sanity check av = self.INI.find('AXIS_%s' % letter.upper(), 'MAX_VELOCITY') # or None aa = self.INI.find('AXIS_%s' % letter.upper(), 'MAX_ACCELERATION') # or None if av is None or aa is None: # some lathe configs have dummy Y axis for axis rotation G code if letter.upper() == "Y" and self.MACHINE_IS_LATHE: pass else: LOG.critical( 'MISSING [AXIS_{}] MAX VELOCITY or MAX ACCELERATION entry in INI file.'.format(letter.upper())) # convert joint number to axis index # used by dro_widget self.GET_AXIS_INDEX_FROM_JOINT_NUM = {} self.GET_JOINT_NUM_FROM_AXIS_INDEX = {} for i in self.AVAILABLE_JOINTS: let = self.GET_NAME_FROM_JOINT[i][0] axisnum = "XYZABCUVW".index(let) self.GET_AXIS_INDEX_FROM_JOINT_NUM[int(i)] = int(axisnum) self.GET_JOINT_NUM_FROM_AXIS_INDEX[int(axisnum)] = int(i) self.NO_HOME_REQUIRED = self.INI.getint("TRAJ", "NO_FORCE_HOMING", fallback=0) # home all check self.HOME_ALL_FLAG = 1 # set Home All Flag only if ALL joints specify a HOME_SEQUENCE jointcount = len(self.AVAILABLE_JOINTS) self.JOINT_SEQUENCE_LIST = {} for j in range(jointcount): seq = self.INI.getint("JOINT_" + str(j), "HOME_SEQUENCE") if seq is None: seq = 0 self.HOME_ALL_FLAG = 0 self.JOINT_SEQUENCE_LIST[j] = seq # joint sequence/type self.JOINT_TYPE = [None] * jointcount self.JOINT_TYPE_INT = [None] * jointcount self.JOINT_SEQUENCE = [None] * jointcount self.HAS_ANGULAR_JOINT = False for j in range(jointcount): section = "JOINT_%d" % j self.JOINT_TYPE[j] = self.INI.find(section, "TYPE") or "LINEAR" if self.JOINT_TYPE[j] == "LINEAR": self.JOINT_TYPE_INT[j] = 1 else: self.JOINT_TYPE_INT[j] = 2 self.HAS_ANGULAR_JOINT = True self.JOINT_SEQUENCE[j] = self.INI.getint(section, "HOME_SEQUENCE", fallback=0) # jog synchronized sequence # gives a list of joints combined to make an axis templist = [] for j in self.AVAILABLE_JOINTS: temp = [] flag = False for hj, hs in list(self.JOINT_SEQUENCE_LIST.items()): if abs(int(hs)) == abs(int(self.JOINT_SEQUENCE_LIST.get(j))): temp.append(hj) if int(hs) < 0: flag = True if flag: templist.append(temp) # remove duplicates self.JOINT_SYNC_LIST = list(set(tuple(sorted(sub)) for sub in templist)) # This is a list of joints that are related to a joint. #ie. JOINT_RELATIONS_LIST(0) will give a list of joints that go with joint 0 # to make an axis or else a list with just 0 in it. # current use case is to find out what other joints should be unhomed if you unhome # a combined joint axis. self.JOINT_RELATIONS_LIST = [None] * jointcount for j in range(jointcount): temp = [] for hj, hs in list(self.JOINT_SEQUENCE_LIST.items()): # the absolute numbers must be equal first if abs(int(hs)) == abs(int(self.JOINT_SEQUENCE_LIST.get(j))): # theN one has to be negative to signal syncing if int(hs) <0 or int(self.JOINT_SEQUENCE_LIST.get(j)) < 0: temp.append(hj) # If empty list: no synced joints, just add the jointcount number if temp == []: temp.append(j) self.JOINT_RELATIONS_LIST[j] = temp # jogging increments increments = self.INI.find("DISPLAY", "INCREMENTS") if increments: if "," in increments: self.JOG_INCREMENTS = [i.strip() for i in increments.split(",")] else: self.JOG_INCREMENTS = increments.split() if not "continuous" in increments: self.JOG_INCREMENTS.insert(0, "Continuous") else: if self.MACHINE_IS_METRIC: self.JOG_INCREMENTS = ["Continuous", ".001 mm", ".01 mm", ".1 mm", "1 mm"] else: self.JOG_INCREMENTS = ["Continuous", ".0001 in", ".001 in", ".01 in", ".1 in"] LOG.warning('Missing [DISPLAY] LINEAR_INCREMENTS- using defaults.') # angular jogging increments increments = self.INI.find("DISPLAY", "ANGULAR_INCREMENTS") if increments: if "," in increments: self.ANGULAR_INCREMENTS = [i.strip() for i in increments.split(",")] else: self.ANGULAR_INCREMENTS = increments.split() if not "continuous" in increments: self.ANGULAR_INCREMENTS.insert(0, "Continuous") else: self.ANGULAR_INCREMENTS = ["Continuous", "1", "45", "180", "360"] if self.HAS_ANGULAR_JOINT: LOG.warning('Missing [DISPLAY] ANGULAR_INCREMENTS- using defaults.') # grid increments grid_increments = self.INI.find("DISPLAY", "GRIDS") if grid_increments: if "," in grid_increments: self.GRID_INCREMENTS = [i.strip() for i in grid_increments.split(",")] else: self.GRID_INCREMENTS = grid_increments.split() flag = True for i in grid_increments: if i.upper() in ('0', 'OFF'): flag = False break if flag: self.GRID_INCREMENTS.insert(0, '0') else: if self.MACHINE_IS_METRIC: self.GRID_INCREMENTS = ["0", ".1 mm", "1 mm", "10 mm", "50 mm"] else: self.GRID_INCREMENTS = ["0", ".5 in", "1 in", "2 in", "6 in"] temp = self.INI.find("TRAJ", "COORDINATES") if temp: self.TRAJ_COORDINATES = temp.lower().replace(" ", "") else: self.TRAJ_COORDINATES = None self.JOINT_COUNT = self.INI.getint("KINS", "JOINTS", fallback=0) # check for weird kinematics like robots self.IS_TRIVIAL_MACHINE = bool('trivkins' in self.get_error_safe_setting("KINS", "KINEMATICS",'trivial')) kinsmodule = self.INI.getstring("KINS", "KINEMATICS", fallback='trivkins') if kinsmodule.split()[0] == "trivkins": self.trivkinscoords = "XYZABCUVW" for item in kinsmodule.split(): if "coordinates=" in item: self.trivkinscoords = item.split("=")[1].upper() safe = 25 if self.MACHINE_IS_METRIC else 1 self.DEFAULT_LINEAR_JOG_VEL = self.get_error_safe_float("DISPLAY", "DEFAULT_LINEAR_VELOCITY", safe) * 60 self.MIN_LINEAR_JOG_VEL = self.get_error_safe_float("DISPLAY", "MIN_LINEAR_VELOCITY", 0) * 60 safe = 125 if self.MACHINE_IS_METRIC else 5 self.MAX_LINEAR_JOG_VEL = self.get_error_safe_float("DISPLAY", "MAX_LINEAR_VELOCITY", safe) * 60 LOG.debug('DEFAULT_LINEAR_VELOCITY = {}'.format(self.DEFAULT_LINEAR_JOG_VEL)) LOG.debug('MIN_LINEAR_VELOCITY = {}'.format(self.MIN_LINEAR_JOG_VEL)) LOG.debug('MAX_LINEAR_VELOCITY = {}'.format(self.MAX_LINEAR_JOG_VEL)) self.DEFAULT_ANGULAR_JOG_VEL = self.get_error_safe_float("DISPLAY", "DEFAULT_ANGULAR_VELOCITY", 6) * 60 self.MIN_ANGULAR_JOG_VEL = self.get_error_safe_float("DISPLAY", "MIN_ANGULAR_VELOCITY", 0) * 60 self.MAX_ANGULAR_JOG_VEL = self.get_error_safe_float("DISPLAY", "MAX_ANGULAR_VELOCITY", 60) * 60 LOG.debug('DEFAULT_ANGULAR_VELOCITY = {}'.format(self.DEFAULT_ANGULAR_JOG_VEL)) LOG.debug('MIN_ANGULAR_VELOCITY = {}'.format(self.MIN_ANGULAR_JOG_VEL)) LOG.debug('MAX_ANGULAR_VELOCITY = {}'.format(self.MAX_ANGULAR_JOG_VEL)) self.AVAILABLE_SPINDLES = self.INI.getint("TRAJ", "SPINDLES", fallback=1) self.SPINDLE_INCREMENT = self.INI.getint("DISPLAY", "SPINDLE_INCREMENT", fallback=100) # FIXME:vvvvv # FIXME: Aren't SPEED values real values? Velocity values are. for i in range(0, self.AVAILABLE_SPINDLES): self['DEFAULT_SPINDLE_{}_SPEED'.format(i)] = \ self.get_error_safe_int("DISPLAY", "DEFAULT_SPINDLE_{}_SPEED".format(i), 200) self['MIN_SPINDLE_{}_SPEED'.format(i)] = \ self.get_error_safe_int("DISPLAY", "MIN_SPINDLE_{}_SPEED".format(i), 100) self['MAX_SPINDLE_{}_SPEED'.format(i)] = \ self.get_error_safe_int("DISPLAY", "MAX_SPINDLE_{}_SPEED".format(i), 2500) self['MAX_SPINDLE_{}_OVERRIDE'.format(i)] = \ self.get_error_safe_float("DISPLAY", "MAX_SPINDLE_{}_OVERRIDE".format(i), 1) * 100 self['MIN_SPINDLE_{}_OVERRIDE'.format(i)] = \ self.get_error_safe_float("DISPLAY", "MIN_SPINDLE_{}_OVERRIDE".format(i), 0.5) * 100 # FIXME:^^^^^ # check Legacy self.DEFAULT_SPINDLE_SPEED = self.INI.getint("DISPLAY", "DEFAULT_SPINDLE_SPEED", fallback=-1) if self.DEFAULT_SPINDLE_SPEED < 0: self.DEFAULT_SPINDLE_SPEED = self.DEFAULT_SPINDLE_0_SPEED # FIXME:vvvvv # FIXME: MIN/MAX_SPINDLE_SPEED are real? self.MIN_SPINDLE_SPEED = self.INI.getint("DISPLAY", "MIN_SPINDLE_SPEED", fallback=-1) if self.MIN_SPINDLE_SPEED < 0: self.MIN_SPINDLE_SPEED = self.MIN_SPINDLE_0_SPEED self.MAX_SPINDLE_SPEED = self.INI.getint("DISPLAY", "MAX_SPINDLE_SPEED", fallback=-1) if self.MAX_SPINDLE_SPEED < 0: self.MAX_SPINDLE_SPEED = self.MAX_SPINDLE_0_SPEED # FIXME:^^^^^ self.MAX_SPINDLE_OVERRIDE = self.INI.getreal("DISPLAY", "MAX_SPINDLE_OVERRIDE", fallback=-1) * 100 if self.MAX_SPINDLE_OVERRIDE < 0: self.MAX_SPINDLE_OVERRIDE = self.MAX_SPINDLE_0_OVERRIDE self.MIN_SPINDLE_OVERRIDE = self.INI.getreal("DISPLAY", "MIN_SPINDLE_OVERRIDE", fallback=-1) * 100 if self.MIN_SPINDLE_OVERRIDE < 0: self.MIN_SPINDLE_OVERRIDE = self.MIN_SPINDLE_0_OVERRIDE self.MAX_FEED_OVERRIDE = self.get_error_safe_float("DISPLAY", "MAX_FEED_OVERRIDE", 1.5) * 100 if self.INI.find("TRAJ", "MAX_LINEAR_VELOCITY") is None: if self.LINUXCNC_IS_RUNNING: LOG.critical('INI Parsing Error, No MAX_LINEAR_VELOCITY Entry in TRAJ') self.MAX_TRAJ_VELOCITY = self.get_error_safe_float("TRAJ", "MAX_LINEAR_VELOCITY", self.get_error_safe_float("AXIS_X", "MAX_VELOCITY", 5)) * 60 # user message dialog system self.USRMESS_BOLDTEXT = self.INI.findall("DISPLAY", "MESSAGE_BOLDTEXT") self.USRMESS_TEXT = self.INI.findall("DISPLAY", "MESSAGE_TEXT") self.USRMESS_TYPE = self.INI.findall("DISPLAY", "MESSAGE_TYPE") self.USRMESS_PINNAME = self.INI.findall("DISPLAY", "MESSAGE_PINNAME") self.USRMESS_DETAILS = self.INI.findall("DISPLAY", "MESSAGE_DETAILS") self.USRMESS_ICON = self.INI.findall("DISPLAY", "MESSAGE_ICON") if len(self.USRMESS_TEXT) != len(self.USRMESS_TYPE): LOG.warning('Invalid message configuration (missing text or type) in INI File [DISPLAY] section') if len(self.USRMESS_TEXT) != len(self.USRMESS_PINNAME): LOG.warning('Invalid message configuration (missing pinname) in INI File [DISPLAY] section') if len(self.USRMESS_TEXT) != len(self.USRMESS_BOLDTEXT): LOG.warning('Invalid message configuration (missing boldtext) in INI File [DISPLAY] section') if len(self.USRMESS_TEXT) != len(self.USRMESS_DETAILS): LOG.warning('Invalid message configuration (missing details) in INI File [DISPLAY] section') if len(self.USRMESS_TEXT) != len(self.USRMESS_ICON): LOG.warning('Invalid message configuration (missing icon) in INI File [DISPLAY] section') if self.USRMESS_ICON == []: temp = 'INFO' else: temp = self.USRMESS_ICON[0] self.USRMESS_ICON = [] for i in self.USRMESS_TEXT: self.USRMESS_ICON.append(temp) try: self.ZIPPED_USRMESS = list( zip(self.USRMESS_BOLDTEXT, self.USRMESS_TEXT, self.USRMESS_DETAILS, self.USRMESS_TYPE, self.USRMESS_PINNAME, self.USRMESS_ICON)) except: self.ZIPPED_USRMESS = None ################################## # user multi message dialog system ################################## self.USRMULTIMESS_ID = self.INI.findall("DISPLAY", "MULTIMESSAGE_ID") or None #print(self.USRMULTIMESS_ID) if not self.USRMULTIMESS_ID is None: for item in self.USRMULTIMESS_ID: NUMBER = self.INI.findall("DISPLAY", "MULTIMESSAGE_{}_NUMBER".format(item)) #print(NUMBER) TYPE = self.INI.findall("DISPLAY", "MULTIMESSAGE_{}_TYPE".format(item)) #print ('Type:',TYPE) TITLE = self.INI.findall("DISPLAY", "MULTIMESSAGE_{}_TITLE".format(item)) #print(TITLE) TEXT = self.INI.findall("DISPLAY", "MULTIMESSAGE_{}_TEXT".format(item)) #print(TEXT) DETAILS = self.INI.findall("DISPLAY", "MULTIMESSAGE_{}_DETAILS".format(item)) #print(DETAILS) ICON = self.INI.findall("DISPLAY", "MULTIMESSAGE_{}_ICON".format(item)) #print(ICON) OPTIONS = self.INI.findall("DISPLAY", "MULTIMESSAGE_{}_OPTIONS".format(item)) #print(OPTIONS) # fix any missing ICON to default to what the first entry was or 'INFO' if len(TEXT) != len(ICON): LOG.warning('Invalid MULTI message configuration (missing icon) in INI File [DISPLAY] section') if ICON == []: temp = 'INFO' else: temp = ICON[0] ICON = [] for i in TEXT: ICON.append(temp) ##print(ICON) # fix any missing ICON to default to what the first entry was or 'LOG=True,LEVEL=DEFAULT' if len(TEXT) != len(OPTIONS): LOG.warning('Invalid message configuration (missing MESSAGE_OPTIONS) in INI File [DISPLAY] section') if OPTIONS == []: temp = 'LOG=True,LEVEL=DEFAULT' else: temp = OPTIONS[0] OPTIONS = [] for i in range(len(TEXT)): OPTIONS.append(temp) # process message OPTIONS for num,i in enumerate(OPTIONS): OPTIONS[num] = self.parse_message_options(i) # ZIP them up neatly try: z = list(zip( TYPE,TITLE,TEXT,DETAILS,ICON,OPTIONS)) except Exception as e: print('error:',e) z = None # make a dict from the raw zipped list # ie for a VFD multi message with 2 dialog messages: # self.VFD_MULTIMESS={1:[].2:[] } if not z is None: d = dict() for num, i in enumerate(NUMBER): d[int(i)] = z[num] self['{}_MULTIMESS'.format(item)] = d else: pass #print('{}_MULTIMESS'.format(item),d) ############## # Embed tabs # ############## # AXIS panel style: self.GLADEVCP = self.INI.find("DISPLAY", "GLADEVCP") # or None # tab style for qtvcp tab. style is used everywhere good_flag = True self.TAB_NAMES = (self.INI.findall("DISPLAY", "EMBED_TAB_NAME")) or None self.TAB_LOCATIONS = (self.INI.findall("DISPLAY", "EMBED_TAB_LOCATION")) or [] self.TAB_CMDS = (self.INI.findall("DISPLAY", "EMBED_TAB_COMMAND")) or [] if self.TAB_NAMES is not None and len(self.TAB_NAMES) != len(self.TAB_CMDS): LOG.critical('Embedded tab configuration -invalid number of TAB_NAMES vs TAB_CMDs') good_flag = False if self.TAB_NAMES is not None and len(self.TAB_LOCATIONS) != len(self.TAB_NAMES): LOG.warning('Embedded tab configuration -invalid number of TAB_NAMES vs TAB_LOCATION - guessing default.') for num, i in enumerate(self.TAB_NAMES): try: if self.TAB_LOCATIONS[num]: continue except: self.TAB_LOCATIONS.append("default") # initial/default self.NATIVE_EMBED = [] self.ZIPPED_TABS = None # if no critical errors if good_flag: # check for duplicate names if qtvcp panels if self.TAB_CMDS is not None: nameList=[] for num,i in enumerate(self.TAB_CMDS): if 'qtvcp' in i: nameList.append( self.TAB_NAMES[num]) # code to check for duplicate names dup = {x for x in nameList if nameList.count(x) > 1} if not dup == set(): LOG.error('Embedded panel tab: Duplicate TAB_NAMES:{} in INI.'.format(dup)) try: self.ZIPPED_TABS = list(zip(self.TAB_NAMES, self.TAB_LOCATIONS, self.TAB_CMDS)) except: self.ZIPPED_TABS = None # find qtvcp embedded - because they are added directly rather then x11 embedding if self.TAB_CMDS is not None: for i in self.TAB_CMDS: if i.split()[0].lower() == 'qtvcp': self.NATIVE_EMBED.append(True) else: self.NATIVE_EMBED.append(False) ################ # MDI commands # ################ # users can specify a label for the MDI action button by adding ',Some\nText' # to the end of the MDI command # here we separate them to two lists (legacy) and one dict # action_button takes it from there. self.MDI_COMMAND_DICT={} self.MDI_COMMAND_LIST = [] self.MDI_COMMAND_LABEL_LIST = [] # suppress error message if there is no section at all if self.INI.hassection('MDI_COMMAND_LIST'): try: for key,value in self.INI.getvariables('MDI_COMMAND_LIST'): # legacy way: list of repeat 'MDI_COMMAND=XXXX' # in this case order matters in the INI if key == 'MDI_COMMAND': LOG.warning("INI file's MDI_COMMAND_LIST is using legacy 'MDI_COMMAND =' entries") temp = (self.INI.findall("MDI_COMMAND_LIST", "MDI_COMMAND")) or None if temp is None: self.MDI_COMMAND_LIST.append(None) self.MDI_COMMAND_LABEL_LIST.append(None) else: for count, i in enumerate(temp): mdidatadict = {} for num,k in enumerate(i.split(',')): if num == 0: self.MDI_COMMAND_LIST.append(k) mdidatadict['cmd'] = k if len(i.split(',')) <2: self.MDI_COMMAND_LABEL_LIST.append(None) mdidatadict['label'] = None else: self.MDI_COMMAND_LABEL_LIST.append(k) mdidatadict['label'] = k self.MDI_COMMAND_DICT[str(count)] = mdidatadict break # new way: 'MDI_COMMAND_SSS = XXXX' (SSS being any string) # order of commands doesn't matter in the INI else: self.MDI_COMMAND_LIST.append(None) self.MDI_COMMAND_LABEL_LIST.append(None) try: name = (key.replace('MDI_COMMAND_','')) mdidatadict = {} for num,k in enumerate(value.split(',')): if num == 0: mdidatadict['cmd'] = k if len(value.split(',')) <2: mdidatadict['label'] = None else: mdidatadict['label'] = k self.MDI_COMMAND_DICT[name] = mdidatadict except Exception as e: LOG.error('INI MDI command parse error:{}'.format(e)) except Exception as e: LOG.error('INI MDI command parse error:{}'.format(e)) ################ # MACRO commands # ################ # users can specify a label for the MACRO action button by adding ',Some\nText' # to the end of the MACRO command # here we separate them to two lists (legacy) and one dict # action_button takes it from there. self.MACRO_COMMAND_DICT={} self.MACRO_COMMAND_LIST = [] self.MACRO_COMMAND_LABEL_LIST = [] # get raw data self.INI_MACROS = self.INI.findall("MACROS", "MACRO") or None if self.INI_MACROS is None: # qtvcp legacy self.INI_MACROS = self.INI.findall("DISPLAY", "MACRO") or None # suppress error message if there is no section at all if self.INI.hassection('MACROS'): try: for key,value in self.INI.getvariables('MACROS'): # legacy way: list of repeat 'MACRO=XXXX' # in this case order matters in the INI if key == 'MACRO': LOG.verbose("INI file's MACRO list is using legacy 'MACRO=' entries") temp = (self.INI.findall("MACROS", "MACRO")) or None if temp is None: self.MACRO_COMMAND_LIST.append(None) self.MACRO_COMMAND_LABEL_LIST.append(None) else: for count, i in enumerate(temp): datadict = {} for num,k in enumerate(i.split(',')): if num == 0: self.MACRO_COMMAND_LIST.append(k) datadict['cmd'] = k if len(i.split(',')) <2: self.MACRO_COMMAND_LABEL_LIST.append(None) datadict['label'] = None else: self.MACRO_COMMAND_LABEL_LIST.append(k) datadict['label'] = k self.MACRO_COMMAND_DICT[str(count)] = datadict break # new way: 'MACRO_COMMAND_SSS = XXXX' (SSS being any string) # order of commands doesn't matter in the INI else: self.MACRO_COMMAND_LIST.append(None) self.MACRO_COMMAND_LABEL_LIST.append(None) try: name = (key.replace('MACRO_COMMAND_','')) mdidatadict = {} for num,k in enumerate(value.split(',')): if num == 0: mdidatadict['cmd'] = k if len(value.split(',')) <2: mdidatadict['label'] = None else: mdidatadict['label'] = k self.MACRO_COMMAND_DICT[name] = mdidatadict except Exception as e: LOG.error('INI MDI command parse error:{}'.format(e)) except Exception as e: LOG.error('INI MACRO command parse error:{}'.format(e)) self.TOOL_FILE_PATH = self.get_error_safe_setting("EMCIO", "TOOL_TABLE") self.POSTGUI_HALFILE_PATH = (self.INI.findall("HAL", "POSTGUI_HALFILE")) or None self.POSTGUI_HAL_COMMANDS = (self.INI.findall("HAL", "POSTGUI_HALCMD")) or None # Some systems need repeat disabled for keyboard jogging because repeat rate is uneven self.DISABLE_REPEAT_KEYS_LIST = self.INI.getstring("DISPLAY", "DISABLE_REPEAT_KEYS") # or None # maximum number of errors shown in on screen display self.MAX_DISPLAYED_ERRORS = self.INI.getint("DISPLAY", "MAX_DISPLAYED_ERRORS", fallback=10) self.TITLE = self.INI.getstring("DISPLAY", "TITLE", fallback="") self.ICON = self.INI.getstring("DISPLAY", "ICON", fallback="") # detect historical lathe config with dummy joint 1 if (self.MACHINE_IS_LATHE and (self.trajcoordinates.upper() == "XZ") and (len(self.AVAILABLE_JOINTS) == 3)): self.LATHE_HISTORICAL_CONFIG = True else: self.LATHE_HISTORICAL_CONFIG = False ################### # helper functions ################### # return a found string or else None by default, anything else by option # since this is used in this file there are some workarounds for plasma machines def _opt_default_warn(self, heading, detail, default, warning): if ('SPINDLE' in detail and self.MACHINE_IS_QTPLASMAC) or \ ('ANGULAR' in detail and not self.HAS_ANGULAR_JOINT): return default elif warning: LOG.warning('INI Parsing Error, No {} Entry in {}, Using: {}'.format(detail, heading, default)) return default def get_error_safe_int(self, heading, detail, default=None, warning = True): result = self.INI.getint(heading, detail) if result is not None: return result else: return self._opt_default_warn(heading, detail, default, warning) def get_error_safe_float(self, heading, detail, default=None, warning = True): result = self.INI.getreal(heading, detail) if result is not None: return result else: return self._opt_default_warn(heading, detail, default, warning) def get_error_safe_setting(self, heading, detail, default=None, warning = True): result = self.INI.find(heading, detail) if result is not None: return result else: return self._opt_default_warn(heading, detail, default, warning) # return a found float or else None by default, anything else by option def get_safe_float(self, heading, detail, default=None): return self.INI.getreal(heading, detail, fallback=default) # return a found integer or else None by default, anything else by option def get_safe_int(self, heading, detail, default=None): return self.INI.getint(heading, detail, fallback=default) def convert_machine_to_metric(self, data): if self.MACHINE_IS_METRIC: return data else: return data * 25.4 def convert_machine_to_imperial(self, data): if self.MACHINE_IS_METRIC: return data * (1 / 25.4) else: return data def convert_metric_to_machine(self, data): if self.MACHINE_IS_METRIC: return data else: return data * (1 / 25.4) def convert_imperial_to_machine(self, data): if self.MACHINE_IS_METRIC: return data * 25.4 else: return data def convert_9_metric_to_machine(self, v): if self.MACHINE_IS_METRIC: return v else: c = [1.0 / 25.4] * 3 + [1] * 3 + [1.0 / 25.4] * 3 return list(map(lambda x, y: x * y, v, c)) def convert_9_imperial_to_machine(self, v): if self.MACHINE_IS_METRIC: c = [25.4] * 3 + [1] * 3 + [25.4] * 3 return list(map(lambda x, y: x * y, v, c)) else: return v def convert_units(self, data): return data * self.MACHINE_UNIT_CONVERSION def convert_units_9(self, v): c = self.MACHINE_UNIT_CONVERSION_9 return list(map(lambda x, y: x * y, v, c)) def convert_units_10(self, v): c = self.MACHINE_UNIT_CONVERSION_10 return list(map(lambda x, y: x * y, v, c)) # This finds the filter program's initializing # program eg python for .py from INI def get_filter_program(self, fname): ext = os.path.splitext(fname)[1] if ext: return self.INI.find("FILTER", ext[1:]) else: return None def get_all_valid_extensions(self): temp = [] try: for i in (self.PROGRAM_FILTERS): for q in i[0].split(','): temp.append('{}'.format(q)) if not '.ngc' in temp: temp.append('.ngc') return temp except Exception as e: LOG.warning('Valid Extension Parsing Error: {}\n Using Default: *'.format(e)) return ('*') def get_filters_extensions(self): all_extensions = [] try: for k, v in self.PROGRAM_FILTERS: k = k.replace('.', ' *.') k = k.replace(' ', '') temp = [] for q in k.split(','): temp.append('{}'.format(q)) all_extensions.append(['{}'.format(v), temp]) all_extensions.append(['All (*)', ['*']]) return all_extensions except Exception as e: LOG.warning('filter Extension Parsing Error: {}\n Using Default: ALL (*)'.format(e)) return [['All (*)', ['*']]] def program_extension_valid(self, fname): filename, file_extension = os.path.splitext(fname) if '*' in self.VALID_PROGRAM_EXTENSIONS: return True elif file_extension.lower() in (self.VALID_PROGRAM_EXTENSIONS): return True return False def get_jnum_from_axisnum(self, axisnum): joint = self.TRAJCO.index( "xyzabcuvw"[axisnum] ) return joint # check to see if file name plus paths from # SUBROUTINE_PATH, USER_M_PATH or PROGRAM_PREFIX from INI # is an existing path (meaning linuxcnc can find it) # fname should just be the filename # returns the full path or None - Unless # using the show option then returns path or None and any # error message text def check_known_paths(self,fname, prefix = True, sub=True, user_m=True, show=False): fname = os.path.split(fname)[1] debugtext = [] if show: debugtext.append(f'Checking known paths for :{fname}') if prefix: path = os.path.expanduser(os.path.join(self.PROGRAM_PREFIX,fname)) if show: debugtext.append(f'Program Prefix: {path}') if os.path.exists(path): if show: return path, debugtext return path if show: debugtext.append('No Program Prefix path matches in INI') if sub: for i in self.SUB_PATH_LIST: path = os.path.expanduser(os.path.join(i,fname)) if show: debugtext.append(f'Subprogram path: {path}') if os.path.exists(path): if show: return path, debugtext return path else: if show: debugtext.append('No Subprogram path matches in INI') if user_m: for i in self.USER_M_PATH_LIST: path = os.path.expanduser(os.path.join(i,fname)) if show: debugtext.append(f'User M code path: {path}') if os.path.exists(path): if show: return path, debugtext return path else: if show: debugtext.append('No User M code path matches in INI') if show: debugtext.append('No INI defined path match found') return None, debugtext return None # same as above but just return True or False def is_in_known_paths(self,fname, prefix = True, sub=True, user_m=True): fname = os.path.split(fname)[1] if self.check_known_paths(fname,prefix,sub,user_m) is None: return False return True def get_ini_mdi_command(self, key): """ returns A MDI command string from the INI heading [MDI_COMMAND_LIST] or None key -- can be a integer or a string using an integer is the legacy way to refer to the nth line. using a string will refer to the specific command regardless what line it is on.""" try: # should fail if not string return self.MDI_COMMAND_DICT[key]['cmd'] except: # fallback to legacy variable try: # should fail if not int return self.MDI_COMMAND_LIST[key] except: return None def get_ini_mdi_label(self, key): """ returns A MDI label string from the INI heading [MDI_COMMAND_LIST] or None key -- can be a integer or a string Using an integer is the legacy way to refer to the nth line in the INI. Using a string will refer to the specific command regardless of what line it is on.""" try: # should fail if not string return self.MDI_COMMAND_DICT[key]['label'] except: # fallback to legacy variable try: # should fail if not int return self.MDI_COMMAND_LABEL_LIST[key] except: return None def get_ini_macro_command(self, key): """ returns A MACRO command string from the INI heading [MACROS] or None key -- can be a integer or a string using an integer is the legacy way to refer to the nth line. using a string will refer to the specific command regardless what line it is on.""" try: # should fail if not string return self.MACRO_COMMAND_DICT[key]['cmd'] except: # fallback to legacy variable try: # should fail if not int return self.MACRO_COMMAND_LIST[key] except: return None def get_ini_macro_label(self, key): """ returns A MACRO label string from the INI heading [MACROS] or None key -- can be a integer or a string Using an integer is the legacy way to refer to the nth line in the INI. Using a string will refer to the specific command regardless of what line it is on.""" try: # should fail if not string return self.MACRO_COMMAND_DICT[key]['label'] except: # fallback to legacy variable try: # should fail if not int return self.MACRO_COMMAND_LABEL_LIST[key] except: return None # Process any multi message options like log level def parse_message_options(self, options): temp = {} if options is None: return l = options.split(',') for i in l: o = i.split('=') if o[0].upper() == "LEVEL": if o[1].upper() == 'WARNING': arg = 1 elif o[1].upper() == 'CRITICAL': arg = 2 else: arg = 0 temp['LEVEL']=arg elif o[0].upper() == "LOG": temp['LOG']= bool(o[1]) else: temp[o[0]]= o[1] return temp ###################################################### def get_linuxcnc_version(self): try: return os.environ['LINUXCNCVERSION'] except: return 'UNAVAILABLE' def get_base(self): if self.RIP_FLAG: base = os.environ.get('EMC2_HOME', None) LOG.debug('Linuxcnc Run-In-Place (RIP) flag true') else: base = os.environ.get('LINUXCNC_HOME', None) # fallback until the RIP_FLAG is common if base is None: base = os.environ.get('EMC2_HOME', None) # catch all if base is None: base = '/usr' if self.RIP_FLAG: LOG.verbose('Linuxcnc Base directory not found in environmental variable: EMC2_HOME') else: LOG.verbose('Linuxcnc Base directory not found in environmental variable: LINUXCNC_HOME') LOG.verbose('Using Linuxcnc Base directory: {}'.format(base)) return base ###################################################### def __getitem__(self, item): return getattr(self, item) def __setitem__(self, item, value): return setattr(self, item, value)