Skip to content

Commit 8e57358

Browse files
Add leader support plus some fixes.
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
1 parent 42ffd6a commit 8e57358

File tree

8 files changed

+90
-19
lines changed

8 files changed

+90
-19
lines changed

.devcontainer/lekiwi-dev/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ FROM mcr.microsoft.com/devcontainers/base:${BASE_IMAGE}
99
# Configuration
1010
################################################################################
1111

12-
ARG RUST_VERSION=1.86.0
12+
ARG RUST_VERSION=1.88.0
1313

1414
################################################################################
1515
# User 'dev'

.devcontainer/lekiwi-dev/devcontainer.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
"--network=host",
4141
"--runtime=nvidia", // Use NVIDIA runtime for GPU access
4242
"--gpus=all", // Use all available GPUs
43-
"--name=lekiwi-dora"
43+
"--name=lekiwi-dev"
44+
],
45+
// 9090: This is a web server that serves the frontend for the Rerun application.
46+
// 9876: This is the grpc server to transmit info for the items to be drawn.
47+
"forwardPorts": [
48+
9090,
49+
9876
4450
]
4551
}

packages/lekiwi_lerobot/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Once you have a dataset you can start training a model. For this, we can rely di
7979
Train imitation learning policies using collected data:
8080

8181
```bash
82-
uv run python -m lerobot.scripts.train \
82+
uv run lerobot-train \
8383
--dataset.repo_id=<username/my_dataset> \
8484
--policy.type=act \
8585
--output_dir=outputs/train/username/my_policy \

packages/lekiwi_lerobot/lekiwi_lerobot/record.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
KeyboardTeleop,
1212
KeyboardTeleopConfig,
1313
)
14+
from lerobot.teleoperators.so101_leader import SO101Leader, SO101LeaderConfig
1415
from lerobot.utils.constants import ACTION, OBS_STR
1516
from lerobot.utils.control_utils import (
1617
init_keyboard_listener,
@@ -32,6 +33,13 @@ def main() -> None:
3233
default="INFO",
3334
help="Set the logging level (default: INFO). Case-insensitive.",
3435
)
36+
parser.add_argument(
37+
"-i",
38+
"--ip",
39+
type=str,
40+
default="127.0.0.1",
41+
help="IP address of the robot (default: 127.0.0.1).",
42+
)
3543
parser.add_argument(
3644
"-r",
3745
"--repo-id",
@@ -59,6 +67,18 @@ def main() -> None:
5967
dest="visualize",
6068
help="Disable Rerun visualization during recording.",
6169
)
70+
parser.add_argument(
71+
"-la",
72+
"--leader-arm",
73+
action="store_true",
74+
help="Use the leader arm for teleoperation (default: False).",
75+
)
76+
parser.add_argument(
77+
"--leader-arm-port",
78+
type=str,
79+
default="/dev/ttyACM0",
80+
help="Serial port for the leader arm (default: /dev/ttyACM0).",
81+
)
6282

6383
args = parser.parse_args()
6484
if args.repo_id is None:
@@ -73,12 +93,17 @@ def main() -> None:
7393
)
7494

7595
# Create the robot and teleoperator configurations
76-
robot_config = LeKiwiClientConfig(remote_ip="127.0.0.1", id="lekiwi")
96+
robot_config = LeKiwiClientConfig(remote_ip=args.ip, id="lekiwi")
7797
keyboard_config = KeyboardTeleopConfig()
98+
if args.leader_arm:
99+
teleop_arm_config = SO101LeaderConfig(port=args.leader_arm_port, id="lekiwi_leader_arm")
78100

79101
robot = LeKiwiClient(robot_config)
80102
keyboard = KeyboardTeleop(keyboard_config)
81-
arm_keyboard_handler = ArmTeleop()
103+
if args.leader_arm:
104+
leader_arm = SO101Leader(teleop_arm_config)
105+
else:
106+
arm_keyboard_handler = ArmTeleop()
82107
# Configure the dataset features
83108
action_features = hw_to_dataset_features(robot.action_features, ACTION)
84109
obs_features = hw_to_dataset_features(robot.observation_features, OBS_STR)
@@ -102,6 +127,8 @@ def main() -> None:
102127
# - Sim robot: this script running on LeKiwi sim: `uv run lekiwi_sim --robot.id=my_awesome_kiwi`
103128
robot.connect()
104129
keyboard.connect()
130+
if args.leader_arm:
131+
leader_arm.connect()
105132

106133
if args.visualize:
107134
logging.info("Initializing Rerun for visualization.")
@@ -113,10 +140,13 @@ def main() -> None:
113140

114141
if not robot.is_connected or not keyboard.is_connected:
115142
raise ValueError("Robot or keyboard is not connected!")
143+
if args.leader_arm and not leader_arm.is_connected:
144+
raise ValueError("Leader arm is not connected!")
116145
logging.info("Robot and keyboard are connected.")
117146
recorded_episodes = 0
118147
while recorded_episodes < args.episodes and not events["stop_recording"]:
119-
arm_keyboard_handler = ArmTeleop()
148+
if not args.leader_arm:
149+
arm_keyboard_handler = ArmTeleop()
120150
logging.info(f"Recording episode {recorded_episodes}")
121151
# Run the record loop
122152
record_loop(
@@ -125,7 +155,7 @@ def main() -> None:
125155
fps=FPS,
126156
dataset=dataset,
127157
keyboard_handler=keyboard,
128-
arm_keyboard_handler=arm_keyboard_handler,
158+
arm_keyboard_handler=leader_arm if args.leader_arm else arm_keyboard_handler,
129159
control_time_s=EPISODE_TIME_SEC,
130160
single_task=args.task,
131161
display_data=args.visualize,
@@ -140,7 +170,7 @@ def main() -> None:
140170
fps=FPS,
141171
dataset=None, # Don't record during reset phase
142172
keyboard_handler=keyboard,
143-
arm_keyboard_handler=arm_keyboard_handler,
173+
arm_keyboard_handler=leader_arm if args.leader_arm else arm_keyboard_handler,
144174
control_time_s=RESET_TIME_SEC,
145175
single_task=args.task,
146176
display_data=args.visualize,
@@ -163,6 +193,8 @@ def main() -> None:
163193

164194
robot.disconnect()
165195
keyboard.disconnect()
196+
if args.leader_arm:
197+
leader_arm.disconnect()
166198
listener.stop()
167199

168200

packages/lekiwi_lerobot/lekiwi_lerobot/utils.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
import time
3-
from typing import Any
3+
from typing import Any, Union
44

55
from lekiwi_teleoperate.teleoperate.arm import ArmTeleop
66
from lerobot.datasets.image_writer import safe_stop_image_writer
@@ -15,6 +15,7 @@
1515
from lerobot.teleoperators.keyboard import (
1616
KeyboardTeleop,
1717
)
18+
from lerobot.teleoperators.so101_leader import SO101Leader
1819
from lerobot.utils.control_utils import (
1920
predict_action,
2021
)
@@ -32,7 +33,7 @@ def record_loop(
3233
fps: int,
3334
dataset: LeRobotDataset | None = None,
3435
keyboard_handler: KeyboardTeleop | None = None,
35-
arm_keyboard_handler: ArmTeleop | None = None,
36+
arm_keyboard_handler: Union[ArmTeleop, SO101Leader, None] = None,
3637
policy: PreTrainedPolicy | None = None,
3738
preprocessor: PolicyProcessorPipeline[dict[str, Any], dict[str, Any]] | None = None,
3839
postprocessor: PolicyProcessorPipeline[PolicyAction, PolicyAction] | None = None,
@@ -101,7 +102,13 @@ def record_loop(
101102
elif policy is None and keyboard_handler is not None and arm_keyboard_handler is not None:
102103
pressed_keys = keyboard_handler.get_action()
103104
base_action = robot._from_keyboard_to_base_action(pressed_keys)
104-
arm_action = arm_keyboard_handler.from_keyboard_to_arm_action(pressed_keys)
105+
106+
# Handle both ArmTeleop (keyboard-based) and SO101Leader (physical arm)
107+
if isinstance(arm_keyboard_handler, SO101Leader):
108+
arm_action = arm_keyboard_handler.get_action()
109+
arm_action = {f"arm_{k}": v for k, v in arm_action.items()}
110+
else:
111+
arm_action = arm_keyboard_handler.from_keyboard_to_arm_action(pressed_keys)
105112

106113
action = {**base_action, **arm_action} # Merge base and arm actions
107114
# TODO(francocipollone): We would probably want to use the teleop_action_processor here.

packages/lekiwi_sim/lekiwi_sim/assets/so_arm100/so_arm100.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
<joint frictionloss="0.1" armature="0.1"/>
3434
<position kp="50" dampratio="1" forcerange="-3.5 3.5"/>
3535
<default class="Rotation">
36-
<joint axis="0 1 0" range="-1.92 1.92"/>
36+
<joint axis="0 -1 0" range="-1.92 1.92"/>
3737
</default>
3838
<default class="Pitch">
3939
<joint axis="1 0 0" range="-1.747 1.747"/>

packages/lekiwi_teleoperate/lekiwi_teleoperate/main.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from lerobot.robots.lekiwi import LeKiwiClient, LeKiwiClientConfig
66
from lerobot.teleoperators.keyboard.teleop_keyboard import KeyboardTeleop, KeyboardTeleopConfig
7+
from lerobot.teleoperators.so101_leader import SO101Leader, SO101LeaderConfig
78
from lerobot.utils.robot_utils import busy_wait
89
from lerobot.utils.visualization_utils import init_rerun, log_rerun_data
910

@@ -59,6 +60,18 @@ def main() -> None:
5960
default="127.0.0.1",
6061
help="IP address of the robot (default: 127.0.0.1).",
6162
)
63+
parser.add_argument(
64+
"-la",
65+
"--leader-arm",
66+
action="store_true",
67+
help="Use the leader arm for teleoperation (default: False).",
68+
)
69+
parser.add_argument(
70+
"--leader-arm-port",
71+
type=str,
72+
default="/dev/ttyACM0",
73+
help="Serial port for the leader arm (default: /dev/ttyACM0).",
74+
)
6275
args = parser.parse_args()
6376
log_level = args.level.upper()
6477
logging.basicConfig(
@@ -69,24 +82,33 @@ def main() -> None:
6982
# Create the robot and teleoperator configurations
7083
robot_config = LeKiwiClientConfig(remote_ip=args.ip, id="my_lekiwi")
7184
keyboard_config = KeyboardTeleopConfig(id="my_laptop_keyboard")
85+
if args.leader_arm:
86+
teleop_arm_config = SO101LeaderConfig(port=args.leader_arm_port, id="lekiwi_leader_arm")
7287

7388
robot = LeKiwiClient(robot_config)
7489
keyboard = KeyboardTeleop(keyboard_config)
7590

7691
# To connect you already should have this script running on LeKiwi: `uv run lekiwi_sim`
7792
robot.connect()
7893
keyboard.connect()
94+
if args.leader_arm:
95+
leader_arm = SO101Leader(teleop_arm_config)
96+
leader_arm.connect()
97+
else:
98+
arm_teleop = ArmTeleop()
7999

80100
init_rerun(session_name="lekiwi_teleop")
81101

82-
if not robot.is_connected or not keyboard.is_connected:
83-
raise ValueError("Robot, leader arm of keyboard is not connected!")
102+
if not robot.is_connected:
103+
raise ValueError("Robot is not connected!")
104+
if not keyboard.is_connected:
105+
raise ValueError("Keyboard is not connected!")
106+
if args.leader_arm and not leader_arm.is_connected:
107+
raise ValueError("Leader arm is not connected!")
84108

85109
logging.info("Robot and keyboard are connected.")
86110
print(COMMANDS_STR.format(**robot_config.teleop_keys, **ArmTeleop.ARM_TELEOP_KEYS))
87111

88-
arm_teleop = ArmTeleop()
89-
90112
try:
91113
while True:
92114
t0 = time.perf_counter()
@@ -95,7 +117,11 @@ def main() -> None:
95117

96118
keyboard_keys = keyboard.get_action()
97119
base_action = robot._from_keyboard_to_base_action(keyboard_keys)
98-
arm_action = arm_teleop.from_keyboard_to_arm_action(keyboard_keys)
120+
if args.leader_arm:
121+
arm_action = leader_arm.get_action()
122+
arm_action = {f"arm_{k}": v for k, v in arm_action.items()}
123+
else:
124+
arm_action = arm_teleop.from_keyboard_to_arm_action(keyboard_keys)
99125

100126
log_rerun_data(observation, {**arm_action, **base_action})
101127

packages/lekiwi_teleoperate/lekiwi_teleoperate/teleoperate/arm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ class ArmTeleop:
1111
"""
1212

1313
ARM_TELEOP_KEYS = {
14-
"shoulder_pan_left": "t",
15-
"shoulder_pan_right": "g",
14+
"shoulder_pan_left": "g",
15+
"shoulder_pan_right": "t",
1616
"shoulder_lift_up": "y",
1717
"shoulder_lift_down": "h",
1818
"elbow_flex_up": "u",

0 commit comments

Comments
 (0)