찹쌀이네 공부 공간

[VLA] SO-ARM101 캘리브레이션 본문

Vision Language Action, VLA

[VLA] SO-ARM101 캘리브레이션

호떡공돌이 2025. 12. 2. 23:24

로봇팔을 조립했으니, 이제 Calibration을 해보자.

 

SO-ARM 101은 Leader arm의 Joint 값들을 그대로 Follower arm으로 옮겨 버리는 Tele-operation을 지원한다.

Inverse/Foward Kinematics 로 풀어내는 건 아닌 듯 하다.

 

추후에 SO-ARM101의 DH-parameter 구하는 방법도 공부해가며 정리하고자 한다.

 

우선, Calibration을 진행해보자. 

 

아래 코드를 통해 연결된 USB 번호를 확인해보자.

(lerobot) seunglee@SEUNGui-MacBookAir ~ % ls /dev/tty.usbmodem*
/dev/tty.usbmodem5AAF2621131 # 연결된 USB 번호

 

이제 아래 명령어를 실행해보면!

lerobot-calibrate
    --robot.type=so101_follower \
    --robot.port=/dev/ttyACM0 \
    --robot.id=my_awesome_follower_arm
    
# 한번에 쓰기
lerobot-calibrate --robot.type=so101_follower --robot.port=/dev/ttyACM0 --robot.id=my_awesome_follower_arm

 

이렇게, Follower arm을 Middle position으로 이동한 뒤 Enter를 누르라고 나온다. 친절하구만ㅋ

Middle position은 영상을 참고한다. ('ㄱ'자 모양으로 꺾기)

 

그 다음엔 각 관절들을 움직이면서 터미널의 아래 값들이 관절에 해당하는 수치가 변하는지 확인한다.

수치 변화에 문제가 없으면 Enter를 누르고 아래 내용을 기억해둔다.

Calibration saved to /Users/seunglee/.cache/huggingface/lerobot/calibration/robots/so101_follower/my_awesome_follower_arm.json
INFO 2025-12-01 22:04:21 follower.py:223 my_awesome_follower_arm SO101Follower disconnected.

 

 

Leader arm도 마찬가지 작업을 수행하면 되는데..

(lerobot) seunglee@SEUNGui-MacBookAir ~ % ls /dev/tty.usbmodem*                                                                                                     
/dev/tty.usbmodem5AAF2620411
lerobot-calibrate \
    --teleop.type=so101_leader \
    --teleop.port=/dev/ttyACM1 \ 
    --teleop.id=my_awesome_leader_arm

# 한번에 쓰기 용
lerobot-calibrate --teleop.type=so101_leader --teleop.port=/dev/ttyACM1 --teleop.id=my_awesome_leader_arm


아래 에러가 발생했다.. 하.. 짜증..

Leader Calibration 과정에서 발생한 에러

에러에 따르면 모터의 엔코더 값이 Max값을 넘어섰다는 것이다.. 내가 중고제품을 구매한 것도 아니고 고작 Link arm을 반바퀴만 회전시켰는데 Max 값을 넘어섰다는게 이해가 안갔다.

 

구글링에서도 나오지 않기에, 직접 모터 엔코더 값을 출력해보기로 했다.

# Leader arm에서 모든 모터의 RAW 값 읽기
from lerobot.teleoperators.so101_leader.so101_leader import SO101Leader, SO101LeaderConfig

config = SO101LeaderConfig(
    port="/dev/tty.usbmodem5AAF2620411",
    id="my_leader",
    use_degrees=False
)

leader = SO101Leader(config)

leader.bus.connect()

# normalize=False → calibration 무시하고 raw encoder 값 그대로 출력
raw_dict = leader.bus.sync_read("Present_Position", motors=None, normalize=False)

leader.bus.disconnect()

print("\n=== RAW ENCODER POSITIONS (Present_Position) ===")
for name, val in raw_dict.items():
    print(f"{name}: {val}")

 

모터 출력 방법을 찾는데에 꽤 애를 먹었다. 구글링으로 안나오고 src/lerobot/motors/motors_bus.py 경로에서 힌트를 얻었고, chatgpt의 마무리가 더해져 출력해볼 수 있었다.

 

Leader arm 모터의 출력 확인 결과, elbow_flex가 33031로 허용 값을 초과해서 출력되고 있어서 발생하는 에러였다.

=== RAW ENCODER POSITIONS (Present_Position) ===
shoulder_pan: 2047
shoulder_lift: 2073
elbow_flex: 33031
wrist_flex: 3471
wrist_roll: 3750
gripper: 309

 

중고제품을 구매한 것도 아닌데 왜 이런 에러가 발생했는지 이해가 안간다... 내가 모터id 입력하는 과정에서 5v, 12v 전원을 반대로 꽂아서 발생했던 것일까... 난 Pybrain이라는 업체가 제공한 팜플렛을 따라했을 뿐인데 환불해야하나, 고장 신고를 해야하나 따져야하나... 어이가 없었다..

 

일단, Leader arm의 elbow_flex 엔코더 값은 읽혀지니까 강제로 Follower arm의 엔코더 값을 Leader arm RAM에 덮어쓰기를 시도했다. 코드는 아래와 같다.

# Leader arm에서 elbow_flex 모터에 RAW 값 쓰기
from lerobot.teleoperators.so101_leader.so101_leader import SO101Leader, SO101LeaderConfig

FOLLOWER_ELBOW_RAW = 3138

config = SO101LeaderConfig(
    port="/dev/tty.usbmodem5AAF2620411",
    id="my_awesome_leader_arm",
    use_degrees=False
)

leader = SO101Leader(config)
leader.bus.connect()

# 1) 모든 조인트 torque off (전체 off)
print("Disable ALL torque…")
leader.bus.disable_torque(None)

# 2) write follower raw value
leader.bus.sync_write(
    "Present_Position",
    {"elbow_flex": FOLLOWER_ELBOW_RAW},
    normalize=False
)

# 3) read back
raw = leader.bus.sync_read(
    "Present_Position",
    motors=["elbow_flex"],
    normalize=False
)

leader.bus.disconnect()
print("Leader elbow_flex RAW =", raw["elbow_flex"])

 

위 코드의 결과 값은 애석하게도 Leader elbow_flex RAW = 3138이 아닌, 761로 나왔다... 덮어쓰기가 안된건지 된거지 확인 차 실시간 모터 값을 출력해보기로 했다. Thanks to Chatgpt.

# Leader arm에서 elbow_flex 모터의 엔코더 값 실시간 읽기
from lerobot.teleoperators.so101_leader.so101_leader import SO101Leader, SO101LeaderConfig
import time

# Leader 설정
config = SO101LeaderConfig(
    port="/dev/tty.usbmodem5AAF2620411",   # <-- 너의 Leader port
    id="my_awesome_leader_arm",
    use_degrees=False
)

leader = SO101Leader(config)
leader.bus.connect()

print("\n===== TEST A: elbow_flex 엔코더 실시간 확인 =====")
print("1) elbow_flex torque OFF 중인지 확인")
leader.bus.disable_torque(["elbow_flex"])

print("2) elbow_flex 관절을 손으로 천천히 움직이세요.")
print("3) 아래의 Present_Position 값이 움직이는 동안 변하는지 확인하세요.\n")

for i in range(100):
    raw = leader.bus.sync_read(
        "Present_Position",
        motors=["elbow_flex"],
        normalize=False
    )
    print(f"Reading {i}: RAW elbow_flex = {raw['elbow_flex']}")
    time.sleep(0.2)

leader.bus.disconnect()
print("\n===== TEST END =====")

 

코드의 결과값은 아래와 같다. 특정 각도 이상으로 elbow_flex를 움직이면 큰 값이 나온다. 하..

Reading 0: RAW elbow_flex = 762
Reading 1: RAW elbow_flex = 762
Reading 2: RAW elbow_flex = 762
Reading 3: RAW elbow_flex = 762
Reading 4: RAW elbow_flex = 761
Reading 5: RAW elbow_flex = 704
Reading 6: RAW elbow_flex = 637
Reading 7: RAW elbow_flex = 574
Reading 8: RAW elbow_flex = 522
Reading 9: RAW elbow_flex = 462
Reading 10: RAW elbow_flex = 396
Reading 11: RAW elbow_flex = 333
Reading 12: RAW elbow_flex = 278
Reading 13: RAW elbow_flex = 238
Reading 14: RAW elbow_flex = 198
Reading 15: RAW elbow_flex = 150
Reading 16: RAW elbow_flex = 102
Reading 17: RAW elbow_flex = 53
Reading 18: RAW elbow_flex = 11
Reading 19: RAW elbow_flex = 32794
Reading 20: RAW elbow_flex = 32862
Reading 21: RAW elbow_flex = 32914
Reading 22: RAW elbow_flex = 32938
Reading 23: RAW elbow_flex = 32933
Reading 24: RAW elbow_flex = 32929
Reading 25: RAW elbow_flex = 32876
Reading 26: RAW elbow_flex = 32855
Reading 27: RAW elbow_flex = 32838
Reading 28: RAW elbow_flex = 32794

 

 

정신을 차리고 copilot의 도움을 받아 예상 원인 코드를 알려달라고 했더니, feetech.py 파일 내 함수를 의심된다더라.

코드를 보니 소름이 돋았다. 와.. copilot... 성능 장난 아니구나.

 

모터가 1회전을 넘어서더라도 항상 0~4095 범위로 제한하는 코드가 필요했던 것이다.

def _get_half_turn_homings(self, positions: dict[NameOrID, Value]) -> dict[NameOrID, Value]:
    """
    On Feetech Motors:
    Present_Position = Actual_Position - Homing_Offset
    """
    half_turn_homings = {}
    for motor, pos in positions.items():
        model = self._get_motor_model(motor)
        max_res = self.model_resolution_table[model] - 1
        half_turn_homings[motor] = pos - int(max_res / 2)

    return half_turn_homings

 

따라서 위 코드를 아래와 같이 수정했다.

    def _get_half_turn_homings(self, positions):
        half_turn_homings = {}
        for motor, pos in positions.items():
            model = self._get_motor_model(motor)
            resolution = self.model_resolution_table[model]
            max_res = resolution - 1

            # 항상 1바퀴 범위로 정규화해야 함!
            normalized_pos = pos % resolution

            offset = normalized_pos - int(max_res / 2)
            half_turn_homings[motor] = offset

        return half_turn_homings

 

 

이제 Tele-operation을 수행해보자. 드디어.

lerobot-teleoperate \
    --robot.type=so101_follower \
    --robot.port=/dev/tty.usbmodem5AAF2621131 \
    --robot.id=my_awesome_follower_arm \
    --teleop.type=so101_leader \
    --teleop.port=/dev/tty.usbmodem5AAF2620411 \
    --teleop.id=my_awesome_leader_arm
    
    
# 한줄로 쓰기 (Mac)   
lerobot-teleoperate --robot.type=so101_follower --robot.port=/dev/tty.usbmodem5AAF2621131 --robot.id=my_awesome_follower_arm --teleop.type=so101_leader --teleop.port=/dev/tty.usbmodem5AAF2620411 --teleop.id=my_awesome_leader_arm


# 한줄로 쓰기 (Ubuntu)
lerobot-teleoperate   --robot.type=so101_follower   --robot.port=/dev/ttyACM1   --robot.id=my_awesome_follower_arm   --teleop.type=so101_leader   --teleop.port=/dev/ttyACM0   --teleop.id=my_awesome_leader_arm

 

 

 

잘 된다. 끝.