First bench session

Run these in order, motor clamped to the bench. Each step has a defined abort. Steps B2, B5, B6 and B7 answer questions the manual leaves open; record what you find.

Before anything: cubemars doctor, then cubemars dump-spec AK40-10.

B0 — adapter only, motor unpowered

cubemars scan --timeout 5

Expect zero frames, a clean exit, no traceback. Proves the transport and the “nothing there” path before the motor can confuse matters.

B1 — motor powered, first frame ever sent

cubemars monitor --id 1

monitor sends zero-gain, zero-torque frames, which produce no motion, so this is safe first contact. Expect feedback, a plausible temperature, fault 0.

Record the MIT reply arbitration id that cubemars scan reports. The manual says “0X00+Drive ID”, which is ambiguous; pin it afterwards with MitMotor(..., reply_mode=MitReplyMode.ARB_MOTOR_ID).

Measured: this driver replies on arbitration id = motor id (0x001 for id 1), not 0x00. Both conventions exist in the wild, which is why the library learns it from the first reply rather than assuming.

Abort if the temperature is implausible or any fault appears.

B2 — sign and gearbox side, hand-rotated, still zero gains

With monitor running, rotate the output shaft exactly one turn by hand.

Δposition

meaning

output-side (expected)

2π × 10

rotor-side

Measured: one hand-turn read 6.3867 rad against 6.2832 expected for output-side and 62.83 for rotor-side. MIT position is output-side, confirming the inference from the datasheet no-load speed.

Then push past ±12.5 rad by hand and watch what the reading does at the limit:

behaviour

pass to MitMotor

rolls over to the far end

wrap_mode=WrapMode.WRAP

sticks at the limit

wrap_mode=WrapMode.SATURATE

Until you have done this, multi-turn unwrapping refuses rather than guessing.

Easier than doing it by hand: drive past the limit under power at a few rad/s.

Note: zero_here() stops the driver replying for about a second. It opens a grace window for exactly that, so the wait is routine — use m.settle(1.5) to keep the loop running through it. Note that keeping the link alive is not by itself what fixes this: staleness is measured on frames received, so transmitting through the gap does not reset it. The grace window does.

B3 — first commanded torque, damping only

m.update(position=0.0, velocity=0.0, kp=0.0, kd=0.3, torque=0.0)

The motor should feel like a viscous brake and must not move on its own. Check that the reported torque opposes the direction you push. First time the motor produces torque.

B4 — 🚩 first commanded motion

cubemars jog --id 1 --position 0.1 --kp 5 --kd 0.3 --zero

Small gain, small step. Verify it moves the right way, settles, and that the reported position matches the command within a few LSB (one LSB is 0.38 mrad).

This is the checkpoint.

B5 — scaling agreement

m.update(position=0.0, velocity=5.0, kp=0.0, kd=1.0, torque=0.0)

Check the reported velocity settles near 5 rad/s. A consistent factor off means your firmware’s field constants differ from the manual’s table, which silently mis-scales everything.

B6 — back-drive check

At 24 V the motor cannot reach the ±45.5 rad/s velocity field under its own power (no-load is ~408 rpm = 42.7 rad/s from Kv/Ke). Spin the output by hand faster than that and see whether the reading saturates or wraps. This is the only remaining velocity-field concern at your supply voltage.

The datasheet is internally inconsistent here: it states a 435 rpm no-load speed, but its own Kv=170 and Ke=5.88 both give 408 rpm at the rated 24 V. 435 rpm implies Kv=181, or a 25.6 V supply. The MIT velocity field was evidently sized to the 435 figure, so there is no designed-in headroom.

B7 — servo mode

First confirm in CubeMarsTool that the driver is in servo mode and that the CAN status rate is not 0. A rate of 0 means the driver never uploads anything; the wiring is fine and nothing arrives.

cubemars scan            # expect "mode servo"
m.update(servo.Duty(0.05))  # 1 s
m.update(servo.Position(90.0))

Repeat B2’s one-turn test in servo mode:

Δposition

meaning

360°

output-side

3600°

rotor-side

That settles servo.position_side, the last unconfirmed constant.

Recording what you found

Measurements belong back in the spec, with their provenance:

from dataclasses import replace
from cubemarspycan import Source, Sourced, get_spec
from cubemarspycan.spec import Side

spec = get_spec("AK40-10").evolve(
    servo=replace(
        get_spec("AK40-10").servo,
        position_side=Sourced(Side.OUTPUT, Source.MEASURED, ref="bench 2026-09-20"),
    )
)