Skip to content

Make the Cartesian in-flight guard refuse when cart_busy cannot be confirmed #8

Description

@X-F-R

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions