1. What you need
- A CANStepper Adapter Board mounted on a NEMA17, diametric magnet centered over the encoder.
- 24 V motor supply and a USB-C cable.
- For more than one board: CAN daisy-chain cables and 120 Ω termination at both physical ends of the chain.
2. Flash the firmware (once per board)
- Arduino IDE or
arduino-cliwith theesp32core. Board: ESP32C3 Dev Module, Tools → USB CDC On Boot: Enabled. - Libraries: FastAccelStepper (gin66) and TMC2209 (janelia-arduino).
- Open
firmware/GrafitoCANStepper_C3/GrafitoCANStepper_C3.inoand upload. - Serial monitor at 115200 baud. Healthy boot looks like:
# GrafitoCANStepper fw 1.2 proto 1
# MT6701 raw=0x2A5F40 angle=238.71 deg
# CAN started (1 Mbps)Use firmware ≥1.2 for high-speed closed-loop (trapezoid + velocity feedforward).
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.2'}
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. High-speed closed loop (recommended)
Firmware ≥1.2 plans a trapezoid and tracks it with feedforward. Production tune measured on a PR42HS40-1204AF-02 NEMA 17 (1.2 A, 3.2 mH) at 24 V:
node.configure_closed_loop_speed(
4800.0, # 800 RPM cruise
run_current=70,
microsteps=8,
stealthchop=False, # SpreadCycle
persist=True,
)
node.enable()
node.set_zero()
node.move_to(720.0, blocking=True)Open-loop run() | Closed-loop move_to | |
|---|---|---|
| Max measured | ~1200 RPM (7200 deg/s) | ~1000 RPM cruise (6000 deg/s) |
| Production | — | 800 RPM (4800 deg/s), 10 min soak 100% success |
Details, matrix tables, and soak log: 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 in Homing and endstops.
9. 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