This guide covers how to initialize the motor and use the various control modes available.
The library uses a context manager (with block) to safely handle connection validation and shutdown.
Use one consistent no-sudo app runtime flow:
- Configure your CAN interface (
can0,can1, etc.) at boot with a root-managed service. - Run the Python app as a normal user.
- Keep application code focused on motor control only.
Default interface behavior:
ServoConfig(...)defaults tocan0.- You only need to pass
can_channelwhen using another interface (for examplecan1).
Reference board and vendor docs:
Configure overlays in /boot/firmware/config.txt:
dtparam=spi=on
dtoverlay=mcp2515-can0,oscillator=12000000,interrupt=25,spimaxfrequency=2000000Reboot after editing config.txt so the overlay is applied:
sudo rebootCreate /etc/systemd/system/can0.service (or can1.service if using can1):
[Unit]
Description=Bring up SocketCAN can0
After=network-pre.target
Before=network.target
Wants=network.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c 'ip link set can0 down || true'
ExecStart=/bin/sh -c 'ip link set can0 up type can bitrate 1000000 restart-ms 100'
ExecStart=/bin/sh -c 'ip link set can0 txqueuelen 65536'
ExecStop=/bin/sh -c 'ip link set can0 down'
[Install]
WantedBy=multi-user.targetEnable and verify:
sudo systemctl daemon-reload
sudo systemctl enable --now can0.service
ip -details link show can0This runs with root at boot, so your Python app can run without sudo.
If your interface is can1, replace can0 with can1 in both the service file and commands above.
import time
from cubemars_servo_can import CubeMarsServoCan, MotorModel, ServoConfig
config = ServoConfig(
motor=MotorModel.AK80_9,
motor_id=1,
)
# Start motor control (interface must already be up via the boot service).
# can_channel defaults to "can0", so it can be omitted.
with CubeMarsServoCan(config) as motor:
print("Motor connected!")
# Control logic goes here.
# ...
time.sleep(1)If your interface is not can0, pass it explicitly:
config = ServoConfig(
motor=MotorModel.AK80_9,
motor_id=1,
can_channel="can1",
)
with CubeMarsServoCan(config) as motor:
...Run it:
uv run your_script.pyYou must explicitly select a control mode before staging a command for that mode.
Command setters stage values; update() checks fresh telemetry and safety state
before transmitting.
| Mode API | Sent CAN command |
|---|---|
set_control_mode(ControlMode.DUTY_CYCLE) |
SET_DUTY |
set_control_mode(ControlMode.Q_AXIS_CURRENT) |
SET_CURRENT |
set_control_mode(ControlMode.CURRENT_BRAKE) |
SET_CURRENT_BRAKE |
set_control_mode(ControlMode.VELOCITY) |
SET_RPM |
set_control_mode(ControlMode.POSITION) |
SET_POS |
set_control_mode(ControlMode.POSITION_VELOCITY) |
SET_POS_SPD |
Moves the motor to a specific motor-shaft angle in radians.
motor.set_control_mode(ControlMode.POSITION)
# Move the motor shaft to 180 degrees.
motor.set_motor_position(3.14)
motor.update()Controls motor-shaft speed in radians per second.
motor.set_control_mode(ControlMode.VELOCITY)
# Spin the motor shaft at 10 rad/s.
motor.set_motor_velocity(10.0)
motor.update()Controls torque through q-axis current.
motor.set_control_mode(ControlMode.Q_AXIS_CURRENT)
# Apply an estimated 0.5 Nm at the motor shaft.
motor.set_motor_torque(0.5)
motor.update()Torque conversion is an ideal estimate:
motor torque = current × Kt
output torque = motor torque × gear ratio
It does not model gearbox loss, controller calibration, saturation, or temperature. Measure and calibrate when torque accuracy matters.
Moves to an output position while respecting output-side velocity and acceleration limits.
motor.set_control_mode(ControlMode.POSITION_VELOCITY)
motor.set_output_position(
3.14,
velocity_radians_per_second=5.0,
acceleration_radians_per_second_squared=10.0,
)
motor.update()Use set_motor_position() with the same keyword arguments when the position,
velocity, and acceleration are expressed on the motor-shaft side.
Applies a non-negative brake current to hold position.
motor.set_control_mode(ControlMode.CURRENT_BRAKE)
motor.set_q_axis_current_amps(2.0)
motor.update()Controls normalized duty cycle directly.
motor.set_control_mode(ControlMode.DUTY_CYCLE)
motor.set_duty_cycle(0.1)
motor.update()motor.set_origin(OriginMode.TEMPORARY)
motor.set_origin(OriginMode.PERSISTENT)Origin operations are immediate and require an entered context. Temporary mode lasts until power loss. Persistent mode saves the current physical position in the motor's nonvolatile parameters; call it deliberately during setup rather than from a repeated control loop.
ControlMode.IDLE is the default and sends zero q-axis current on each update.
- Position, velocity, current, and torque commands are checked against motor and Servo protocol limits.
- Position-velocity mode validates target velocity and acceleration before frame packing.
- Current brake mode rejects negative current.
- Invalid modes, non-finite values, and out-of-range values raise typed exceptions instead of being silently clamped.
- Stale or malformed telemetry attempts zero current, then raises
MotorConnectionError. - A reported driver fault attempts zero current, then raises
MotorFaultError. - The first over-temperature sample suppresses motion. The configured consecutive count causes a typed thermal fault.
- Position modes hold the latest reported position during the pre-trip thermal guard; other modes send zero current.
- The guard clears only below the configured cooldown margin.
close()is idempotent and attempts zero current before releasing resources.
These are software safeguards, not a replacement for hardware limits, mechanical protection, an emergency stop, or commissioning tests.
config = ServoConfig(
motor=MotorModel.AK80_9,
motor_id=1,
max_driver_temperature_celsius=70.0,
overtemperature_trip_count=3,
cooldown_margin_celsius=2.0,
telemetry_timeout_seconds=0.1,
)The context manager is the recommended lifecycle. You can call close() to end
control early; repeated calls are safe, and context exit will not duplicate the
cleanup.
After a successful context entry, properties expose the most recent decoded
sample. Call update() regularly so safety checks and commands run against
fresh telemetry.
motor.update()
position = motor.output_position_radians
velocity = motor.output_velocity_radians_per_second
current = motor.q_axis_current_amps
temperature = motor.temperature_celsius
torque_estimate = motor.output_torque_newton_metersMotor-shaft position, velocity, acceleration, and torque estimates are also
available through properties prefixed with motor_.