Grafito CANStepper

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

ModeClassG-code factoryMotor mapping
CartesianCartesianGCodeController.from_cartesian1:1 — motor X = workspace X (Klipper kinematics: cartesian)
CoreXYCoreXYGCodeController.from_corexyBelts (A=X+Y), (B=X-Y)
Ad-hocletter → AxisGCodeController.from_axesSame 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 all

Or 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 all

Supported commands

CommandBehaviour
G0 / G1Linear move. X Y Z E targets; F feedrate mm/min
G4Dwell P milliseconds or S seconds
G21Millimetres (default). G20 inches → error
G28Soft home only: enable + logical zero (set_zero). For IO8 endstop or StallGuard search use node.home(...) — see Homing
G90 / G91Absolute / relative XYZ
G92Set logical position without moving
M17Enable drivers
M18 / M84Disable drivers
M82 / M83E absolute / relative
M105Stub temperature line (MCU temp if available)
M112Emergency stop (bus.estop_all())
M114Report logical position
M115Firmware / node versions
M220 S<pct>Speed factor %
M400Finish moves (no-op; moves are already blocking)

Comments: ; ... and (...). Optional line numbers N12 ....

Units and feedrate

QuantityConvention
Positionmm (via each axis rotation_distance)
Fmm/min (standard slicer G-code)
Internal motionconverted 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 2

Issues 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 3

Uses 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 2

Type 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.gcode

Sample G-code files

FileContents
examples/gcode/square.gcodeSoft home + rectangle
examples/gcode/diagonal_box.gcodeBox + diagonal
examples/gcode/relative_jog.gcodeG91 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
M18

5. 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.

On this page