Context
The motion entry points pack their arguments and send them without validating what the caller passed. Three gaps, all on the client side of the wire, all in src/litearm/arm.py:
- No non-finite check anywhere.
grep -rn 'isfinite\|isnan\|isinf' src/litearm/ returns nothing. movej validates len(q) and speed but not the values (src/litearm/arm.py:1926).
- No soft-limit check. Nothing compares a target against the firmware's
q_min/q_max, even though params().all_joint_params() exposes exactly those values (src/litearm/params.py:28).
- Speed is validated on two of six entry points.
movej (src/litearm/arm.py:1930) and movej_sync (:1959) both raise InvalidCommandError("speed 需 0..1"). move_p (:1968), move_l (:1792), move_c (:1816) and move_path (:1832) do not check it at all.
Expected
An argument the client can see is unusable should be refused locally, naming the argument, before a frame goes out. That is what movej already does for speed, and what set_joint_limits does for q_min < q_max (src/litearm/params.py:74).
Actual
Each gap has its own consequence.
Non-finite targets. The value is packed as f32 and sent. The arrival check then compares the arm's state against the target, and every comparison against a NaN is false, so the pose can never be judged reached. The call runs to the end of move_timeout and then raises a timeout that names the motion, not the argument. What the firmware does with a NaN target before that is not something I verified.
Soft-limit violations. The frame goes out and the firmware clamps it silently, so the arm stops at the limit while the caller is told it arrived. A caller asking for q = 3.5 on an axis limited to 2.809547 gets a successful return and an arm that is somewhere else. This is the failure mode that is hardest to notice, because nothing raises.
Unvalidated speed. move_l(pose, speed=5.0) is accepted while movej(q, speed=5.0) is refused, for the same argument with the same meaning, in the same module.
Reproduce
By inspection, with the greps above; all three are visible in src/litearm/arm.py without running anything.
For the speed gap specifically:
arm.movej([0.0] * 7, speed=5.0) # raises InvalidCommandError: speed 需 0..1
arm.move_l(pose, speed=5.0) # accepted, frame sent
For the soft-limit gap, read the limits, then ask for something outside them:
print([jp.q_min for jp in arm.params().all_joint_params()])
arm.movej([10.0] * 7) # accepted; firmware clamps; reports arrival
I found these while porting the client to C++, where all three are prechecked before the frame is built. I have not run the Python examples above against this implementation.
Suggested direction
Precheck at the shared entry, not in each method, so the six entry points cannot drift apart again. The values a non-finite or soft-limit check needs are already available: n from the session, and q_min/q_max from the joint parameters. Reading the limits once when the session is established keeps the check local and costs no round trip per command.
Note that the port found one exemption worth keeping: home() targets zeros and must not be prechecked, because an arm that has drifted outside its soft limits is precisely when you need home() to be reachable. Its own docstring already explains the equivalent reasoning at src/litearm/arm.py:2152.
Context
The motion entry points pack their arguments and send them without validating what the caller passed. Three gaps, all on the client side of the wire, all in
src/litearm/arm.py:grep -rn 'isfinite\|isnan\|isinf' src/litearm/returns nothing.movejvalidateslen(q)andspeedbut not the values (src/litearm/arm.py:1926).q_min/q_max, even thoughparams().all_joint_params()exposes exactly those values (src/litearm/params.py:28).movej(src/litearm/arm.py:1930) andmovej_sync(:1959) both raiseInvalidCommandError("speed 需 0..1").move_p(:1968),move_l(:1792),move_c(:1816) andmove_path(:1832) do not check it at all.Expected
An argument the client can see is unusable should be refused locally, naming the argument, before a frame goes out. That is what
movejalready does forspeed, and whatset_joint_limitsdoes forq_min < q_max(src/litearm/params.py:74).Actual
Each gap has its own consequence.
Non-finite targets. The value is packed as f32 and sent. The arrival check then compares the arm's state against the target, and every comparison against a NaN is false, so the pose can never be judged reached. The call runs to the end of
move_timeoutand then raises a timeout that names the motion, not the argument. What the firmware does with a NaN target before that is not something I verified.Soft-limit violations. The frame goes out and the firmware clamps it silently, so the arm stops at the limit while the caller is told it arrived. A caller asking for
q = 3.5on an axis limited to2.809547gets a successful return and an arm that is somewhere else. This is the failure mode that is hardest to notice, because nothing raises.Unvalidated speed.
move_l(pose, speed=5.0)is accepted whilemovej(q, speed=5.0)is refused, for the same argument with the same meaning, in the same module.Reproduce
By inspection, with the greps above; all three are visible in
src/litearm/arm.pywithout running anything.For the speed gap specifically:
For the soft-limit gap, read the limits, then ask for something outside them:
I found these while porting the client to C++, where all three are prechecked before the frame is built. I have not run the Python examples above against this implementation.
Suggested direction
Precheck at the shared entry, not in each method, so the six entry points cannot drift apart again. The values a non-finite or soft-limit check needs are already available:
nfrom the session, andq_min/q_maxfrom the joint parameters. Reading the limits once when the session is established keeps the check local and costs no round trip per command.Note that the port found one exemption worth keeping:
home()targets zeros and must not be prechecked, because an arm that has drifted outside its soft limits is precisely when you needhome()to be reachable. Its own docstring already explains the equivalent reasoning atsrc/litearm/arm.py:2152.