Context
Arm._reject_if_cart_in_flight() (src/litearm/arm.py:1571) is the reverse guard that keeps a caller out of zero-gravity while a Cartesian plan is still running. Its docstring is explicit about why it exists: entering mid-trajectory leaves the firmware with feedforward only, so the arm coasts to a stop on friction, which is worse than the controlled takeover movej() gives you.
The second of its two signals is a fresh cart_busy (bit10) reading, and the docstring states the failure policy (src/litearm/arm.py:1596):
状态取不到 (超时/链路断/固件回 ERR) ⇒ 保守拒绝: "未确认就不拦"漏过去的正是 coast 那一侧。
The guard is implemented as (src/litearm/arm.py:1606):
try:
busy = self.get_status_now().value.cart_busy
except (MotionTimeoutError, TransportError, CommandRejectedError):
raise InvalidCommandError(CART_IN_FLIGHT_UNCONFIRMED_MESSAGE) from None
if busy:
raise InvalidCommandError(CART_IN_FLIGHT_GUARD_MESSAGE)
Expected
When the state cannot be confirmed, the guard refuses, as its docstring promises.
Actual
get_status_now() can return a stale state instead of raising, when the firmware refuses the 0x40 and a passive status frame lands first. That is reported separately as #7; the important part here is what it does to this guard.
get_status_now() then returns the last state the passive stream happened to cache. If that cached frame predates the plan, cart_busy reads 0, the guard falls through to if busy: on a false value, and the command proceeds into zero-gravity while the arm is still executing the trajectory. The except clause never runs, so the conservative branch the docstring describes is unreachable exactly when it is needed.
This is the one place in the module where a stale read does not merely mislead the caller but removes a guard.
Reproduce
By inspection of the two functions together; I found it while porting the client to C++ and verified the source paths there.
# src/litearm/arm.py:1321 — the window
def _done(a) -> bool:
return bool(a._queues.get(err_key)) or a.status_seq != seq0
Any passive status frame arriving before our ERR{0x40,...} satisfies the right-hand half and ends the wait.
I have not reproduced this against the Python implementation on hardware. In the C++ port the same path is pinned by a test, zero_g_is_refused_conservatively_when_the_status_is_unavailable, which fails open if the equivalent of _done is not fixed.
Suggested direction
Fix the root cause in #7 so this except can do its job. If you prefer to defend here as well, gate on the state's age or on status_seq having advanced past the value read before the 0x40 was written, rather than on the state merely being present.
Context
Arm._reject_if_cart_in_flight()(src/litearm/arm.py:1571) is the reverse guard that keeps a caller out of zero-gravity while a Cartesian plan is still running. Its docstring is explicit about why it exists: entering mid-trajectory leaves the firmware with feedforward only, so the arm coasts to a stop on friction, which is worse than the controlled takeovermovej()gives you.The second of its two signals is a fresh
cart_busy(bit10) reading, and the docstring states the failure policy (src/litearm/arm.py:1596):The guard is implemented as (
src/litearm/arm.py:1606):Expected
When the state cannot be confirmed, the guard refuses, as its docstring promises.
Actual
get_status_now()can return a stale state instead of raising, when the firmware refuses the0x40and a passive status frame lands first. That is reported separately as #7; the important part here is what it does to this guard.get_status_now()then returns the last state the passive stream happened to cache. If that cached frame predates the plan,cart_busyreads 0, the guard falls through toif busy:on a false value, and the command proceeds into zero-gravity while the arm is still executing the trajectory. Theexceptclause never runs, so the conservative branch the docstring describes is unreachable exactly when it is needed.This is the one place in the module where a stale read does not merely mislead the caller but removes a guard.
Reproduce
By inspection of the two functions together; I found it while porting the client to C++ and verified the source paths there.
Any passive status frame arriving before our
ERR{0x40,...}satisfies the right-hand half and ends the wait.I have not reproduced this against the Python implementation on hardware. In the C++ port the same path is pinned by a test,
zero_g_is_refused_conservatively_when_the_status_is_unavailable, which fails open if the equivalent of_doneis not fixed.Suggested direction
Fix the root cause in #7 so this
exceptcan do its job. If you prefer to defend here as well, gate on the state's age or onstatus_seqhaving advanced past the value read before the0x40was written, rather than on the state merely being present.