1. What you need
- A CANStepper Adapter Board mounted on a NEMA17, diametric magnet centered over the encoder.
- 5–24 V DC supply (typically 24 V; max 24 V) and a USB-C cable.
- For more than one board: CAN daisy-chain cables. Termination is enabled by default on each board (0 Ω jumper → 120 Ω path). Keep it on the two physical ends of the bus; remove the 0 Ω resistor on mid-chain boards (or short the termination pads on ends if needed). See Hardware → CAN bus rules.
2. Flash the firmware (once per board)
Full steps (Arduino IDE, arduino-cli, CANopen node 2, failure table):
Flashing firmware.
- Apply main Vin (typically 24 V) — USB does not power the ESP32 for programming. Keep power connected during upload.
- Arduino IDE or
arduino-cliwith theesp32core. Board: ESP32C3 Dev Module, Tools → USB CDC On Boot: Enabled. - Libraries: FastAccelStepper (gin66) and TMC2209 (janelia-arduino).
- Download firmware 1.12 from the Firmware page (or GrafitoCANStepper_C3.ino), open it, and upload over USB-C with Vin applied.
- Serial monitor at 115200 baud. Healthy boot looks like:
# GrafitoCANStepper fw 1.12 proto 1
# HOME pin GPIO8 INPUT_PULLUP raw=HIGH (strapping: keep HIGH at power-on)
# HOME: INPUT_PULLUP + endstop enable=1 active_high=1 action=stop (fw 1.12)
# POWER: enable_on_boot=0 hold<=5 run<=40 standstill=freewheel (fw 1.9)
# MT6701 SSI mode=3 encoder-loss-filter=25ms
# CAN started (1 Mbps)1.9 does not energize the motor at boot. Call node.enable() before
the first move. Use firmware ≥1.2 for high-speed closed-loop (trapezoid +
velocity feedforward). For a PLC / EDS workflow, flash
CANopen 2.1 instead of this sketch.
3. Install the Python library
The PyPI name is grafito-canstepper; you import it as canstepper.
pip install grafito-canstepperPackage page: pypi.org/project/grafito-canstepper
Editable install from the monorepo (development):
git clone https://github.com/Grafito-Innovations/Grafito-Edge-Services.git
cd Grafito-Edge-Services/can_stepper
pip install -U pip setuptools
pip install -e ".[dev]"4. First motion
from canstepper import CANStepperBus
bus = CANStepperBus.serial("/dev/ttyACM0") # "COM5" on Windows
print(bus.discover()) # {1: '1.12'}
node = bus.node(1)
node.set_run_current(40).set_hold_current(15).set_microsteps(16)
node.enable()
node.move_to(360.0, blocking=True) # one full revolution
print(node.get_position())Wrong direction? node.set_direction(inverted=True).
Make it permanent: node.save_config().
Important for closed loop: positive commands must make the encoder angle
increase. If the shaft drives itself off-target (NO_PROGRESS), invert
and save.
5. Closed-loop S-curve (recommended)
Firmware ≥1.11 plans a 7-segment S-curve and tracks it with velocity + acceleration feedforward. Precision tune on a PR42HS40-1204AF-02 NEMA 17 at 24 V (fw 1.11 confirmation, LUT on):
node.calibrate_encoder_lut()
node.configure_closed_loop_speed(
720.0, # 120 RPM cruise
accel_deg_s2=1440.0,
run_current=40,
microsteps=8,
stealthchop=False, # SpreadCycle
persist=True,
)
node.enable()
node.set_zero()
node.move_to(180.0, blocking=True)
print(node.get_position()) # median |err| 0.125°Open-loop run() | Closed-loop move_to | |
|---|---|---|
| Max measured | ~3000 RPM (2 µstep, 80%) | precision 120 RPM (median |err| 0.125°) |
| Not reachable | 5000 RPM | — |
Error table and RPM ladder: Closed-loop speed tuning.
6. Give each board an address
Every board ships as node 1. With one board at a time on the bus:
bus.node(1).set_node_id(2) # applies, saves, and re-addresses the handle7. Real-world units
from canstepper import Axis
axis = Axis(node, rotation_distance=8.0) # 8 mm leadscrew -> everything in mm
axis.move_to(25.0, speed=10.0, blocking=True)8. Homing
# Physical endstop on IO8 (mind the strapping-pin rule in the hardware guide):
node.configure_endstop(enabled=True, active_high=False, action=1)
axis.home(method="endstop", direction=-1, speed=8.0, backoff=1.0)
# No switch? Sensorless homing against the hard stop:
node.set_stall_threshold(60)
axis.home(method="stallguard", direction=-1, speed=5.0, current_percent=25)More detail and example scripts in Homing and endstops:
PYTHONPATH=. python3 examples/endstop_monitor.py /dev/ttyACM0 1 30
PYTHONPATH=. python3 examples/home_endstop.py /dev/ttyACM0 1 -1
PYTHONPATH=. python3 examples/home_sensorless.py /dev/ttyACM0 1 -1 609. Emergency stop, three scopes
node.estop() # one node — latched until node.enable()
bus.group([2, 3]).estop() # a set of nodes
bus.estop_all() # every node, one broadcast frame