import os from typing import List, Union import logging import re from math import isclose logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s: %(message)s') FLOAT_OR_INT = r'\d+(\.\d*)?' RE_LAYER_COUNT = re.compile(r';LAYER_COUNT:(\d+)', re.MULTILINE) RE_LAYER = re.compile(r'(;LAYER:\d+)', re.MULTILINE) RE_LAYER_NUMBER = re.compile(r';LAYER:(\d+)') RE_FOOTER_START = re.compile(r'(;TIME_ELAPSED:\d+\.\d+)\n[^;]', re.MULTILINE) RE_FAN_SPEED = re.compile(r'(M106 .+)\n', re.MULTILINE) RE_LAYER_ZHEIGHT = re.compile(r'G0 .*Z({})'.format(FLOAT_OR_INT)) RE_EXTRUSION = re.compile(r'G1 .*E({})'.format(FLOAT_OR_INT)) RE_MILLIMETERS = re.compile(r'({})mm'.format(FLOAT_OR_INT)) class GcodeFileAnalyzer: @classmethod def LoadFile(cls, path): logging.info('Loading %s', path) with open(path, 'r') as f: return cls([l.strip('\n') for l in f]) @classmethod def SplitFileAtLayers(cls, path, layerNumbers: List[Union[int, str]]): gcode = cls.LoadFile(path) for i, lines in enumerate(gcode.ExportSplitted(layerNumbers)): outPath = os.path.join(os.path.dirname(path), '{:02d}_{}'.format(i, os.path.basename(path))) logging.info('Writing %s', outPath) with open(outPath, 'wt') as f: f.write('\n'.join(lines)) def __init__(self, lines): self.lines: list(str) = lines self.header: list(str) = [] self.footer: list(str) = [] self.layers: list('Layer') = [] self.layerCount: int = 0 self.fanSpeedCode: str = '' self.__AnalyzeGcode() def __AnalyzeGcode(self) -> None: logging.info('Analyzing GCode') fullCode = '\n'.join(self.lines) # Find footer footerStartMatch = RE_FOOTER_START.search(fullCode) startOfFooterIndex = self.lines.index(footerStartMatch.group(1)) + 1 self.footer = self.lines[startOfFooterIndex:] # Find layers layerCountMatch = RE_LAYER_COUNT.search(fullCode) self.layerCount = int(layerCountMatch.group(1)) layersList = RE_LAYER.findall(fullCode) if len(layersList) != self.layerCount: logging.warning('Gcode states wrong layer count {}. But found {} layers.' .format(layerCountMatch.group(0), len(layersList))) self.layerCount = len(layersList) layersIndices = [self.lines.index(layer) for layer in layersList] + [startOfFooterIndex] self.layers = [Layer(self.lines[i:j]) for i, j in zip(layersIndices[:-1], layersIndices[1:])] # Find header self.header = self.lines[:layersIndices[0]] # Find fan speed code fanSpeedMatch = RE_FAN_SPEED.search(fullCode) if fanSpeedMatch: self.fanSpeedCode = fanSpeedMatch.group(1) def ExportSplitted(self, splitBeforeLayers: list) -> list: splitBeforeLayers = self._FindLayerIndices(splitBeforeLayers) assert all(l in range(1, self.layerCount) for l in splitBeforeLayers) outGcodes = [] for startLayerIndex, endLayerIndex in zip([0] + splitBeforeLayers, splitBeforeLayers + [self.layerCount]): startLayer = self.layers[startLayerIndex] endLayer = self.layers[endLayerIndex - 1] outGcodes.append(self._ExtractLayerSubset(startLayer, endLayer)) return outGcodes def _FindLayerIndices(self, layerIndicesOrHeights: List[Union[int, str]]) -> List[int]: """ Translate heights to the corresponding layer index """ layerIndices = [] for layerIndexOrHeight in layerIndicesOrHeights: if isinstance(layerIndexOrHeight, int): # already a layer index layerIndices.append(layerIndexOrHeight) elif isinstance(layerIndexOrHeight, str): # extract millimeters millisMatch = RE_MILLIMETERS.search(layerIndexOrHeight) if not millisMatch: raise ValueError('{} is not a valid millimeters measure'.format(layerIndexOrHeight)) # find layer at index height: float = float(millisMatch.group(1)) for layer in self.layers: if isclose(height, layer.zHeight, abs_tol=0.001): layerIndices.append(layer.number) break else: raise RuntimeError('Layer at Z position {} could not be found'.format(layerIndexOrHeight)) return layerIndices def _ExtractLayerSubset(self, startLayer: 'Layer', endLayer: 'Layer') -> List[str]: # Add default header lines = list(self.header) logging.info('Exporting Layers {} ({} mm) ... {} ({} mm)' .format(startLayer.number, startLayer.zHeight, endLayer.number, endLayer.zHeight)) if startLayer.number != 0: # Add extra initalization lines += self._GetAdditionalHeader(startLayer) # Add layers for layer in self.layers[startLayer.number:endLayer.number + 1]: lines += layer.lines # Add footer if endLayer.number + 1 == self.layerCount: lines += self.footer # default footer else: # End extrusion must be patched lines += self._GetPatchedFooter(endLayer) return lines def _GetAdditionalHeader(self, fromLayer: 'Layer') -> List[str]: return [ '', ';GENERATED_EXTRA_INIT', self.fanSpeedCode, 'G0 Z{:.3f}'.format(fromLayer.zHeight + 1), # Approach from above 'G92 E{:.3f}'.format(fromLayer.startExtrusion - 10), # Extrude 10 more mm '', ] def _GetPatchedFooter(self, endLayer: 'Layer') -> List[str]: footerExtrusionIndex = -1 for i, line in enumerate(self.footer): if RE_EXTRUSION.search(line): footerExtrusionIndex = i break else: raise RuntimeError('Extrusion not found in footer') patchedFooter = list(self.footer) patchedFooter[footerExtrusionIndex] = '; {}'.format(patchedFooter[footerExtrusionIndex]) return [ '', ';PATCHED_FOOTER', 'G1 F1500 E{:.3f}'.format(endLayer.endExtrusion - 6.5), '', ] + patchedFooter class Layer: def __init__(self, lines): self.lines = lines self.zHeight: float = 0 self.startExtrusion: float = 0 self.endExtrusion: float = 0 self.number: int = 0 self.__AnalyzeGcode() def __AnalyzeGcode(self): """ Analyze layer GCode to find its Z height and extrusion position """ # Find Layer number for line in self.lines: match = RE_LAYER_NUMBER.search(line) if match: self.number = int(match.group(1)) break else: raise RuntimeError('No layer number found for {}'.format(self.lines[0])) # Find Z height for line in self.lines: match = RE_LAYER_ZHEIGHT.search(line) if match: self.zHeight = float(match.group(1)) break else: raise RuntimeError('No Z height found for {}'.format(self.lines[0])) # Find Start Extrusion for line in self.lines: match = RE_EXTRUSION.search(line) if match: self.startExtrusion = float(match.group(1)) break else: raise RuntimeError('No startExtrusion found for {}'.format(self.lines[0])) # Find End Extrusion for line in reversed(self.lines): match = RE_EXTRUSION.search(line) if match: self.endExtrusion = float(match.group(1)) break else: raise RuntimeError('No endExtrusion found for {}'.format(self.lines[0])) if __name__ == '__main__': # GcodeFileAnalyzer.SplitFileAtLayers('test/BigL.gcode', ['2.8001mm', 300]) # GcodeFileAnalyzer.SplitFileAtLayers(r"Q:\DIY\3Dprint\Models\Thingiverse\Murmelbahnen\The_Cyclone_triple_lift_triple_track_marble_machine\files\gcode\Marble_machine.gcode", # ['97.3mm', 405, 480]) GcodeFileAnalyzer.SplitFileAtLayers(r"Q:\DIY\3Dprint\Models\Thingiverse\Shuttle\Shuttle.gcode", [334])