diff --git a/src/lerobot/scripts/lerobot_teleoperate.py b/src/lerobot/scripts/lerobot_teleoperate.py index 5a9fdbc..be4fc99 100644 --- a/src/lerobot/scripts/lerobot_teleoperate.py +++ b/src/lerobot/scripts/lerobot_teleoperate.py @@ -85,6 +85,7 @@ from lerobot.teleoperators import ( # noqa: F401 Teleoperator, TeleoperatorConfig, + alicia_d_leader, bi_so100_leader, gamepad, homunculus, @@ -160,8 +161,11 @@ def teleop_loop( # Process action for robot through pipeline robot_action_to_send = robot_action_processor((teleop_action, obs)) - # Send processed action to robot (robot_action_processor.to_output should return dict[str, Any]) - _ = robot.send_action(robot_action_to_send) + # When the teleoperator controls the robot through a direct hardware + # connection, sending the action again from the computer would duplicate + # the command. Keep processing it for visualization, but skip transmission. + if not teleop.directly_controls_robot: + _ = robot.send_action(robot_action_to_send) if display_data: # Process robot observation through pipeline diff --git a/tests/test_control_robot.py b/tests/test_control_robot.py index f0caff2..1ba8ac3 100644 --- a/tests/test_control_robot.py +++ b/tests/test_control_robot.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest.mock import patch +from unittest.mock import PropertyMock, patch from lerobot.scripts.lerobot_calibrate import CalibrateConfig, calibrate from lerobot.processor import make_default_processors @@ -23,7 +23,7 @@ from lerobot.scripts.lerobot_teleoperate import TeleoperateConfig, teleoperate from tests.fixtures.constants import DUMMY_REPO_ID from tests.mocks.mock_robot import MockRobot, MockRobotConfig -from tests.mocks.mock_teleop import MockTeleopConfig +from tests.mocks.mock_teleop import MockTeleop, MockTeleopConfig def test_calibrate(): @@ -43,6 +43,24 @@ def test_teleoperate(): teleoperate(cfg) +def test_teleoperate_skips_send_action_when_teleop_directly_controls_robot(): + robot_cfg = MockRobotConfig() + teleop_cfg = MockTeleopConfig() + cfg = TeleoperateConfig( + robot=robot_cfg, + teleop=teleop_cfg, + teleop_time_s=0.1, + ) + + with ( + patch.object(MockTeleop, "directly_controls_robot", new_callable=PropertyMock, return_value=True), + patch.object(MockRobot, "send_action", autospec=True) as mock_send_action, + ): + teleoperate(cfg) + + mock_send_action.assert_not_called() + + def test_record_and_resume(tmp_path): robot_cfg = MockRobotConfig() teleop_cfg = MockTeleopConfig()