From 9b97234ffea04e516704960fde9cd02ad9e7f531 Mon Sep 17 00:00:00 2001 From: Sergey Vishnikin Date: Tue, 31 Jan 2017 18:52:33 +0300 Subject: [PATCH] Parser for gcode files has been added --- octoprint_printhistory/parser.py | 283 ++++++++++++++++++ octoprint_printhistory/parser_test/README.txt | 1 + .../parser_test/cura_test.gcode | 17 ++ .../parser_test/simplify3d_test.gcode | 199 ++++++++++++ .../parser_test/slic3r_test.gcode | 144 +++++++++ 5 files changed, 644 insertions(+) create mode 100644 octoprint_printhistory/parser.py create mode 100644 octoprint_printhistory/parser_test/README.txt create mode 100644 octoprint_printhistory/parser_test/cura_test.gcode create mode 100644 octoprint_printhistory/parser_test/simplify3d_test.gcode create mode 100644 octoprint_printhistory/parser_test/slic3r_test.gcode diff --git a/octoprint_printhistory/parser.py b/octoprint_printhistory/parser.py new file mode 100644 index 0000000..b50f278 --- /dev/null +++ b/octoprint_printhistory/parser.py @@ -0,0 +1,283 @@ +# coding=utf-8 +import re +import os +import io +import unittest +import ConfigParser + +VERSION_REGEX = re.compile(r"(\d+)?\.(\d+)?\.?(\*|\d+)") +BUFFER_SIZE = 8192 + + +class UniversalParser: + def __init__(self, file_path): + self.parser_factory = None + self.name = None + self.version = None + self.file = open(file_path, "r") + for parser in [CuraParser(), Slic3rParser(), Simplify3DParser()]: + if parser.detect(self.file): + self.parser_factory = parser + self.name = parser.name + self.version = parser.version + + def parse(self): + parameters = self.parser_factory.parse(self.file) + parameters.update({"slicer_name": self.name}) + parameters.update({"slicer_version": self.version}) + return parameters + + +class BaseParser: + def parse(self, gcode_file): + parameters = {} + parameters.update(self.parse_header(gcode_file)) + parameters.update(self.parse_bottom(gcode_file)) + gcode_file.close() + return parameters + + +class CuraParser(BaseParser): + def __init__(self): + self.name = "cura" + self.version = None + + def detect(self, gcode_file): + detected = False + # on the third line + # ;Generated with Cura_SteamEngine 2.3.1 + for _ in range(3): + line = gcode_file.readline() + if re.search(r"Cura_SteamEngine", line): + detected = True + self.version = VERSION_REGEX.search(line).group(0) + gcode_file.seek(0) + return detected + + def parse_header(self, gcode_file): + parameters = {} + for i in range(15): + line = gcode_file.readline() + if line.startswith(";"): + line = line.replace(";", "") + if line.startswith("TIME") or line.startswith("LAYER_COUNT"): + splitted = line.split(":", 1) + parameters.update({splitted[0]: splitted[1].strip()}) + gcode_file.seek(0) + return parameters + + def parse_bottom(self, gcode_file): + parameters = {} + settings_reversed = [] + settings = [] + for line in reverse_readline(gcode_file): + if line.startswith(";SETTING_3"): + line = line.replace(";SETTING_3", "") + settings_reversed.append(line) + else: + break + [settings.append(x.strip()) for x in reversed(settings_reversed)] + settings = "".join(settings).replace("\\\\n", "\n") + settings = settings.replace('{"global_quality": "', "") + settings = settings.replace('"}', "") + config = ConfigParser.RawConfigParser(allow_no_value=True) + config.readfp(io.BytesIO(settings)) + try: + for section in ["values", "metadata"]: + for option in config.options(section): + parameters.update({option: config.get(section, option)}) + except ConfigParser.NoOptionError: + pass + return parameters + + +class Slic3rParser(BaseParser): + def __init__(self): + self.name = "slic3r" + self.version = None + self.is_parameter = re.compile(r"^[\w _]+={1}") + + def detect(self, gcode_file): + detected = False + # on the first line + # ; generated by Slic3r 1.2.9 on 2017-01-30 at 21:53:46 + line = gcode_file.readline() + gcode_file.seek(0) + if re.search(r"Slic3r", line): + self.version = VERSION_REGEX.search(line).group(0) + detected = True + return detected + + def parse_header(self, gcode_file): + parameters = {} + for line in gcode_file: + if line.startswith(";"): + line = line.split(";")[1].strip() + if self.is_parameter.match(line): + splitted = line.split("=") + param = splitted[0].strip() + value = splitted[1].strip() + parameters.update({param: value}) + elif line == "\n": + continue + else: + break + gcode_file.seek(0) + return parameters + + def parse_bottom(self, gcode_file): + parameters = {} + for line in reverse_readline(gcode_file): + if line.startswith(";"): + line = line.split(";")[1].strip() + if self.is_parameter.match(line): + splitted = line.split("=") + param = splitted[0].strip() + value = splitted[1].strip() + parameters.update({param: value}) + elif line == "\n": + continue + else: + break + return parameters + + +class Simplify3DParser(BaseParser): + def __init__(self): + self.name = "simplify3d" + self.version = None + # checks if string starts with a word followed by a comma + self.is_parameter = re.compile(r"^\w+,{1}") + self.is_bparameter = re.compile(r"^[\w ]+:{1}") + + def detect(self, gcode_file): + detected = False + # on the first line + # ; G-Code generated by Simplify3D(R) Version 3.1.0 + line = gcode_file.readline() + gcode_file.seek(0) + if re.search(r"Simplify3D\(R\)", line): + self.version = VERSION_REGEX.search(line).group(0) + detected = True + return detected + + def parse_header(self, gcode_file): + parameters = {} + for line in gcode_file: + if line.startswith(";"): + # extract a string between semicolons ; =>target string<= ; + line = line.split(";")[1].strip() + if self.is_parameter.match(line): + comma_split = line.split(",") + param = comma_split[0] + value = ",".join(comma_split[1:]) + try: + parameters.update({param: value}) + except ValueError: + pass + # comment section is ended + else: + break + gcode_file.seek(0) + return parameters + + def parse_bottom(self, gcode_file): + # Filament length is a duplicate + # ; Build Summary + # ; Build time: 3 hours 30 minutes + # ; Filament length: 54599.6 mm (54.60 m) + # ; Plastic volume: 131327.49 mm^3 (131.33 cc) + # ; Plastic weight: 164.16 g (0.36 lb) + # ; Material cost: 20.52 + parameters = {} + for line in reverse_readline(gcode_file): + if line.startswith(";"): + line = line.split(";")[1].strip() + if self.is_bparameter.match(line): + splitted = line.split(":") + param = splitted[0] + value = splitted[1].strip() + parameters.update({param: value}) + else: + break + # Rename "Filament length" + return parameters + +def reverse_readline(fh, buf_size=BUFFER_SIZE): + """a generator that returns the lines of a file in reverse order + It's a memory-efficient way to read file backwards. + http://stackoverflow.com/questions/2301789/read-a-file-in-reverse-order-using-python + """ + segment = None + offset = 0 + fh.seek(0, os.SEEK_END) + file_size = remaining_size = fh.tell() + while remaining_size > 0: + offset = min(file_size, offset + buf_size) + fh.seek(file_size - offset) + buffer = fh.read(min(remaining_size, buf_size)) + remaining_size -= buf_size + lines = buffer.split('\n') + # the first line of the buffer is probably not a complete line so + # we'll save it and append it to the last line of the next buffer + # we read + if segment is not None: + # if the previous chunk starts right from the beginning of line + # do not concact the segment to the last line of new chunk + # instead, yield the segment first + if buffer[-1] is not '\n': + lines[-1] += segment + else: + yield segment + segment = lines[0] + for index in range(len(lines) - 1, 0, -1): + if len(lines[index]): + yield lines[index] + # Don't yield None if the file was empty + if segment is not None: + yield segment + + +class TestUniversalParser(unittest.TestCase): + def setUp(self): + self.simplify3d_file = "parser_test/simplify3d_test.gcode" + self.slic3r_file = "parser_test/slic3r_test.gcode" + self.cura_file = "parser_test/cura_test.gcode" + + def test_simplify3d_detection(self): + uparser = UniversalParser(self.simplify3d_file) + self.assertEqual(uparser.name, "simplify3d") + + def test_simplify3d_parse(self): + uparser = UniversalParser(self.simplify3d_file) + result = uparser.parse() + self.assertEqual(len(result), 187) + self.assertIn("Filament length", result) + self.assertEqual(result["slicer_version"], "3.1.0") + + def test_slic3r_detection(self): + uparser = UniversalParser(self.slic3r_file) + self.assertEqual(uparser.name, "slic3r") + + def test_slic3r_parse(self): + uparser = UniversalParser(self.slic3r_file) + result = uparser.parse() + self.assertEqual(len(result), 137) + self.assertIn("thin_walls", result) + self.assertIn("support material extrusion width", result) + self.assertEqual(result["slicer_version"], "1.2.9") + + def test_cura_detection(self): + uparser = UniversalParser(self.cura_file) + self.assertEqual(uparser.name, "cura") + + def test_cura_parse(self): + uparser = UniversalParser(self.cura_file) + result = uparser.parse() + self.assertEqual(len(result), 12) + self.assertIn("adhesion_type", result) + self.assertEqual(result["slicer_version"], "2.3.1") + + +if __name__ == "__main__": + unittest.main() diff --git a/octoprint_printhistory/parser_test/README.txt b/octoprint_printhistory/parser_test/README.txt new file mode 100644 index 0000000..25f79e7 --- /dev/null +++ b/octoprint_printhistory/parser_test/README.txt @@ -0,0 +1 @@ +Do not use those files for printing, they are prepared for testing purposes. diff --git a/octoprint_printhistory/parser_test/cura_test.gcode b/octoprint_printhistory/parser_test/cura_test.gcode new file mode 100644 index 0000000..db93087 --- /dev/null +++ b/octoprint_printhistory/parser_test/cura_test.gcode @@ -0,0 +1,17 @@ +;FLAVOR:RepRap +;TIME:23464 +;Generated with Cura_SteamEngine 2.3.1 +M84 +M84 +M84;M84 +;LAYER_COUNT:1476 +;LAYER:0 +;M84 +M84 +M84 +;End of Gcode +;SETTING_3 {"global_quality": "[general]\\nversion = 2\\nname = empty\\ndefiniti +;SETTING_3 on = custom\\n\\n[metadata]\\nquality_type = low\\ntype = quality_cha +;SETTING_3 nges\\n\\n[values]\\ninfill_sparse_density = 30\\nadhesion_type = ski +;SETTING_3 rt\\nspeed_travel = 150\\nspeed_print = 47\\nsupport_enable = True\\n +;SETTING_3 material_diameter = 1.75\\n\\n"} diff --git a/octoprint_printhistory/parser_test/simplify3d_test.gcode b/octoprint_printhistory/parser_test/simplify3d_test.gcode new file mode 100644 index 0000000..47ad280 --- /dev/null +++ b/octoprint_printhistory/parser_test/simplify3d_test.gcode @@ -0,0 +1,199 @@ +; G-Code generated by Simplify3D(R) Version 3.1.0 +; Nov 6, 2016 at 7:19:14 PM +; Settings Summary +; processName,Process1 +; applyToModels,Dæk med runding +; profileName,MM2PLA (modified) +; profileVersion,2016-11-05 15:37:44 +; baseProfile, +; printMaterial,PLA +; printQuality,High +; printExtruders, +; extruderName,Primary Extruder +; extruderToolheadNumber,0 +; extruderDiameter,0.6 +; extruderAutoWidth,1 +; extruderWidth,0.72 +; extrusionMultiplier,1 +; extruderUseRetract,1 +; extruderRetractionDistance,0.5 +; extruderExtraRestartDistance,0 +; extruderRetractionZLift,0.25 +; extruderRetractionSpeed,1800 +; extruderUseCoasting,0 +; extruderCoastingDistance,0.2 +; extruderUseWipe,0 +; extruderWipeDistance,5 +; primaryExtruder,0 +; layerHeight,0.3 +; topSolidLayers,6 +; bottomSolidLayers,4 +; perimeterOutlines,2 +; printPerimetersInsideOut,1 +; startPointOption,2 +; startPointOriginX,0 +; startPointOriginY,0 +; startPointOriginZ,300 +; sequentialIslands,0 +; spiralVaseMode,0 +; firstLayerHeightPercentage,90 +; firstLayerWidthPercentage,120 +; firstLayerUnderspeed,0.4 +; useRaft,0 +; raftExtruder,0 +; raftLayers,3 +; raftOffset,3 +; raftSeparationDistance,0.05 +; raftInfill,85 +; disableRaftBaseLayers,0 +; useSkirt,0 +; skirtExtruder,0 +; skirtLayers,2 +; skirtOutlines,6 +; skirtOffset,0 +; usePrimePillar,0 +; primePillarExtruder,999 +; primePillarWidth,12 +; primePillarLocation,7 +; primePillarSpeedMultiplier,1 +; useOozeShield,0 +; oozeShieldExtruder,999 +; oozeShieldOffset,2 +; oozeShieldOutlines,1 +; oozeShieldSidewallShape,1 +; oozeShieldSidewallAngle,30 +; oozeShieldSpeedMultiplier,1 +; infillExtruder,0 +; internalInfillPattern,Rectilinear +; externalInfillPattern,Rectilinear +; infillPercentage,30 +; outlineOverlapPercentage,15 +; infillExtrusionWidthPercentage,125 +; minInfillLength,5 +; infillLayerInterval,1 +; infillAngles,45,-45 +; overlapInfillAngles,0 +; generateSupport,1 +; supportExtruder,0 +; supportInfillPercentage,30 +; supportExtraInflation,1 +; denseSupportLayers,0 +; denseSupportInfillPercentage,75 +; supportLayerInterval,1 +; supportHorizontalPartOffset,0.3 +; supportUpperSeparationLayers,1 +; supportLowerSeparationLayers,1 +; supportType,0 +; supportGridSpacing,4 +; maxOverhangAngle,60 +; supportAngles,0 +; temperatureName,Primary Extruder,Heated Bed +; temperatureNumber,0,2 +; temperatureSetpointCount,1,1 +; temperatureSetpointLayers,1,1 +; temperatureSetpointTemperatures,200,60 +; temperatureStabilizeAtStartup,1,1 +; temperatureHeatedBed,0,1 +; temperatureRelayBetweenLayers,0,0 +; temperatureRelayBetweenLoops,0,0 +; fanLayers,1,5 +; fanSpeeds,0,60 +; blipFanToFullPower,0 +; adjustSpeedForCooling,1 +; minSpeedLayerTime,15 +; minCoolingSpeedSlowdown,20 +; increaseFanForCooling,0 +; minFanLayerTime,45 +; maxCoolingFanSpeed,100 +; increaseFanForBridging,0 +; bridgingFanSpeed,100 +; use5D,1 +; relativeEdistances,0 +; allowEaxisZeroing,1 +; independentExtruderAxes,0 +; includeM10123,0 +; stickySupport,1 +; applyToolheadOffsets,0 +; gcodeXoffset,0 +; gcodeYoffset,0 +; gcodeZoffset,0.1 +; overrideMachineDefinition,1 +; machineTypeOverride,0 +; strokeXoverride,240 +; strokeYoverride,310 +; strokeZoverride,225 +; originOffsetXoverride,0 +; originOffsetYoverride,0 +; originOffsetZoverride,0 +; homeXdirOverride,-1 +; homeYdirOverride,-1 +; homeZdirOverride,-1 +; flipXoverride,1 +; flipYoverride,-1 +; flipZoverride,1 +; toolheadOffsets,0,0|0,0|0,0|0,0|0,0|0,0 +; overrideFirmwareConfiguration,0 +; firmwareTypeOverride,RepRap (Marlin/Repetier/Sprinter) +; GPXconfigOverride,r2 +; baudRateOverride,115200 +; overridePrinterModels,0 +; printerModelsOverride +; startingGcode,G28 X0 Y0 ;move X/Y to min endstops,G1 Z15.0 F{travel_speed} ;move the platform down 15mm,G92 E0 ;zero the extruded length,G1 F200 E20 ;extrude 3mm of feed stock,G92 E0,G1 X20 Y20 F{travel_speed},G28 Z0 +; layerChangeGcode, +; retractionGcode, +; toolChangeGcode, +; endingGcode,M104 S0 ; turn off extruder,M140 S0 ; turn off bed,G28 X0 ; home X axis,M106 S0,M84 ; disable motors, +; exportFileFormat,gcode +; celebration,0 +; celebrationSong,Random Song +; postProcessing, +; defaultSpeed,4200 +; outlineUnderspeed,0.3 +; solidInfillUnderspeed,0.7 +; supportUnderspeed,0.7 +; rapidXYspeed,9000 +; rapidZspeed,1002 +; minBridgingArea,50 +; bridgingExtraInflation,0 +; bridgingExtrusionMultiplier,1 +; bridgingSpeedMultiplier,1 +; filamentDiameter,1.75 +; filamentPricePerKg,125 +; filamentDensity,1.25 +; useMinPrintHeight,0 +; minPrintHeight,0 +; useMaxPrintHeight,0 +; maxPrintHeight,0 +; useDiaphragm,0 +; diaphragmLayerInterval,20 +; robustSlicing,1 +; mergeAllIntoSolid,0 +; onlyRetractWhenCrossingOutline,1 +; retractBetweenLayers,1 +; useRetractionMinTravel,0 +; retractionMinTravel,3 +; retractWhileWiping,0 +; onlyWipeOutlines,1 +; avoidCrossingOutline,0 +; maxMovementDetourFactor,3 +; toolChangeRetractionDistance,0 +; toolChangeExtraRestartDistance,0 +; toolChangeRetractionSpeed,360 +; allowThinWallGapFill,1 +; thinWallAllowedOverlapPercentage,10 +; horizontalSizeCompensation,0 +M84 +M84 +M84 +M84 +M84;M84 +M84 +;M84 +M84 +M84 +; Build Summary +; Build time: 3 hours 30 minutes +; Filament length: 54599.6 mm (54.60 m) +; Plastic volume: 131327.49 mm^3 (131.33 cc) +; Plastic weight: 164.16 g (0.36 lb) +; Material cost: 20.52 diff --git a/octoprint_printhistory/parser_test/slic3r_test.gcode b/octoprint_printhistory/parser_test/slic3r_test.gcode new file mode 100644 index 0000000..f6a1db5 --- /dev/null +++ b/octoprint_printhistory/parser_test/slic3r_test.gcode @@ -0,0 +1,144 @@ +; generated by Slic3r 1.2.9 on 2017-01-30 at 21:53:46 + +; external perimeters extrusion width = 0.50mm +; perimeters extrusion width = 0.72mm +; infill extrusion width = 0.72mm +; solid infill extrusion width = 0.72mm +; top infill extrusion width = 0.72mm +; support material extrusion width = 0.50mm +M84 +M84 +M84; M84 +M84 +M84 +M84 +; filament used = 8088.2mm (57.2cm3) + +; avoid_crossing_perimeters = 0 +; bed_shape = 0x0,200x0,200x200,0x200 +; bed_temperature = 0 +; before_layer_gcode = +; bridge_acceleration = 0 +; bridge_fan_speed = 100 +; brim_width = 0 +; complete_objects = 0 +; cooling = 1 +; default_acceleration = 0 +; disable_fan_first_layers = 3 +; duplicate_distance = 6 +; end_gcode = M104 S0 ; turn off temperature\nG28 X0 ; home X axis\nM84 ; disable motors\n +; extruder_clearance_height = 20 +; extruder_clearance_radius = 20 +; extruder_offset = 0x0 +; extrusion_axis = E +; extrusion_multiplier = 1 +; fan_always_on = 0 +; fan_below_layer_time = 60 +; filament_colour = #FFFFFF +; filament_diameter = 3 +; first_layer_acceleration = 0 +; first_layer_bed_temperature = 0 +; first_layer_extrusion_width = 200% +; first_layer_speed = 30 +; first_layer_temperature = 200 +; gcode_arcs = 0 +; gcode_comments = 0 +; gcode_flavor = reprap +; infill_acceleration = 0 +; infill_first = 0 +; layer_gcode = +; max_fan_speed = 100 +; max_print_speed = 80 +; max_volumetric_speed = 0 +; min_fan_speed = 35 +; min_print_speed = 10 +; min_skirt_length = 0 +; notes = +; nozzle_diameter = 0.5 +; only_retract_when_crossing_perimeters = 1 +; ooze_prevention = 0 +; output_filename_format = [input_filename_base].gcode +; perimeter_acceleration = 0 +; post_process = +; pressure_advance = 0 +; resolution = 0 +; retract_before_travel = 2 +; retract_layer_change = 0 +; retract_length = 2 +; retract_length_toolchange = 10 +; retract_lift = 0 +; retract_restart_extra = 0 +; retract_restart_extra_toolchange = 0 +; retract_speed = 40 +; skirt_distance = 6 +; skirt_height = 1 +; skirts = 1 +; slowdown_below_layer_time = 5 +; spiral_vase = 0 +; standby_temperature_delta = -5 +; start_gcode = G28 ; home all axes\nG1 Z5 F5000 ; lift nozzle\n +; temperature = 200 +; threads = 2 +; toolchange_gcode = +; travel_speed = 130 +; use_firmware_retraction = 0 +; use_relative_e_distances = 0 +; use_volumetric_e = 0 +; vibration_limit = 0 +; wipe = 0 +; z_offset = 0 +; dont_support_bridges = 1 +; extrusion_width = 0 +; first_layer_height = 0.35 +; infill_only_where_needed = 0 +; interface_shells = 0 +; layer_height = 0.3 +; raft_layers = 0 +; seam_position = aligned +; support_material = 1 +; support_material_angle = 0 +; support_material_contact_distance = 0.2 +; support_material_enforce_layers = 0 +; support_material_extruder = 1 +; support_material_extrusion_width = 0 +; support_material_interface_extruder = 1 +; support_material_interface_layers = 3 +; support_material_interface_spacing = 0 +; support_material_interface_speed = 100% +; support_material_pattern = pillars +; support_material_spacing = 2.5 +; support_material_speed = 60 +; support_material_threshold = 0 +; xy_size_compensation = 0 +; bottom_solid_layers = 3 +; bridge_flow_ratio = 1 +; bridge_speed = 60 +; external_fill_pattern = rectilinear +; external_perimeter_extrusion_width = 0 +; external_perimeter_speed = 50% +; external_perimeters_first = 0 +; extra_perimeters = 1 +; fill_angle = 45 +; fill_density = 20% +; fill_pattern = honeycomb +; gap_fill_speed = 20 +; infill_every_layers = 1 +; infill_extruder = 1 +; infill_extrusion_width = 0 +; infill_overlap = 15% +; infill_speed = 80 +; overhangs = 1 +; perimeter_extruder = 1 +; perimeter_extrusion_width = 0 +; perimeter_speed = 60 +; perimeters = 3 +; small_perimeter_speed = 15 +; solid_infill_below_area = 70 +; solid_infill_every_layers = 0 +; solid_infill_extruder = 1 +; solid_infill_extrusion_width = 0 +; solid_infill_speed = 20 +; thin_walls = 1 +; top_infill_extrusion_width = 0 +; top_solid_infill_speed = 15 +; top_solid_layers = 3