Grafito CANStepper

Installation

pip install grafito-canstepper

Python 3.9+. The only runtime dependency is pyserial.

Connecting

from canstepper import CANStepperBus

bus = CANStepperBus.serial("/dev/ttyACM0")
print(bus.discover())        # {1: '1.0', 2: '1.0'}

discover() broadcasts a ping and returns every node that answered with its firmware version. The bus object owns a receive thread; use it as a context manager or call bus.close() when done.

Nodes

node = bus.node(1)

# Configuration setters chain:
node.set_run_current(40).set_hold_current(15).set_microsteps(16)
node.set_max_speed(720.0).set_acceleration(1440.0)
node.enable()

# Motion:
node.move_to(180.0, blocking=True)   # absolute degrees
node.move_by(-45.0)                  # relative
node.run(90.0)                       # continuous deg/s, signed
node.stop()                          # ramped stop
node.set_zero()                      # define zero here

blocking=True waits for the firmware's move done event (closed loop: trapezoid finished and settled within pid_tolerance) and raises NodeFault if the axis faults.

Closed-loop trapezoid (firmware ≥1.2)

Closed-loop position moves plan a rest-to-rest trapezoid (cl_max_speed / cl_max_accel) and track it with velocity feedforward (v = v_ff + PID(r − encoder)):

node.configure_closed_loop_speed(
    4800.0,                 # production cruise ≈ 800 RPM
    run_current=70,
    microsteps=8,
    stealthchop=False,      # SpreadCycle
    persist=True,
)
node.move_to(720.0, blocking=True)
Open-loop run()Closed-loop cruise
Max measured*~1200 RPM~1000 RPM
Production soak800 RPM, 10 min 100% success

*PR42HS40-1204AF-02 @ 24 V — see Closed-loop speed tuning.

Host-side trap helpers (same math as firmware): canstepper.kinematics.plan_trapezoid, eval_trapezoid, trapezoid_time.

Parameters

Every firmware setting is a named parameter — set it, read it back, persist it:

from canstepper import Param

node.set_param(Param.CL_MAX_SPEED, 1440.0)  # trap cruise vmax
node.set_param("stall_threshold", 60)       # names work too
print(node.get_param("cl_max_speed"))
node.save_config()                          # survives power cycles

Writes are verified: the node acknowledges every set, and a rejected value (outside the hardware sanity range) raises ParamRejected. The full table is in the protocol reference.

Homing (endstop / sensorless / set-zero)

Three firmware methods; full walkthrough and example scripts: Homing and endstops.

node.configure_endstop(enabled=True, active_high=False, action=1)
node.home(method="endstop", direction=-1, speed_deg_s=20.0)

node.set_stall_threshold(60)
node.home(method="stallguard", direction=-1, speed_deg_s=25.0)  # or "sensorless"

node.home(method="set_zero")
PYTHONPATH=. python3 examples/home_endstop.py /dev/ttyACM0 1 -1
PYTHONPATH=. python3 examples/home_sensorless.py /dev/ttyACM0 1 -1 60

Telemetry

Nodes stream telemetry continuously. The last decoded values are always available without touching the bus:

st = node.state
print(st.position_deg, st.velocity_deg_s, st.age())   # age in seconds

Blocking getters poll the node directly:

node.get_position()      # degrees, straight from the encoder
node.get_motion()        # (velocity, position error)
node.get_pid_status()    # loop state, fault, output
node.get_can_health()    # bus-off counters etc.
node.get_env()           # (MCU temp °C, bus V placeholder)
node.get_stallguard()    # StallGuard result

TMC2209 driver diagnostics (firmware ≥1.4)

TEL_DRIVER carries full TMC status: OTPW / OT, short-to-GND, open load, StealthChop/standstill, GSTAT, and cs_actual:

from canstepper import CANStepperBus

with CANStepperBus.serial("/dev/ttyACM0") as bus:
    node = bus.node(1)
    node.enable()
    d = node.get_driver_status()
    print(d.uart_ok, d.stallguard)
    print("OTPW", d.otpw, "OT", d.over_temp_shutdown)
    print("thermal_warning", d.thermal_warning, "thermal_fault", d.thermal_fault)
    print("shorts", d.any_short, "stealth", d.stealth_chop, "cs", d.cs_actual)
FieldMeaning
otpwOver-temperature pre-warning (driver still running)
over_temp_shutdownTMC OT shutdown — motion stopped, Fault.DRIVER_OT (4)
temp_120ctemp_157cTMC internal temperature comparator flags
short_to_gnd_* / low_side_short_*Bridge short flags → Fault.DRIVER_SHORT (5)
open_load_*Open-load flags (often set at standstill — interpret carefully)
cs_actualActual current scale 0–31
stealth_chop / standstillChopper mode / standstill state

OTPW alone does not stop the motor. OT or shorts stop motion until you call enable() again (and the driver has recovered). Older firmware only returns StallGuard + UART-ok; decode still works.

Cooling: frequent OTPW or OT under continuous high speed is often because the TMC has no heatsink. See Hardware → cooling.

Firmware that packs these flags is in the public sketch (download / preview; functions tmcPollStatus, sendDriver, tmcDriverService) — excerpt also on the protocol page.

Or subscribe:

from canstepper import Tel

bus.subscribe(node_id=1, msg_id=Tel.POSITION, callback=lambda f: print(f))
bus.on_event(lambda nid, evt, detail, data: print(nid, evt.name))

G-code (Klipper-inspired)

Drive a Cartesian or CoreXY mechanism with standard G/M lines — see the dedicated G-code layer page (Cartesian, GCodeController.from_cartesian, from_corexy, G1, G28, M112, …).

Groups and e-stop scopes

feeders = bus.group([6, 7, 8])
feeders.enable().set_param("run_current", 35).run(720.0)

node.estop()        # one node
feeders.estop()     # the group (one frame per node)
bus.estop_all()     # the whole bus, one broadcast frame

E-stop is latched: the node ignores motion until enable() is called. Host-side motion attempts against a latched node raise EStopActive.

Host-side speed policy

The firmware never clamps motion. If you want guardrails, declare them on the host:

node.set_speed_limits(min_speed=1.0, max_speed=1800.0)
node.run(3600.0)    # raises LimitViolation before anything hits the wire

Errors

All errors derive from CANStepperError: RequestTimeout, ParamRejected, UnknownParam, NodeFault, HomingFailed, EStopActive, LimitViolation, NotHomed, ConfigError, TransportError.

On this page