# Grafito CANStepper — full codebase brief for AI coding assistants # Package: grafito-canstepper (PyPI) · import: canstepper · docs: https://docs.grafito.in # Last updated: 2026-10-08 · library version: 0.2.2 · GCSP firmware: 1.12 · CANopen firmware: 2.1 ================================================================================ 1. WHAT THIS PROJECT IS ================================================================================ CANStepper is a NEMA 17 closed-loop stepper adapter board: ESP32-C3 + TMC2209 + MT6701 magnetic encoder (16384 counts/rev) + CAN @ 1 Mbps. Up to 31 boards daisy-chain; host connects USB serial to ANY board (serial-to-CAN bridge). This directory (`can_stepper/` in Grafito-Edge-Services monorepo) contains: - `canstepper/` Python host library (grafito-canstepper on PyPI) - `firmware/` Arduino sketches (GCSP 1.12 + optional CANopen 2.1) - `examples/` Runnable customer scripts - `tests/` pytest suite (software simulator, no hardware) - `tools/` Hardware validation & CANopen bring-up scripts - `website/` docs.grafito.in Next.js site (MDX docs + firmware downloads) - `docs/` Markdown mirrors / deep references Customers typically: pip install → USB to one board → discover nodes → enable → move. ================================================================================ 2. INSTALL & MINIMAL WORKING SCRIPT ================================================================================ pip install grafito-canstepper # Python ≥3.9, depends on pyserial only pip install grafito-canstepper[toml] # optional: machine.toml loading on Py 3.9 from canstepper import CANStepperBus PORT = "/dev/ttyACM0" # Linux; macOS often /dev/tty.usbmodem*; Windows COMx with CANStepperBus.serial(PORT) as bus: nodes = bus.discover(timeout=2.0) # {node_id: "major.minor", ...} node = bus.node(sorted(nodes)[0]) node.set_param("endstop_enable", 0) # if GPIO8 false-triggers HOME (bench) node.set_run_current(40).set_hold_current(15).set_microsteps(16) node.enable() # REQUIRED on fw ≥1.9 (motors off at boot) node.move_to(180.0, blocking=True) # degrees, encoder-closed-loop trap move Development from git: cd can_stepper && pip install -e ".[dev]" && python3 -m pytest ================================================================================ 3. REPOSITORY LAYOUT (file map for navigation) ================================================================================ can_stepper/ ├── canstepper/ # PYTHON PACKAGE — start here for API │ ├── __init__.py # Public exports, __version__ │ ├── bus.py # CANStepperBus: connect, discover, routing, estop_all │ ├── node.py # StepperNode: motion, params, homing, telemetry getters │ ├── group.py # NodeGroup: fan-out commands to node list │ ├── axis.py # Axis: mm/units via rotation_distance │ ├── kinematics.py # Cartesian, CoreXY, DualMotorAxis, IndependentDualAxis, MotionGroup │ ├── gcode.py # GCodeController, parse_gcode_line (G0/G1/G28/M112/…) │ ├── config.py # Machine.from_toml() declarative machine builder │ ├── protocol.py # GCSP v1: Cmd, Tel, Param, Frame codecs — wire truth │ ├── telemetry.py # NodeState, DriverStatus, PidStatus, … dataclasses │ ├── tmc2209.py # commanded_rms_amps(), full_scale_rms_amps() from cs_actual │ ├── exceptions.py # CANStepperError hierarchy │ ├── stall_protect.py # StallGuardEstop helper │ ├── sim.py # SimNetwork transport for tests │ ├── canopen402.py # CANopen CiA 402 object dict, EDS/DCF render, slave sim │ └── transport/ │ ├── base.py # Transport ABC │ └── serial_bridge.py # USB line protocol reader/writer thread ├── firmware/ │ ├── GrafitoCANStepper_C3/ # GCSP v1 production sketch (fw 1.12) │ └── GrafitoCANStepper_C3_CANopen/ # CiA 301/402 PLC sketch (fw 2.1) + canopen_stack.h ├── examples/ # Copy-paste starting points (see section 8) ├── tests/ # pytest; uses SimNetwork from conftest ├── tools/ # board_e2e.py, hw_validate.py, canopen_*.py ├── docs/ # protocol.md, quickstart.md, PRD, … ├── website/ # docs.grafito.in (MDX in website/docs/) ├── llms.txt # Short LLM index (also at /llms.txt on docs site) └── llms-full.txt # This file Obsolete (do NOT recommend): CANStepperNode_C3/, canbus.py — legacy bring-up only. ================================================================================ 4. ARCHITECTURE — API LAYERS (bottom to top) ================================================================================ Layer 0 — Transport SerialBridgeTransport(port): reads " \n" lines, background RX thread. SimNetwork([node_ids]): in-process firmware simulation for tests. CANStepperBus(transport) wraps either. Layer 1 — Bus (canstepper/bus.py) CANStepperBus.serial(port) factory for USB .discover() → {id: fw_string} .node(id) → StepperNode .group([ids]) → NodeGroup .estop_all() single broadcast ESTOP .subscribe(node_id, msg_id, callback) telemetry tap .on_event(callback) EVENT frames (MOVE_DONE, FAULT, …) Layer 2 — Node (canstepper/node.py) Motion (degrees): .move_to(deg, blocking=False) absolute; CL trap on fw ≥1.2 .move_by(deg, blocking=False) relative .run(deg_s) continuous velocity (open-loop stepping) .stop() ramped stop .set_zero() define current angle as 0° Power / safety: .enable() / .disable() / .estop() / .stop() Homing: .home(method="endstop"|"stallguard"|"set_zero", direction=±1, speed_deg_s=…) .configure_endstop(enabled, active_high, action) Parameters: .set_param(Param.X or "name", value) verified write; raises ParamRejected .get_param(name) read back .save_config() persist to flash Telemetry: .state cached NodeState (POSITION, MOTION, …) .get_position(), .get_motion(), .get_driver_status(), .get_pid_status(), … Config chain setters (return self): .set_run_current(pct), .set_hold_current(pct), .set_microsteps(n), .set_max_speed(deg_s), .set_acceleration(deg_s2), .configure_closed_loop_speed(vmax, run_current=…, microsteps=…, stealthchop=…) Host policy: .limits = SpeedLimits(min_speed=, max_speed=) enforced BEFORE sending to firmware Layer 3 — Group (canstepper/group.py) Same methods as node but fan-out: group.enable(), group.run(speed), group.estop() Layer 4 — Axis (canstepper/axis.py) Axis(node, rotation_distance=8.0) # mm per motor rev (leadscrew pitch or belt calc) .move_to(mm), .move_by(mm), .run(mm_s), .home(…) Converts mm ↔ degrees internally. Layer 5 — Kinematics (canstepper/kinematics.py) Cartesian({"x": axis_x, "y": axis_y, …}) independent axes CoreXY(motor_a, motor_b, …) belt CoreXY inverse kinematics DualMotorAxis(leader, follower, …) on-bus FOLLOW mode (gantry Z) IndependentDualAxis(left, right, …) dual screws with encoder gate MotionGroup([axes…]) synchronized multi-axis moves Layer 6 — G-code (canstepper/gcode.py) GCodeController.from_cartesian(cart) or .from_corexy(corexy) .run_line("G1 X10 Y20 F1200") or .run_file(path) Supports G0/G1, G28 homing, G90/G91, M112 estop, etc. Layer 7 — Machine config (canstepper/config.py) Machine.from_toml("examples/machine.toml") → builds bus, nodes, axes, cartesian/corexy Layer 8 — Protocol / roll-your-own host (canstepper/protocol.py) Frame encoding, Cmd/Tel/Param enums, PARAMS registry with ranges. CAN ID = (node_id << 6) | msg_id; node_id 0 = broadcast. Layer 9 — CANopen PLC path (canstepper/canopen402.py) Separate firmware. EDS/DCF generation, SDO helpers, pytest slave. NOT interchangeable with GCSP on the same physical CAN bus. ================================================================================ 5. GCSP v1 WIRE PROTOCOL (summary) ================================================================================ Physical: CAN 2.0A standard 11-bit IDs, 1 Mbps, daisy-chain, 120Ω termination at bus ends. Addressing: CAN_ID = (node_id << 6) | msg_id node_id: 0=broadcast, 1–31=board msg_id: 0–31 commands (host→node), 32–63 telemetry (node→host) Serial bridge (USB 115200): " \n" Lines starting with "#" are firmware debug — ignore. Commands (Cmd enum — see protocol.py): PING, ESTOP, STOP, ENABLE, MOVE_ABS(f64°), MOVE_REL(f64°), MOVE_VEL(f32 deg/s), SET_ZERO, HOME(u8 method, i8 dir, f32 speed), SET_PARAM, GET_PARAM, SAVE_CONFIG, LOAD_DEFAULTS, FOLLOW (leader/follower), FOLLOW_SYNC, SET_POSITION Telemetry (Tel enum): STATUS, POSITION(f64°), MOTION(vel, err), TARGET, EVENT, DRIVER(TMC 8B), ENV(temp, Vbus placeholder), PARAM reply, FOLLOW_STATUS, CAN_HEALTH, ENC_COUNTS, PID_STATUS Key parameters (Param enum — full table in protocol.py PARAMS dict): NODE_ID, STEPS_PER_REV, MICROSTEPS, RUN_CURRENT, HOLD_CURRENT, STALL_THRESHOLD, INVERT_DIR, CLOSED_LOOP, MAX_SPEED, ACCELERATION, CL_MAX_SPEED, CL_MAX_ACCEL, PID_KP/KI/KD, PID_TOLERANCE, FAST_RATE_HZ, SLOW_RATE_HZ, ENABLE_ON_BOOT, ENDSTOP_ENABLE, ENDSTOP_ACTIVE_HIGH, ENDSTOP_ACTION, HOMING_*, STEALTHCHOP Fault codes: NONE, ENCODER, NO_PROGRESS, HOMING_TIMEOUT, DRIVER_OT(4), DRIVER_SHORT(5) Events: BOOT, ENDSTOP_HIT/RELEASED, STALL, HOMING_DONE/FAILED, MOVE_DONE, ESTOP, FAULT ================================================================================ 6. FIRMWARE SKETCHS ================================================================================ GCSP production — firmware/GrafitoCANStepper_C3/ (version 1.12) Protocol: GCSP v1 · nodes 1–31 · Python library is primary host. Boot: motors disabled until ENABLE; low-power policy. Motion: 7-segment S-curve + Ka feedforward; optional 200-step encoder LUT. Flash: ESP32C3 Dev Module, USB CDC On Boot=Enabled Libraries: FastAccelStepper, TMC2209 (janelia-arduino) Vin required (5–24 V, typically 24 V) for upload/run; USB-C is data. CANopen PLC — firmware/GrafitoCANStepper_C3_CANopen/ (version 2.1) Protocol: CiA 301 + CiA 402 subset (pp/pv/hm) · nodes 1–127 Motion matches GCSP 1.12 (S-curve, Ka, LUT). EDS/DCF in firmware folder and website/public/firmware/canopen/ Enable: controlword 6→7→15 · position counts = encoder (16384/rev) Manufacturer objects: 0x2000 node ID, 0x2003 run current, 0x2006 invert, 0x2007 CL, 0x200C endstop enable LUT/S-curve: 0x201A Ka, 0x201B jerk (0=auto), 0x201C enable, 0x201D cmd (0 cal / 1 en / 2 dis / 3 clear) Host tools: tools/canopen_bringup.py, canopen_plc_e2e.py, canopen_two_node.py CRITICAL: Never mix GCSP and CANopen nodes on one CAN bus — COB-ID map collides with GCSP framing. ================================================================================ 7. COMMON CUSTOMER WORKFLOWS (copy-paste patterns) ================================================================================ 7a) First connect + discover with CANStepperBus.serial(PORT) as bus: print(bus.discover()) 7b) Bench motion when GPIO8 floats (false HOME) node.set_param(Param.ENDSTOP_ENABLE, 0) # RAM only unless save_config() 7c) Closed-loop production tune (fw ≥1.2) node.configure_closed_loop_speed( 4800.0, # deg/s cruise (~800 RPM on NEMA17 @ 24V) run_current=70, microsteps=8, stealthchop=False, persist=True, ) node.move_to(720.0, blocking=True) 7d) Homing — physical endstop on IO8 (active low, pull-up) node.configure_endstop(enabled=True, active_high=False, action=1) node.home(method="endstop", direction=-1, speed_deg_s=20.0) 7e) Homing — sensorless StallGuard node.set_stall_threshold(60) node.home(method="stallguard", direction=-1, speed_deg_s=25.0) 7f) Belt / leadscrew in millimetres from canstepper import Axis axis = Axis(node, rotation_distance=40.0) # GT2-20T ≈ 40 mm/rev axis.move_to(50.0, blocking=True) # 50 mm 7g) Dual Z gantry (leader–follower on CAN, no host loop) from canstepper import DualMotorAxis dual = DualMotorAxis(bus.node(1), bus.node(2), …) dual.move_to_mm(100.0) 7h) Cartesian G-code from canstepper import Cartesian, GCodeController cart = Cartesian({"x": axis_x, "y": axis_y}) gc = GCodeController.from_cartesian(cart) gc.run_line("G28 X Y") gc.run_line("G1 X50 Y50 F3000") 7i) TMC driver diagnostics + motor current (library ≥0.2.1) d = node.get_driver_status() d.cs_actual # 0–31 scale from TEL_DRIVER d.commanded_rms_amps() # host-side Irms from TMC2209 formula (100 mΩ sense) # OTPW = warning only; OT or shorts → fault, motion stops until enable() after cool-down 7j) E-stop scopes node.estop() # one node, latched bus.group([1,2,3]).estop() # group bus.estop_all() # broadcast, one frame 7k) Simulator (no hardware — same API) from canstepper.sim import SimNetwork bus = CANStepperBus(SimNetwork([1, 2])) bus.discover(timeout=0.1) 7l) CANopen PLC bring-up (NOT GCSP) PYTHONPATH=. python3 tools/canopen_bringup.py /dev/ttyACM0 --node 1 See docs.grafito.in/docs/canopen for SDO indices and CiA 402 enable sequence. PLC gains (REAL32): 0x200F Kp, 0x2010 Ki, 0x2011 Kd, 0x201A Ka, 0x2012 tolerance. ================================================================================ 8. EXAMPLES/ DIRECTORY (runnable scripts) ================================================================================ Basic: spin_one_motor.py single-node move two_motors_together.py multi-node live_position_stream.py telemetry stream closed_loop_speed.py CL tuning demo Homing: home_endstop.py, home_sensorless.py, home_set_zero.py, home_axis_mm.py endstop_monitor.py, endstop_boot_test.py G-code: gcode_cartesian.py, gcode_corexy.py, gcode_repl.py, gcode_from_file.py gcode/square.gcode, diagonal_box.gcode, relative_jog.gcode Multi-axis / machines: dual_motor_axes.py, dual_motor_axes_360.py, dual_z_gantry.py dual_screw_oscillate.py, dual_screw_protected.py, scion_rootstock_dual.py corexy_plotter.py, feeder_group_estop.py Belt / high speed: belt_move_mm.py, belt_oscillate.py, belt_speed_ramp.py, high_speed_test.py Stall / protection: phase0_stallguard.py, phase1_stall_estop.py Machine config: machine.toml declarative example for Machine.from_toml Run pattern: cd can_stepper && PYTHONPATH=. python3 examples/spin_one_motor.py /dev/ttyACM0 1 ================================================================================ 9. TESTS & VALIDATION TOOLS ================================================================================ Unit/integration (simulator, no hardware): cd can_stepper && python3 -m pytest Tests: test_protocol, test_bus_node, test_axis, test_gcode, test_kinematics, test_config, test_group, test_canopen, test_canopen_plc_e2e, test_tmc2209 Hardware validation: tools/board_e2e.py PORT NODE new board E2E + CL tune tools/hw_validate.py PORT NODE full API suite (restores params) tools/hw_validate_two_node.py dual-node tools/cl_speed_validate.py OL vs CL speed ladder tools/cl_matrix.py characterization matrix CANopen: tools/canopen_bringup.py, canopen_plc_e2e.py, canopen_two_node.py, canopen_sync_ol_spin.py Encoder / calibration: tools/diag_encoder_magnet.py, recalibrate_node.py ================================================================================ 10. EXCEPTIONS (canstepper/exceptions.py) ================================================================================ All derive from CANStepperError: RequestTimeout — PARAM reply or blocking move timed out ParamRejected — value outside firmware sanity range UnknownParam — bad param name/id NodeFault — fault byte set (encoder, OT, short, …) HomingFailed — homing timeout or abort EStopActive — motion rejected while latched e-stop LimitViolation — host SpeedLimits or Axis soft limit NotHomed — move blocked because require_homing=True ConfigError — machine.toml or config issue TransportError — serial I/O failure GCodeError — unsupported/invalid G-code EncoderGateTimeout — IndependentDualAxis partner didn't settle StallGuardTrip — stall protection helper ================================================================================ 11. DESIGN PRINCIPLES (do not contradict in generated code) ================================================================================ 1. Firmware does NOT clamp motion to "safe" speeds — defaults are starting points. 2. Host library may enforce SpeedLimits / Axis max_speed / soft limits — optional. 3. E-stop is latched until explicit enable(). 4. Closed loop default ON — encoder is source of truth for POSITION telemetry. 5. Parameters persist with save_config(); RAM changes lost on power cycle otherwise. 6. One USB board bridges the whole bus — any node's USB port works. 7. CAN termination: 120Ω on physical bus ends; mid-chain boards should remove 0Ω jumper. 8. TMC2209 VREF NC + UART current control on this board — run_current is % scale 1–100. 9. commanded_rms_amps() converts cs_actual to motor Irms; SMPS input current is different (bus power). ================================================================================ 12. TROUBLESHOOTING QUICK REFERENCE ================================================================================ | Symptom | Likely cause | Fix | | --- | --- | --- | | discover() empty | wrong port, no Vin, no CAN wiring | check /dev/ttyACM*, 24V, CAN_H/L | | Motor doesn't move | fw 1.9 boot disabled | node.enable() | | Immediate homing/fault at boot | GPIO8 floating | ENDSTOP_ENABLE=0 for bench | | Moves wrong direction | mechanical | invert_dir=1 or Param.INVERT_DIR | | OT / thermal fault | no heatsink on TMC | add heatsink, lower run_current | | Short fault at standstill | open-load flags normal at rest | interpret DRIVER flags carefully | | Mixed nodes weird IDs | GCSP + CANopen same bus | flash one protocol only | | blocking move timeout | CL never settles | tune PID, lower speed, check load | | Old library | missing Irms helpers | pip install -U grafito-canstepper | Full page: https://docs.grafito.in/docs/troubleshooting ================================================================================ 13. DOCUMENTATION URLS (human-readable MDX on docs.grafito.in) ================================================================================ /docs overview, block diagram /docs/quickstart first motion /docs/hardware wiring, termination, power /docs/firmware GCSP 1.12 / CANopen 2.1 downloads /docs/flashing Arduino IDE + arduino-cli flash (GCSP and CANopen) /docs/canopen CANopen 2.1, EDS, CiA 402, PLC gain tuning /docs/python-library API tour + Irms /docs/protocol GCSP wire reference /docs/homing endstop + stallguard /docs/gcode G-code layer /docs/motion dual-motor, leader–follower /docs/belt-axis mm moves, soak testing /docs/closed-loop-tuning trap + v_ff, RPM tables /docs/machine-config machine.toml /docs/simulator SimNetwork /docs/troubleshooting fault diagnosis /docs/ai-assistants this LLM onboarding page /docs/mechanical-cad STEP, PDF drawings LLM index files: https://docs.grafito.in/llms.txt https://docs.grafito.in/llms-full.txt ================================================================================ 14. HOW AI ASSISTANTS SHOULD USE THIS REPO ================================================================================ For customer support / code generation: 1. Read llms.txt (index) then this file (llms-full.txt) for context. 2. Prefer high-level API (Axis, Cartesian, GCodeController) over raw frames. 3. Always include enable() for fw ≥1.9; check discover() first. 4. Use SimNetwork in tests; suggest pytest before hardware scripts. 5. Ask protocol (GCSP vs CANopen) before giving CAN IDs or object indices. 6. Point to examples/ for working patterns; don't invent API not in canstepper/. For Cursor / Claude Code / Codex / Grok in IDE: - Add to prompt: "Read https://docs.grafito.in/llms-full.txt" or @-mention can_stepper/llms-full.txt - Workspace root for library code: can_stepper/canstepper/ - Run tests: cd can_stepper && python3 -m pytest For product/marketing questions (not coding): https://grafito.in/llms-full.txt and https://grafito.in/llms.txt ================================================================================ 15. VERSION & LINKS ================================================================================ Library: grafito-canstepper 0.2.1 · import canstepper · __version__ in __init__.py PyPI: https://pypi.org/project/grafito-canstepper/ GitHub: https://github.com/Grafito-Innovations/Grafito-Edge-Services/tree/main/can_stepper Docs: https://docs.grafito.in Shop: https://grafito.in/shop/products/canstepper-adapter-board/ Contact: hello@grafito.in · Grafito Innovations, Kochi/Kerala, India