cilynx / pygdk

Python G-code Development Kit
GNU General Public License v3.0
15 stars 4 forks source link

pygdk

Python G-code Development Kit. A library to directly generate gcode for CNC machines based on object features without abstract design, slicing, processing, etc.

Disclaimer

Use this software at your own risk. Check the gcode thoroughly before running it on your machine. Everything you do with this software is your choice and responsibility. I hope it ss useful, but I cannot be held responsible for injury or damage, even if it's due to errors in this software. CNC machines are dangerous. Be smart about it.

Scope

This project makes a solid effort to produce generic, standard gcode that will run properly on any CNC controller. It does not currently leverage any advanced or proprietary features/codes and if we add any of that sort of thing in the future, they'll be tied to machine profiles so they're only used when expressly configured.

That said, I am limited in platforms I can personally test. I'm currently testing on:

If anyone is interested in providing feedback on other platforms, I'd love to hear it.

Quickstart

While getting your feet wet, start with a simple design like the following bangle:

from pygdk import Mill

onefinity = Mill('onefinity.json')

onefinity.material = 'Soft Wood'
onefinity.tool = '1/4" Downcut'

onefinity.helix(c_x=0, c_y=0, diameter=67, depth=21, z_step=10)
onefinity.helix(c_x=0, c_y=0, diameter=77, depth=21, z_step=10, outside=True)

onefinity.print_gcode() # Dump gcode to stdout
onefinity.CAMotics()    # Simulate gcode in CAMotics
onefinity.send_gcode()   # Send gcode to the machine

This design assumes your stock is 21mm thick and that you'll be zeroing Z on the surface.

quickstart

Simulate

This step is technically optional, but it's good practice to simulate your gcode before you run it on your machine. Even if you do everything right, pygdk is very early in development and is likely to have bugs that you can catch in simulation before anything bad happens in the real world.

Personally, I'm a fan of CAMotics as it integrates with and is made by the same folks as the open-source buildbotics controller Onefinity uses.

UPDATE 10/21: There is now a CAMotics helper, so you don't have to save the file and call CAMotics yourself. Just call machine.CAMotics() at the end of your script and pygdk will save your gcode to a file and bring it up in CAMotics for you.

simulate-camotics

The first time you run CAMotics, you'll need to setup your tool table by right-clicking in the blank Tool Table section and selecting Load Tool Table. If you don't already have a tool table you want to use, you can load in tools.json -- pygdk's default tool table has more information in it than CAMotics can leverage, but it is backwards compatible.

Execute

If you use OctoPrint, you can set OctoPrint Server and OctoPrint API Key in your machine configuration to enable sending your generated gcode directly to OctoPrint by calling the machine.OctoPrint() helper at the end of your script.

If not, you can copy the gcode to your machine however you would normally do it. This might be via the machine's native web interface, a USB stick, or you can even push gcode to BuildBotics controllers from inside CAMotics.

Take a deep breath, remember that you are personally responsible for everything good or bad that your machine does, then let it rip.

Machine Configuration

{
   "Name": "Onefinity",
   "Controller": {
      "Flavor": "Buildbotics",
      "Tasmota": ["192.168.1.10",1],
      "Boot Wait": 35,
      "Shutdown Wait": 10,
      "Hostname / IP": "onefinity.local"
   },
   "Accessories": {
       "Screen": {
          "Tasmota": ["192.168.1.10",2],
          "Auto": "Before"
       },
       "VFD": {
          "Tasmota": ["192.168.1.11",2],
          "Auto": "Before"
       },
       "Light": {
          "WeMo": ["192.168.1.12",1],
          "Auto": "After"
       }
   },
   "Max Feed Rate (mm/min)": 10000,
   "Max Spindle RPM": 24000,
   "Tool Table": "tools.json",
   "Plotter": {
      "Magazine": [
         ["","Light Blue","Lime",""],
         ["Black","Red","Blue","Orange"]
      ],
      "Slot Zero": [806,33],
      "Pen Spacing": 13.7,
      "Z-Touch": -123,
      "Z-Click": -88,
      "Z-Stage": -110
   }
}

Name is the human-readable name of your machine. It's not used for anything internally, just for printing in output so you know which machine we're talking about.

Controller is a optional parameter set, but unless you're planning on just generating gcode and handling all of the logistics of the machine yourself, you probably want to define them.

Flavor is used by Machine.send_gcode(). If it is defined, pygdk will use this information to integrate properly with your controller. Valid options are Buildbotics and OctoPrint.

Boot Wait is used by Machine.power_on() and Machine.power_off(). If it is defined, pygdk will wait this many seconds after powering on the machine, before asking it to do anything.

Shutdown Wait is used by Machine.power_on() and Machine.power_off(). If it is defined, pygdk will wait this many seconds after sending the controller the shutdown signal, before turning off the power.

Hostname / IP is an optional parameter used by Machine.send_gcode(). If it is defined, pygdk will use this host information to communicate with your machine controller.

Tasmota and WeMo are used by Machine.power_on() and Machine.power_off(). If one or the other is defined as the IP address and socket ID of a Tasmota or WeMo compatible smart plug, pygdk will use this smart plug to power on and off your controller and/or accessories.

Accessories are anything that's plugged into a smart plug as defined above. Set Auto to Before for the accessory to power up before the controller or After to power up after the controller has fully booted. Most controllers like their screens to be on before they're booted and I like to use my machine lights as an indicator of if I'm fully booted / fully shut down.

Max Feed Rate (mm/min) is an optional parameter used by the motion planner. If it is defined, pygdk will use this value as the default feed for rapids and will ensure that calculated feeds are less than or equal to this value.

Max Spindle RPM is an optional parameter used by the feeds-and-speeds planner. If it is defined, pygdk will will ensure that calculated RPM are less than or equal to this value and will adjust feeds to maintain appropriate chipload.

Tool Table is an optional CAMotics-backwards-compatible tool table used by the feeds-and-speeds planner. If it is defined, pygdk will load tool parameters from this JSON file.

Plotter is a parameter set required for plotter-flavored initialization, but ignored otherwise. All Plotter parameters are required for plotting.

Magazine is a matrix of pen colors in the magazine. Any empty slots can be represented by an empty string. These are the color names you use for Machine.pen_color() and Turtle.pencolor().

Slot Zero is the absolute machine [x,y] coordinates when the back left pen slot is directly below the pen changer post.

Pen Spacing is the distance between pen slots. In the current magazine design, it is 13.7, but this is subject to change. If Pen 0 selects correctly, but others do not, you can adjust this value accordingly.

Z-Touch is the absolute machine z coordinate where the actuated pen just touches the drawing surface. You want this to be pretty well dead-on, but if you're 0.5mm to close, that's better than not touching -- you'll just have a little more play in the pen tip.

Z-Click is the absolute machine z coordinate where the selected pen actuates. This needs to be close to dead-on, but you may need to fudge it 1mm or so to account for differences between pens.

Z-Stage is an absolute machine z coordinate where an actuated pen will not touch the drawing surface and the pens can still slide under the pen changer post without colliding. It doesn't matter if you err closer to the drawing surface or the changer post so long as you're not running into either one.

API

Machine

Initialization

from pygdk import Machine

machine = Machine('onefinity.json')

To get started, import the Machine class and create your primary Machine object that you'll use to do pretty much everything else. Check out onefinity.json for a fleshed out configuration and rf30.json for a minimal example.

Configuration

Material
machine.material = 'Soft Wood'

Defining your workpiece material is not required, but if you do, pygdk will attempt to automatically determine appropriate feeds and speeds. If you don't define it, you're on your own and will have to set your feed and rpm manually before pygdk will generate your gcode. Check out feeds-and-speeds.json for supported tool and workpiece materials.

Feed
machine.feed = 500

This is the feed for your G1 (cutting, drawing, etc.) moves. It is separate from max_feed which is the feed used for G0 (rapid) moves and is defined in your machine configuration JSON. If you do not define the workpiece material and tool specifications, you have to set the feed manually before pygdk can generate gcode. Setting feed manually will override a previously calculated value.

Constant Surface Speed
machine.css = 1000

Constant Surface Speed is the speed of the cutter against the workpiece, regardless of the operation. On a lathe, it's the speed the workpiece is passing by the stationary tool. On a mill, it's the speed each flute of the endmill is sweeping through the workpiece. CSS will be automatically set if you define your tool parameters and workpiece material. If you find you have to set this manually, either because your tool/material combination is not currently in pygdk's lookup table or because the values we have don't work well, please cut an issue or send a PR with updated parameters to improve and extend tables/feeds-and-speeds.json.

When you set css, spindle rpm will automatically be calculated and set for you.

RPM
machine.rpm = 15000

Spindle RPM is the speed at which your tool rotates. If you define your tool parameters and workpiece material, pygdk will calculate and set rpm for you automatically. Expressly setting rpm will override a previously calculated value. If you find you're having to override automatically calculated values or manually set due to missing tool/material combinations, please cut an issue or send a PR with updated parameters to improve and extend feeds-and-speeds.json.

When you set rpm and have your tool parameters defined, css will be automatically calculated and set for you.

Motion Primitives

Linear
machine.rapid(x,y,z)
machine.irapid(u,v,w)
machine.li(x,y,z)
machine.ili(u,v,w)

rapid moves (G0) at max_feed generally not cutting/drawing to the absolute point [x,y,z] in the currently defined coordinate system.

irapid moves (G0) at max_feed generally not cutting/drawing to the relative point [u,v,w] away from the current position.

li moves (G1) at feed, generally cutting/drawing to the absolute point [x,y,z] in the currently defined coordinate system.

ili moves (G1) at feed, generally cutting/drawing to the relative point [u,v,w] away from the current position.

All moves take an optional comment string parameter that will be included on the relevant line of gcode.

Canned Features

Bolt Circle
machine.bolt_circle(c_x, c_y, n, r, depth)

c_x is the x coordinate of the center of the bolt circle

c_y is the y coordinate of the center of the bolt circle

n is the number of bolt holes to put around the perimeter

r is the radius of the bolt circle

depth is how deep to drill each hole

bolt-circle

Circular Pocket
machine.circular_pocket(c_x, c_y, diameter, depth, step=None, finish=0.1, retract=True)

c_x is the x coordinate of the center of the pocket

c_y is the y coordinate of the center of the pocket

diameter is the diameter of the pocket

depth is how deep to make the pocket

step is how much material to take off with each pass, but will be automatically calculated if not provided

finish is how much material to leave for the finishing pass

retract is whether or not to retract the cutter to a safe position outside of the pocket after completing the operation

circular-pocket

Frame
machine.frame(c_x, c_y, x, y, z_top=0, z_bottom=0, z_step=None, inside=False, r=None, r_steps=10)

Like helix, but rectangular.

c_x is the x coordinate of the center of the frame

c_y is the y coordinate of the center of the frame

x is the x-dimension of the frame

y is the y-dimension of the frame

z_top is the top of the frame -- usually 0

z_bottom is the bottom depth of the frame -- usually something negative

z_step is how far down z to move with each pass

inside is whether the cutter is inside or outside the requested dimensions

r is the corner radius

frame

Helix
machine.helix(c_x, c_y, diameter, depth, z_step=0.1, outside=False, retract=True):

Follows a circular path in [x,y] while steadily spiraling down in z.

c_x is the x coordinate of the center of the helix

c_y is the y coordinate of the center of the helix

diameter is the diameter of the helix

depth is how deep to cut in total

z_step is how far to move down for each rotation of the helix

If outside is False (the default), the cutter will run inside the requested diameter. If outside is True, the cutter will run outside the requested diameter.

retract is whether or not to retract the cutter to a safe position outside of the helix after performing the operation. If you're moving somewhere else, you probably want it to be True, but if you're going to slot off sideways from within the helix, you might want it to be False. helix

Mill Drill
machine.mill_drill(c_x, c_y, diameter, depth, z_step=0.1, retract=True)

Uses a helix under the hood to drill a hole that is up to 2x the diameter of the endmill being used.

c_x is the x coordinate of the center of the hole

c_y is the y coordinate of the center of the hole

diameter is the diameter of the hole

depth is how deep to drill

z_step is how far to move down for each rotation of the helix

retract is whether or not to retract the cutter to a safe position outside of the hole after performing the operation. Generally, you probably want to do this so it defaults to True, but it's useful to set to False when mill-drilling to start a pocket.

mill-drill

Pocket Circle
machine.pocket_circle(c_x, c_y, n, r, depth, diameter)

Like a Bolt Circle, but all the holes are Circular Pockets

c_x is the x coordinate of the center of the pocket circle

c_y is the y coordinate of the center of the pocket circle

n is the number of pocket holes to put around the perimeter

r is the radius of the pocket circle

depth is how deep to mill each pocket

diameter is the diameter of each pocket pocket-circle

Rectangular Pocket
machine.rectangular_pocket(c_x, c_y, x, y, z_top=0, z_bottom=0, z_step=None undercut=False, retract=True):

c_x is the x coordinate of the center of the pocket

c_y is the y coordinate of the center of the pocket

x is the x-dimension of the pocket

y is the y-dimension of the pocket

z_top is the top of the pocket -- usually 0

z_bottom is the bottom depth of the pocket -- usually something negative

z_step is how far down z to move with each pass when initially spiraling in

undercut is whether or not to put "mouse ears" in the corners to provide clearance for sharp corners to mate into the pocket

rectangular-pocket

Plotter

Configuration

{
   "Name": "Onefinity",
   "Type": "Mill",
   "Max Feed Rate (mm/min)": 10000,
   "Max Spindle RPM": 24000,
   "Tool Table": "tools.json",
   "Plotter": {
      "Magazine": [
         ["","Light Blue","Lime",""],
         ["Black","Red","Blue","Orange"]
      ],
      "Slot Zero": [806,33],
      "Pen Spacing": 13.7,
      "Z-Touch": -123,
      "Z-Click": -88,
      "Z-Stage": -110
   }
}

To use the plotter functionality, you need to define both your loaded colors and the functional offsets in your machine configuration.

For color names, you can use anything you like and just make sure you remember them as you'll use these exact color names to select the pens later on. The array in the config above corresponds to the the pens in the picture below.

plotter-colors

Until I figure out an automated routine to handle offset calibration, you'll need to jog your machine around manually and record the appropriate coordinates in your config.

Make sure you have homed your machine and zeroed out any local offsets before starting this process.

With the magazine installed on the carriage and the changer installed on the front of the right-side Y-rail, jog the magazine over such that the back-left pen slot is centered under the changer post. Record your current [x,y] as your Slot Zero. I round this to the nearest mm as the clicky-tops of the pens have more lateral slop in them than that anyway.

There's no pen is slot zero in the picture below due to some bad design choices on my part, but hopefully you get the idea.

slot-zero

Pen Spacing is a function of the magazine design and for the current incarnation is 13.7. Future magazines designed for different pens may have different spacing. For the curious, this is the center-to-center offset between slots in both X and Y and allows pygdk to address any slot by adding multiples of this offset to the known position of Slot Zero.

Load a pen into any slot of the magazine and manually actuate it so the tip is sticking out ready to draw. Slowly jog down in Z until you see the pen just barely move up in the magazine. (Check out this video at 0.25x speed and you'll see the motion in the black pen that I'm talking about.) You can also jog around in X,Y to see when you start drawing a line. Find the highest Z-coordinate where you draw a consistent line and set that as your Z-Touch. I round this one down (away from zero) to the next mm as it's better to have a little more unnecessary motion in the magazine than to have the pen skip due to imperfections in the drawing surface.

z-touch

Leave the pen actuated and jog up and over so that your loaded pen is under the pen changer post. Very slowly, jog up to find the lowest Z-coordinate that actuates the clicky-top. This is your Z-Click. I round this one up to the nearest mm to account for differences between pens. With the Sharpies I'm using, there's about 2mm between the actuation point and bottoming out the clicker, so an extra fraction of a mm isn't going to hurt anything.

z-click

Jog the magazine down now such that you unactuated pen tops are just a bit lower than the bottom of the changer post. This doesn't have to be super precise. You're looking for a happy position that is safely well higher than your drawing surface but also lower than the changer post so you don't crash into it. Once you find your happy place that doesn't draw and doesn't crash into the changer, set that as your Z-Stage.

z-stage

FTC Comment

As an Amazon Affiliate, I earn a small commission from qualifying purchases made from my referral links, which helps to fund more open-source projects like this one.