Troubleshooting

Nothing arrives on the bus

In order of likelihood:

  1. Servo mode with the CAN status rate set to 0. The driver never uploads anything. The wiring is fine. Check this first in CubeMarsTool; it is the commonest cause and it looks exactly like a dead bus.

  2. MIT mode, and nothing is commanding. A MIT driver only replies when it receives a frame. cubemars scan --poke 4 sends enter-MIT frames to ids 1–4 to elicit replies. (It leaves those drivers in MIT mode.)

  3. Bitrate. AK drivers are 1 Mbit/s. On socketcan the kernel owns this; cubemars doctor shows what each interface is actually configured for.

  4. Power, wiring, termination.

cubemars scan distinguishes these and says which it thinks it is seeing.

Feedback stops after a while, or never starts

A control loop with no sleep starves the receive thread. Frames are delivered by a python-can notifier thread; a tight loop that only sends and computes may never release the GIL, so nothing is ever delivered and the state latch stays empty. Every real control loop sleeps or blocks on a timer — but a busy-wait will look exactly like a dead motor.

This bit during development: a stepped-simulator test left the latch permanently empty until the harness was given a yield.

Position lags the setpoint by a constant-looking amount

Almost always a missing velocity feedforward. With velocity=0 while commanding a moving position, the kd term opposes the motion you are asking for, giving an error of about kd * v / kp. Feed the target’s derivative as velocity. Measured 3.5x improvement on real hardware - see units.md.

state.seq jumps by more than one

Normal. The latch holds only the newest frame, so if several arrived between two update() calls the sequence number advances by several. It means your loop is slower than the motor’s reply rate, not that frames were lost. Compare seq deltas against bus.transport.stats.rx if you want the real reception count.

The motor lurches right after zero_here()

Zeroing moves the coordinate system. A position setpoint that was in flight is then expressed in the old frame, and the motor drives back toward where it just came from.

zero_here() now handles this for you: it stages zero gains and puts them on the wire before moving the origin. Staging alone is not enough - hold() only assigns to the staged command, it does not transmit - which is why calling hold() yourself was never quite the fix it appeared to be.

Found while writing examples/homing.py: after homing to a stop and backing off 0.05 rad, the zero appeared to read 0.0498 rad instead of 0 - the motor was being commanded back to the pre-zero target.

StaleFeedbackError right after zero_here()

The driver stops replying for about a second while it zeroes.

The reason a bare time.sleep() fails is not that it sends nothing. Staleness is measured against the last frame received, so transmitting through the gap does not reset it either - settle() alone would raise just the same. What fixes it is that zero_here() opens an explicit grace window (grace_s, default 1.5 s) that tolerates the silence, and only the fatal limit is suppressed, so the staleness warning still fires and a link that never comes back is still visible. Use motor.settle(1.5) to keep the loop running through the window.

Found on the bench, not in simulation - the simulator zeroes instantly.

StaleFeedbackError

Feedback is older than SafetyPolicy.stale_fatal_s. A safe stop has already been sent. Usual causes: the motor lost power, the loop is slower than the warn threshold, or the link is saturated. MotorBus.transport.stats.tx_percentiles() shows whether sends are taking longer than the loop period.

MotorFault

The driver reported a non-zero fault code. A safe stop has already gone out, and the exception is raised on your thread, not the receive thread. Address the cause, then motor.clear_fault().

Codes are 0–7 for CAN feedback (manual v1.0.18 p.45). Code 7, motor stall, is new in v1.0.18; libraries written against older manuals raise KeyError on it.

The serial GET_VALUES reply uses a completely different fault table where 1 means over-voltage rather than over-temperature. Serial is out of scope for v1, but SerialFault exists so nobody reaches for the wrong one later.

ServoModeNotConfirmed

No 0x29 status and no 0x2C acknowledgement arrived. The message triages what was actually seen: nothing at all, frames on another id, or frames on your id with an unrecognised function (usually a driver still in MIT mode — construct with assume_mit=True to send the MIT exit frame on entry).

The manual documents no frame that causes servo-mode entry, so this library detects rather than guesses. Servo mode is selected in CubeMarsTool.

SpecIncompleteError

A conversion needs a constant this spec does not have — usually pole_pairs or Kt for a model whose datasheet has not been transcribed. Measure it and record it with spec.evolve(...) using Source.MEASURED. Raw wire values remain available meanwhile.

This is deliberate. Guessing is what produced an 8.6× position error and a 78× torque error in the library this one replaces.

CapabilityError on set_origin

OriginMode.PERMANENT writes flash and the manual restricts it to dual-encoder models. The AK40-10 has one encoder, so it is refused and no frame is sent. Use OriginMode.TEMPORARY (the default).

UnresolvedFrameError on status.output_rad

The manual never says whether servo position is rotor- or output-side. position_deg is always available. Settle it with bench step B7.

The motor twitches at “zero” torque

It shouldn’t, but the floor is real: no MIT field can encode an exact zero, so a commanded 0.0 N·m arrives as +1.22 mN·m on an AK40-10 — half an LSB. That is 2% of the motor’s 60 mN·m break-away torque, so a healthy motor cannot move on it. If yours does, suspect mechanical binding or a mis-set Kt.

Commands near full scale behave strangely

They shouldn’t, and this library guards it, but the protocol hazard is worth knowing. With position, velocity, Kp and Kd all at maximum, a torque near 4.9976 N·m packs to FF FF FF FF FF FF FF FE — which is byte-identical to the set position to zero frame. The neighbouring codes are enter and exit MIT mode.

pack_command detects the collision and steps one torque LSB (2.4 mN·m) away. Other implementations, including TMotorCANControl, do not.