Files
GcodeSplitter/GcodeFileSplitter.py

216 lines
8.4 KiB
Python

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])