canstepper includes an optional G-code translation layer so host scripts
and simple senders can drive boards with familiar G/M commands.
This is inspired by the command set documented for
Klipper G-Codes and common Marlin /
RepRap practice. It is not a port of Klipper firmware or its MCU protocol —
commands are mapped onto GCSP + the Python library (Cartesian, CoreXY, Axis).
Kinematics modes
| Mode | Class | G-code factory | Motor mapping |
|---|---|---|---|
| Cartesian | Cartesian | GCodeController.from_cartesian | 1:1 — motor X = workspace X (Klipper kinematics: cartesian) |
| CoreXY | CoreXY | GCodeController.from_corexy | Belts (A=X+Y), (B=X-Y) |
| Ad-hoc | letter → Axis | GCodeController.from_axes | Same as cartesian moves, no named kinematics object |
Use cartesian for bed/gantry mills, routers, and classic cartesian printers where each stepper drives one axis. Use CoreXY when two motors share the XY plane through belts.
Quick start (cartesian mechanism)
from canstepper import CANStepperBus, Cartesian, GCodeController, Param
with CANStepperBus.serial("/dev/ttyACM0") as bus:
for n in (bus.node(1), bus.node(2), bus.node(3)):
n.enable().set_closed_loop(True).set_run_current(45)
n.set_param(Param.CL_MAX_SPEED, 1500)
# Independent X/Y belts + Z leadscrew (Klipper-style cartesian)
cart = Cartesian.from_nodes(
bus.node(1), bus.node(2), bus.node(3),
rotation_distance=40.0, # X/Y mm per rev
rotation_distance_z=8.0, # Z mm per rev
)
g = GCodeController.from_cartesian(cart, bus=bus)
g.run("G28") # soft home: zero at current pose
g.run("G90")
g.run("G1 X20 Y10 Z2 F600") # F = mm/min → path speed
print(g.run("M114")) # ok X:... Y:... Z:...
g.run("M112") # estop allOr build axes yourself (different soft limits / dual-Z on gantry):
from canstepper import Axis, Cartesian, DualMotorAxis, GCodeController
x = Axis(bus.node(1), rotation_distance=40.0, name="X")
y = Axis(bus.node(2), rotation_distance=40.0, name="Y")
z = DualMotorAxis(bus.node(3), bus.node(4), rotation_distance=8.0, name="Z")
cart = Cartesian(x, y, z)
g = GCodeController.from_cartesian(cart, bus=bus)Quick start (CoreXY)
from canstepper import CANStepperBus, CoreXY, GCodeController, Param
with CANStepperBus.serial("/dev/ttyACM0") as bus:
a, b = bus.node(1), bus.node(2)
for n in (a, b):
n.enable().set_closed_loop(True).set_run_current(45)
n.set_param(Param.CL_MAX_SPEED, 1500)
xy = CoreXY(a, b, rotation_distance=40.0) # mm per rev
g = GCodeController.from_corexy(xy, bus=bus)
g.run("G28") # soft home: zero at current pose
g.run("G90") # absolute
g.run("G1 X20 Y10 F600") # F = mm/min → 10 mm/s path
print(g.run("M114")) # ok X:... Y:...
g.run("M112") # estop allSupported commands
| Command | Behaviour |
|---|---|
| G0 / G1 | Linear move. X Y Z E targets; F feedrate mm/min |
| G4 | Dwell P milliseconds or S seconds |
| G21 | Millimetres (default). G20 inches → error |
| G28 | Soft home only: enable + logical zero (set_zero). For IO8 endstop or StallGuard search use node.home(...) — see Homing |
| G90 / G91 | Absolute / relative XYZ |
| G92 | Set logical position without moving |
| M17 | Enable drivers |
| M18 / M84 | Disable drivers |
| M82 / M83 | E absolute / relative |
| M105 | Stub temperature line (MCU temp if available) |
| M112 | Emergency stop (bus.estop_all()) |
| M114 | Report logical position |
| M115 | Firmware / node versions |
| M220 S<pct> | Speed factor % |
| M400 | Finish moves (no-op; moves are already blocking) |
Comments: ; ... and (...). Optional line numbers N12 ....
Units and feedrate
| Quantity | Convention |
|---|---|
| Position | mm (via each axis rotation_distance) |
F | mm/min (standard slicer G-code) |
| Internal motion | converted to mm/s for Cartesian / CoreXY / MotionGroup |
- Cartesian: motor travel = workspace travel on each axis (identity map).
- CoreXY: belt equations (A = X+Y), (B = X-Y).
Example scripts
Ship with the monorepo under can_stepper/examples/. Run from that directory
with PYTHONPATH=. if the installed PyPI package is older than this feature.
1. CoreXY square / diagonal (gcode_corexy.py)
cd can_stepper
PYTHONPATH=. python3 examples/gcode_corexy.py /dev/ttyACM0 1 2Issues a short G-code program (G28, box path, diagonal, M114, M18) on motors A=node 1, B=node 2 in closed loop.
2. Cartesian mechanism X/Y[/Z] (gcode_cartesian.py)
PYTHONPATH=. python3 examples/gcode_cartesian.py /dev/ttyACM0 1 2
# optional Z leadscrew on node 3:
PYTHONPATH=. python3 examples/gcode_cartesian.py /dev/ttyACM0 1 2 3Uses Cartesian + GCodeController.from_cartesian — independent steppers,
1:1 workspace mapping (not CoreXY belt math).
3. Interactive REPL (gcode_repl.py)
PYTHONPATH=. python3 examples/gcode_repl.py /dev/ttyACM0 1 2Type lines at the gcode> prompt (help, quit). Good for jogging and
checking M114 live.
4. Run a .gcode file (gcode_from_file.py)
PYTHONPATH=. python3 examples/gcode_from_file.py examples/gcode/square.gcode
PYTHONPATH=. python3 examples/gcode_from_file.py examples/gcode/diagonal_box.gcode /dev/ttyACM0 1 2
PYTHONPATH=. python3 examples/gcode_from_file.py examples/gcode/relative_jog.gcodeSample G-code files
| File | Contents |
|---|---|
examples/gcode/square.gcode | Soft home + rectangle |
examples/gcode/diagonal_box.gcode | Box + diagonal |
examples/gcode/relative_jog.gcode | G91 relative jogs |
Example square.gcode:
; CoreXY square demo (mm)
G21
G90
M17
G28
M114
G1 F600
G1 X20 Y0
G1 X20 Y15
G1 X0 Y15
G1 X0 Y0
M114
M185. Run many lines in Python
g.run_many([
"G28",
"G1 X10 Y0 F600",
"G1 X10 Y10",
"G1 X0 Y0",
"M114",
])
# or a whole file
g.run_file("path/to/job.gcode")Extending
def my_probe(line):
# custom behaviour
return "ok"
g.register("G29", my_probe)Unsupported commands raise GCodeError.
Related
- Python library — Axis / Cartesian / CoreXY APIs
- Firmware — board sketch download
- Motion — Cartesian, CoreXY, DualMotorAxis without G-code
- Klipper G-Codes — external vocabulary reference