Duet3D Logo Duet3D
    • Tags
    • Documentation
    • Order
    • Register
    • Login

    Cura Script to Automatically Probe Only Printed Area

    Scheduled Pinned Locked Moved
    General Discussion
    15
    60
    7.5k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • Luke'sLaboratoryundefined
      Luke'sLaboratory @insertnamehere
      last edited by

      @insertnamehere

      Care to share?

      Luke
      http://lukeslab.online

      insertnamehereundefined 1 Reply Last reply Reply Quote 1
      • insertnamehereundefined
        insertnamehere @Luke'sLaboratory
        last edited by

        @Luke-sLaboratory said in Cura Script to Automatically Probe Only Printed Area:

        @insertnamehere

        Care to share?

        I was afraid that someone would ask that. ☺
        Its butt-ugly code right now. Let me clean it up and I'll post it here.

        1 Reply Last reply Reply Quote 0
        • insertnamehereundefined
          insertnamehere
          last edited by insertnamehere

          For @Luke-sLaboratory.

          This is @mwolter 's code modified to work with Slic3r, PrusaSlicer or Slic3r++. Follow the instructions in the comments to configure Slic3r.

          Slightly changed so that the minimum size of the mesh is 3x3 on prints with small base size.

          #!/usr/bin/python
          """
          	Slic3r post-processing script for RepRap firmware printers which dynamically defines the mesh grid dimensions (M557) based on the print dimensions. 
          {1}
          Usage:
          {1}
          	Slic3r Settings:
          	In Print Settings > Output Options
          	1. turn no "Verbose G-code"
          	2. in "Post-processing scripts" type the full path to python and the full path to this script
          	e.g. <Python Path>\python.exe  <Script Path>\meshcalc.py;
          
          	In Printer Settings > Custom G-code > Start G-code
          	Make sure the start g-code contains the M557 command, and that you probe the bed and load the compensation map,  e.g.
          	M557 X10:290 Y10:290 S20	; Setup default grid
          	G29							; Mesh bed probe
          	G29 S1						; Load compensation map
          		
          	Script Settings
          	probeSpacing = 20 - change this to the preferred probe point spacing in M557
          		
          	Note: The minimum X and Y of the probed area is limited to 2 times the probeSpacing.
          	This is so that prints with a small footprint will have a minimum 3x3 probe mesh
          {1}
          Args:
          {1}
          	Path: The path parameter will be provided by Slic3r.
          {1}
          Requirements:
          {1}
          	The latest version of Python.
          	Note that I use this on windows and haven't tried it on any other platform.
          	Also this script assumes that the bed origin (0,0) is NOT the centre of the bed. Go ahead and modify this script as required.
          {1}
          Credit:
          {1}
          	Based on code originally posted by CCS86 on https://forum.duet3d.com/topic/15302/cura-script-to-automatically-probe-only-printed-area?_=1587348242875.
          	and maybe 90% or more is code posted by MWOLTER on the same thread.
          	Thank you both.
          """
          
          import sys
          import re
          import math
          import os
          
          probeSpacing = 20   		# set your required probe point spacing for M557
          
          def main(fname):	
          	print("Starting Mesh Calculations")
          
          	try:
          		_Slic3rFile = open(fname, encoding='utf-8')
          	except TypeError:
          		try:
          			_Slic3rFile = open(fname)
          		except:
          			print("Open file exception. Exiting.")
          			error()
          	except FileNotFoundError:
          		print('File not found. Exiting.')
          		error()
          		
          	lines = _Slic3rFile.readlines()
          	_Slic3rFile.close()
          
          	linesNew = calcBed(lines)
           
          	_Slic3rFile = open(fname, "r+")
          	_Slic3rFile.seek(0)                       
          	_Slic3rFile.truncate()
          	for element in linesNew:
          		_Slic3rFile.write(element)
          	_Slic3rFile.close()
          	
          	return
          
          def error():
          	# remove the next 2 lines to close console automatically
          	print("Press Enter to close") 
          	input()
          	sys.exit()
           
          def calcBed(lines):
          	bounds = findBounds(lines)
          	bed = findBed()
           
          	for axis in bounds:
          		if bounds[axis]['max'] - bounds[axis]['min'] < bed[axis]:
          			print(f'Success: {axis} mesh is smaller than bed')
          			
          		else:
          			print('Error: Mesh is larger than bed. Exiting.')
          			error()
           
          		for limit in bounds[axis]:
          			if limit == 'min':
          				if (bed[axis]) - bounds[axis][limit] > 0: 
          					print(f'Success: {axis} {limit} coordinate is on the bed.')
          				else:
          					print(f'Error: {axis} {limit} coordinate is off the bed. Exiting.')
          					error()
           
          			if limit == 'max':
          				if (bed[axis]) - bounds[axis][limit] > 0: 
          					print(f'Success: {axis} {limit} coordinate is on the bed.')
          				else:
          					print(f'Error: {axis} {limit} coordinate is off the bed. Exiting.')
          					error()
          	return fillGrid(bounds, lines)
          	
          def findBed():
          	bed = {
          		'X': 0,
          		'Y': 0,
          		}
          
          	bedCorners = os.environ.get("SLIC3R_BED_SHAPE")
          	maxXY = bedCorners.split(',')[2].split('x')
          	bed['X'] = int(maxXY[0])
          	bed['Y'] = int(maxXY[1])
          	print(bed)
          
          	return bed
           
          def findBounds(lines):
          	bounds = {
          		'X': {'min': 9999, 'max': 0},
          		'Y': {'min': 9999, 'max': 0},
          		}
          	
          	parsing = False
          	for line in lines:
          		if "move to next layer (0)" in line:
          			parsing = True
          			continue
          		elif "move to next layer (1)" in line:
          			break
           
          		if parsing:
          			# Get coordinates on this line
          			for match in re.findall(r'([YX])([\d.]+)\s', line):
          				# Get axis letter
          				axis = match[0]
           
          				# Skip axes we don't care about
          				if axis not in bounds:
          					continue
           
          				# Parse parameter value
          				value = float(match[1])
           
          				# Update bounds
          				bounds[axis]['min'] = math.floor(min(bounds[axis]['min'], value))
          				bounds[axis]['max'] = math.ceil(max(bounds[axis]['max'], value))
          				
          	# make sure the bounds are at least 2 x Probe Point Spacing, for small prints.			
          	if parsing:
          		global probeSpacing
          		
          		for axis in bounds:
          			spacing = (bounds[axis]['max'] - bounds[axis]['min'])/2
          			if spacing < probeSpacing:
          				probeSpacing = spacing
          
          	print("Bounds are: " + str(bounds))			
          	return bounds
           
           
          def fillGrid(bounds, lines):
          	# Fill in the level command template
          	gridNew = 'M557 X%d:%d Y%d:%d S%d' % (
          		bounds['X']['min'], bounds['X']['max'],
          		bounds['Y']['min'], bounds['Y']['max'],
          		probeSpacing
          	)
           
          	# Replace M557 command in GCODE
          	linesNew = []
          	for line in lines:
          		if line.startswith('M557'):
          			linesNew.append(re.sub(r'^M557 X\d+:\d+ Y\d+:\d+ S\d+', gridNew, line, flags=re.MULTILINE))
          			print('New M557: ' + linesNew[-1])
          		else:
          			linesNew.append(line)
          	return linesNew
            
          if __name__ == '__main__':
          	if sys.argv[1]:
          		main(fname = sys.argv[1])
          	else:
          		print('Error: Proper Slic3r post processing command is python3')
          		error()
          
          

          I've updated with improvements so that the console remains open if there is an error, and the probe point spacing automatically reduces in size for small prints.

          Luke'sLaboratoryundefined 1 Reply Last reply Reply Quote 2
          • Luke'sLaboratoryundefined
            Luke'sLaboratory @insertnamehere
            last edited by

            @insertnamehere

            You the Bomb!

            Thanks!

            Luke
            http://lukeslab.online

            1 Reply Last reply Reply Quote 0
            • Luke'sLaboratoryundefined
              Luke'sLaboratory
              last edited by

              Alright - I've made an improvement to the script, I have an extra-large machine and depending on how much of the bed I'm using at any given point, I can exceed the points-per-axis limitations of duet firmware (currently 21:21). It now will run the calculations, and if there are more than 21 points required, it converts over to using the P parameter instead of the S parameter, trying to honor the original requested spacing.

              #!/usr/bin/python
              """
              	Slic3r post-processing script for RepRap firmware printers which dynamically defines the mesh grid dimensions (M557) based on the print dimensions. 
              {1}
              Usage:
              {1}
              	Slic3r Settings:
              	In Print Settings > Output Options
              	1. turn no "Verbose G-code"
              	2. in "Post-processing scripts" type the full path to python and the full path to this script
              	e.g. <Python Path>\python.exe  <Script Path>\meshcalc.py;
              {1}
              	In Printer Settings > Custom G-code > Start G-code
              	Make sure the start g-code contains the M557 command, and that you probe the bed and load the compensation map,  e.g.
              	M557 X10:290 Y10:290 S20	; Setup default grid
              	G29							; Mesh bed probe
              	G29 S1						; Load compensation map
              		
              	Script Settings
              	probeSpacing = 20 - change this to the preferred probe point spacing in M557
              		
              	Note: The minimum X and Y of the probed area is limited to 2 times the probeSpacing.
              	This is so that prints with a small footprint will have a minimum 3x3 probe mesh
              {1}
              Args:
              {1}
              	Path: The path parameter will be provided by Slic3r.
              {1}
              Requirements:
              {1}
              	The latest version of Python.
              	Note that I use this on windows and haven't tried it on any other platform.
              	Also this script assumes that the bed origin (0,0) is NOT the centre of the bed. Go ahead and modify this script as required.
              {1}
              Credit:
              {1}
              	Based on code originally posted by CCS86 on https://forum.duet3d.com/topic/15302/cura-script-to-automatically-probe-only-printed-area?_=1587348242875.
              	and maybe 90% or more is code posted by MWOLTER on the same thread.
              	Thank you both. 
              """
               
              import sys
              import re
              import math
              import os
               
              probeSpacing = 20   		# set your required probe point spacing for M557
               
              def main(fname):	
              	print("Starting Mesh Calculations")
               
              	try:
              		_Slic3rFile = open(fname, encoding='utf-8')
              	except TypeError:
              		try:
              			_Slic3rFile = open(fname)
              		except:
              			print("Open file exception. Exiting.")
              			error()
              	except FileNotFoundError:
              		print('File not found. Exiting.')
              		error()
              		
              	lines = _Slic3rFile.readlines()
              	_Slic3rFile.close()
               
              	linesNew = calcBed(lines)
               
              	_Slic3rFile = open(fname, "r+")
              	_Slic3rFile.seek(0)                       
              	_Slic3rFile.truncate()
              	for element in linesNew:
              		_Slic3rFile.write(element)
              	_Slic3rFile.close()
              	
              	return
               
              def error():
              	# remove the next 2 lines to close console automatically
              	print("Press Enter to close") 
              	input()
              	sys.exit()
               
              def calcBed(lines):
              	bounds = findBounds(lines)
              	bed = findBed()
               
              	for axis in bounds:
              		if bounds[axis]['max'] - bounds[axis]['min'] < bed[axis]:
              			print(f'Success: {axis} mesh is smaller than bed')
              			
              		else:
              			print('Error: Mesh is larger than bed. Exiting.')
              			error()
               
              		for limit in bounds[axis]:
              			if limit == 'min':
              				if (bed[axis]) - bounds[axis][limit] > 0: 
              					print(f'Success: {axis} {limit} coordinate is on the bed.')
              				else:
              					print(f'Error: {axis} {limit} coordinate is off the bed. Exiting.')
              					error()
               
              			if limit == 'max':
              				if (bed[axis]) - bounds[axis][limit] > 0: 
              					print(f'Success: {axis} {limit} coordinate is on the bed.')
              				else:
              					print(f'Error: {axis} {limit} coordinate is off the bed. Exiting.')
              					error()
              	return fillGrid(bounds, lines)
              	
              def findBed():
              	bed = {
              		'X': 0,
              		'Y': 0,
              		}
               
              	bedCorners = os.environ.get("SLIC3R_BED_SHAPE")
              	maxXY = bedCorners.split(',')[2].split('x')
              	bed['X'] = int(maxXY[0])
              	bed['Y'] = int(maxXY[1])
              	print(bed)
               
              	return bed
               
              def findBounds(lines):
              	bounds = {
              		'X': {'min': 9999, 'max': 0},
              		'Y': {'min': 9999, 'max': 0},
              		}
              	
              	parsing = False
              	for line in lines:
              		if "move to next layer (0)" in line:
              			parsing = True
              			continue
              		elif "move to next layer (1)" in line:
              			break
               
              		if parsing:
              			# Get coordinates on this line
              			for match in re.findall(r'([YX])([\d.]+)\s', line):
              				# Get axis letter
              				axis = match[0]
               
              				# Skip axes we don't care about
              				if axis not in bounds:
              					continue
               
              				# Parse parameter value
              				value = float(match[1])
               
              				# Update bounds
              				bounds[axis]['min'] = math.floor(min(bounds[axis]['min'], value))
              				bounds[axis]['max'] = math.ceil(max(bounds[axis]['max'], value))
              				
              	# make sure the bounds are at least 2 x Probe Point Spacing, for small prints.
                  # also, make sure that the maximum amount of points isn't exceeded.
              	if parsing:
              		global probeSpacing
              		
              		for axis in bounds:
              			spacing = (bounds[axis]['max'] - bounds[axis]['min'])/2
              			if spacing < probeSpacing:
              				probeSpacing = spacing
               
              	print("Bounds are: " + str(bounds))			
              	return bounds
               
               
              def fillGrid(bounds, lines):
                  #Check the quantity of points - cannot exceed 21points per axis, otherwise will throw error and ruin print by not running a mesh
                  X_points=(bounds['X']['max']-bounds['X']['min'])/probeSpacing
                  Y_points=(bounds['Y']['max']-bounds['Y']['min'])/probeSpacing
                  if X_points>21 or Y_points>21:
                   Points=True
              	 #basically, if its over 21, just use 21, if not, round up, keeping roughly the same spacing for the non-affected axis
                   if X_points>21: X_points = 21 
                   else: X_points = math.ceil(X_points)
                   if Y_points>21: Y_points=21
                   else:Y_points = math.ceil(Y_points)
                   print('With your required print footprint, you\'ll exceed 21 points on either axis, changing to point based. Your new point grid is {}:{} points'.format(X_points,Y_points))
              
                  else: 
                   Points=False
                      
                  if Points == True:
                      # Fill in the level command template
                      gridNew = 'M557 X{}:{} Y{}:{} P{}:{}'.format(bounds['X']['min'], bounds['X']['max'],bounds['Y']['min'], bounds['Y']['max'], X_points, Y_points)
                  else:
              	    # Fill in the level command template 
              	    gridNew = 'M557 X{}:{} Y{}:{} S{}'.format(bounds['X']['min'], bounds['X']['max'],bounds['Y']['min'], bounds['Y']['max'], probeSpacing)
              
              	# Replace M557 command in GCODE
                  linesNew = []
                  for line in lines:
                      if line.startswith('M557'):
                          linesNew.append(re.sub(r'^M557 X\d+:\d+ Y\d+:\d+ S\d+', gridNew, line, flags=re.MULTILINE))
                          print('New M557: ' + linesNew[-1])
                      else:
                          linesNew.append(line)
                  return linesNew
                
              if __name__ == '__main__':
              	if sys.argv[1]:
              		main(fname = sys.argv[1])
              	else:
              		print('Error: Proper Slic3r post processing command is python3')
              		error()
              

              Luke
              http://lukeslab.online

              zaptaundefined 1 Reply Last reply Reply Quote 1
              • Baenwortundefined
                Baenwort
                last edited by

                Does this work for Deltas who don't have their M557 as a x and y coordinate?

                insertnamehereundefined 1 Reply Last reply Reply Quote 0
                • insertnamehereundefined
                  insertnamehere @Baenwort
                  last edited by

                  @Baenwort said in Cura Script to Automatically Probe Only Printed Area:

                  Does this work for Deltas who don't have their M557 as a x and y coordinate?

                  No it won't. But it could be modified.

                  Baenwortundefined 1 Reply Last reply Reply Quote 0
                  • zaptaundefined
                    zapta @Luke'sLaboratory
                    last edited by

                    @Luke-sLaboratory said in Cura Script to Automatically Probe Only Printed Area:

                    "move to next layer (0)"

                    Do we need to add to the prusaslicer gcode settings generation of layer markers or is there a setting to have it enabled automatically.

                    zaptaundefined 1 Reply Last reply Reply Quote 0
                    • zaptaundefined
                      zapta @zapta
                      last edited by zapta

                      I setup my prusaslicer to use this script and it works very well. I looked for some time for per-print mesh automation and this one does the job. Thanks for sharing it.

                      The file version I am using is in the github link below. It uses utility classes to handle intervals and rectangles but otherwise it's the same flow. There are still a few TODOs but I am using it with my regular prints.

                      https://github.com/zapta/misc/blob/master/duet3d_automesh/duet3d_automesh.py

                      1 Reply Last reply Reply Quote 1
                      • Baenwortundefined
                        Baenwort @insertnamehere
                        last edited by

                        @insertnamehere

                        It would be great!

                        zaptaundefined 1 Reply Last reply Reply Quote 0
                        • zaptaundefined
                          zapta @Baenwort
                          last edited by zapta

                          I cleaned up the python script. It now has command line flags that allow customization (set them in the slicer post processing command line).

                          https://github.com/zapta/misc/blob/master/duet3d_automesh/duet3d_automesh.py

                          I am very happy with the per-print quick partial meshing, getting good first layer without worrying about leveling. Ideally the slicers would provide the first layer's bounding box as place holders we can embed in gcode.

                          tcjundefined 1 Reply Last reply Reply Quote 1
                          • tcjundefined
                            tcj @zapta
                            last edited by

                            @zapta could you please change the script that it can handle negative coordinates in --meshable ?
                            This will make it usable for Delta printers, because
                            "For Cartesian printers, specify minimum and maximum X and Y values to probe and the probing interval. For Delta printers, specify the probing radius. If you define both, the probing area will be the intersection of the rectangular area and the circle. "
                            https://duet3d.dozuki.com/Wiki/Gcode#Section_M557_Set_Z_probe_point_or_define_probing_grid

                            Thank you

                            zaptaundefined 1 Reply Last reply Reply Quote 1
                            • zaptaundefined
                              zapta @tcj
                              last edited by

                              @tcj, I made the change. Can you give it another try?

                              https://github.com/zapta/misc/tree/master/duet3d_automesh

                              tcjundefined 1 Reply Last reply Reply Quote 0
                              • tcjundefined
                                tcj @zapta
                                last edited by tcj

                                @zapta thank you for the effort, but ist does not work yet
                                only defining the default meshable area within the script (line 50) by

                                default="-185:185,-185:185"
                                

                                works,
                                but when adding

                                <path to your python3> <path_to_the_duet3d_automesh.py file> --meshable "-185:185,-185:185"
                                

                                to the Post-processing script, it fails

                                zaptaundefined 1 Reply Last reply Reply Quote 1
                                • zaptaundefined
                                  zapta @tcj
                                  last edited by

                                  @tcj, try this syntax for the flags (notice the '=')

                                  --meshable=-30:250,-3:280

                                  tcjundefined 1 Reply Last reply Reply Quote 1
                                  • tcjundefined
                                    tcj @zapta
                                    last edited by

                                    @zapta 👍 👏

                                    Thank you

                                    1 Reply Last reply Reply Quote 0
                                    • DK90undefined
                                      DK90
                                      last edited by DK90

                                      @zapta said in Cura Script to Automatically Probe Only Printed Area:

                                      --meshable=-30:250,-3:280

                                      Hello, i have the following error when i am trying to ad the postprocess
                                      can some one help me please?

                                      Thanks

                                      Post-processing script C:\Users\Dominic\AppData\Local\Programs\Python\Python36-32\python.exe C:\Program Files\Prusa3D\PrusaSlicer\duet3d_automesh.py file on file D:\2_Projekte\3D Druck\2_Modelle\28_R2D2_Did3D\R2D2_Did3D_SW_FullPack_h\STL_Final\zumdruckenbereit\Cube_PLA_Center foot_0.2mm_PET_2h24m.gcode failed.
                                      Error code: 2
                                      ```~~~~
                                      tcjundefined 1 Reply Last reply Reply Quote 0
                                      • tcjundefined
                                        tcj @DK90
                                        last edited by

                                        @DK90 the space in "Program Files" leads to this error
                                        change it to

                                        C:\Users\Dominic\AppData\Local\Programs\Python\Python36-32\python.exe "C:\Program Files\Prusa3D\PrusaSlicer\duet3d_automesh.py"
                                        

                                        notice the "

                                        DK90undefined 1 Reply Last reply Reply Quote 1
                                        • DK90undefined
                                          DK90 @tcj
                                          last edited by DK90

                                          @tcj said in Cura Script to Automatically Probe Only Printed Area:

                                          C:\Users\Dominic\AppData\Local\Programs\Python\Python36-32\python.exe "C:\Program Files\Prusa3D\PrusaSlicer\duet3d_automesh.py"

                                          Oh 🙂 thank you now ot works 🙂
                                          but wehn i start the print, there is now mesh leveling ....
                                          where is my error?

                                          Start G-Code

                                          M83  ; extruder relative mode
                                          M140 S[first_layer_bed_temperature] ; set bed temp
                                          
                                          M190 S[first_layer_bed_temperature] ; wait for bed temp
                                          
                                          
                                          
                                          ; For automesh
                                               M557 TBD  ; parameters will be set automatically
                                               G28  ;home
                                               ;G29 S1 P"heightmap.csv"; Hoehenkarte Laden
                                          
                                          M109 S[first_layer_temperature] ; wait for extruder temp
                                          ;G29 S1 P"heightmap.csv"; Hoehenkarte Laden
                                          
                                          
                                          ; Reinigungs Fahrt
                                          G1 X-3.0 Y-10 F1500.0 ; go outside print area
                                          G92 E0.0
                                          G1 Z0
                                          G1 E8 ; Purge Bubble
                                          G1 X60.0 E9.0  F1000.0 ; intro line
                                          G1 X100.0 E12.5  F1000.0 ; intro line
                                          G92 E0.0
                                          

                                          G-Code layer change setting

                                          ; Automesh: begin layer [layer_num]
                                          
                                          tcjundefined 1 Reply Last reply Reply Quote 0
                                          • tcjundefined
                                            tcj @DK90
                                            last edited by tcj

                                            @DK90 said in Cura Script to Automatically Probe Only Printed Area:

                                            but wehn i start the print, there is now mesh leveling ....

                                            and it never will without G29 😉

                                            this is a part of my start sequence

                                            M140 S[first_layer_bed_temperature]
                                            M104 S[first_layer_temperature]
                                            M116; wait for temperatures to be reached
                                             
                                            ; For automesh
                                            M557 TBD  ; parameters will be set automatically
                                            G28 ; home
                                            G29 ; mesh
                                            
                                            1 Reply Last reply Reply Quote 0
                                            • First post
                                              Last post
                                            Unless otherwise noted, all forum content is licensed under CC-BY-SA