Skip to content

Commit f7dd951

Browse files
committed
fix servoL clamping: correct CSE position every clamped tick to prevent gap accumulation
1 parent 80a7d80 commit f7dd951

2 files changed

Lines changed: 53 additions & 45 deletions

File tree

‎parol6/commands/servo_commands.py‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -293,9 +293,12 @@ def execute_step(self, state: ControllerState) -> ExecutionStatusCode:
293293
for i in range(6):
294294
self._q_commanded[i] += dq[i] / ratio
295295
self._clamped = True
296+
# Correct CSE position every clamped tick to prevent
297+
# gap accumulation between Cartesian planner and joints
298+
PAROL6_ROBOT.robot.fkine_into(self._q_commanded, self._fk_buf)
299+
cse.correct_position(self._fk_buf)
296300
elif self._clamped:
297-
# Exiting clamp — re-sync CSE to actual robot TCP so
298-
# there's no accumulated position gap to snap across
301+
# Exiting clamp — final re-sync
299302
PAROL6_ROBOT.robot.fkine_into(self._q_commanded, self._fk_buf)
300303
cse.correct_position(self._fk_buf)
301304
self._q_ik_seed[:] = self._q_commanded
@@ -318,8 +321,18 @@ def execute_step(self, state: ControllerState) -> ExecutionStatusCode:
318321
self.set_move_position(state, self._steps_buf)
319322

320323
if finished and not self._ik_stopping:
321-
self.finish()
322-
cse.active = False
323-
return ExecutionStatusCode.COMPLETED
324+
if self._clamped:
325+
# CSE converged in Cartesian space but joints lagged due to
326+
# velocity clamping. Re-sync CSE from actual commanded
327+
# position and let it drive the remaining gap.
328+
PAROL6_ROBOT.robot.fkine_into(self._q_commanded, self._fk_buf)
329+
cse.sync_pose(self._fk_buf)
330+
cse.set_limits(self.p.speed, self.p.accel)
331+
self._q_ik_seed[:] = self._q_commanded
332+
self._clamped = False
333+
else:
334+
self.finish()
335+
cse.active = False
336+
return ExecutionStatusCode.COMPLETED
324337

325338
return ExecutionStatusCode.EXECUTING
Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
"""Test that ServoL joint-velocity clamping doesn't cause TCP lurching.
22
3-
When IK requires joint velocities that exceed hardware limits (e.g. near a
4-
wrist singularity), ServoLCommand clamps the joint deltas. This can cause
5-
the CSE's internal Cartesian state to diverge from the actual robot TCP.
6-
7-
The fix re-syncs the CSE position when exiting a clamp period so there's no
8-
accumulated gap to snap across. This test verifies that the per-tick TCP
9-
displacement stays within bounds throughout the entire motion — including
10-
the clamp → free transition.
3+
When IK requires joint velocities that exceed hardware limits, ServoLCommand
4+
clamps the joint deltas proportionally. The per-tick CSE position correction
5+
keeps the Cartesian planner in sync with the actual robot joints, preventing
6+
gap accumulation and lurch on clamp exit.
7+
8+
This test verifies that:
9+
1. Clamping actually occurs (the test is meaningful)
10+
2. No single tick produces a TCP displacement larger than limits allow
11+
3. The final position converges accurately despite clamping
1112
"""
1213

1314
import math
@@ -22,9 +23,7 @@
2223

2324

2425
_q_rad_buf = np.zeros(6, dtype=np.float64)
25-
_T_buf = np.asfortranarray(
26-
np.eye(4, dtype=np.float64)
27-
) # F-order for pinokin Eigen binding
26+
_T_buf = np.asfortranarray(np.eye(4, dtype=np.float64))
2827
_rpy_buf = np.zeros(3, dtype=np.float64)
2928

3029

@@ -39,7 +38,7 @@ def _home_steps() -> np.ndarray:
3938

4039

4140
def _fk_mm_rpy(q_steps: np.ndarray) -> np.ndarray:
42-
"""FK from steps → [x_mm, y_mm, z_mm, rx_deg, ry_deg, rz_deg]."""
41+
"""FK from steps -> [x_mm, y_mm, z_mm, rx_deg, ry_deg, rz_deg]."""
4342
from pinokin import so3_rpy
4443

4544
steps_to_rad(np.asarray(q_steps, dtype=np.int32), _q_rad_buf)
@@ -59,64 +58,55 @@ def _fk_mm_rpy(q_steps: np.ndarray) -> np.ndarray:
5958

6059

6160
class TestServoLClampResync:
62-
"""Verify no TCP lurch when exiting joint-velocity clamping."""
61+
"""Verify no TCP lurch when joint-velocity clamping is active."""
6362

64-
def test_no_tcp_lurch_through_wrist_rotation(self) -> None:
65-
"""A large wrist rotation target should not cause per-tick TCP jumps.
63+
def test_no_lurch_and_accurate_convergence(self) -> None:
64+
"""A linear target that triggers clamping should converge accurately.
6665
67-
The target rotates RZ by 90° from home, which forces large J4/J6
68-
deltas that trigger clamping. We verify that:
66+
A +30mm X offset from home triggers clamping because the IK solution
67+
requires joint velocities exceeding hardware limits. We verify that:
6968
1. Clamping actually occurs (the test is meaningful)
70-
2. No single tick produces a TCP displacement larger than what the
71-
CSE's max velocity allows (with generous margin for Ruckig jerk).
69+
2. No single tick produces a TCP displacement larger than limits allow
70+
3. The motion converges to <1mm of the target
7271
"""
7372
state = ControllerState()
7473
home = _home_steps()
7574
state.Position_in[:] = home
7675

77-
# Target: same position, rotated 90° around Z
7876
home_pose = _fk_mm_rpy(home)
7977
target_pose = list(home_pose)
80-
target_pose[5] += 90.0 # rotate RZ by 90° (crosses wrist singularity)
78+
target_pose[0] += 30.0 # +30mm X triggers clamping
8179

8280
cmd = ServoLCommand(ServoLCmd(pose=target_pose, speed=1.0, accel=1.0))
8381
cmd.do_setup(state)
8482

85-
# Max displacement per tick from CSE limits (generous 3x margin for jerk overshoot)
86-
max_lin_per_tick = LIMITS.cart.jog.velocity.linear * INTERVAL_S * 3.0 # meters
87-
max_ang_per_tick = LIMITS.cart.jog.velocity.angular * INTERVAL_S * 3.0 # rad
83+
# Max displacement per tick (generous 3x margin for Ruckig jerk overshoot)
84+
max_lin_per_tick = LIMITS.cart.jog.velocity.linear * INTERVAL_S * 3.0
85+
max_ang_per_tick = LIMITS.cart.jog.velocity.angular * INTERVAL_S * 3.0
8886

8987
prev_pose_mm_rpy: np.ndarray | None = None
9088
clamped_any = False
91-
max_lin_jump = 0.0
92-
max_ang_jump = 0.0
9389

9490
for tick in range(2000):
9591
cmd.do_setup(state)
9692
status = cmd.execute_step(state)
9793

98-
# Simulate firmware tracking: Position_in follows Position_out
9994
state.Position_in[:] = state.Position_out
10095

10196
current_pose = _fk_mm_rpy(state.Position_in)
10297

10398
if prev_pose_mm_rpy is not None:
10499
lin_delta = np.linalg.norm(current_pose[:3] - prev_pose_mm_rpy[:3])
105-
# Angle deltas (handle wrapping)
106-
ang_deltas = []
107-
for i in range(3):
108-
d = abs(
100+
ang_deltas = [
101+
abs(
109102
(current_pose[3 + i] - prev_pose_mm_rpy[3 + i] + 180) % 360
110103
- 180
111104
)
112-
ang_deltas.append(d)
105+
for i in range(3)
106+
]
113107
ang_delta = max(ang_deltas)
114108

115-
max_lin_jump = max(max_lin_jump, lin_delta)
116-
max_ang_jump = max(max_ang_jump, ang_delta)
117-
118-
# Check: no single tick exceeds CSE velocity limits
119-
assert lin_delta < max_lin_per_tick * 1000.0, ( # convert m to mm
109+
assert lin_delta < max_lin_per_tick * 1000.0, (
120110
f"Tick {tick}: linear TCP jump {lin_delta:.3f}mm exceeds "
121111
f"limit {max_lin_per_tick * 1000:.3f}mm"
122112
)
@@ -125,7 +115,6 @@ def test_no_tcp_lurch_through_wrist_rotation(self) -> None:
125115
f"limit {math.degrees(max_ang_per_tick):.3f}°"
126116
)
127117

128-
# Detect if clamping occurred (cmd._clamped is set in execute_step)
129118
if cmd._clamped:
130119
clamped_any = True
131120

@@ -134,8 +123,14 @@ def test_no_tcp_lurch_through_wrist_rotation(self) -> None:
134123
if status.name == "COMPLETED":
135124
break
136125

137-
# Verify the test actually exercised the clamp path
138126
assert clamped_any, (
139127
"Test did not trigger joint-velocity clamping — "
140-
"adjust target to force a wrist singularity crossing"
128+
"adjust target to force clamping"
129+
)
130+
131+
# Verify final accuracy
132+
final_pose = _fk_mm_rpy(state.Position_in)
133+
pos_error = np.linalg.norm(np.array(final_pose[:3]) - np.array(target_pose[:3]))
134+
assert pos_error < 1.0, (
135+
f"Position error {pos_error:.3f}mm exceeds 1.0mm tolerance"
141136
)

0 commit comments

Comments
 (0)