Skip to content

Commit 7719882

Browse files
refactor(robots): update lekiwi for the latest motor bus api
chore(teleop): Add missing abstract methods to keyboard implementation refactor(robots): update lekiwi client and host code for the new api chore(config): update host lekiwi ip in client config chore(examples): move application scripts to the examples directory fix(motors): missing type check condition in set_half_turn_homings fix(robots): fix assumption in calibrate() for robots with more than just an arm fix(robot): change Mode to Operating_Mode in configure write for lekiwi fix(robots): make sure message is display in calibrate() method lekiwi fix(robots): no need for .tolist() in lekiwi host app fix(teleop): fix is_connected in teleoperator keyboard fix(teleop): always display calibration message in so100 fix(robots): fix send_action in lekiwi_client debug(examples): configuration for lekiwi client app fix(robots): fix send_action in lekiwi client part 2 refactor(robots): use dicts in lekiwi for get_obs and send_action dbg(robots): check sent action wheels lekiwi debug(robots): fix overflow base commands debug(robots): fix how we deal with negative values lekiwi debug(robots): lekiwi sign degrees fix fix(robots): right motors id in lekiwi host chore(doc): update todos chore(doc): added todos
1 parent da05753 commit 7719882

File tree

12 files changed

+187
-141
lines changed

12 files changed

+187
-141
lines changed

lerobot/common/robots/lekiwi/lekiwi_client_app.py renamed to examples/robots/lekiwi_client_app.py

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,11 @@
1414

1515
import logging
1616

17-
import numpy as np
18-
19-
from lerobot.common.datasets.lerobot_dataset import LeRobotDataset
20-
from lerobot.common.robots.config import RobotMode
17+
from lerobot.common.robots.lekiwi.config_lekiwi import LeKiwiClientConfig, RobotMode
18+
from lerobot.common.robots.lekiwi.lekiwi_client import LeKiwiClient
2119
from lerobot.common.teleoperators.keyboard import KeyboardTeleop, KeyboardTeleopConfig
2220
from lerobot.common.teleoperators.so100 import SO100Leader, SO100LeaderConfig
2321

24-
from .config_lekiwi import LeKiwiClientConfig
25-
from .lekiwi_client import LeKiwiClient
26-
2722
DUMMY_FEATURES = {
2823
"observation.state": {
2924
"dtype": "float64",
@@ -82,26 +77,24 @@
8277

8378
def main():
8479
logging.info("Configuring Teleop Devices")
85-
leader_arm_config = SO100LeaderConfig(port="/dev/tty.usbmodem58760429271")
80+
leader_arm_config = SO100LeaderConfig(port="/dev/tty.usbmodem58760434171")
8681
leader_arm = SO100Leader(leader_arm_config)
8782

8883
keyboard_config = KeyboardTeleopConfig()
8984
keyboard = KeyboardTeleop(keyboard_config)
9085

9186
logging.info("Configuring LeKiwi Client")
92-
robot_config = LeKiwiClientConfig(
93-
id="daemonlekiwi", calibration_dir=".cache/calibration/lekiwi", robot_mode=RobotMode.TELEOP
94-
)
87+
robot_config = LeKiwiClientConfig(id="lekiwi", robot_mode=RobotMode.TELEOP)
9588
robot = LeKiwiClient(robot_config)
9689

9790
logging.info("Creating LeRobot Dataset")
9891

99-
# TODO(Steven): Check this creation
100-
dataset = LeRobotDataset.create(
101-
repo_id="user/lekiwi",
102-
fps=10,
103-
features=DUMMY_FEATURES,
104-
)
92+
# # TODO(Steven): Check this creation
93+
# dataset = LeRobotDataset.create(
94+
# repo_id="user/lekiwi2",
95+
# fps=10,
96+
# features=DUMMY_FEATURES,
97+
# )
10598

10699
logging.info("Connecting Teleop Devices")
107100
leader_arm.connect()
@@ -110,30 +103,32 @@ def main():
110103
logging.info("Connecting remote LeKiwi")
111104
robot.connect()
112105

106+
if not robot.is_connected or not leader_arm.is_connected or not keyboard.is_connected:
107+
logging.error("Failed to connect to all devices")
108+
return
109+
113110
logging.info("Starting LeKiwi teleoperation")
114111
i = 0
115112
while i < 1000:
116113
arm_action = leader_arm.get_action()
117114
base_action = keyboard.get_action()
118-
action = np.append(arm_action, base_action) if base_action.size > 0 else arm_action
115+
action = {**arm_action, **base_action} if len(base_action) > 0 else arm_action
119116

120117
# TODO(Steven): Deal with policy action space
121118
# robot.set_mode(RobotMode.AUTO)
122119
# policy_action = policy.get_action() # This might be in body frame, key space or smt else
123120
# robot.send_action(policy_action)
124-
125121
action_sent = robot.send_action(action)
126122
observation = robot.get_observation()
127123

128-
frame = {"action": action_sent}
129-
frame.update(observation)
124+
frame = {**action_sent, **observation}
130125
frame.update({"task": "Dummy Task Dataset"})
131126

132127
logging.info("Saved a frame into the dataset")
133-
dataset.add_frame(frame)
128+
# dataset.add_frame(frame)
134129
i += 1
135130

136-
dataset.save_episode()
131+
# dataset.save_episode()
137132
# dataset.push_to_hub()
138133

139134
logging.info("Disconnecting Teleop Devices and LeKiwi Client")

lerobot/common/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,5 @@
4848
HF_LEROBOT_HOME = Path(os.getenv("HF_LEROBOT_HOME", default_cache_path)).expanduser()
4949

5050
# calibration dir
51-
default_calibration_path = HF_LEROBOT_HOME / ".calibration"
51+
default_calibration_path = HF_LEROBOT_HOME / "calibration"
5252
HF_LEROBOT_CALIBRATION = Path(os.getenv("HF_LEROBOT_CALIBRATION", default_calibration_path)).expanduser()

lerobot/common/motors/motors_bus.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ def set_half_turn_homings(self, motors: NameOrID | list[NameOrID] | None = None)
563563
motors = self.names
564564
elif isinstance(motors, (str, int)):
565565
motors = [motors]
566-
else:
566+
elif not isinstance(motors, list):
567567
raise TypeError(motors)
568568

569569
self.reset_calibration(motors)
@@ -626,6 +626,7 @@ def _normalize(self, data_name: str, ids_values: dict[int, int]) -> dict[int, fl
626626
min_ = self.calibration[name].range_min
627627
max_ = self.calibration[name].range_max
628628
bounded_val = min(max_, max(min_, val))
629+
# TODO(Steven): normalization can go boom if max_ == min_, we should add a check probably in record_ranges_of_motions (which probably indicates the user forgot to move a motor)
629630
if self.motors[name].norm_mode is MotorNormMode.RANGE_M100_100:
630631
normalized_values[id_] = (((bounded_val - min_) / (max_ - min_)) * 200) - 100
631632
elif self.motors[name].norm_mode is MotorNormMode.RANGE_0_100:

lerobot/common/robots/config.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
11
import abc
2-
import enum
32
from dataclasses import dataclass
43
from pathlib import Path
54

65
import draccus
76

87

9-
class RobotMode(enum.Enum):
10-
TELEOP = 0
11-
AUTO = 1
12-
13-
148
@dataclass(kw_only=True)
159
class RobotConfig(draccus.ChoiceRegistry, abc.ABC):
1610
# Allows to distinguish between different robots of the same type
1711
id: str | None = None
1812
# Directory to store calibration file
1913
calibration_dir: Path | None = None
20-
robot_mode: RobotMode | None = None
2114

2215
@property
2316
def type(self) -> str:

lerobot/common/robots/lekiwi/config_lekiwi.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import enum
1516
from dataclasses import dataclass, field
1617

1718
from lerobot.common.cameras.configs import CameraConfig
@@ -20,6 +21,12 @@
2021
from ..config import RobotConfig
2122

2223

24+
class RobotMode(enum.Enum):
25+
TELEOP = 0
26+
AUTO = 1
27+
28+
29+
# TODO(Steven): Consider sending config at initial step over a socket
2330
@RobotConfig.register_subclass("lekiwi")
2431
@dataclass
2532
class LeKiwiConfig(RobotConfig):
@@ -48,7 +55,7 @@ class LeKiwiConfig(RobotConfig):
4855
@dataclass
4956
class LeKiwiClientConfig(RobotConfig):
5057
# Network Configuration
51-
remote_ip: str = "172.18.133.90"
58+
remote_ip: str = "172.18.129.208"
5259
port_zmq_cmd: int = 5555
5360
port_zmq_observations: int = 5556
5461

@@ -68,3 +75,5 @@ class LeKiwiClientConfig(RobotConfig):
6875
"quit": "q",
6976
}
7077
)
78+
79+
robot_mode: RobotMode | None = None

lerobot/common/robots/lekiwi/lekiwi.py

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,10 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17-
import base64
1817
import logging
1918
import time
2019
from typing import Any
2120

22-
import cv2
23-
2421
from lerobot.common.cameras.utils import make_cameras_from_configs
2522
from lerobot.common.constants import OBS_IMAGES, OBS_STATE
2623
from lerobot.common.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
@@ -64,8 +61,8 @@ def __init__(self, config: LeKiwiConfig):
6461
"arm_gripper": Motor(6, "sts3215", MotorNormMode.RANGE_0_100),
6562
# base
6663
"base_left_wheel": Motor(7, "sts3215", MotorNormMode.RANGE_M100_100),
67-
"base_back_wheel": Motor(8, "sts3215", MotorNormMode.RANGE_M100_100),
68-
"base_right_wheel": Motor(9, "sts3215", MotorNormMode.RANGE_M100_100),
64+
"base_right_wheel": Motor(8, "sts3215", MotorNormMode.RANGE_M100_100),
65+
"base_back_wheel": Motor(9, "sts3215", MotorNormMode.RANGE_M100_100),
6966
},
7067
calibration=self.calibration,
7168
)
@@ -119,23 +116,33 @@ def connect(self) -> None:
119116
def is_calibrated(self) -> bool:
120117
return self.bus.is_calibrated
121118

119+
# TODO(Steven): I think we should extend this to give the user the option of re-calibrate
120+
# calibrate(recalibrate: bool = False) -> None:
121+
# If true, then we overwrite the previous calibration file with new values
122122
def calibrate(self) -> None:
123123
logger.info(f"\nRunning calibration of {self}")
124+
125+
motors = self.arm_motors + self.base_motors
126+
124127
self.bus.disable_torque(self.arm_motors)
125128
for name in self.arm_motors:
126129
self.bus.write("Operating_Mode", name, OperatingMode.POSITION.value)
127130

128131
input("Move robot to the middle of its range of motion and press ENTER....")
129-
homing_offsets = self.bus.set_half_turn_homings(self.arm_motors)
132+
homing_offsets = self.bus.set_half_turn_homings(motors)
130133

131-
full_turn_motor = "arm_wrist_roll"
132-
unknown_range_motors = [name for name in self.arm_motors if name != full_turn_motor]
133-
logger.info(
134+
# TODO(Steven): Might be worth to do this also in other robots but it should be added in the docs
135+
full_turn_motor = [
136+
motor for motor in motors if any(keyword in motor for keyword in ["wheel", "gripper"])
137+
]
138+
unknown_range_motors = [motor for motor in motors if motor not in full_turn_motor]
139+
140+
print(
134141
f"Move all arm joints except '{full_turn_motor}' sequentially through their "
135142
"entire ranges of motion.\nRecording positions. Press ENTER to stop..."
136143
)
137144
range_mins, range_maxes = self.bus.record_ranges_of_motion(unknown_range_motors)
138-
for name in [*self.base_motors, full_turn_motor]:
145+
for name in full_turn_motor:
139146
range_mins[name] = 0
140147
range_maxes[name] = 4095
141148

@@ -159,7 +166,7 @@ def configure(self):
159166
# and torque can be safely disabled to run calibration.
160167
self.bus.disable_torque(self.arm_motors)
161168
for name in self.arm_motors:
162-
self.bus.write("Mode", name, OperatingMode.POSITION.value)
169+
self.bus.write("Operating_Mode", name, OperatingMode.POSITION.value)
163170
# Set P_Coefficient to lower value to avoid shakiness (Default is 32)
164171
self.bus.write("P_Coefficient", name, 16)
165172
# Set I_Coefficient and D_Coefficient to default value 0 and 32
@@ -171,15 +178,15 @@ def configure(self):
171178
self.bus.write("Acceleration", name, 254)
172179

173180
for name in self.base_motors:
174-
self.bus.write("Mode", name, OperatingMode.VELOCITY.value)
181+
self.bus.write("Operating_Mode", name, OperatingMode.VELOCITY.value)
175182

176-
self.bus.enable_torque()
183+
self.bus.enable_torque() # TODO(Steven): Operation has failed with: ConnectionError: Failed to write 'Lock' on id_=6 with '1' after 1 tries. [TxRxResult] Incorrect status packet!
177184

178185
def get_observation(self) -> dict[str, Any]:
179186
if not self.is_connected:
180187
raise DeviceNotConnectedError(f"{self} is not connected.")
181188

182-
obs_dict = {}
189+
obs_dict = {OBS_IMAGES: {}}
183190

184191
# Read actuators position for arm and vel for base
185192
start = time.perf_counter()
@@ -192,12 +199,7 @@ def get_observation(self) -> dict[str, Any]:
192199
# Capture images from cameras
193200
for cam_key, cam in self.cameras.items():
194201
start = time.perf_counter()
195-
frame = cam.async_read()
196-
ret, buffer = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 90])
197-
if ret:
198-
obs_dict[f"{OBS_IMAGES}.{cam_key}"] = base64.b64encode(buffer).decode("utf-8")
199-
else:
200-
obs_dict[f"{OBS_IMAGES}.{cam_key}"] = ""
202+
obs_dict[OBS_IMAGES][cam_key] = cam.async_read()
201203
dt_ms = (time.perf_counter() - start) * 1e3
202204
logger.debug(f"{self} read {cam_key}: {dt_ms:.1f}ms")
203205

@@ -229,15 +231,19 @@ def send_action(self, action: dict[str, Any]) -> dict[str, Any]:
229231
present_pos = self.bus.sync_read("Present_Position", self.arm_motors)
230232
goal_present_pos = {key: (g_pos, present_pos[key]) for key, g_pos in arm_goal_pos.items()}
231233
arm_safe_goal_pos = ensure_safe_goal_position(goal_present_pos, self.config.max_relative_target)
234+
arm_goal_pos = arm_safe_goal_pos
235+
236+
# TODO(Steven): Message fetching failed: Magnitude 34072 exceeds 32767 (max for sign_bit_index=15)
237+
# TODO(Steven): IMO, this should be a check in the motor bus
232238

233239
# Send goal position to the actuators
234-
self.bus.sync_write("Goal_Position", arm_safe_goal_pos)
240+
self.bus.sync_write("Goal_Position", arm_goal_pos)
235241
self.bus.sync_write("Goal_Speed", base_goal_vel)
236242

237-
return {**arm_safe_goal_pos, **base_goal_vel}
243+
return {**arm_goal_pos, **base_goal_vel}
238244

239245
def stop_base(self):
240-
self.bus.sync_write("Goal_Speed", {name: 0 for name in self.base_motors}, num_retry=5)
246+
self.bus.sync_write("Goal_Speed", dict.fromkeys(self.base_motors, 0), num_retry=5)
241247
logger.info("Base motors stopped")
242248

243249
def disconnect(self):

0 commit comments

Comments
 (0)