Skip to content

Commit 1d1f61c

Browse files
committed
fix: accumulate EEF teleop commands
1 parent 4b3475b commit 1d1f61c

2 files changed

Lines changed: 49 additions & 9 deletions

File tree

dimos/control/tasks/eef_twist_task/eef_twist_task.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ def __init__(self, name: str, config: EEFTwistTaskConfig) -> None:
7878
self._lock = threading.Lock()
7979
self._latest_twist: TwistStamped | None = None
8080
self._last_update_time = 0.0
81+
self._last_commanded_positions: NDArray[np.float64] | None = None
82+
self._last_commanded_pose: pinocchio.SE3 | None = None
8183

8284
def claim(self) -> ResourceClaim:
8385
return ResourceClaim(self._joint_names, self._config.priority, ControlMode.SERVO_POSITION)
@@ -92,10 +94,6 @@ def on_ee_twist_command(self, twist: TwistStamped, t_now: float) -> bool:
9294
logger.warning("EEFTwistTask rejecting non-finite twist", task=self._name)
9395
return False
9496
with self._lock:
95-
if np.allclose(values, 0.0):
96-
self._clear_locked()
97-
self._last_update_time = t_now
98-
return True
9997
self._latest_twist = twist
10098
self._last_update_time = t_now
10199
return True
@@ -109,13 +107,25 @@ def compute(self, state: CoordinatorState) -> JointCommandOutput | None:
109107
self._config.timeout > 0
110108
and state.t_now - self._last_update_time > self._config.timeout
111109
):
112-
self._clear_locked()
110+
self._clear_locked(clear_target=True)
113111
return None
114112

115113
q_current = self._get_current_joints(state)
116114
if q_current is None or not np.all(np.isfinite(q_current)):
117115
return None
118-
target_pose = self._ik.forward_kinematics(q_current)
116+
q_current = np.asarray(q_current, dtype=np.float64)
117+
if np.allclose(twist_to_numpy(twist), 0.0):
118+
if self._last_commanded_positions is None:
119+
return None
120+
return JointCommandOutput(
121+
joint_names=self._joint_names_list,
122+
positions=self._last_commanded_positions.tolist(),
123+
mode=ControlMode.SERVO_POSITION,
124+
)
125+
126+
target_pose = self._last_commanded_pose
127+
if target_pose is None:
128+
target_pose = self._ik.forward_kinematics(q_current)
119129
dt = min(max(state.dt, 0.0), _MAX_DT)
120130
candidate = self._integrate_twist(target_pose, twist, dt)
121131

@@ -139,9 +149,13 @@ def compute(self, state: CoordinatorState) -> JointCommandOutput | None:
139149
)
140150
return None
141151

152+
commanded_positions = np.asarray(q_solution, dtype=np.float64).reshape(-1)
153+
self._last_commanded_positions = commanded_positions.copy()
154+
self._last_commanded_pose = self._ik.forward_kinematics(commanded_positions)
155+
142156
return JointCommandOutput(
143157
joint_names=self._joint_names_list,
144-
positions=q_solution.flatten().tolist(),
158+
positions=commanded_positions.tolist(),
145159
mode=ControlMode.SERVO_POSITION,
146160
)
147161

@@ -160,8 +174,11 @@ def _get_current_joints(self, state: CoordinatorState) -> NDArray[np.floating[An
160174
positions.append(pos)
161175
return np.array(positions, dtype=np.float64)
162176

163-
def _clear_locked(self) -> None:
177+
def _clear_locked(self, *, clear_target: bool = False) -> None:
164178
self._latest_twist = None
179+
if clear_target:
180+
self._last_commanded_positions = None
181+
self._last_commanded_pose = None
165182

166183
def _integrate_twist(
167184
self, pose: pinocchio.SE3, twist: TwistStamped, dt: float

dimos/control/tasks/eef_twist_task/test_eef_twist_task.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def __init__(self) -> None:
4242
self.solution = np.array([0.01, 0.02, 0.03], dtype=np.float64)
4343
self.converged = True
4444
self.final_error = 0.0
45+
self.solve_from_pose = False
4546

4647
def forward_kinematics(self, q_current: NDArray[np.float64]) -> FakePose:
4748
self.fk_calls.append(q_current.copy())
@@ -51,6 +52,8 @@ def solve(
5152
self, pose: FakePose, q_current: NDArray[np.float64]
5253
) -> tuple[NDArray[np.float64], bool, float]:
5354
self.solve_calls.append(pose.copy())
55+
if self.solve_from_pose:
56+
return pose.translation.copy(), self.converged, self.final_error
5457
return self.solution.copy(), self.converged, self.final_error
5558

5659

@@ -124,6 +127,23 @@ def test_integration_uses_current_fk_and_coordinator_dt(
124127
assert fake_ik.solve_calls[1].translation[0] > fake_ik.solve_calls[0].translation[0]
125128

126129

130+
def test_commands_accumulate_from_last_command_with_stale_feedback(
131+
task: EEFTwistTask, fake_ik: FakeIK
132+
) -> None:
133+
fake_ik.solve_from_pose = True
134+
assert task.on_ee_twist_command(_twist(1.0), t_now=1.0)
135+
136+
first = task.compute(_state(1.01, positions=[0.0, 0.0, 0.0], dt=0.01))
137+
second = task.compute(_state(1.02, positions=[0.0, 0.0, 0.0], dt=0.01))
138+
139+
assert first is not None
140+
assert second is not None
141+
assert first.positions is not None
142+
assert second.positions is not None
143+
assert second.positions[0] > first.positions[0]
144+
assert second.positions[0] == pytest.approx(0.02)
145+
146+
127147
def test_non_converged_ik_solution_is_accepted_when_joint_delta_is_safe(
128148
task: EEFTwistTask, fake_ik: FakeIK
129149
) -> None:
@@ -190,4 +210,7 @@ def test_timeout_and_zero_command_clear_then_next_nonzero_reseeds(
190210
assert fake_ik.solve_calls[-1].translation[0] > 1.0
191211

192212
assert task.on_ee_twist_command(_twist(0.0), t_now=2.02)
193-
assert not task.is_active()
213+
assert task.is_active()
214+
held = task.compute(_state(2.03, positions=[0.0, 0.0, 0.0]))
215+
assert held is not None
216+
assert held.positions == [1.01, 0.0, 0.0]

0 commit comments

Comments
 (0)