One obvious difference between robotics and other types of software engineering is the fact that hardware and the physical world are the core of the development problem. While understanding everything that goes into the physics and machines that run robotics is an incredibly complex topic worthy of multiple books, in this article I want to focus on the RobStride family of motors and provide a brief primer into using them so that the many software engineers that are just starting to get into the hardware space with the AI boom have a solid place to jump from with these incredible actuators. The core reason I am putting this article together is that I found the documentation around scripts for these motors to be lacking (or in some cases completely missing via dead links), so hopefully I can save others some time and headaches.
So first off, what is a RobStride motor? These are a specific family of motors that are being used in modern robotics, including the Seeed Studio ReBot or the open sourced HuggingFace LeRobot Humanoid. While other common motors used in smaller robotics projects, like the Feetech motors in the SO-ARM101, are gearbox servos or stepper motors,
the RobStride family of motors are Quasi-Direct Drive (QDD) actuators centered around a high-torque brushless DC (BLDC) outrunner with a planetary reduction gearbox using dual high-resolution magnetic encoders. To put that in more easily digestible terms, the inside of the motor stays completely still and the core mechcanism uses electrical signals to move the outer shell around the center by smoothly shifting magnetic forces to cause a spinning effect.
Setup
For this article, I'm going to use a RobStride O0 motor as it's one of the smaller and cheaper models. These motors expect 48 volts to operate, so my example wiring uses a small-ish e-bike battery and a SavvyCAN USB to CAN FD converter that I've pulled from my LeRobot Humanoid project to power the motor and communicate with it from my Linux laptop (other operating systems might work, but it got finicky with my Mac, whereas things just worked with my Linux machine).
As for wiring, you will have a hot and ground wire coming from the battery and tied into the motor. The ground will also need to be split to connect to the CAN converter. The converter will also have high and low data wires that you will connect to the motor. When all is said and done, your setup should look like this:
Hardware Note on CAN Termination: CAN bus networks require 120 ohm termination resistors on both ends of the bus to prevent signal reflections. Most USB-to-CAN adapters (like the SavvyCAN adapter that I'm using) have a built-in termination resistor enabled by default. However, if you are using a different CAN interface and experience dropped packets or framing errors, verify whether your adapter or motor needs its termination resistor toggled on.
RobStride Modes
When you first pull the RobStride motor out of the box, it will be set to the RobStride Private mode, though it will also support the MIT and CANopen modes. Each of these has a distinct way of framing packets, determining how feedback is triggered, and which low-level loops execute on the motor driver.
Private mode is a proprietary protocol that can be used during initial bench testing, when you need to reconfigure to a default system, or when you plan to use native position/velocity profile commands rather than a pure torque-feedforward control. This mode uses 29-bit extended frame IDs. MIT mode uses an 11-bit standard CAN frame format that is optimized for rapid lightweight packet transmission. Instead of separate position or velocity loops, it provides a low-latency control that takes five simultaneous parameters in a tight loop: target position, target velocity, position stiffness, velocity damping, and feed-forward torque. This is the default mode used by the LeRobot Humanoid project and other robotics projects. Finally there's the CANopen mode. This mode is used for various existing industrial machine architectures and is useful for multi-vendor interoperability. For this article I will focus on MIT mode.
Initial Testing
Now that we've covered some topics about the RobStride motor family, let's get to the interesting stuff: actually using the actuator.
On your Linux computer, you'll want to make sure you have the can-utils package installed to be able to communicate with the motor.
sudo apt-get update && sudo apt-get install -y can-utils
Once you have that utility package installed, you will want to set up your can0 interface to use a 1 Mbps bitrate and be in the 'on' configuration. It's worth noting that Linux will reset this interface on every reboot unless it's saved under system network configurations.
sudo ip link set can0 type can bitrate 1000000
sudo ip link set can0 up
With that out of the way, let's turn on sniffing for the live CAN bus.
candump can0 -c -t d -e
Since the default motor configuration is in Private mode with an ID of 127, I'll start with the assumption that you have the same values. If you've already changed your mode or ID, then you're ahead of the game and can likely just read this to see if there's anything new to learn.
From a second terminal window, try pinging the motor to see if you get a response in the sniffer terminal window.
cansend can0 0000FD7F#0000000000000000
You should get a 29-bit reply with an Arbitration ID (ArbID) of 0x00007FFD containing a 64-bit MCU UID.
(000.000411) can0 00007FFE [8] 2D 19 30 02 0C 34 37 01
If that worked, then everything should be set up correctly and we're ready to move on to doing more.
Discovery Scanner
One of the first hurdles I ran into when testing out the LeRobot Humanoid was that the script provided for locating a motor and checking its ID only worked in Private mode, so as soon as I converted to MIT mode the motor was invisible to the default provided script (though I recently rewrote and published it in the repo, so if you're trying that project out you shouldn't have any trouble now :)). This script will help you identify a motor in any mode/ID configuration connected to your machine to verify that it's properly communicating. You'll also want to make sure you install the Python package 'can'.
import can
import time
INTERFACE = 'socketcan'
CHANNEL = 'can0'
BITRATE = 1000000
def make_ext_id(comm_type: int, host_id: int, target_id: int) -> int:
return ((comm_type & 0x1F) << 24) | ((host_id & 0xFF) << 8) | (target_id & 0xFF)
def flush_bus(bus):
while bus.recv(timeout=0.005) is not None:
pass
def ping_private(bus, mid: int) -> bool:
flush_bus(bus)
arb_id = make_ext_id(0x00, 0xFD, mid)
try:
bus.send(can.Message(arbitration_id=arb_id, data=[0]*8, is_extended_id=True))
except Exception:
return False
deadline = time.monotonic() + 0.02
while time.monotonic() < deadline:
msg = bus.recv(timeout=0.005)
if msg and msg.is_extended_id and ((msg.arbitration_id >> 8) & 0xFF) == mid:
return True
return False
def ping_mit(bus, mid: int) -> bool:
flush_bus(bus)
try:
bus.send(can.Message(arbitration_id=mid, data=[0xFF]*7 + [0xFB], is_extended_id=False))
except Exception:
return False
deadline = time.monotonic() + 0.02
while time.monotonic() < deadline:
msg = bus.recv(timeout=0.005)
if msg and not msg.is_extended_id and len(msg.data) > 0 and msg.data[0] == mid:
return True
return False
def ping_canopen(bus, mid: int) -> bool:
flush_bus(bus)
req_id = 0x600 + mid
data = [0x40, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00]
try:
bus.send(can.Message(arbitration_id=req_id, data=data, is_extended_id=False))
except Exception:
return False
deadline = time.monotonic() + 0.02
while time.monotonic() < deadline:
msg = bus.recv(timeout=0.005)
if msg and not msg.is_extended_id and msg.arbitration_id == (0x580 + mid):
return True
return False
def find_all():
try:
bus = can.interface.Bus(interface=INTERFACE, channel=CHANNEL, bitrate=BITRATE)
except Exception as e:
print(f"CAN Interface Error: {e}")
return
deadline = time.monotonic() + 1.0
while time.monotonic() < deadline:
msg = bus.recv(timeout=0.1)
if msg:
if not msg.is_extended_id and 0x700 <= msg.arbitration_id <= 0x77F:
node_id = msg.arbitration_id - 0x700
print(f"CANopen Bootup/Heartbeat detected from Node ID: {node_id}")
print("\nScanning IDs 0 through 127 across all protocols")
found = []
for mid in range(128):
if ping_private(bus, mid):
print(f"Found Motor ID {mid:3d} (0x{mid:02X}) | Protocol: PRIVATE Mode (29-bit)")
found.append((mid, "Private"))
if ping_canopen(bus, mid):
print(f"Found Motor ID {mid:3d} (0x{mid:02X}) | Protocol: CANopen (CiA 402, 11-bit)")
found.append((mid, "CANopen"))
if ping_mit(bus, mid):
print(f"Found Motor ID {mid:3d} (0x{mid:02X}) | Protocol: MIT Mode (11-bit)")
found.append((mid, "MIT"))
print("\n" + "="*55)
print(f" SCAN COMPLETE: Found {len(found)} active motor(s) on bus.")
print("="*55)
for mid, proto in found:
print(f" CAN ID: {mid:<3d} (0x{mid:02X}) | Protocol: {proto}")
print("="*55 + "\n")
bus.shutdown()
if __name__ == "__main__":
find_all()
python3 discovery.py
Scanning IDs 0 through 127 across all protocols
Found Motor ID 127 (0x7F) | Protocol: PRIVATE Mode (29-bit)
=======================================================
SCAN COMPLETE: Found 1 active motor(s) on bus.
=======================================================
CAN ID: 127 (0x7F) | Protocol: Private
=======================================================
Changing Protocols
Now that you can find the mode and ID for your motor, it's time to switch those modes around. Assuming you're in private mode, you can use the following script to convert your motor into MIT mode. It's worth noting that you will need to power cycle (unplug and plug back in) the motor when you change the mode and again when you change the ID for everything to store correctly in the motor's flash memory.
import can
import time
import sys
INTERFACE = 'socketcan'
CHANNEL = 'can0'
BITRATE = 1000000
TARGET_ID = 1 # This should be changed to whatever your motor's ID actually is.
def make_ext_id(comm_type: int, host_id: int, target_id: int) -> int:
return ((comm_type & 0x1F) << 24) | ((host_id & 0xFF) << 8) | (target_id & 0xFF)
def switch_private_to_mit(bus, motor_id: int):
print(f"Sending Type 25 protocol switch (Private -> MIT) to ID {motor_id}")
arb_id = make_ext_id(0x19, 0xFD, motor_id)
data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x02, 0x00]
bus.send(can.Message(arbitration_id=arb_id, data=data, is_extended_id=True))
time.sleep(0.05)
print(f"Sending Type 22 Flash Commit to ID {motor_id}")
save_id = make_ext_id(22, 0xFD, motor_id)
save_data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
bus.send(can.Message(arbitration_id=save_id, data=save_data, is_extended_id=True))
time.sleep(0.1)
def set_mit_id(bus, old_id: int, new_id: int):
print(f"Sending Command 0xFA (Modify MIT ID {old_id} -> {new_id})")
msg = can.Message(
arbitration_id=old_id,
data=[0xFF]*6 + [new_id & 0xFF, 0xFA],
is_extended_id=False
)
bus.send(msg)
time.sleep(0.1)
def power_cycle_prompt(description: str):
print("\n" + "="*65)
print(f" [POWER CYCLE REQUIRED] {description}")
print(" 1. Unplug 48V power. 2. Wait 3s. 3. Plug 48V power back in.")
input(" >>> Press [ENTER] once re-powered")
print("="*65)
time.sleep(1.0)
def main():
try:
bus = can.interface.Bus(interface=INTERFACE, channel=CHANNEL, bitrate=BITRATE)
except Exception as e:
print(f"Failed to open CAN bus: {e}")
return 1
motor_id = int(input("Enter current Private Motor ID (e.g. 127): "))
switch_private_to_mit(bus, motor_id)
power_cycle_prompt("Switch committed. Power-cycle to boot into MIT mode.")
if motor_id != TARGET_ID:
set_mit_id(bus, motor_id, TARGET_ID)
power_cycle_prompt(f"ID updated to {TARGET_ID}. Power-cycle to finalize.")
print(f"\nSUCCESS: Motor converted to MIT Mode (Target ID: {TARGET_ID}).")
bus.shutdown()
if __name__ == "__main__":
sys.exit(main())
result:
python3 private_to_mit.py
Enter current Private Motor ID (e.g. 127): 127
Sending Type 25 protocol switch (Private -> MIT) to ID 127
Sending Type 22 Flash Commit to ID 127
=================================================================
[POWER CYCLE REQUIRED] Switch committed. Power-cycle to boot into MIT mode.
1. Unplug 48V power. 2. Wait 3s. 3. Plug 48V power back in.
>>> Press [ENTER] once re-powered
=================================================================
Sending Command 0xFA (Modify MIT ID 127 -> 1)
=================================================================
[POWER CYCLE REQUIRED] ID updated to 1. Power-cycle to finalize.
1. Unplug 48V power. 2. Wait 3s. 3. Plug 48V power back in.
>>> Press [ENTER] once re-powered
=================================================================
SUCCESS: Motor converted to MIT Mode (Target ID: 1).
If you need to change back to Private mode from MIT mode, you can do that with this even more simple script:
import can
import time
import sys
INTERFACE = 'socketcan'
CHANNEL = 'can0'
BITRATE = 1000000
TARGET_ID = 127
def make_ext_id(comm_type: int, host_id: int, target_id: int, extra_data: int = 0) -> int:
return ((comm_type & 0x1F) << 24) | ((extra_data & 0xFF) << 16) | ((host_id & 0xFF) << 8) | (target_id & 0xFF)
def switch_mit_to_private(bus, motor_id: int):
print(f"Sending MIT Command 8 (Switch to Private) to ID {motor_id}")
switch_msg = can.Message(
arbitration_id=motor_id,
data=[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFD],
is_extended_id=False
)
bus.send(switch_msg)
time.sleep(0.1)
def main():
try:
bus = can.interface.Bus(interface=INTERFACE, channel=CHANNEL, bitrate=BITRATE)
except Exception as e:
print(f"Failed to open CAN bus: {e}")
return 1
motor_id = int(input("Enter current MIT Motor ID (e.g. 1): "))
switch_mit_to_private(bus, motor_id)
print("\n" + "="*60)
print("1. Unplug 48V power. 2. Wait 3s. 3. Plug 48V power back in.")
input(">>> Press [ENTER] once re-powered...")
print("="*60)
print(f"\nSUCCESS: Motor switched to Private Mode.")
bus.shutdown()
if __name__ == "__main__":
sys.exit(main())
result:
python3 mit_to_private.py
Enter current MIT Motor ID (e.g. 1): 1
Sending MIT Command 8 (Switch to Private) to ID 1
============================================================
1. Unplug 48V power. 2. Wait 3s. 3. Plug 48V power back in.
>>> Press [ENTER] once re-powered...
============================================================
SUCCESS: Motor switched to Private Mode.
MIT Motor Control (Sway Test)
With a traditional servo, you control it using high mechanical impedance (high stiffness). You command the device to move to angle theta, and if an obstacle gets in the way, the controller cranks up the power to force its way through.
With a Quasi-Direct Drive motor, the low-ratio gearing makes the shaft easily backdrivable, meaning external forces can physically push and turn the motor from the outside unless overridden by high active torque. Instead of forcing a rigid position, we use impedance control to make the motor behave like a virtual mechanical spring attached to a shock absorber. Rather than commanding a hard position, we tell the motor where its target spring anchor is (p_target), how stiff the spring should be (Kp), and how much shock absorption to apply (Kd).
This is representable with the following equation:
torque = torque_ff + Kp * (p_target - p_actual) + Kd * (v_target - v_actual)
where Kp * (p_target - p_actual) represents the virtual stiffness (spring) pulling towards a target as an expression of Hooke's Law in physics, Kd * (v_target - v_actual) represents the virtual damper/shock absorber that resists rapid velocity changes and suppresses oscillations, and torque_ff (feed-forward torque) represents the baseline force needed to cancel external loads, like gravity, without needing a position error to build up torque.
We can use this information to set up our own sway test for the motor to essentially define the shape, scale, and speed of the movement using a p_target(time) of amplitude * sin(angular_frequency * time) and v_target(time) of angular_frequency * amplitude * cos(angular_frequency * time)
Be sure to have the motor in a safe spot or held down when running this script. If the motor isn't already in the zero position, it will rapidly move to it to start the oscillation test.
import can
import math
import time
MOTOR_ID = 1
INTERFACE = 'socketcan'
CHANNEL = 'can0'
BITRATE = 1000000
CMD_ENABLE = 0xFC
CMD_DISABLE = 0xFD
def float_to_uint(x, x_min, x_max, bits):
x_clamped = max(x_min, min(x_max, x))
return int(((x_clamped - x_min) / (x_max - x_min)) * ((1 << bits) - 1))
def send_mit_position(bus, motor_id, pos_deg, kp=10.0, kd=0.2):
p_rad = math.radians(pos_deg)
q = float_to_uint(p_rad, -12.57, 12.57, 16)
v = 2048 # 0 velocity -> 2048 is the midpoint for a 12-bit unsigned number (range 0->4095)
kp_u = float_to_uint(kp, 0.0, 500.0, 12)
kd_u = float_to_uint(kd, 0.0, 5.0, 12)
t = 2048 # 0 torque
data = [
(q >> 8) & 0xFF,
q & 0xFF,
(v >> 4) & 0xFF,
((v & 0x0F) << 4) | ((kp_u >> 8) & 0x0F),
kp_u & 0xFF,
(kd_u >> 4) & 0xFF,
((kd_u & 0x0F) << 4) | ((t >> 8) & 0x0F),
t & 0xFF
]
bus.send(can.Message(arbitration_id=motor_id, data=data, is_extended_id=False))
def run_test():
try:
bus = can.interface.Bus(interface=INTERFACE, channel=CHANNEL, bitrate=BITRATE)
except Exception as e:
print(f"Error opening CAN bus: {e}")
return
print(f"Enabling Motor {MOTOR_ID} in MIT Mode")
bus.send(can.Message(arbitration_id=MOTOR_ID, data=[0xFF]*7 + [CMD_ENABLE], is_extended_id=False))
time.sleep(0.5)
print("Running 10s sway test (-90° to +90°)... Press Ctrl+C to stop.")
start_time = time.time()
try:
while (time.time() - start_time) < 10.0:
elapsed = time.time() - start_time
target_angle = 90.0 * math.sin(2 * math.pi * 0.2 * elapsed)
send_mit_position(bus, MOTOR_ID, target_angle)
time.sleep(0.02)
print("Returning to 0.0 degrees")
for _ in range(50):
send_mit_position(bus, MOTOR_ID, 0.0)
time.sleep(0.02)
finally:
print(f"Disabling Motor {MOTOR_ID}")
bus.send(can.Message(arbitration_id=MOTOR_ID, data=[0xFF]*7 + [CMD_DISABLE], is_extended_id=False))
bus.shutdown()
if __name__ == "__main__":
run_test()
When you run this, you should see your motor return to its zeroth position and then sway between -90 and 90 degrees before returning to zero.
python3 sway.py
Enabling Motor 1 in MIT Mode
Running 10s sway test (-90° to +90°)... Press Ctrl+C to stop.
Returning to 0.0 degrees
Disabling Motor 1
Set MIT Mode Motor to Mechanical Zero (Calibration)
In the previous code snippet, you may have seen your motor jump quickly to its zero position. To avoid that, or to set a default zero in a newly assembled robot, you can manually set the current position of the motor to be the new zero value. This is commonly done as one of the first steps in calibrating a robot so you have more control over what the max and min values should be for the device as it's moving around.
import can
import time
MOTOR_ID = 1
INTERFACE = 'socketcan'
CHANNEL = 'can0'
BITRATE = 1000000
def main():
bus = can.interface.Bus(interface=INTERFACE, channel=CHANNEL, bitrate=BITRATE)
print(f"Setting Zero on Motor ID {MOTOR_ID} (MIT Mode)...")
bus.send(can.Message(arbitration_id=MOTOR_ID, data=[0xFF]*7 + [0xFE], is_extended_id=False))
time.sleep(0.1)
print("SUCCESS: MIT zero position set.")
bus.shutdown()
if __name__ == "__main__":
main()
python3 setzero.py
Setting Zero on Motor ID 1 (MIT Mode)...
SUCCESS: MIT zero position set.
Once you have that zero position set, you can move the motor however you need to to record top and bottom bounds, then run this script to return to the calibrated zero position.
import can
import time
import math
import sys
# Fixed Configuration
INTERFACE = 'socketcan'
CHANNEL = 'can0'
BITRATE = 1000000
MOTOR_ID = 1 # Set your fixed Motor ID here
DURATION = 3.0 # Set your fixed duration in seconds here
# RobStride MIT Mode Limit Constants
P_MIN, P_MAX = -12.57, 12.57 # Position range (rad)
V_MIN, V_MAX = -33.0, 33.0 # Velocity range (rad/s)
KP_MIN, KP_MAX = 0.0, 500.0 # Stiffness range (N·m/rad)
KD_MIN, KD_MAX = 0.0, 5.0 # Damping range (N·m·s/rad)
def float_to_uint(x: float, x_min: float, x_max: float, bits: int) -> int:
x_clamped = max(float(x_min), min(float(x_max), float(x)))
return int(((x_clamped - x_min) / float(x_max - x_min)) * ((1 << bits) - 1))
def uint_to_float(x_int: int, x_min: float, x_max: float, bits: int) -> float:
return float(x_int) * float(x_max - x_min) / float((1 << bits) - 1) + x_min
def pack_mit_frame(p_des: float, v_des: float, kp: float, kd: float, t_ff: float) -> list:
"""
Packs MIT parameters into standard 8-byte payload:
- Position: 16 bits
- Velocity: 12 bits
- Kp: 12 bits
- Kd: 12 bits
- Torque: 12 bits
"""
p_u = float_to_uint(p_des, P_MIN, P_MAX, 16)
kp_u = float_to_uint(kp, KP_MIN, KP_MAX, 12)
kd_u = float_to_uint(kd, KD_MIN, KD_MAX, 12)
# Midpoint 0x800 corresponds to 0.0 for velocity and feed-forward torque
v_u = 0x800 if v_des == 0.0 else float_to_uint(v_des, V_MIN, V_MAX, 12)
t_u = 0x800
return [
(p_u >> 8) & 0xFF,
p_u & 0xFF,
(v_u >> 4) & 0xFF,
((v_u & 0x0F) << 4) | ((kp_u >> 8) & 0x0F),
kp_u & 0xFF,
(kd_u >> 4) & 0xFF,
((kd_u & 0x0F) << 4) | ((t_u >> 8) & 0x0F),
t_u & 0xFF
]
def return_to_zero_mit(bus, motor_id: int, duration: float):
print(f"\n[MIT Mode] Connecting to Motor ID {motor_id} on {CHANNEL}...")
# Enable Motor
enable_msg = can.Message(
arbitration_id=motor_id,
data=[0xFF] * 7 + [0xFC],
is_extended_id=False
)
bus.send(enable_msg)
time.sleep(0.1)
# Query starting position with zero effort (Kp=0, Kd=0, Tau=0)
start_rad = None
zero_effort_data = [0x7F, 0xFF, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00]
for _ in range(15):
bus.send(can.Message(arbitration_id=motor_id, data=zero_effort_data, is_extended_id=False))
msg = bus.recv(timeout=0.02)
if msg and not msg.is_extended_id and len(msg.data) >= 3:
p_raw = (msg.data[1] << 8) | msg.data[2]
start_rad = uint_to_float(p_raw, P_MIN, P_MAX, 16)
break
time.sleep(0.02)
if start_rad is None:
print("ERROR: Could not read response from motor. Check CAN connection, bitrate, and Motor ID.")
return
print(f"Current Position: {math.degrees(start_rad):+06.1f}° ({start_rad:+05.2f} rad)")
print(f"Homing to 0.0 rad over {duration:.1f}s")
# Smooth S-Curve Trajectory Generation
dt = 0.02 # 50 Hz control loop
steps = int(duration / dt)
for step in range(steps + 1):
alpha = 0.5 * (1.0 - math.cos((step / float(steps)) * math.pi))
target_rad = start_rad * (1.0 - alpha) + 0.0 * alpha
frame_data = pack_mit_frame(
p_des=target_rad,
v_des=0.0,
kp=15.0,
kd=0.3,
t_ff=0.0
)
bus.send(can.Message(arbitration_id=motor_id, data=frame_data, is_extended_id=False))
time.sleep(dt)
time.sleep(0.5)
# Disable motor
disable_msg = can.Message(
arbitration_id=motor_id,
data=[0xFF] * 7 + [0xFD],
is_extended_id=False
)
bus.send(disable_msg)
print("SUCCESS: Motor homed to 0.0° and safely disabled.")
def main():
try:
bus = can.interface.Bus(interface=INTERFACE, channel=CHANNEL, bitrate=BITRATE)
except Exception as e:
print(f"Failed to initialize CAN interface '{CHANNEL}': {e}")
sys.exit(1)
try:
return_to_zero_mit(bus, motor_id=MOTOR_ID, duration=DURATION)
finally:
bus.shutdown()
if __name__ == "__main__":
main()
python3 movetozero.py
[MIT Mode] Connecting to Motor ID 1...
Current Position: +282.0° (+4.92 rad)
Homing to 0.0 rad over 3.0s...
SUCCESS: Motor homed to 0.0° and safely disabled.
Reading More Data
With the core concepts behind movement and configuration out of the way, the last functionality that I want to highlight is that you can read multiple values from the motor to make decisions about things such as lowering applied force when a motor is starting to overheat to avoid burning it out, or understanding how quickly a motor is moving to ensure that it's within a safe limit.
In this example you can see how to read angle, velocity, torque, temperature, and get a general health status from the motor as it's operating, which you may do within a ROS2 node to trigger various actions.
import can
import time
import math
import sys
INTERFACE = 'socketcan'
CHANNEL = 'can0'
BITRATE = 1000000
MOTOR_ID = 1
# RobStride RS-00 Parameter Limits
P_MIN, P_MAX = -12.57, 12.57 # Position range (rad)
V_MIN, V_MAX = -33.0, 33.0 # Velocity range (rad/s)
T_MIN, T_MAX = -14.0, 14.0 # Torque range (Nm)
def uint_to_float(x_int: int, x_min: float, x_max: float, bits: int) -> float:
return float(x_int) * float(x_max - x_min) / float((1 << bits) - 1) + x_min
def main():
try:
bus = can.interface.Bus(interface=INTERFACE, channel=CHANNEL, bitrate=BITRATE)
except Exception as e:
print(f"Failed to open CAN bus: {e}")
return
print(f"Streaming MIT Telemetry for Motor ID {MOTOR_ID}... (Press Ctrl+C to exit)\n")
# Enable Motor in MIT Mode:
enable_frame = can.Message(
arbitration_id=MOTOR_ID,
data=[0xFF] * 7 + [0xFC],
is_extended_id=False
)
bus.send(enable_frame)
time.sleep(0.05)
# Passive telemetry polling frame (Kp=0, Kd=0, torque_ff=0 -> transparent tracking)
# Byte 0-1: Pos = 0x7FFF (0 rad)
# Byte 2-3: Vel = 0x800 (0 rad/s), Kp = 0x000 (0)
# Byte 4-5: Kp low = 0x00, Kd = 0x000 (0)
# Byte 6-7: Kd low = 0x0, Torque = 0x800 (0 Nm)
passive_payload = [0x7F, 0xFF, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00]
try:
while True:
bus.send(can.Message(arbitration_id=MOTOR_ID, data=passive_payload, is_extended_id=False))
msg = bus.recv(timeout=0.04)
if msg and not msg.is_extended_id and len(msg.data) >= 8:
data = msg.data
resp_id = data[0]
if resp_id == MOTOR_ID:
p_raw = (data[1] << 8) | data[2]
v_raw = (data[3] << 4) | (data[4] >> 4)
t_raw = ((data[4] & 0x0F) << 8) | data[5]
temp_raw = (data[6] << 8) | data[7]
if temp_raw >= 0x8000:
temp_raw -= 0x10000
temp = temp_raw / 10.0
pos_deg = math.degrees(uint_to_float(p_raw, P_MIN, P_MAX, 16))
vel = uint_to_float(v_raw, V_MIN, V_MAX, 12)
torque = uint_to_float(t_raw, T_MIN, T_MAX, 12)
sys.stdout.write(
f"\r[RS00 MIT ID:{MOTOR_ID:02d}] "
f"Angle: {pos_deg:+06.1f}° | "
f"Vel: {vel:+05.2f} rad/s | "
f"Torque: {torque:+05.2f} Nm | "
f"Temp: {temp:04.1f}°C | Status: OK"
)
sys.stdout.flush()
time.sleep(0.05)
except KeyboardInterrupt:
print("\nExiting telemetry monitor...")
finally:
# Disable
disable_frame = can.Message(
arbitration_id=MOTOR_ID,
data=[0xFF] * 7 + [0xFD],
is_extended_id=False
)
try:
bus.send(disable_frame)
except Exception:
pass
bus.shutdown()
if __name__ == "__main__":
main()
This should output a continuous stream of status values, like this example where I turned on the status script and physically moved the motor's center to change the velocity and angle values during testing.
[RS00 MIT ID:01] Angle: +000.5° | Vel: -0.01 rad/s | Torque: -0.01 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +000.5° | Vel: -0.06 rad/s | Torque: +0.00 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +000.5° | Vel: +0.06 rad/s | Torque: +0.01 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +000.5° | Vel: -0.06 rad/s | Torque: +0.02 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +000.5° | Vel: -0.02 rad/s | Torque: -0.01 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +000.6° | Vel: +0.04 rad/s | Torque: -0.04 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +001.4° | Vel: +0.51 rad/s | Torque: -0.04 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +002.5° | Vel: +0.59 rad/s | Torque: -0.09 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +004.6° | Vel: +0.88 rad/s | Torque: -0.02 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +007.7° | Vel: +1.15 rad/s | Torque: -0.08 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +011.5° | Vel: +1.47 rad/s | Torque: -0.04 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +016.0° | Vel: +1.68 rad/s | Torque: -0.08 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +021.2° | Vel: +1.85 rad/s | Torque: -0.06 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +026.8° | Vel: +2.01 rad/s | Torque: -0.09 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +032.7° | Vel: +2.10 rad/s | Torque: -0.02 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +038.9° | Vel: +2.25 rad/s | Torque: -0.02 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +045.3° | Vel: +2.17 rad/s | Torque: -0.03 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +051.9° | Vel: +2.26 rad/s | Torque: -0.06 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +058.5° | Vel: +2.28 rad/s | Torque: +0.00 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +065.3° | Vel: +2.31 rad/s | Torque: -0.02 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +072.3° | Vel: +2.43 rad/s | Torque: -0.06 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +079.4° | Vel: +2.47 rad/s | Torque: +0.04 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +086.6° | Vel: +2.43 rad/s | Torque: +0.00 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +093.8° | Vel: +2.51 rad/s | Torque: +0.01 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +101.1° | Vel: +2.51 rad/s | Torque: -0.02 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +108.3° | Vel: +2.44 rad/s | Torque: -0.00 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +115.3° | Vel: +2.28 rad/s | Torque: +0.11 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +121.6° | Vel: +1.96 rad/s | Torque: +0.13 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +127.1° | Vel: +1.75 rad/s | Torque: +0.09 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +131.8° | Vel: +1.47 rad/s | Torque: +0.03 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +135.6° | Vel: +1.02 rad/s | Torque: +0.15 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +138.1° | Vel: +0.56 rad/s | Torque: +0.19 Nm | Temp: 22.0°C | Status: OK
[RS00 MIT ID:01] Angle: +139.0° | Vel: +0.14 rad/s | Torque: +0.02 Nm | Temp: 22.0°C | Status: OK
Conclusion
So that was a lot, but there's so much more to these actuators. You can find the manual for them online to get information about physical traits, such as mounting hole distances, but the links for any source code are dead at this current point in time, so hopefully the code samples and explanations provided here will be helpful in getting you started with these motors that are quickly becoming more popular.
If you want to learn more about values that can be read from the motor, physical properties, and byte codes, I highly recommend checking out the official documentation here.




Top comments (1)
Wow! Great technical articles. I learn from it.