Troubleshooting¶
Nothing arrives on the bus¶
In order of likelihood:
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.
MIT mode, and nothing is commanding. A MIT driver only replies when it receives a frame.
cubemars scan --poke 4sends enter-MIT frames to ids 1–4 to elicit replies. (It leaves those drivers in MIT mode.)Bitrate. AK drivers are 1 Mbit/s. On socketcan the kernel owns this;
cubemars doctorshows what each interface is actually configured for.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_VALUESreply uses a completely different fault table where 1 means over-voltage rather than over-temperature. Serial is out of scope for v1, butSerialFaultexists 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.