Source code for qtpyvcp.plugins.tool_table

"""Tool Table data plugin.

Exposes all the info available in the tool table. Watches the
tool table file for changes and re-loads as needed.

Tool Table YAML configuration:

.. code-block:: yaml

    data_plugins:
      tooltable:
        kwargs:
          # specify the columns that should be read and writen to the
          # tooltable file. To use all columns set to: TPXYZABCUVWDIJQR
          columns: PTDZR
          # specify text to be added before the tool table data
          file_header_template: |
            LinuxCNC Tool Table
            -------------------

            QtPyVCP will preserve comments before the opening semicolon.
"""

import os
from itertools import takewhile
from datetime import datetime

import linuxcnc

from qtpy.QtCore import QFileSystemWatcher, QTimer, Signal, Slot

import qtpyvcp
from qtpyvcp.utilities.info import Info
from qtpyvcp.utilities.logger import getLogger
from qtpyvcp.plugins import DataPlugin, DataChannel, getPlugin

CMD = linuxcnc.command()
LOG = getLogger(__name__)
STATUS = getPlugin('status')
STAT = STATUS.stat
INFO = Info()

IN_DESIGNER = os.getenv('DESIGNER', False)


[docs]def merge(a, b): """Shallow merge two dictionaries""" r = a.copy() r.update(b) return r
DEFAULT_TOOL = { 'A': 0.0, 'B': 0.0, 'C': 0.0, 'D': 0.0, 'I': 0.0, 'J': 0.0, 'P': -1, 'Q': 0, 'T': -1, 'U': 0.0, 'V': 0.0, 'W': 0.0, 'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'R': '', } NO_TOOL = merge(DEFAULT_TOOL, {'T': 0, 'R': 'No Tool Loaded'}) # FILE_HEADER = """ # LinuxCNC Tool Table # ------------------- # # (QtPyVCP will preserve any comments before this separator.) # --- # Generated by: QtPyVCP ToolTable plugin ({version}) # Generated on: {datetime:%x %I:%M:%S %p} # # """ COLUMN_LABELS = { 'A': 'A Offset', 'B': 'B Offset', 'C': 'C Offset', 'D': 'Diameter', 'I': 'Fnt Ang', 'J': 'Bak Ang', 'P': 'Pocket', 'Q': 'Orient', 'R': 'Remark', 'T': 'Tool', 'U': 'U Offset', 'V': 'V Offset', 'W': 'W Offset', 'X': 'X Offset', 'Y': 'Y Offset', 'Z': 'Z Offset', } def makeLorumIpsumToolTable(): return {i: merge(DEFAULT_TOOL, {'T': i, 'P': i, 'R': 'Lorum Ipsum ' + str(i)}) for i in range(10)}
[docs]class ToolTable(DataPlugin): TOOL_TABLE = {0: NO_TOOL} DEFAULT_TOOL = DEFAULT_TOOL COLUMN_LABELS = COLUMN_LABELS tool_table_changed = Signal(dict) def __init__(self, columns='TPXYZDR', file_header_template=None): super(ToolTable, self).__init__() self.fs_watcher = None self.orig_header_lines = [] self.file_header_template = file_header_template or '' self.columns = self.validateColumns(columns) or [c for c in 'TPXYZDR'] self.setCurrentToolNumber(0) self.tool_table_file = INFO.getToolTableFile() if not os.path.exists(self.tool_table_file): return self.loadToolTable() self.current_tool.setValue(self.TOOL_TABLE[STATUS.tool_in_spindle.getValue()]) # update signals STATUS.tool_in_spindle.notify(self.setCurrentToolNumber) STATUS.tool_table.notify(lambda *args: self.loadToolTable()) @DataChannel def current_tool(self, chan, item=None): """Current Tool Info Available items: * T -- tool number * P -- pocket number * X -- x offset * Y -- y offset * Z -- z offset * A -- a offset * B -- b offset * C -- c offset * U -- u offset * V -- v offset * W -- w offset * I -- front angle * J -- back angle * Q -- orientation * R -- remark Rules channel syntax:: tooltable:current_tool tooltable:current_tool?X tooltable:current_tool?x_offset :param item: the name of the tool data item to get :return: dict, int, float, str """ if item is None: return self.TOOL_TABLE[STAT.tool_in_spindle] return self.TOOL_TABLE[STAT.tool_in_spindle].get(item[0].upper())
[docs] def initialise(self): self.fs_watcher = QFileSystemWatcher() self.fs_watcher.addPath(self.tool_table_file) self.fs_watcher.fileChanged.connect(self.onToolTableFileChanged)
[docs] @staticmethod def validateColumns(columns): """Validate display column specification. The user can specify columns in multiple ways, method is used to make sure that that data is validated and converted to a consistent format. Args: columns (str | list) : A string or list of the column IDs that should be shown in the tooltable. Returns: None if not valid, else a list of uppercase column IDs. """ if not isinstance(columns, (basestring, list, tuple)): return return [col for col in [col.strip().upper() for col in columns] if col in 'TPXYZABCUVWDIJQR' and not col == '']
[docs] def newTool(self, tnum=None): """Get a dict of default tool values for a new tool.""" if tnum is None: tnum = len(self.TOOL_TABLE) new_tool = DEFAULT_TOOL.copy() new_tool.update({'T': tnum, 'P': tnum, 'R': 'New Tool'}) return new_tool
def onToolTableFileChanged(self, path): LOG.debug('Tool Table file changed: {}'.format(path)) # ToolEdit deletes the file and then rewrites it, so wait # a bit to ensure the new data has been writen out. QTimer.singleShot(50, self.reloadToolTable) def setCurrentToolNumber(self, tool_num): self.current_tool.setValue(self.TOOL_TABLE[tool_num]) def reloadToolTable(self): # rewatch the file if it stop being watched because it was deleted if self.tool_table_file not in self.fs_watcher.files(): self.fs_watcher.addPath(self.tool_table_file) # reload with the new data tool_table = self.loadToolTable() self.tool_table_changed.emit(tool_table) def iterTools(self, tool_table=None, columns=None): tool_table = tool_table or self.TOOL_TABLE columns = self.validateColumns(columns) or self.columns for tool in sorted(tool_table.iterkeys()): tool_data = tool_table[tool] yield [tool_data[key] for key in columns] def loadToolTable(self, tool_file=None): if tool_file is None: tool_file = self.tool_table_file if not os.path.exists(tool_file): if IN_DESIGNER: lorum_tooltable = makeLorumIpsumToolTable() self.current_tool.setValue(lorum_tooltable) return lorum_tooltable LOG.critical("Tool table file does not exist: {}".format(tool_file)) return {} with open(tool_file, 'r') as fh: lines = [line.strip() for line in fh.readlines()] # find opening colon, and get header data so it can be restored for rlnum, line in enumerate(reversed(lines)): if line.startswith(';'): lnum = len(lines) - rlnum raw_header = lines[:lnum] lines = lines[lnum:] self.orig_header_lines = list(takewhile(lambda l: not l.strip() == '---' and not l.startswith(';Tool'), raw_header)) break table = {0: NO_TOOL,} for line in lines: data, sep, comment = line.partition(';') tool = DEFAULT_TOOL.copy() for item in data.split(): descriptor = item[0] if descriptor in 'TPXYZABCUVWDIJQR': value = item.lstrip(descriptor) if descriptor in ('T', 'P', 'Q'): try: tool[descriptor] = int(value) except: LOG.error('Error converting value to int: {}'.format(value)) break else: try: tool[descriptor] = float(value) except: LOG.error('Error converting value to float: {}'.format(value)) break tool['R'] = comment.strip() tnum = tool['T'] if tnum == -1: continue # add the tool to the table table[tnum] = tool # update tooltable self.__class__.TOOL_TABLE = table self.current_tool.setValue(self.TOOL_TABLE[STATUS.tool_in_spindle.getValue()]) # import json # print json.dumps(table, sort_keys=True, indent=4) self.tool_table_changed.emit(table) return table.copy() def getToolTable(self): return self.TOOL_TABLE.copy()
[docs] def saveToolTable(self, tool_table, columns=None, tool_file=None): """Write tooltable data to file. Args: tool_table (dict) : Dictionary of dictionaries containing the tool data to write to the file. columns (str | list) : A list of data columns to write. If `None` will use the value of ``self.columns``. tool_file (str) : Path to write the tooltable too. Defaults to ``self.tool_table_file``. """ columns = self.validateColumns(columns) or self.columns if tool_file is None: tool_file = self.tool_table_file lines = [] header_lines = [] # restore file header if self.file_header_template: try: header_lines = self.file_header_template.format( version=qtpyvcp.__version__, datetime=datetime.now()).lstrip().splitlines() header_lines.append('') # extra new line before table header except: pass if self.orig_header_lines: try: self.orig_header_lines.extend(header_lines[header_lines.index('---'):]) header_lines = self.orig_header_lines except ValueError: header_lines = self.orig_header_lines lines.extend(header_lines) # create the table header items = [] for col in columns: if col == 'R': continue w = (6 if col in 'TPQ' else 8) - 1 if col == self.columns[0] else 0 items.append('{:<{w}}'.format(COLUMN_LABELS[col], w=w)) items.append('Remark') lines.append(';' + ' '.join(items)) # add the tools for tool_num in sorted(tool_table.iterkeys())[1:]: items = [] tool_data = tool_table[tool_num] for col in columns: if col == 'R': continue items.append('{col}{val:<{w}}' .format(col=col, val=tool_data[col], w=6 if col in 'TPQ' else 8)) comment = tool_data.get('R', '') if comment is not '': items.append('; ' + comment) lines.append(''.join(items)) # for line in lines: # print line # write to file with open(tool_file, 'w') as fh: fh.write('\n'.join(lines)) fh.write('\n') # new line at end of file fh.flush() os.fsync(fh.fileno()) CMD.load_tool_table()