-
Notifications
You must be signed in to change notification settings - Fork 799
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add FeetechMotorsBus, SO-100, Moss-v1 #419
Conversation
f6b661d
to
9d23d04
Compare
…ace/lerobot into user/rcadene/2024_09_04_feetech
9c6ebce
to
aa22946
Compare
… in the camera driver classes.
… in the camera driver classes. Fixed test_cameras.py script so it passes.
def find_available_ports(): | ||
ports = [] | ||
for path in Path("/dev").glob("tty*"): | ||
ports.append(str(path)) | ||
return ports | ||
|
||
|
||
def find_port(): | ||
print("Finding all available ports for the DynamixelMotorsBus.") | ||
ports_before = find_available_ports() | ||
print(ports_before) | ||
|
||
print("Remove the usb cable from your DynamixelMotorsBus and press Enter when done.") | ||
input() | ||
|
||
time.sleep(0.5) | ||
ports_after = find_available_ports() | ||
ports_diff = list(set(ports_before) - set(ports_after)) | ||
|
||
if len(ports_diff) == 1: | ||
port = ports_diff[0] | ||
print(f"The port of this DynamixelMotorsBus is '{port}'") | ||
print("Reconnect the usb cable.") | ||
elif len(ports_diff) == 0: | ||
raise OSError(f"Could not detect the port. No difference was found ({ports_diff}).") | ||
else: | ||
raise OSError(f"Could not detect the port. More than one port was found ({ports_diff}).") | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to scripts/find_motors_bus_port.py
# Set expected baudrate for the bus | ||
self.set_bus_baudrate(BAUDRATE) | ||
|
||
if not self.are_motors_configured(): | ||
input( | ||
"\n/!\\ A configuration issue has been detected with your motors: \n" | ||
"If it's the first time that you use these motors, press enter to configure your motors... but before " | ||
"verify that all the cables are connected the proper way. If you find an issue, before making a modification, " | ||
"kill the python process, unplug the power cord to not damage the motors, rewire correctly, then plug the power " | ||
"again and relaunch the script.\n" | ||
) | ||
print() | ||
self.configure_motors() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to scripts/configure_motor.py
present_idx = self._read_with_motor_ids(self.motor_models, [idx], "ID")[0] | ||
present_idx = self.read_with_motor_ids(self.motor_models, [idx], "ID")[0] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made it public since it is used outside of the class by scripts/configure_motor.py
(same for feetech)
# to [-1, 1] with 1 indicates original rotation direction and -1 inverted. | ||
signed_drive_mode = -(drive_mode * 2 - 1) | ||
position *= signed_drive_mode | ||
return position | ||
|
||
|
||
def compute_nearest_rounded_position(position, models): | ||
delta_turn = convert_degrees_to_steps(ROTATED_POSITION_DEGREE, models) | ||
nearest_pos = np.round(position.astype(float) / delta_turn) * delta_turn | ||
return nearest_pos.astype(position.dtype) | ||
|
||
|
||
def run_arm_calibration(arm: MotorsBus, robot_type: str, arm_name: str, arm_type: str): | ||
"""This function ensures that a neural network trained on data collected on a given robot | ||
can work on another robot. For instance before calibration, setting a same goal position | ||
for each motor of two different robots will get two very different positions. But after calibration, | ||
the two robots will move to the same position.To this end, this function computes the homing offset | ||
and the drive mode for each motor of a given robot. | ||
|
||
Homing offset is used to shift the motor position to a ]-2048, +2048[ nominal range (when the motor uses 2048 steps | ||
to complete a half a turn). This range is set around an arbitrary "zero position" corresponding to all motor positions | ||
being 0. During the calibration process, you will need to manually move the robot to this "zero position". | ||
|
||
Drive mode is used to invert the rotation direction of the motor. This is useful when some motors have been assembled | ||
in the opposite orientation for some robots. During the calibration process, you will need to manually move the robot | ||
to the "rotated position". | ||
|
||
After calibration, the homing offsets and drive modes are stored in a cache. | ||
|
||
Example of usage: | ||
```python | ||
run_arm_calibration(arm, "koch", "left", "follower") | ||
``` | ||
""" | ||
if (arm.read("Torque_Enable") != TorqueMode.DISABLED.value).any(): | ||
raise ValueError("To run calibration, the torque must be disabled on all motors.") | ||
|
||
print(f"\nRunning calibration of {robot_type} {arm_name} {arm_type}...") | ||
|
||
print("\nMove arm to zero position") | ||
print("See: " + URL_TEMPLATE.format(robot=robot_type, arm=arm_type, position="zero")) | ||
input("Press Enter to continue...") | ||
|
||
# We arbitrarily chose our zero target position to be a straight horizontal position with gripper upwards and closed. | ||
# It is easy to identify and all motors are in a "quarter turn" position. Once calibration is done, this position will | ||
# correspond to every motor angle being 0. If you set all 0 as Goal Position, the arm will move in this position. | ||
zero_target_pos = convert_degrees_to_steps(ZERO_POSITION_DEGREE, arm.motor_models) | ||
|
||
# Compute homing offset so that `present_position + homing_offset ~= target_position`. | ||
zero_pos = arm.read("Present_Position") | ||
zero_nearest_pos = compute_nearest_rounded_position(zero_pos, arm.motor_models) | ||
homing_offset = zero_target_pos - zero_nearest_pos | ||
|
||
# The rotated target position corresponds to a rotation of a quarter turn from the zero position. | ||
# This allows to identify the rotation direction of each motor. | ||
# For instance, if the motor rotates 90 degree, and its value is -90 after applying the homing offset, then we know its rotation direction | ||
# is inverted. However, for the calibration being successful, we need everyone to follow the same target position. | ||
# Sometimes, there is only one possible rotation direction. For instance, if the gripper is closed, there is only one direction which | ||
# corresponds to opening the gripper. When the rotation direction is ambiguous, we arbitrarely rotate clockwise from the point of view | ||
# of the previous motor in the kinetic chain. | ||
print("\nMove arm to rotated target position") | ||
print("See: " + URL_TEMPLATE.format(robot=robot_type, arm=arm_type, position="rotated")) | ||
input("Press Enter to continue...") | ||
|
||
rotated_target_pos = convert_degrees_to_steps(ROTATED_POSITION_DEGREE, arm.motor_models) | ||
|
||
# Find drive mode by rotating each motor by a quarter of a turn. | ||
# Drive mode indicates if the motor rotation direction should be inverted (=1) or not (=0). | ||
rotated_pos = arm.read("Present_Position") | ||
drive_mode = (rotated_pos < zero_pos).astype(np.int32) | ||
|
||
# Re-compute homing offset to take into account drive mode | ||
rotated_drived_pos = apply_drive_mode(rotated_pos, drive_mode) | ||
rotated_nearest_pos = compute_nearest_rounded_position(rotated_drived_pos, arm.motor_models) | ||
homing_offset = rotated_target_pos - rotated_nearest_pos | ||
|
||
print("\nMove arm to rest position") | ||
print("See: " + URL_TEMPLATE.format(robot=robot_type, arm=arm_type, position="rest")) | ||
input("Press Enter to continue...") | ||
print() | ||
|
||
# Joints with rotational motions are expressed in degrees in nominal range of [-180, 180] | ||
calib_mode = [CalibrationMode.DEGREE.name] * len(arm.motor_names) | ||
|
||
# TODO(rcadene): make type of joints (DEGREE or LINEAR) configurable from yaml? | ||
if robot_type == "aloha" and "gripper" in arm.motor_names: | ||
# Joints with linear motions (like gripper of Aloha) are experessed in nominal range of [0, 100] | ||
calib_idx = arm.motor_names.index("gripper") | ||
calib_mode[calib_idx] = CalibrationMode.LINEAR.name | ||
|
||
calib_data = { | ||
"homing_offset": homing_offset.tolist(), | ||
"drive_mode": drive_mode.tolist(), | ||
"start_pos": zero_pos.tolist(), | ||
"end_pos": rotated_pos.tolist(), | ||
"calib_mode": calib_mode, | ||
"motor_names": arm.motor_names, | ||
} | ||
return calib_data | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to lerobot/common/robot_devices/robots/dynamixel_calibration.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
review with Simon
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed with Simon
Co-authored-by: jess-moss <jess.moss@huggingface.co> Co-authored-by: Simon Alibert <75076266+aliberts@users.noreply.github.com>
What this does
Feetech motor bus, SO-100, Moss-v1
lerobot/common/robot_devices/motors/feetech.py
lerobot/configs/robot/so100.yaml
andlerobot/configs/robot/moss.yaml
examples/10_use_so100.md
andexamples/11_use_moss.md
lerobot/scripts/find_motors_bus_port.py
scripts/configure_motor.py
Auto-calibration
lerobot/common/robot_devices/robots/dynamixel_calibration.py
lerobot/common/robot_devices/robots/feetech_calibration.py
with autocalibration for SO-100 and Moss-v1 follower armsMinor
read_with_motor_ids(...num_retry=NUM_READ_RETRY
andwrite_with_motor_ids(...num_retry=NUM_WRITE_RETRY
) to set them to a low value of retries duringconfigure_motor.py
and thus speedup configuration processSee tutorial: https://github.com/huggingface/lerobot/blob/1d92acf1e3d6123874605d8048e110d58a383868/examples/10_use_so100.md
How it was tested
https://huggingface.co/spaces/lerobot/visualize_dataset?dataset=cadene%2Fmoss_debug&episode=0
https://huggingface.co/spaces/lerobot/visualize_dataset?dataset=cadene%2Fso100_debug&episode=0
How to checkout & try? (for the reviewer)