Python G-code Development Kit. A library to directly generate gcode for CNC machines based on object features without abstract design, slicing, processing, etc.
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.
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:
- Onefinity Woodworker with the stock Onefinity-flavored Buildbotics controller, Teco L510-101-H1 VFD, and a cheap 1.5kW, 24kRPM, air-cooled spindle
- DIY Kossel (delta) 3D-Printer running a knock-off Smoothieboard and OctoPi
- DIY CNC retrofit Rong Fu RF-30 Mill running a knock-off Smoothieboard and OctoPi
- DIY modernized Denford ORAC Lathe running a knock-off Smoothieboard
If anyone is interested in providing feedback on other platforms, I'd love to hear it.
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.
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.
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.
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.
{
"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.
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.
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.
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.
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.
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.
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.
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
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
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
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
.
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.
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
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
{
"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.
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.
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.
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.
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
.
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.