Cura Script to Automatically Probe Only Printed Area
-
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.
-
-
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()
-
Does this work for Deltas who don't have their M557 as a x and y coordinate?
-
@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.
-
@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.
-
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
-
It would be great!
-
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.
-
@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_gridThank you
-
@tcj, I made the change. Can you give it another try?
-
@zapta thank you for the effort, but ist does not work yet
only defining the default meshable area within the script (line 50) bydefault="-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
-
@tcj, try this syntax for the flags (notice the '=')
--meshable=-30:250,-3:280
-
Thank you
-
@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 ```~~~~
-
@DK90 the space in "Program Files" leads to this error
change it toC:\Users\Dominic\AppData\Local\Programs\Python\Python36-32\python.exe "C:\Program Files\Prusa3D\PrusaSlicer\duet3d_automesh.py"
notice the "
-
@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]
-
@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
-
oh yes that sounds good....that was my mistake...:D Thank you
-
@DK90, if you use panel due and prusaslicer (and maybe cura, didn't try), try to enable Support Remaining Time in the Printer Settings | General, tab. This will include M73 time markers in the gcode that which cause the post processing script to show remaining hours and minutes in the Standby fields of Panel Duet (ignore the '-' prefix).
This time estimation is much more consistent than the ones provided by Duet.