Source code for grbl.parser

#!/usr/bin/env python
# Prototype for a G-code parser intended for grbl CNC G-code files.

import re
import sys
import argparse
import logging

# Global log handler for the module, with default null logger.
logger = logging.getLogger('grblparser')
logger.setLevel(logging.DEBUG)
logger.addHandler(logging.NullHandler())

################################################################
[docs]class GCodeError(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value)
# E.g. raise GCodeError("Incompatible word grouping: '%s'" % words) ################################################################ # Define a mapping of modal groups. No more than one command from each group can appear in a block. g_groups = { 'G4':0, 'G10':0, 'G28':0, 'G30':0, 'G53':0, 'G92':0, 'G92.1':0, 'G92.2':0, 'G92.3':0, 'G0':1, 'G1':1, 'G2':1, 'G3':1, 'G38.2':1, 'G80':1, 'G81':1, 'G82':1, 'G83':1, 'G84':1, 'G85':1, 'G86':1, 'G87':1, 'G88':1, 'G89':1, 'G17':2, 'G18':2, 'G19':2, 'G90':3, 'G91':3, 'G93':5, 'G94':5, 'G20':6, 'G21':6, 'G40':7, 'G41':7, 'G42':7, 'G43':8, 'G49':8, 'G98':10, 'G99':10, 'G54':12, 'G55':12, 'G56':12, 'G57':12, 'G58':12, 'G59':12, 'G59.1':12, 'G59.2':12, 'G59.3':12, 'G61':13, 'G61.1':13, 'G64':13 } m_groups = { 'M0':4, 'M1':4, 'M2':4, 'M30':4, 'M60':4, 'M6':6, 'M3':7, 'M4':7, 'M5':7, 'M7':8, 'M8':8, 'M9':8, 'M48':9, 'M49':9 } # Define valid command letter codes. letter_codes = 'ABCDFGHIJKLMNPQRSTXYZ' grbl_unsupported_g_codes = [ 'G41', 'G42', 'G43', 'G59.1', 'G59.2', 'G59.3', 'G61.1', 'G64', 'G81', 'G82', 'G83', 'G84', 'G85', 'G86', 'G87', 'G88', 'G89', 'G92.2', 'G92.3', 'G98', 'G99' ] grbl_unsupported_m_codes = [ 'M48', 'M49', 'M60' ] ################################################################
[docs]class GrblBlock(object): """Representation of a single line of G-code.""" def __init__(self, line = None): # list of comment strings self.comments = [] # integer line number, if present self.line_number = None # list of code words as (number-string, value-string) tuples self.codes = [] # integer line number from source file, if applicable self.source_line = None # any remaining unparsed text self.extra = None # process any provided initialization string (e.g. line of G code) if line is not None: self._parse_string(line) return ################################################################ def _parse_string(self, block): """Parse a string representing a line of G-code (a single block), updating the internal state. For now, this ignores most possible syntax errors and uses a liberal interpretation, but it can raise GCodeError for invalid input. """ # Remove right-side whitespace. block = block.strip() # Check for standard comments in parenthesis. It is legal for multiple comments # to appear on a line. The .*? notation specifies a non-greedy match. comments = re.findall(r'\((.*?)\)', block) if comments: # save a list of all comment strings self.comments += comments # extract the text with the comments removed block = re.sub(r'\(.*?\)', '', block) # As an extension, check for trailing comments delineated either by a # semicolon or two or more slashes. The (?: notation identifies a group # which is matched but not stored. The .*? notation specifies a # non-greedy match. trailing_match = re.search(r'(.*?)\s*(?:;+|//+)\s*(.*)$', block) if trailing_match is not None: block = trailing_match.group(1) self.comments.append(trailing_match.group(2)) # Remove all whitespace. block = re.sub(r'\s+', '', block) # Capitalize all letter codes. block = block.upper() # Check for an initial line number and save it as a string. The number # should be an integer with no more than five digits. line_number_match = re.match(r'N([0-9])(.*)$', block) if line_number_match is not None: self.line_number = line_number_match.group(1) block = line_number_match.group(2) # Find all words, e.g. letter-number pairs. This will return a list of # tuples of the form (letter-string, value-string). Each letter should # be a member of the set ABCDFGHIJKLMNPQRSTXYZ. self.codes = re.findall(r'([a-zA-Z])([0-9\.+-]+)', block) # Find any unmatched text by removing all the word matches. remainder = re.sub(r'([a-zA-Z])([0-9\.+-]+)', '', block) if len(remainder) > 0: self.extra = remainder # Return the same object for convenience. return self ################################################################
[docs] def is_trivial(self): """Returns true if the block has no meaningful content.""" return len(self.comments) == 0 and len(self.codes) == 0
def __repr__(self): return "<GrblBlock: srcline %d: comments:%s code:%s>" % (self.source_line, self.comments, self.codes)
[docs] def g_codes(self): """Return a list of all numeric values from words containing a G command code.""" return [ float(value) if '.' in value else int(value) for (code, value) in self.codes if code == 'G']
[docs] def m_codes(self): """Return a list of all numeric values from words containing a M command code.""" return [ float(value) if '.' in value else int(value) for (code, value) in self.codes if code == 'M']
################################################################
[docs]class GrblParser(object): """Representation of a g-code interpreter for parsing and checking g-code streams for the grbl G-code interpreter. This class tracks the expected internal state of the interpreter and can apply code transformations and regenerate clean output. """ def __init__(self, args): return #================================================================ #================================================================
[docs] def process_block(self, props): """Interpret a property dictionary representing a single line of G-code and update the internal interpreter state to reflect the execution effects. """ return
################################################################
[docs]def parse_gcode_file(input_file): """Parse an entire G-code file into a list of block property objects, one per meaningful block.""" result = [] for num, line in enumerate(input_file, start=1): try: # New property object representing the block. props = GrblBlock(line) if props.extra is not None: logger.warning("Ignoring spurious input on line %d: '%s'", num, props.extra) # drop any trivial lines (e.g. blank lines, lines with only a line number) if not props.is_trivial(): props.source_line = num result.append(props) except GCodeError as err: logger.warning("Ignoring error on input line %d: %s", num, err.value) return result
################################################################ if __name__ == "__main__": # Define command line argument interface parser = argparse.ArgumentParser(description='Process grbl g-code stream.') parser.add_argument('input', type=argparse.FileType('r'), help='Path of g-code file to process.') parser.add_argument('-l','--log', help='Path for optional log file.') parser.add_argument('-v','--verbose', action='store_true', help='Enable more detailed console output.') args = parser.parse_args() # Configure the library logger. Open a file-based log handler on request: if args.log is not None: file_handler = logging.FileHandler(args.log) file_handler.setFormatter(logging.Formatter('%(levelname)s:%(name)s:%(message)s')) file_handler.setLevel(logging.DEBUG) logger.addHandler(file_handler) # Always create a warning stream to the console: console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter('%(levelname)s:%(name)s:%(message)s')) console_handler.setLevel(logging.WARNING) # Increase the level of output on request. if args.verbose: console_handler.setLevel(logging.DEBUG) logger.addHandler(console_handler) # parser = GrblParser(args) logger.info("Reading %s", args.input.name) blocks = parse_gcode_file(args.input) for b in blocks: print b # close input file args.input.close()