That’s what CAN is for.
Get a USB-CAN adapter like this one (should work on windows or linux, but linux e.g. raspberry pi highly recommended)
USB is great for flexible and powerful configuration and debugging. But when it comes to robust and reliable control in a high-EMI environment, CAN is king.
You can of course use CAN from Arduino if that is your tool of choice (e.g. mcp2515) but personally I find the python-CAN libraries very easy to use.
e.g. this test example: (you may need to modify it e.g. the mouse input stuff is super hacky)
import time
import can
import struct
from evdev import InputDevice, categorize, ecodes
dev = InputDevice('/dev/input/event0') # need to change for your device
# to find out which is the mouse, hd /dev/input/event0 (move the mouse) then try event1 etc
bustype = 'socketcan'
channel = 'can1'
bus = can.interface.Bus(channel=channel, bustype=bustype)
#** odrive_can_cmd()
# see: https://github.com/ODriveRobotics/ODrive/blob/master/docs/can-protocol.md
# @param node_id: ODrive assigned CAN bus node ID, one per axis
# @param cmd_id: CMD ID as per messages table
# @param data: Python list of signals, according to messages table
# @param format: Python Struct.pack format string for signals in 8-byte CAN message, encoded according to signals table
def odrive_can_cmd(node_id, cmd_id, data=[], format=''):
data_frame = struct.pack(format , *data)
msg = can.Message(arbitration_id=((node_id << 5) | cmd_id), data=data_frame, is_extended_id=False)
bus.send(msg)
# read initial pos from periodic broadcast msgs (this is a SLOW way todo it, but it's ok as we only need it once here)
def odrive_getPos(node_id):
while(1):
msg = bus.recv()
if(msg.arbitration_id == (node_id << 5) + 0x9):
data = struct.unpack('ff', msg.data)
return data[0]
# Constants
ODCMD_SET_INPUT_POS = 0x00c
ODCMD_RESET_ERRORS = 0x018
ODCMD_SET_AXIS_STATE = 0x007
AXIS_STATE_CLOSED_LOOP_CONTROL = 8
node_id_x=10
node_id_y=11
xpos = odrive_getPos(node_id_x)
ypos = odrive_getPos(node_id_y)
print("Initial pos x=%f, y=%f" % (xpos, ypos))
# set input_pos to current position - sometimes this is not done by ODrive e.g in input_filter mode
odrive_can_cmd(node_id_x, ODCMD_SET_INPUT_POS, [xpos, 0, 0], 'fhh')
odrive_can_cmd(node_id_y, ODCMD_SET_INPUT_POS, [ypos, 0, 0], 'fhh')
# clear faults
odrive_can_cmd(node_id_x, ODCMD_RESET_ERRORS)
odrive_can_cmd(node_id_y, ODCMD_RESET_ERRORS)
# put into position-hold
odrive_can_cmd(node_id_x, ODCMD_SET_AXIS_STATE, [AXIS_STATE_CLOSED_LOOP_CONTROL], 'h')
odrive_can_cmd(node_id_y, ODCMD_SET_AXIS_STATE, [AXIS_STATE_CLOSED_LOOP_CONTROL], 'h')
def mousecan(xpos, ypos):
for event in dev.read_loop():
if (event.type == ecodes.EV_REL):
if(event.code == 0):
xpos = xpos - event.value * 0.002
odrive_can_cmd(node_id_x, ODCMD_SET_INPUT_POS, [xpos, 0, 0], 'fhh')
if(event.code == 1):
ypos = ypos - event.value * 0.002
odrive_can_cmd(node_id_y, ODCMD_SET_INPUT_POS, [ypos, 0, 0], 'fhh')
print("xpos: %f ypos: %f" % (xpos, ypos))
mousecan(xpos, ypos)