#!/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()