├── .gitattributes ├── .gitignore ├── OpenbuildsGRBL.cps ├── README.md ├── datasheets ├── Autodesk CAM Post Processor Documentation.url ├── Autodesk Post Processor Manual.pdf └── PostProcessor Class Reference.url ├── f360-easel.cps └── images ├── AddingCustom PostProcessor.PNG └── SettingHomeLocation.PNG /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### NotepadPP ### 2 | # Notepad++ backups # 3 | *.bak 4 | 5 | 6 | ### Windows ### 7 | # Windows image file caches 8 | Thumbs.db 9 | ehthumbs.db 10 | 11 | # Folder config file 12 | Desktop.ini 13 | 14 | # Recycle Bin used on file shares 15 | $RECYCLE.BIN/ 16 | 17 | # Windows Installer files 18 | *.cab 19 | *.msi 20 | *.msm 21 | *.msp 22 | 23 | # Windows shortcuts 24 | *.lnk 25 | -------------------------------------------------------------------------------- /OpenbuildsGRBL.cps: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Custom Post-Processor for GRBL based Openbuilds-style CNC and Laser-Cutting machines 4 | Using Exiting Post Processors as inspiration 5 | For documentation, see GitHub Wiki : https://github.com/Strooom/GRBL-Post-Processor/wiki 6 | This post-Processor should work on GRBL-based machines such as 7 | * Openbuilds - OX, C-Beam 8 | * Inventables - X-Carve 9 | * ShapeOko / Carbide3D 10 | * your spindle is Makita RT0700 or Dewalt 611 11 | 12 | 22/AUG/2016 - V1 : Kick Off 13 | 23/AUG/2016 - V2 : Added Machining Time to Operations overview at file header 14 | 24/AUG/2016 - V3 : Added extra user properties - further cleanup of unused variables 15 | 07/SEP/2016 - V4 : Added support for INCHES. Added a safe retract at beginning of first section 16 | 11/OCT/2016 - V5 17 | 30/JAN/2017 - V6 : Modified capabilities to also allow waterjet, laser-cutting.. 18 | 25/JUL/2018 - V7 : Code review to eliminate a bug causing GRBL Error 33. Compared this postProcessor with the latest grbl.cps 19 | 16/NOV/2018 - V8 : Added extra coolant modes : Flood = M8, Mist = M7, Flood+Mist 20 | 05/DEC/2019 - V9 : Modified capabilities to also allow turning. 21 | */ 22 | 23 | description = "Openbuilds Grbl"; 24 | vendor = "Openbuilds"; 25 | vendorUrl = "http://openbuilds.com"; 26 | model = "OX"; 27 | description = "Open Hardware Desktop CNC Router"; 28 | legal = "Copyright (C) 2012-2018 by Autodesk, Inc."; 29 | certificationLevel = 2; 30 | 31 | extension = "nc"; // file extension of the gcode file 32 | setCodePage("ascii"); // character set of the gcode file 33 | //setEOL(CRLF); // end-of-line type : default for Windows OS is CRLF (so that's why this line is commented out), change to CR, LF, CRLF or LFCR if you are on another OS... 34 | 35 | capabilities = CAPABILITY_MILLING | CAPABILITY_JET | CAPABILITY_TURNING; // intended for a CNC, so Milling, or 2D machines, such as lasers.. I also added turning, as apparently it works fine for a lathe running GRBL 36 | tolerance = spatial(0.002, MM); // when linearizing a move, fusion will create linear segments which are within this amount of the actual path... Smaller values will result in more and smaller linear segments. GRBL.cps uses 0.002 mm 37 | minimumChordLength = spatial(0.25, MM); // minimum lenght of an arc, if Fusion needs a short arc, it will linearize... Problem with very small arcs is that rounding errors resulting from limited number of digits, result in GRBL error 33 38 | minimumCircularRadius = spatial(0.5, MM); // minimum radius of an arc.. Fusion will linearize if you need a smaller arc. Same problem with rounding errors as above 39 | maximumCircularRadius = spatial(1000, MM); 40 | minimumCircularSweep = toRad(0.1); 41 | maximumCircularSweep = toRad(350); // maximum angle of an arc. 350 prevents Fusion from outputting full circles.. Although GRBL can do this (with some special GCODE syntax) it is more robust to not do it and stick to standard G2 G3 use.. Fusion will split a longer arc into multiple smaller G2 G3 arcs 42 | allowHelicalMoves = true; 43 | allowedCircularPlanes = (1 << PLANE_XY) | (1 << PLANE_ZX) | (1 << PLANE_YZ); // This is safer (instead of using 'undefined'), as it enumerates the allowed planes in GRBL 44 | 45 | // user-defined properties : defaults are set, but they can be changed from a dialog box in Fusion when doing a post. 46 | properties = 47 | { 48 | spindleOnOffDelay: 1.0, // time (in seconds) the spindle needs to get up to speed or stop 49 | spindleTwoDirections : false, // true : spindle can rotate clockwise and counterclockwise, will send M3 and M4. false : spindle can only go clockwise, will only send M3 50 | hasCoolant : false, // true : machine uses the coolant output, M8 M9 will be sent. false : coolant output not connected, so no M8 M9 will be sent 51 | hasSpeedDial : true, // true : the spindle is of type Makita RT0700, Dewalt 611 with a Dial to set speeds 1-6. false : other spindle 52 | machineHomeZ : -10, // absolute machine coordinates where the machine will move to at the end of the job - first retracting Z, then moving home X Y 53 | machineHomeX : -10, 54 | machineHomeY : -10 55 | }; 56 | 57 | propertyDefinitions = 58 | { 59 | spindleOnOffDelay: {title: "Spindle On/Off Delay (s)", description: "Time the spindle needs to get up to speed or stop.", type: "number"}, 60 | spindleTwoDirections: {title: "Bidirectional Spindle", description: "True if the spindle can rotate clockwise and counterclockwise, will send M3 and M4. False if the spindle can only go clockwise, will only send M3.", type: "boolean"}, 61 | hasCoolant: {title: "Has Coolant", description: "True if the machine uses the coolant output (M8 M9 will be sent). False if the coolant output not connected.", type: "boolean"}, 62 | hasSpeedDial: {title: "Has Speed Dial", description: "True if the spindle is of type Makita RT0700, Dewalt 611 with a Dial to set speeds 1-6.", type: "boolean"}, 63 | machineHomeZ: {title: "Z Home Position", description: "Absolute machine coordinates where the machine will move to at the end of the job - first retracting Z, then moving home X Y", type: "number"}, 64 | machineHomeX: {title: "X Home Position", description: "Absolute machine coordinates where the machine will move to at the end of the job - first retracting Z, then moving home X Y", type: "number"}, 65 | machineHomeY: {title: "Y Home Position", description: "Absolute machine coordinates where the machine will move to at the end of the job - first retracting Z, then moving home X Y", type: "number"}, 66 | }; 67 | 68 | // creation of all kinds of G-code formats - controls the amount of decimals used in the generated G-Code 69 | var gFormat = createFormat({prefix:"G", decimals:0}); 70 | var mFormat = createFormat({prefix:"M", decimals:0}); 71 | 72 | var xyzFormat = createFormat({decimals:(unit == MM ? 4 : 6)}); 73 | var feedFormat = createFormat({decimals:(unit == MM ? 1 : 3)}); 74 | var rpmFormat = createFormat({decimals:0}); 75 | var secFormat = createFormat({decimals:1, forceDecimal:true, trim:false}); 76 | var taperFormat = createFormat({decimals:1, scale:DEG}); 77 | 78 | var xOutput = createVariable({prefix:"X"}, xyzFormat); 79 | var yOutput = createVariable({prefix:"Y"}, xyzFormat); 80 | var zOutput = createVariable({prefix:"Z"}, xyzFormat); 81 | var feedOutput = createVariable({prefix:"F"}, feedFormat); 82 | var sOutput = createVariable({prefix:"S", force:true}, rpmFormat); 83 | 84 | var iOutput = createReferenceVariable({prefix:"I"}, xyzFormat); 85 | var jOutput = createReferenceVariable({prefix:"J"}, xyzFormat); 86 | var kOutput = createReferenceVariable({prefix:"K"}, xyzFormat); 87 | 88 | var gMotionModal = createModal({}, gFormat); // modal group 1 // G0-G3, ... 89 | var gPlaneModal = createModal({onchange:function () {gMotionModal.reset();}}, gFormat); // modal group 2 // G17-19 90 | var gAbsIncModal = createModal({}, gFormat); // modal group 3 // G90-91 91 | var gFeedModeModal = createModal({}, gFormat); // modal group 5 // G93-94 92 | var gUnitModal = createModal({}, gFormat); // modal group 6 // G20-21 93 | 94 | function toTitleCase(str) 95 | { 96 | // function to reformat a string to 'title case' 97 | return str.replace(/\w\S*/g, function(txt) 98 | { 99 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 100 | }); 101 | } 102 | 103 | function rpm2dial(rpm) 104 | { 105 | // translates an RPM for the spindle into a dial value, eg. for the Makita RT0700 and Dewalt 611 routers 106 | // additionally, check that spindle rpm is between minimum and maximum of what our spindle can do 107 | 108 | // array which maps spindle speeds to router dial settings, 109 | // according to Makita RT0700 Manual : 1=10000, 2=12000, 3=17000, 4=22000, 5=27000, 6=30000 110 | var speeds = [0, 10000, 12000, 17000, 22000, 27000, 30000]; 111 | 112 | if (rpm < speeds[1]) 113 | { 114 | alert("Warning", rpm + " rpm is below minimum spindle RPM of " + speeds[1] + " rpm"); 115 | return 1; 116 | } 117 | 118 | if (rpm > speeds[speeds.length - 1]) 119 | { 120 | alert("Warning", rpm + " rpm is above maximum spindle RPM of " + speeds[speeds.length - 1] + " rpm"); 121 | return (speeds.length - 1); 122 | } 123 | 124 | var i; 125 | for (i=1; i < (speeds.length-1); i++) 126 | { 127 | if ((rpm >= speeds[i]) && (rpm <= speeds[i+1])) 128 | { 129 | return ((rpm - speeds[i]) / (speeds[i+1] - speeds[i])) + i; 130 | } 131 | } 132 | 133 | alert("Error", "Error in calculating router speed dial.."); 134 | error("Fatal Error calculating router speed dial"); 135 | return 0; 136 | } 137 | 138 | function writeBlock() 139 | { 140 | writeWords(arguments); 141 | } 142 | 143 | function writeComment(text) 144 | { 145 | // Remove special characters which could confuse GRBL : $, !, ~, ?, (, ) 146 | // In order to make it simple, I replace everything which is not A-Z, 0-9, space, : , . 147 | // Finally put everything between () as this is the way GRBL & UGCS expect comments 148 | writeln("(" + String(text).replace(/[^a-zA-Z\d :=,.]+/g, " ") + ")"); 149 | } 150 | 151 | function onOpen() 152 | { 153 | 154 | // here you set all the properties of your machine, so they can be used later on 155 | var myMachine = getMachineConfiguration(); 156 | myMachine.setWidth(600); 157 | myMachine.setDepth(800); 158 | myMachine.setHeight(130); 159 | myMachine.setMaximumSpindlePower(700); 160 | myMachine.setMaximumSpindleSpeed(30000); 161 | myMachine.setMilling(true); 162 | myMachine.setTurning(false); 163 | myMachine.setToolChanger(false); 164 | myMachine.setNumberOfTools(1); 165 | myMachine.setNumberOfWorkOffsets(6); 166 | myMachine.setVendor("OpenBuilds"); 167 | myMachine.setModel("OX CNC 1000 x 750"); 168 | myMachine.setControl("GRBL V0.9j"); 169 | 170 | writeln("%"); // open the file with a '%'. GRBL does not really do anything with this, but as this is so part of classic GCODE, I decided to leave it in :-) 171 | 172 | var productName = getProduct(); 173 | writeComment("Made in : " + productName); 174 | writeComment("G-Code optimized for " + myMachine.getVendor() + " " + myMachine.getModel() + " with " + myMachine.getControl() + " controller"); 175 | 176 | writeln(""); 177 | 178 | if (programName) 179 | { 180 | writeComment("Program Name : " + programName); 181 | } 182 | if (programComment) 183 | { 184 | writeComment("Program Comments : " + programComment); 185 | } 186 | 187 | var numberOfSections = getNumberOfSections(); 188 | writeComment(numberOfSections + " Operation" + ((numberOfSections == 1)?"":"s") + " :"); 189 | 190 | for (var i = 0; i < numberOfSections; ++i) 191 | { 192 | var section = getSection(i); 193 | var tool = section.getTool(); 194 | var rpm = section.getMaximumSpindleSpeed(); 195 | 196 | if (section.hasParameter("operation-comment")) 197 | { 198 | writeComment((i+1) + " : " + section.getParameter("operation-comment")); 199 | } 200 | else 201 | { 202 | writeComment(i+1); 203 | } 204 | if (section.workOffset > 0) 205 | { 206 | writeComment(" Work Coordinate System : G" + (section.workOffset + 53)); 207 | } 208 | writeComment(" Tool : " + toTitleCase(getToolTypeName(tool.type)) + " " + tool.numberOfFlutes + " Flutes, Diam = " + xyzFormat.format(tool.diameter) + "mm, Len = " + tool.fluteLength + "mm"); 209 | if (properties.hasSpeedDial) 210 | { 211 | writeComment(" Spindle : RPM = " + rpm + ", set router dial to " + rpm2dial(rpm)); 212 | } 213 | else 214 | { 215 | writeComment(" Spindle : RPM = " + rpm); 216 | } 217 | var machineTimeInSeconds = section.getCycleTime(); 218 | var machineTimeHours = Math.floor(machineTimeInSeconds / 3600); 219 | machineTimeInSeconds = machineTimeInSeconds % 3600; 220 | var machineTimeMinutes = Math.floor(machineTimeInSeconds / 60); 221 | var machineTimeSeconds = Math.floor(machineTimeInSeconds % 60); 222 | var machineTimeText = " Machining time : "; 223 | if (machineTimeHours > 0) 224 | { 225 | machineTimeText = machineTimeText + machineTimeHours + " hours " + machineTimeMinutes + " min "; 226 | } 227 | else if (machineTimeMinutes > 0) 228 | { 229 | machineTimeText = machineTimeText + machineTimeMinutes + " min "; 230 | } 231 | machineTimeText = machineTimeText + machineTimeSeconds + " sec"; 232 | writeComment(machineTimeText); 233 | } 234 | writeln(""); 235 | 236 | writeBlock(gAbsIncModal.format(90), gFeedModeModal.format(94)); 237 | writeBlock(gPlaneModal.format(17)); 238 | switch (unit) 239 | { 240 | case IN: 241 | writeBlock(gUnitModal.format(20)); 242 | break; 243 | case MM: 244 | writeBlock(gUnitModal.format(21)); 245 | break; 246 | } 247 | 248 | writeln(""); 249 | } 250 | 251 | function onComment(message) 252 | { 253 | writeComment(message); 254 | } 255 | 256 | function forceXYZ() 257 | { 258 | xOutput.reset(); 259 | yOutput.reset(); 260 | zOutput.reset(); 261 | } 262 | 263 | function forceAny() 264 | { 265 | forceXYZ(); 266 | feedOutput.reset(); 267 | } 268 | 269 | function onSection() 270 | { 271 | var nmbrOfSections = getNumberOfSections(); // how many operations are there in total 272 | var sectionId = getCurrentSectionId(); // what is the number of this operation (starts from 0) 273 | var section = getSection(sectionId); // what is the section-object for this operation 274 | 275 | 276 | // check RadiusCompensation setting 277 | var radComp = getRadiusCompensation(); 278 | var sectionId = getCurrentSectionId(); 279 | if (radComp != RADIUS_COMPENSATION_OFF) 280 | { 281 | alert("Error", "RadiusCompensation is not supported in GRBL - Change RadiusCompensation in CAD/CAM software to Off/Center/Computer"); 282 | error("Fatal Error in Operation " + (sectionId + 1) + ": RadiusCompensation is found in CAD file but is not supported in GRBL"); 283 | return; 284 | } 285 | 286 | 287 | // Insert a small comment section to identify the related G-Code in a large multi-operations file 288 | var comment = "Operation " + (sectionId + 1) + " of " + nmbrOfSections; 289 | if (hasParameter("operation-comment")) 290 | { 291 | comment = comment + " : " + getParameter("operation-comment"); 292 | } 293 | writeComment(comment); 294 | writeln(""); 295 | 296 | // To be safe (after jogging to whatever position), move the spindle up to a safe home position before going to the initial position 297 | // At end of a section, spindle is retracted to clearance height, so it is only needed on the first section 298 | // it is done with G53 - machine coordinates, so I put it in front of anything else 299 | if(isFirstSection()) 300 | { 301 | writeBlock(gAbsIncModal.format(90)); // Set to absolute coordinates 302 | if (isMilling()) 303 | { 304 | writeBlock(gFormat.format(53), gMotionModal.format(0), "Z" + xyzFormat.format(properties.machineHomeZ)); // Retract spindle to Machine Z Home 305 | } 306 | } 307 | 308 | // Write the WCS, ie. G54 or higher.. default to WCS1 / G54 if no or invalid WCS in order to prevent using Machine Coordinates G53 309 | if ((section.workOffset < 1) || (section.workOffset > 6)) 310 | { 311 | alert("Warning", "Invalid Work Coordinate System. Select WCS 1..6 in CAM software. In Fusion360, set the WCS in CAM-workspace | Setup-properties | PostProcess-tab. Selecting default WCS1/G54"); 312 | writeBlock(gFormat.format(54)); // output what we want, G54 313 | } 314 | else 315 | { 316 | writeBlock(gFormat.format(53 + section.workOffset)); // use the selected WCS 317 | } 318 | 319 | var tool = section.getTool(); 320 | 321 | // Insert the Spindle start command 322 | if (tool.clockwise) 323 | { 324 | writeBlock(sOutput.format(tool.spindleRPM), mFormat.format(3)); 325 | } 326 | else if (properties.spindleTwoDirections) 327 | { 328 | writeBlock(sOutput.format(tool.spindleRPM), mFormat.format(4)); 329 | } 330 | else 331 | { 332 | alert("Error", "Counter-clockwise Spindle Operation found, but your spindle does not support this"); 333 | error("Fatal Error in Operation " + (sectionId + 1) + ": Counter-clockwise Spindle Operation found, but your spindle does not support this"); 334 | return; 335 | } 336 | 337 | // Wait some time for spindle to speed up - only on first section, as spindle is not powered down in-between sections 338 | if(isFirstSection()) 339 | { 340 | onDwell(properties.spindleOnOffDelay); 341 | } 342 | 343 | // If the machine has coolant, write M8, else write M9 344 | if (properties.hasCoolant) 345 | { 346 | if (tool.coolant == COOLANT_FLOOD) 347 | { 348 | writeBlock(mFormat.format(8)); 349 | } 350 | else if (tool.coolant == COOLANT_MIST) 351 | { 352 | writeBlock(mFormat.format(7)); 353 | } 354 | else if (tool.coolant == COOLANT_FLOOD_MIST) 355 | { 356 | writeBlock(mFormat.format(7)); 357 | writeBlock(mFormat.format(8)); 358 | } 359 | else 360 | { 361 | writeBlock(mFormat.format(9)); 362 | } 363 | } 364 | 365 | var remaining = currentSection.workPlane; 366 | if (!isSameDirection(remaining.forward, new Vector(0, 0, 1))) 367 | { 368 | alert("Error", "Tool-Rotation detected - GRBL only supports 3 Axis"); 369 | error("Fatal Error in Operation " + (sectionId + 1) + ": Tool-Rotation detected but GRBL only supports 3 Axis"); 370 | } 371 | setRotation(remaining); 372 | 373 | forceAny(); // this ensures all axis and feed are output at the beginning of the section 374 | 375 | // Rapid move to initial position, first XY, then Z 376 | var initialPosition = getFramePosition(currentSection.getInitialPosition()); 377 | writeBlock(gAbsIncModal.format(90), gMotionModal.format(0), xOutput.format(initialPosition.x), yOutput.format(initialPosition.y)); 378 | writeBlock(gMotionModal.format(0), zOutput.format(initialPosition.z)); 379 | } 380 | 381 | function onDwell(seconds) 382 | { 383 | writeBlock(gFormat.format(4), "P" + secFormat.format(seconds)); 384 | } 385 | 386 | function onSpindleSpeed(spindleSpeed) 387 | { 388 | writeBlock(sOutput.format(spindleSpeed)); 389 | } 390 | 391 | function onRapid(_x, _y, _z) 392 | { 393 | var x = xOutput.format(_x); 394 | var y = yOutput.format(_y); 395 | var z = zOutput.format(_z); 396 | if (x || y || z) 397 | { 398 | writeBlock(gMotionModal.format(0), x, y, z); 399 | feedOutput.reset(); // after a G0, we will always resend the Feedrate... Is this useful ? 400 | } 401 | } 402 | 403 | function onLinear(_x, _y, _z, feed) 404 | { 405 | var x = xOutput.format(_x); 406 | var y = yOutput.format(_y); 407 | var z = zOutput.format(_z); 408 | var f = feedOutput.format(feed); 409 | 410 | if (x || y || z || f) 411 | { 412 | writeBlock(gMotionModal.format(1), x, y, z, f); 413 | } 414 | } 415 | 416 | function onRapid5D(_x, _y, _z, _a, _b, _c) 417 | { 418 | alert("Error", "Tool-Rotation detected - GRBL only supports 3 Axis"); 419 | error("Tool-Rotation detected but GRBL only supports 3 Axis"); 420 | } 421 | 422 | function onLinear5D(_x, _y, _z, _a, _b, _c, feed) 423 | { 424 | alert("Error", "Tool-Rotation detected - GRBL only supports 3 Axis"); 425 | error("Tool-Rotation detected but GRBL only supports 3 Axis"); 426 | } 427 | 428 | function onCircular(clockwise, cx, cy, cz, x, y, z, feed) 429 | { 430 | var start = getCurrentPosition(); 431 | 432 | switch (getCircularPlane()) 433 | { 434 | case PLANE_XY: 435 | writeBlock(gPlaneModal.format(17), gMotionModal.format(clockwise ? 2 : 3), xOutput.format(x), yOutput.format(y), zOutput.format(z), iOutput.format(cx - start.x, 0), jOutput.format(cy - start.y, 0), feedOutput.format(feed)); 436 | break; 437 | case PLANE_ZX: 438 | writeBlock(gPlaneModal.format(18), gMotionModal.format(clockwise ? 2 : 3), xOutput.format(x), yOutput.format(y), zOutput.format(z), iOutput.format(cx - start.x, 0), kOutput.format(cz - start.z, 0), feedOutput.format(feed)); 439 | break; 440 | case PLANE_YZ: 441 | writeBlock(gPlaneModal.format(19), gMotionModal.format(clockwise ? 2 : 3), xOutput.format(x), yOutput.format(y), zOutput.format(z), jOutput.format(cy - start.y, 0), kOutput.format(cz - start.z, 0), feedOutput.format(feed)); 442 | break; 443 | default: 444 | linearize(tolerance); 445 | } 446 | } 447 | 448 | function onSectionEnd() 449 | { 450 | xOutput.reset(); // resetting, so everything that comes after this section, will get X, Y, Z, F outputted, even if their values did not change.. 451 | yOutput.reset(); 452 | zOutput.reset(); 453 | feedOutput.reset(); 454 | 455 | writeln(""); // add a blank line at the end of each section 456 | } 457 | 458 | function onClose() 459 | { 460 | writeBlock(gAbsIncModal.format(90)); // Set to absolute coordinates for the following moves 461 | if (isMilling()) // For CNC we move the Z-axis up, for lasercutter it's not needed 462 | { 463 | writeBlock(gAbsIncModal.format(90), gFormat.format(53), gMotionModal.format(0), "Z" + xyzFormat.format(properties.machineHomeZ)); // Retract spindle to Machine Z Home 464 | } 465 | writeBlock(mFormat.format(5)); // Stop Spindle 466 | if (properties.hasCoolant) 467 | { 468 | writeBlock(mFormat.format(9)); // Stop Coolant 469 | } 470 | onDwell(properties.spindleOnOffDelay); // Wait for spindle to stop 471 | writeBlock(gAbsIncModal.format(90), gFormat.format(53), gMotionModal.format(0), "X" + xyzFormat.format(properties.machineHomeX), "Y" + xyzFormat.format(properties.machineHomeY)); // Return to home position 472 | 473 | writeBlock(mFormat.format(30)); // Program End 474 | writeln("%"); // EndOfFile marker - GRBL doesn't use it / ignores it, but it's so much tradition, so I left it in.. 475 | } 476 | 477 | 478 | 479 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GRBL-Post-Processor 2 | 3 | A custom Post Processor for Autodesk Fusion360 4 | Creates .nc files optimized for GRBL based Openbuilds-style machines 5 | 6 | See wiki for more details : https://github.com/Strooom/GRBL-Post-Processor/wiki 7 | -------------------------------------------------------------------------------- /datasheets/Autodesk CAM Post Processor Documentation.url: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=http://cam.autodesk.com/posts/reference/index.html 3 | -------------------------------------------------------------------------------- /datasheets/Autodesk Post Processor Manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strooom/GRBL-Post-Processor/f8333e9b85ae53842f7cacff6f9562fb51aecf83/datasheets/Autodesk Post Processor Manual.pdf -------------------------------------------------------------------------------- /datasheets/PostProcessor Class Reference.url: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=http://cam.autodesk.germinateapps.com/posts/reference/classPostProcessor.html 3 | -------------------------------------------------------------------------------- /f360-easel.cps: -------------------------------------------------------------------------------- 1 | /** 2 | Experimental PP for Fusion360 3 | Modified from the Grbl post made by Autodesk 4 | **/ 5 | 6 | description = "Easel"; 7 | vendor = "Inventables, Inc."; 8 | vendorUrl = "https://inventables.com"; 9 | legal = "Copyright (C) 2012-2015 by Autodesk, Inc."; 10 | certificationLevel = 2; 11 | minimumRevision = 24000; 12 | 13 | extension = "nc"; 14 | setCodePage("ascii"); 15 | 16 | capabilities = CAPABILITY_MILLING; 17 | tolerance = spatial(0.01, MM); 18 | 19 | minimumChordLength = spatial(0.01, MM); 20 | minimumCircularRadius = spatial(0.01, MM); 21 | maximumCircularRadius = spatial(1000, MM); 22 | minimumCircularSweep = toRad(0.01); 23 | maximumCircularSweep = toRad(180); 24 | allowHelicalMoves = false; 25 | allowedCircularPlanes = undefined; // allow any circular motion 26 | 27 | // user-defined properties 28 | properties = { 29 | clampOffset: 0.0 30 | // All have been removed 31 | }; 32 | 33 | var numberOfToolSlots = 1; 34 | 35 | var mapCoolantTable = new Table( 36 | [9, 8], 37 | {initial:COOLANT_OFF, force:true}, 38 | "Invalid coolant mode" 39 | ); 40 | 41 | var gFormat = createFormat({prefix:"G", decimals:0}); 42 | var mFormat = createFormat({prefix:"M", decimals:0}); 43 | 44 | var xyzFormat = createFormat({decimals:(unit == MM ? 3 : 4)}); 45 | var feedFormat = createFormat({decimals:(unit == MM ? 1 : 2)}); 46 | var toolFormat = createFormat({decimals:0}); 47 | var rpmFormat = createFormat({decimals:0}); 48 | var secFormat = createFormat({decimals:3, forceDecimal:true}); // seconds - range 0.001-1000 49 | var taperFormat = createFormat({decimals:1, scale:DEG}); 50 | 51 | var xOutput = createVariable({prefix:"X"}, xyzFormat); 52 | var yOutput = createVariable({prefix:"Y"}, xyzFormat); 53 | var zOutput = createVariable({prefix:"Z"}, xyzFormat); 54 | var feedOutput = createVariable({prefix:"F"}, feedFormat); 55 | var sOutput = createVariable({prefix:"S", force:true}, rpmFormat); 56 | 57 | // circular output 58 | var iOutput = createReferenceVariable({prefix:"I"}, xyzFormat); 59 | var jOutput = createReferenceVariable({prefix:"J"}, xyzFormat); 60 | var kOutput = createReferenceVariable({prefix:"K"}, xyzFormat); 61 | 62 | var gMotionModal = createModal({}, gFormat); // modal group 1 // G0-G3, ... 63 | var gPlaneModal = createModal({onchange:function () {gMotionModal.reset();}}, gFormat); // modal group 2 // G17-19 64 | var gAbsIncModal = createModal({}, gFormat); // modal group 3 // G90-91 65 | var gFeedModeModal = createModal({}, gFormat); // modal group 5 // G93-94 66 | var gUnitModal = createModal({}, gFormat); // modal group 6 // G20-21 67 | 68 | var WARNING_WORK_OFFSET = 0; 69 | 70 | var stock = { 71 | minX: 0, 72 | minY: 0, 73 | minZ: 0, 74 | maxX: 0, 75 | maxY: 0, 76 | maxZ: 0 77 | } 78 | 79 | // collected state 80 | var sequenceNumber; 81 | var currentWorkOffset; 82 | 83 | function writeTool(tool) { 84 | // TOOL/MILL, Diameter, Corner radius, Height, Taper Angle 85 | var toolString = "TOOL/MILL" 86 | + "," + xyzFormat.format(tool.diameter) 87 | + "," + xyzFormat.format(tool.cornerRadius) 88 | + "," + xyzFormat.format(tool.fluteLength) 89 | + "," + xyzFormat.format(tool.taperAngle); 90 | 91 | writeComment(toolString); 92 | } 93 | 94 | function writeStockAndOffset(stock, wcs) { 95 | try { 96 | // STOCK/BLOCK, Length, Width, Height, Origin X, Origin Y, Origin Z 97 | var blockString = "STOCK/BLOCK" 98 | + "," + xyzFormat.format(stock.maxX - stock.minX) 99 | + "," + xyzFormat.format(stock.maxY - stock.minY) 100 | + "," + xyzFormat.format(stock.maxZ - stock.minZ) 101 | + "," + wcs; 102 | 103 | 104 | writeComment(blockString); 105 | } catch(e) { 106 | writeComment(e.message) 107 | } 108 | } 109 | /** 110 | Writes the specified block. 111 | */ 112 | function writeBlock() { 113 | if (properties.showSequenceNumbers) { 114 | writeWords2("N" + sequenceNumber, arguments); 115 | sequenceNumber += properties.sequenceNumberIncrement; 116 | } else { 117 | writeWords(arguments); 118 | } 119 | } 120 | 121 | 122 | function formatComment(text) { 123 | return "(" + String(text).replace(/[\(\)]/g, "") + ")"; 124 | } 125 | 126 | /** 127 | Output a comment. 128 | */ 129 | function writeComment(text) { 130 | writeln(formatComment(text)); 131 | } 132 | 133 | function onOpen() { 134 | if (!properties.separateWordsWithSpace) { 135 | setWordSeparator(""); 136 | } 137 | 138 | sequenceNumber = properties.sequenceNumberStart; 139 | 140 | // absolute coordinates and feed per min 141 | writeBlock(gAbsIncModal.format(90), gFeedModeModal.format(94)); 142 | writeBlock(gPlaneModal.format(17)); 143 | 144 | switch (unit) { 145 | case IN: 146 | writeBlock(gUnitModal.format(20)); 147 | break; 148 | case MM: 149 | writeBlock(gUnitModal.format(21)); 150 | break; 151 | } 152 | } 153 | 154 | function onParameter(name, value) { 155 | if(name === 'stock-lower-x') { stock.minX = value; } 156 | if(name === 'stock-lower-y') { stock.minY = value; } 157 | if(name === 'stock-lower-z') { stock.minZ = value; } 158 | if(name === 'stock-upper-x') { stock.maxX = value; } 159 | if(name === 'stock-upper-y') { stock.maxY = value; } 160 | if(name === 'stock-upper-z') { stock.maxZ = value; } 161 | } 162 | 163 | function onComment(message) { 164 | writeComment(message); 165 | } 166 | 167 | /** Force output of X, Y, and Z. */ 168 | function forceXYZ() { 169 | xOutput.reset(); 170 | yOutput.reset(); 171 | zOutput.reset(); 172 | } 173 | 174 | /** Force output of X, Y, Z, and F on next output. */ 175 | function forceAny() { 176 | forceXYZ(); 177 | feedOutput.reset(); 178 | } 179 | 180 | function onSection() { 181 | var insertToolCall = isFirstSection() || 182 | currentSection.getForceToolChange && currentSection.getForceToolChange() || 183 | (tool.number != getPreviousSection().getTool().number); 184 | 185 | var retracted = false; // specifies that the tool has been retracted to the safe plane 186 | var newWorkOffset = isFirstSection() || 187 | (getPreviousSection().workOffset != currentSection.workOffset); // work offset changes 188 | var newWorkPlane = isFirstSection() || 189 | !isSameDirection(getPreviousSection().getGlobalFinalToolAxis(), currentSection.getGlobalInitialToolAxis()); 190 | if (insertToolCall || newWorkOffset || newWorkPlane) { 191 | 192 | // stop spindle before retract during tool change 193 | if (insertToolCall && !isFirstSection()) { 194 | onCommand(COMMAND_STOP_SPINDLE); 195 | } 196 | } 197 | 198 | writeln(""); 199 | writeTool(tool); 200 | writeStockAndOffset(stock, currentSection.wcsOrigin); 201 | 202 | if (insertToolCall) { 203 | retracted = true; 204 | onCommand(COMMAND_COOLANT_OFF); 205 | 206 | if (tool.number > numberOfToolSlots) { 207 | warning(localize("Tool number exceeds maximum value.")); 208 | return 209 | } 210 | 211 | writeBlock("T" + toolFormat.format(tool.number), mFormat.format(6)); 212 | } 213 | 214 | if (insertToolCall || 215 | isFirstSection() || 216 | (rpmFormat.areDifferent(tool.spindleRPM, sOutput.getCurrent())) || 217 | (tool.clockwise != getPreviousSection().getTool().clockwise)) { 218 | if (tool.spindleRPM < 1) { 219 | error(localize("Spindle speed out of range.")); 220 | } 221 | if (tool.spindleRPM > 99999) { 222 | warning(localize("Spindle speed exceeds maximum value.")); 223 | } 224 | writeBlock( 225 | sOutput.format(tool.spindleRPM), mFormat.format(tool.clockwise ? 3 : 4) 226 | ); 227 | } 228 | 229 | // wcs 230 | 231 | var workOffset = currentSection.workOffset; 232 | if (workOffset == 0) { 233 | warningOnce(localize("Work offset has not been specified. Using G54 as WCS."), WARNING_WORK_OFFSET); 234 | workOffset = 1; 235 | } 236 | if (workOffset > 0) { 237 | if (workOffset > 6) { 238 | error(localize("Work offset out of range.")); 239 | return; 240 | } else { 241 | if (workOffset != currentWorkOffset) { 242 | writeBlock(gFormat.format(53 + workOffset)); // G54->G59 243 | currentWorkOffset = workOffset; 244 | } 245 | } 246 | } 247 | 248 | forceXYZ(); 249 | 250 | { // pure 3D 251 | var remaining = currentSection.workPlane; 252 | if (!isSameDirection(remaining.forward, new Vector(0, 0, 1))) { 253 | error(localize("Tool orientation is not supported.")); 254 | return; 255 | } 256 | setRotation(remaining); 257 | } 258 | 259 | // set coolant after we have positioned at Z 260 | { 261 | var c = mapCoolantTable.lookup(tool.coolant); 262 | if (c) { 263 | writeBlock(mFormat.format(c)); 264 | } else { 265 | warning(localize("Coolant not supported.")); 266 | } 267 | } 268 | 269 | forceAny(); 270 | 271 | var initialPosition = getFramePosition(currentSection.getInitialPosition()); 272 | if (!retracted) { 273 | if (getCurrentPosition().z < initialPosition.z) { 274 | writeBlock(gMotionModal.format(0), zOutput.format(initialPosition.z)); 275 | } 276 | } 277 | 278 | if (insertToolCall || retracted) { 279 | var lengthOffset = tool.lengthOffset; 280 | if (lengthOffset > numberOfToolSlots) { 281 | error(localize("Length offset out of range.")); 282 | return; 283 | } 284 | 285 | gMotionModal.reset(); 286 | writeBlock(gPlaneModal.format(17)); 287 | 288 | if (!machineConfiguration.isHeadConfiguration()) { 289 | writeBlock( 290 | gAbsIncModal.format(90), 291 | gMotionModal.format(0), xOutput.format(initialPosition.x), yOutput.format(initialPosition.y) 292 | ); 293 | writeBlock(gMotionModal.format(0), zOutput.format(initialPosition.z)); 294 | } else { 295 | writeBlock( 296 | gAbsIncModal.format(90), 297 | gMotionModal.format(0), 298 | xOutput.format(initialPosition.x), 299 | yOutput.format(initialPosition.y), 300 | zOutput.format(initialPosition.z) 301 | ); 302 | } 303 | } else { 304 | writeBlock( 305 | gAbsIncModal.format(90), 306 | gMotionModal.format(0), 307 | xOutput.format(initialPosition.x), 308 | yOutput.format(initialPosition.y) 309 | ); 310 | } 311 | } 312 | 313 | function onDwell(seconds) { 314 | if (seconds > 99999.999) { 315 | warning(localize("Dwelling time is out of range.")); 316 | } 317 | seconds = clamp(0.001, seconds, 99999.999); 318 | writeBlock(gFormat.format(4), "P" + secFormat.format(seconds)); 319 | } 320 | 321 | function onSpindleSpeed(spindleSpeed) { 322 | writeBlock(sOutput.format(spindleSpeed)); 323 | } 324 | 325 | var pendingRadiusCompensation = -1; 326 | 327 | function onRadiusCompensation() { 328 | pendingRadiusCompensation = radiusCompensation; 329 | } 330 | 331 | function clampOffsetDistance() { 332 | if (unit == MM) { 333 | return properties.clampOffset * 25.4; 334 | } else { 335 | return properties.clampOffset; 336 | } 337 | } 338 | 339 | function onRapid(_x, _y, _z) { 340 | if (_x) { _x += clampOffsetDistance(); } 341 | if (_y) { _y += clampOffsetDistance(); } 342 | var x = xOutput.format(_x); 343 | var y = yOutput.format(_y); 344 | var z = zOutput.format(_z); 345 | if (x || y || z) { 346 | if (pendingRadiusCompensation >= 0) { 347 | error(localize("Radius compensation mode cannot be changed at rapid traversal.")); 348 | return; 349 | } 350 | writeBlock(gMotionModal.format(0), x, y, z); 351 | feedOutput.reset(); 352 | } 353 | } 354 | 355 | function onLinear(_x, _y, _z, feed) { 356 | // at least one axis is required 357 | if (_x) { _x += clampOffsetDistance(); } 358 | if (_y) { _y += clampOffsetDistance(); } 359 | if (pendingRadiusCompensation >= 0) { 360 | // ensure that we end at desired position when compensation is turned off 361 | xOutput.reset(); 362 | yOutput.reset(); 363 | } 364 | var x = xOutput.format(_x); 365 | var y = yOutput.format(_y); 366 | var z = zOutput.format(_z); 367 | var f = feedOutput.format(feed); 368 | if (x || y || z) { 369 | if (pendingRadiusCompensation >= 0) { 370 | error(localize("Radius compensation mode is not supported.")); 371 | return; 372 | } else { 373 | writeBlock(gMotionModal.format(1), x, y, z, f); 374 | } 375 | } else if (f) { 376 | if (getNextRecord().isMotion()) { // try not to output feed without motion 377 | feedOutput.reset(); // force feed on next line 378 | } else { 379 | writeBlock(gMotionModal.format(1), f); 380 | } 381 | } 382 | } 383 | 384 | function onRapid5D(_x, _y, _z, _a, _b, _c) { 385 | error(localize("Multi-axis motion is not supported.")); 386 | } 387 | 388 | function onLinear5D(_x, _y, _z, _a, _b, _c, feed) { 389 | error(localize("Multi-axis motion is not supported.")); 390 | } 391 | 392 | function onCircular(clockwise, cx, cy, cz, x, y, z, feed) { 393 | // one of X/Y and I/J are required and likewise 394 | 395 | if (pendingRadiusCompensation >= 0) { 396 | error(localize("Radius compensation cannot be activated/deactivated for a circular move.")); 397 | return; 398 | } 399 | 400 | linearize(tolerance); 401 | } 402 | 403 | var mapCommand = { 404 | COMMAND_STOP:0, 405 | COMMAND_END:2, 406 | COMMAND_SPINDLE_CLOCKWISE:3, 407 | COMMAND_SPINDLE_COUNTERCLOCKWISE:4, 408 | COMMAND_STOP_SPINDLE:5, 409 | COMMAND_COOLANT_ON:8, 410 | COMMAND_COOLANT_OFF:9 411 | }; 412 | 413 | function onCommand(command) { 414 | switch (command) { 415 | case COMMAND_START_SPINDLE: 416 | onCommand(tool.clockwise ? COMMAND_SPINDLE_CLOCKWISE : COMMAND_SPINDLE_COUNTERCLOCKWISE); 417 | return; 418 | case COMMAND_LOCK_MULTI_AXIS: 419 | return; 420 | case COMMAND_UNLOCK_MULTI_AXIS: 421 | return; 422 | case COMMAND_BREAK_CONTROL: 423 | return; 424 | case COMMAND_TOOL_MEASURE: 425 | return; 426 | } 427 | 428 | var stringId = getCommandStringId(command); 429 | var mcode = mapCommand[stringId]; 430 | if (mcode != undefined) { 431 | writeBlock(mFormat.format(mcode)); 432 | } else { 433 | onUnsupportedCommand(command); 434 | } 435 | } 436 | 437 | function onSectionEnd() { 438 | // noop 439 | } 440 | 441 | function onClose() { 442 | // noop 443 | } 444 | -------------------------------------------------------------------------------- /images/AddingCustom PostProcessor.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strooom/GRBL-Post-Processor/f8333e9b85ae53842f7cacff6f9562fb51aecf83/images/AddingCustom PostProcessor.PNG -------------------------------------------------------------------------------- /images/SettingHomeLocation.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strooom/GRBL-Post-Processor/f8333e9b85ae53842f7cacff6f9562fb51aecf83/images/SettingHomeLocation.PNG --------------------------------------------------------------------------------