Instructions to use lerobot-omx/omx-setup with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LeRobot
How to use lerobot-omx/omx-setup with LeRobot:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
- OMX ๋ชจํฐ ํธ๋ฌ๋ธ์ํ ๊ฐ์ด๋
OMX ๋ชจํฐ ํธ๋ฌ๋ธ์ํ ๊ฐ์ด๋
ROBOTIS OpenManipulator-X์ Dynamixel ๋ชจํฐ๊ฐ ๊ณผ์ด๋๊ฑฐ๋ ์บ๋ฆฌ๋ธ๋ ์ด์ ์ด ๋ง์ง ์์ ๋ ์ฐธ๊ณ ํ๋ ๋ฌธ์์ ๋๋ค.
OMX + LeRobot ๋ชจ๋ฐฉํ์ต ์ค์ต์ ์ํด ์ํ์ฐฉ์คํ๋ ๊ณผ์ ์ค์ ํจ๊ณผ๊ฐ ์์๋ ๋ฐฉ๋ฒ๋ง ์ ๋ฆฌํ์ต๋๋ค.
1~3์ฅ์ ์ ์ด ์ํํธ์จ์ด์ ๋ฌด๊ดํ๊ฒ Dynamixel Protocol 2.0์ ์ฐ๋ ๋ชจ๋ ํ๊ฒฝ์ ์ ์ฉ๋ฉ๋๋ค. 4์ฅ๊ณผ 5์ฅ ์ผ๋ถ๋ LeRobot ์ ์ฉ์ ๋๋ค.
๋ชฉ์ฐจ
์ฆ์์ผ๋ก ์ฐพ๊ธฐ
| ์ฆ์ | ๊ฐ๋ ๊ณณ |
|---|---|
| ๋ฌด์์ด ๋ฌธ์ ์ธ์ง ๋ชจ๋ฅด๊ฒ ๋ค | 1์ฅ |
| ๋นจ๊ฐ๋ถ์ด ์ผ์ง๊ณ Overload(์๋ฌ 32)๊ฐ ๋ฌ๋ค | 2์ฅ |
| ์๋ฌด๊ฒ๋ ์คํํ์ง ์๋๋ฐ ๋ชจํฐ๊ฐ ๋จ๊ฒ๋ค | 1-3์ |
| ํฐ ๋ชฉํ๋ ๋ฐ๋ผ๊ฐ๋๋ฐ ์๊ฒ ์์ง์ด๋ฉด ๋ฉ์ถ๋ค | 2-2์ |
| ํน์ ๊ตฌ๊ฐ์์๋ง ์ ์์ง์ธ๋ค | 2-4์ |
| ์ค์ ์ด ์คํํ ๋๋ง๋ค ๋๋์๊ฐ๋ค | 2-5์ , 3-3์ |
| ๋์ฌ๋ฅผ ์กฐ์๋๋ ๊ฐ๋ ๋ฒ์๊ฐ ๋ฌ๋ผ์ก๋ค | 3์ฅ |
| ๋ฆฌ๋์๊ณผ ํ๋ก์์ ์์ธ๊ฐ ์ ๋ง๋๋ค | 4์ฅ |
| ํ๋ก์ ๊ทธ๋ฆฝํผ๊ฐ ์ถฉ๋ถํ ์ ์ด๋ฆฐ๋ค | 4-4์ |
| ์ ์ด ๋ฃจํ๊ฐ ๋๋ฆฌ๋ค, ์นด๋ฉ๋ผ ์ค์ ์ด ์ ๋จน๋๋ค | 5์ฅ |
| ์ต์ ์ ์คฌ๋๋ฐ ๋ฌด์๋๋ค | 5์ฅ |
์ฅ ๊ตฌ์ฑ
- ์ค๋น (๋ชจํฐ ์ํ ์ฝ๊ธฐ, ์ด๋๋ก ๊ฐ์ง ํ์ , ์๋ฌ ์ง์ฐ๊ณ ํ ํฌ ๋๊ธฐ)
- ๊ทธ๋ฆฝํผ์ Overload(์๋ฌ 32)๊ฐ ๋ฐ๋ณต๋ ๋
- ๊ฐ๋ ๋ฒ์๋ฅผ ์ธก์ ํด ๋ฐ์ํ๊ธฐ
- ๋ฆฌ๋์๊ณผ ํ๋ก์์์ด ์ ๋ง์ ๋ (LeRobot)
- ๊ทธ ๋ฐ์ ์์ฃผ ๊ฑธ๋ฆฌ๋ ๊ฒ
- ์ฐธ๊ณ : ์ด ๋ก๋ด์์ ์ธก์ ๋ ๊ฐ
- ์ฐธ๊ณ ๋ฉ๋ชจ
์ด ์ ์ฅ์
ROBOTIS OMX + LeRobot ํ๊ฒฝ์์ ํ๋ก์ ๊ทธ๋ฆฝํผ(ID 16) ๊ณผ๋ถํ๋ฅผ ํด๊ฒฐํ ์ค์ ๊ณผ, ๊ทธ ๊ณผ์ ์์ ํจ๊ณผ๊ฐ ์์๋ ๋ฐฉ๋ฒ์ ๋ด์์ต๋๋ค.
ํต์ฌ ์ค์ ์ Current_Limit 600, Goal_Current 100(์ด๋ ์ 200), Position ๊ฒ์ธ P 800 / I 0 / D 0, ๊ฐ๋ ๋ฒ์ 2123 ~ 2873 ์
๋๋ค. ๋ช
๋ น์ค ์ต์
์ผ๋ก --teleop.gripper_open_pos=95 ๋ฅผ ์ผ์ต๋๋ค. ์ ์ฒด ๊ฐ์ 6์ฅ์ ์์ต๋๋ค.
| ํ์ผ | ๋ด์ฉ |
|---|---|
omx_patches.diff |
LeRobot ์์ค ์์ ๋ด์ญ. git apply ๋ก ์ ์ฉ |
omx_calibration.tar.gz |
~/.cache/huggingface/lerobot/calibration ์ ์ฒด |
omx_calibration/ |
์ ์์นด์ด๋ธ๋ฅผ ํผ์น ๊ฒ. ์น์์ ๋ฐ๋ก ์ฝ์ ์ ์์ |
์ด ์ ์ฅ์์ ์ซ์๋ ์ฐ๋ฆฌ์ ์ธก์ ๊ฒฐ๊ณผ์ ๋๋ค. ๊ทธ๋๋ก ์ฐ๋ ๊ฒ๋ณด๋ค, ๊ฐ์ ์ ์ฐจ๋ก ๋ณธ์ธ ๋ก๋ด์ ์ธก์ ํ๋ ๊ฒ์ด ์ข์ต๋๋ค. ์๋์ ๊ฐ์ด๋๊ฐ ์์ต๋๋ค.
์๋ ๋ฐฉ๋ฒ๋ค์ ๋ฐ๋ผํ๊ธฐ ์ํด์๋ dynamixel-sdk ๊ฐ ํ์ํ๋ ์ค์นํด์ฃผ์ธ์.
๋ค์ ๋ช ๋ น์ shell์์ ์คํํด ์ค์นํ์ธ์.
pip install dynamixel-sdk
๋๋ถ๋ถ์ ํฐ๋ฏธ๋์ ๊ทธ๋๋ก ๋ถ์ฌ๋ฃ์ผ๋ฉด ์คํ๋๋ ๋ธ๋ก์ ๋๋ค. ํฌํธ๋ ๋ชจ๋ ๋ธ๋ก์ด ์๋์ผ๋ก ์ฐพ์ผ๋ฏ๋ก, ๋ชจํฐ ID์ ๊ฐ๋ง ํ์ธํ๋ฉด ๋ฉ๋๋ค.
์ฌ๋ฌ ๋ฒ ๋ฐ๋ณตํด์ ์ฐ๋ ์ธ ๊ฐ(motor_status.py, watch_motor.py, compare_pos.py)๋ง ํ์ผ๋ก ๋ง๋ค์ด ๋๊ณ ์ธ์๋ฅผ ๋ฐ๊ฟ ํธ์ถํฉ๋๋ค.
1. ์ค๋น
1-1. ๋ชจํฐ ์ํ ์ฝ๊ธฐ
๋ฌธ์ ๊ฐ ์๊ธฐ๋ฉด ๊ฐ์ฅ ๋จผ์ ์คํํฉ๋๋ค. ์ง๋จ๊ณผ ๊ฒ์ฆ์ ๊ณ์ ์ฐ๋ฏ๋ก ํ์ผ๋ก ๋ง๋ค์ด ๋์ธ์.
# motor_status.py
import sys, glob
from dynamixel_sdk import PortHandler, PacketHandler, COMM_SUCCESS
USAGE = """์ฌ์ฉ๋ฒ: python motor_status.py [port]
์: python motor_status.py (์ฐ๊ฒฐ๋ ๋ชจ๋ ํฌํธ ํ๊ธฐ)
python motor_status.py /dev/ttyUSB0 (ํน์ ํฌํธ๋ง)"""
if len(sys.argv) > 2:
sys.exit(USAGE)
PORTS = ([sys.argv[1]] if len(sys.argv) == 2
else sorted(glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')))
if not PORTS:
sys.exit('์๋ฆฌ์ผ ํฌํธ๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค. USB ์ฐ๊ฒฐ๊ณผ ์ ์์ ํ์ธํ์ธ์.')
ERR_BITS = {0: 'InputVoltage', 2: 'Overheating', 3: 'EncoderError',
4: 'ElectricalShock', 5: 'Overload'}
HEADER = (f'{"ID":>3} {"model":>6} {"pos":>6} {"cur":>5} '
f'{"temp":>5} {"tq":>3} error')
ph = PacketHandler(2.0)
def rd(p, mid, addr, nb, signed=False):
fn = {1: ph.read1ByteTxRx, 2: ph.read2ByteTxRx, 4: ph.read4ByteTxRx}[nb]
v, r, _ = fn(p, mid, addr)
if r != COMM_SUCCESS:
return None
if signed and v >= 2 ** (8 * nb - 1):
v -= 2 ** (8 * nb)
return v
total = 0
for dev in PORTS:
p = PortHandler(dev)
if not p.openPort():
print(f'{dev}: ์ด๊ธฐ ์คํจ (dialout ๊ทธ๋ฃน ๊ถํ์ ํ์ธํ์ธ์)')
continue
p.setBaudRate(1_000_000)
ids = [mid for mid in range(1, 31) if ph.ping(p, mid)[1] == COMM_SUCCESS]
if ids:
print(f'=== {dev} ===')
print(HEADER)
for mid in ids:
err = rd(p, mid, 70, 1)
names = [n for b, n in ERR_BITS.items() if err and err >> b & 1]
print(f'{mid:>3} {ph.ping(p, mid)[0]:>6} {rd(p, mid, 132, 4, True):>6} '
f'{rd(p, mid, 126, 2, True):>5} {rd(p, mid, 146, 1):>5} '
f'{rd(p, mid, 64, 1):>3} {err} {names}')
print()
total += len(ids)
p.closePort()
if total == 0:
sys.exit('๋ชจํฐ๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค. ์ ์, ๋ฐฐ์ , ํฌํธ ๊ถํ์ ํ์ธํ์ธ์.')
print(f'๋ชจํฐ {total}๊ฐ ๋ฐ๊ฒฌ')
๋ค์ ๋ช ๋ น์ shell์์ ์คํํ์ธ์.
python motor_status.py
์ถ๋ ฅ ์์:
=== /dev/ttyUSB0 ===
ID model pos cur temp tq error
11 1060 2048 0 30 0 0 []
12 1060 1024 3 31 0 0 []
16 1200 2338 -162 44 1 32 ['Overload']
=== /dev/ttyUSB1 ===
ID model pos cur temp tq error
1 1200 2048 0 29 0 0 []
6 1190 1187 2 30 1 0 []
๋ชจํฐ 12๊ฐ ๋ฐ๊ฒฌ
๋ชจํฐ๋ฅผ ๋ชป ์ฐพ์ผ๋ฉด ํฌํธ๊ฐ ๋ณด์ด๋์ง๋ถํฐ ํ์ธํ์ธ์.
ls /dev/ttyUSB* /dev/ttyACM*
lerobot-find-port
ํฌํธ๋ ๋ณด์ด๋๋ฐ ๋ชจํฐ๊ฐ ์์ผ๋ฉด ์ ์๊ณผ ๋ฐฐ์ ์, ํฌํธ๊ฐ ์์ ์์ผ๋ฉด USB ์ฐ๊ฒฐ์ ํ์ธํฉ๋๋ค. ์ด๊ธฐ ์คํจ๊ฐ ๋์ค๋ฉด ์๋ฆฌ์ผ ํฌํธ ์ ๊ทผ ๊ถํ์ด ์๋ ๊ฒ์
๋๋ค.
1-2. ์ด๋๋ก ๊ฐ์ง ํ์
error ์ด๋ก ๊ฒฐ์ ํฉ๋๋ค.
| error | ์๋ฏธ | ํ ์ผ |
|---|---|---|
| 0 | ์๋ฌ ์์ | ์์ธ๊ฐ ์ ๋ง์ผ๋ฉด 4์ฅ, ์์ง์ด์ง ์์ผ๋ฉด 2-2์ |
| 32 | Overload (์ต๋ ์ถ๋ ฅ ์ง์) | 2์ฅ |
| 4 | Overheating (์จ๋ ํ๊ณ ์ด๊ณผ) | ์์ ๋๊น์ง ๊ธฐ๋ค๋ฆฐ ๋ค 2์ฅ |
| ๊ทธ ์ธ | ๊ดํธ ์์ ๋นํธ ์ด๋ฆ ์ฐธ๊ณ | ์ ์, ๋ฐฐ์ , ๋ชจํฐ ์ํ ์ ๊ฒ |
์ฌ๋ฌ ๋นํธ๊ฐ ๋์์ ์ค ์ ์์ต๋๋ค. 36์ด๋ฉด Overload์ Overheating์ด ํจ๊ป ์ ๊ฒ์ ๋๋ค.
์๋ฌ๊ฐ 0์ด ์๋ ๋ชจํฐ๋ ํ ํฌ๋ฅผ ์ผ๊ฑฐ๋ ๋ ์ ์๊ณ EEPROM ์ฐ๊ธฐ๋ ๊ฑฐ๋ถ๋๋ฏ๋ก, ๋ฌด์์ ํ๋ 1-3์ ์ ๋จผ์ ์คํํ์ธ์.
1-3. ์๋ฌ ์ง์ฐ๊ณ ํ ํฌ ๋๊ธฐ
์๋ฌ๋ ์๋์ผ๋ก ์ง์์ง์ง ์๊ณ reboot์ด๋ ์ ์ ์ฌํฌ์ ๊น์ง ์ ์ง๋๋ฏ๋ก ๋ช ์์ ์ผ๋ก ์ง์์ผ ํฉ๋๋ค.
์ ์ด ํ๋ก๊ทธ๋จ์ ๋น์ ์ ์ข ๋ฃํ ๋ค ์๋ฌด๊ฒ๋ ์คํํ์ง ์๋๋ฐ ๋ชจํฐ๊ฐ ๋จ๊ฑฐ์ด ๊ฒฝ์ฐ์๋ ์ด๊ฑธ ์ฐ์ธ์. ๊ณผ๋ถํ๊ฐ ๋ ๋ชจํฐ๋ ํ ํฌ ๋๊ธฐ ์ฐ๊ธฐ๊ฐ ๊ฑฐ๋ถ๋์ด ํ ํฌ๊ฐ ์ผ์ง ์ฑ ๋จ์ต๋๋ค.
์ฐ๊ฒฐ๋ ๋ชจ๋ ๋ชจํฐ๋ฅผ ์ฒ๋ฆฌํ๋ฏ๋ก ๊ณ ์น ๊ฒ์ด ์์ต๋๋ค. ๋ค์ ์คํฌ๋ฆฝํธ๋ฅผ shell์ ๊ทธ๋๋ก ๋ถ์ฌ๋ฃ์ผ์ธ์.
python - <<'EOF'
import glob, time
from dynamixel_sdk import PortHandler, PacketHandler, COMM_SUCCESS
ph = PacketHandler(2.0)
done = 0
for dev in sorted(glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')):
p = PortHandler(dev)
if not p.openPort():
continue
p.setBaudRate(1_000_000)
for mid in range(1, 31):
if ph.ping(p, mid)[1] != COMM_SUCCESS:
continue
err, _, _ = ph.read1ByteTxRx(p, mid, 70)
if err: # ์๋ฌ๊ฐ ์์ ๋๋ง reboot
ph.reboot(p, mid); time.sleep(1.5)
ph.write1ByteTxRx(p, mid, 64, 0) # Torque_Enable = 0
err, _, _ = ph.read1ByteTxRx(p, mid, 70)
tq, _, _ = ph.read1ByteTxRx(p, mid, 64)
print(f'{dev} id {mid}: error={err} torque={tq}')
done += 1
p.closePort()
print(f'\n๋ชจํฐ {done}๊ฐ ์ฒ๋ฆฌ')
EOF
๋ชจ๋ error=0 torque=0์ด๋ฉด ๋ค์ ๋จ๊ณ๋ก ๊ฐ๋๋ค. error๊ฐ ๊ทธ๋๋ก ๋จ์ผ๋ฉด ๋ก๋ด ์ ์์ ๊ป๋ค ์ผ๊ณ ๋ค์ ์คํํ์ธ์.
reboot์ RAM๋ ์ด๊ธฐํํ๋ฏ๋ก, ์๋ฌ๊ฐ ๊ฑธ๋ ธ๋ ๋ชจํฐ๋ Position ๊ฒ์ธ๊ณผ Goal_Current๊ฐ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ๋์๊ฐ๋๋ค. 2์ฅ์์ ๊ทธ ๊ฐ๋ค์ ์กฐ์ ํ ๋ค๋ผ๋ฉด ๋ค์ ์จ์ผ ํฉ๋๋ค. ๋ฉ์ฉกํ ๋ชจํฐ๋ rebootํ์ง ์์ผ๋ฏ๋ก ์ค์ ์ด ์ ์ง๋ฉ๋๋ค.
2. ๊ทธ๋ฆฝํผ์ Overload(์๋ฌ 32)๊ฐ ๋ฐ๋ณต๋ ๋
Overload๋ ์ ๋ฅ๊ฐ Current_Limit์ ๋๋ ๊ฒ์ด ์๋๋ผ ์ต๋ ์ถ๋ ฅ(PWM)์ด ์ง์๋ ๋ ๋ฐ์ํ๋ฏ๋ก, Current_Limit์ ์ฌ๋ฆฌ๋ ๊ฒ์ผ๋ก๋ ํด๊ฒฐ๋์ง ์์ต๋๋ค. PWM์ด ํฌํ๋๋์ง๋ถํฐ ํ์ธํ์ธ์.
2-1. PWM ํ์ธ
์กฐ์น๋ง๋ค ๋ค์ ์คํํ๋ฏ๋ก ํ์ผ๋ก ๋ง๋ค์ด ๋์ธ์.
# watch_motor.py
import sys, glob, time
from dynamixel_sdk import PortHandler, PacketHandler, COMM_SUCCESS
USAGE = """์ฌ์ฉ๋ฒ: python watch_motor.py <id> <goal_position> [seconds]
์: python watch_motor.py 16 2400
python watch_motor.py 16 2400 60
ํฌํธ๋ ์๋์ผ๋ก ์ฐพ์ต๋๋ค.
์ฃผ์: ํ ํฌ๋ฅผ ์ผ๊ณ ๋ชจํฐ๋ฅผ ์์ง์
๋๋ค. ์ฃผ๋ณ์ ๋น์ฐ๊ณ ์คํํ์ธ์."""
if len(sys.argv) not in (3, 4):
sys.exit(USAGE)
try:
MOTOR_ID = int(sys.argv[1])
GOAL = int(sys.argv[2])
SECONDS = float(sys.argv[3]) if len(sys.argv) == 4 else 10.0
except ValueError:
sys.exit(USAGE)
ph = PacketHandler(2.0)
p = None
for dev in sorted(glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')):
q = PortHandler(dev)
if not q.openPort():
continue
q.setBaudRate(1_000_000)
if ph.ping(q, MOTOR_ID)[1] == COMM_SUCCESS:
p = q
print(f'{dev} ์์ id {MOTOR_ID} ๋ฐ๊ฒฌ')
break
q.closePort()
if p is None:
sys.exit(f'id {MOTOR_ID} ๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค. motor_status.py ๋ก ID๋ฅผ ํ์ธํ์ธ์.')
def rd(addr, nb, signed=False):
fn = {1: ph.read1ByteTxRx, 2: ph.read2ByteTxRx, 4: ph.read4ByteTxRx}[nb]
v, r, _ = fn(p, MOTOR_ID, addr)
if r != COMM_SUCCESS:
return None
if signed and v >= 2 ** (8 * nb - 1):
v -= 2 ** (8 * nb)
return v
pwm_limit = rd(36, 2)
print(f'์์: pos={rd(132, 4, True)} mode={rd(11, 1)} '
f'CurrentLimit={rd(38, 2)} GoalCurrent={rd(102, 2)} PWMLimit={pwm_limit}')
print(f' P={rd(84, 2)} I={rd(82, 2)} D={rd(80, 2)} error={rd(70, 1)}\n')
ph.write1ByteTxRx(p, MOTOR_ID, 64, 1) # Torque on
ph.write4ByteTxRx(p, MOTOR_ID, 116, GOAL) # Goal_Position
t0 = time.perf_counter()
peak_cur = peak_pwm = 0
try:
while time.perf_counter() - t0 < SECONDS:
pos, cur = rd(132, 4, True), rd(126, 2, True)
pwm, err = rd(124, 2, True), rd(70, 1)
peak_cur = max(peak_cur, abs(cur or 0))
peak_pwm = max(peak_pwm, abs(pwm or 0))
print(f' t={time.perf_counter() - t0:5.2f} pos={pos:5d} '
f'cur={cur:5d} pwm={pwm:5d} err={err}')
if err:
print(f'\n>>> ์๋ฌ {err} ๋ฐ์')
break
time.sleep(0.04)
except KeyboardInterrupt:
pass
ph.write1ByteTxRx(p, MOTOR_ID, 64, 0) # Torque off
p.closePort()
print(f'\n์ ๋ฅ ์ต๋ {peak_cur}')
print(f'PWM ์ต๋ {peak_pwm} / PWM_Limit {pwm_limit}')
if pwm_limit and peak_pwm >= pwm_limit * 0.95:
print('โ PWM ํฌํ. 2-2์ ๊ณผ 2-3์ ๋ก ๊ฐ์ธ์.')
else:
print('โ PWM ํฌํ ์๋. 2-2์ ์ ๋จผ์ ๋ณด๊ณ , ๊ฒ์ธ์ด ์ ์์ด๋ฉด 2-4์ ๋ก ๊ฐ์ธ์.')
๋ค์ ๋ช ๋ น์ shell์์ ์คํํ์ธ์.
python watch_motor.py 16 2400
์ํ๋ง์ด ๋๋ฆฌ๋ฉด ํผํฌ๋ฅผ ๋์น๋ฏ๋ก 40 ms ๊ฐ๊ฒฉ์ผ๋ก ๊ธฐ๋กํฉ๋๋ค. ๋ง์ง๋ง ์ค์ ํ์ ์ ๋ฐ๋ผ ๋ค์ ์ ๋ก ๊ฐ์ธ์.
2-2. Position ๊ฒ์ธ ์กฐ์
ํฐ ๋ชฉํ๊ฐ์ ๋ฐ๋ผ๊ฐ๋๋ฐ ์์ ๋จ๊ณ๋ก ์ฌ๋ฆฌ๋ฉด ์ค๊ฐ์ ๋ฉ์ถ๋ ๊ฒฝ์ฐ์ ๋๋ค. Current-Based Position Control์์ ์ ๋ฅ ์ง๋ น์ ์์น ์ค์ฐจ์ P ๊ฒ์ธ์ ๊ณฑํด ๋ง๋ค์ด์ง๋ฏ๋ก, P๊ฐ ๋ฎ๊ฑฐ๋ D๊ฐ P๋ณด๋ค ํฌ๋ฉด ์ ๋ฅ๊ฐ ์ถฉ๋ถํ ์ฌ๋ผ๊ฐ์ง ์์ต๋๋ค.
2-1์ ์ถ๋ ฅ์ P= ์ D= ๋ฅผ ๋ณด์ธ์. D๊ฐ P๋ณด๋ค ํฌ๋ฉด ์ด ๊ฒฝ์ฐ์
๋๋ค.
๋ฌธ์ ๊ฐ ๋๋ ๋ชจํฐ ํ๋์๋ง ์ฐ์ธ์. ๋ค๋ฅธ ๊ด์ ์ ๊ฒ์ธ์ ๊ฑด๋๋ฆฌ์ง ์์ต๋๋ค.
๋ค์ ์คํฌ๋ฆฝํธ๋ฅผ shell์ ๋ถ์ฌ๋ฃ์ผ์ธ์.
python - <<'EOF'
# โโโ ์ด ๋ ์ค๋ง ํ์ธํ์ธ์ โโโ
MOTOR_ID = 16 # 1-1์ ์ถ๋ ฅ์์ ๊ทธ๋ฆฝํผ ๋ชจํฐ์ ID
P, I, D = 800, 0, 0 # P๋ 800์์ ์์. ๋ ์ฌ๋ฆฌ๋ฉด ๋ชฉํ๋ฅผ ์ง๋์ณ ํ๋ค๋ฆฝ๋๋ค
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
import glob
from dynamixel_sdk import PortHandler, PacketHandler, COMM_SUCCESS
ph = PacketHandler(2.0)
p = None
for dev in sorted(glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')):
q = PortHandler(dev)
if not q.openPort():
continue
q.setBaudRate(1_000_000)
if ph.ping(q, MOTOR_ID)[1] == COMM_SUCCESS:
p = q
print(f'{dev} ์์ id {MOTOR_ID} ๋ฐ๊ฒฌ\n')
break
q.closePort()
if p is None:
raise SystemExit(f'id {MOTOR_ID} ๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค.')
for addr, val, name in [(84, P, 'P_Gain'), (82, I, 'I_Gain'), (80, D, 'D_Gain')]:
r, e = ph.write2ByteTxRx(p, MOTOR_ID, addr, val)
back, _, _ = ph.read2ByteTxRx(p, MOTOR_ID, addr)
print(f'{name}({addr}) <- {val}: {"OK" if back == val else "์คํจ"} '
f'(์ฝ๊ธฐ {back}) {ph.getTxRxResult(r)} {ph.getRxPacketError(e)}')
p.closePort()
EOF
์ธ ์ค ๋ชจ๋ OK์ฌ์ผ ํฉ๋๋ค. RAM ์์ญ์ด๋ผ ์ ์์ ๋๋ฉด ์ด๊ธฐํ๋ฉ๋๋ค.
๋ฐ๊พผ ๋ค 2-1์ ์ ๋ค์ ์คํํด ์์น ์ค์ฐจ๊ฐ ๋ช ์นด์ดํธ ์ด๋ด๋ก ์ค์๋์ง ํ์ธํ์ธ์.
2-3. Goal_Current ๋ด๋ฆฌ๊ธฐ
2-1์ ์์ PWM์ด ํฌํ๋ ๊ฒฝ์ฐ์ ๋๋ค. PWM์ด ํฌํ๋ ์ํ์์ ๋์จ ์ ๋ฅ๊ฐ์ด ๊ทธ ๋ชจํฐ์ ์ ๋ฅ ์ฒ์ฅ์ด๊ณ , Goal_Current๋ฅผ ์ฒ์ฅ๋ณด๋ค ๋๊ฒ ์ก์ผ๋ฉด ์ ๋ฅ๊ฐ ๋ชฉํ๊ฐ์ ์์ ๋ฟ์ง ๋ชปํด PWM์ด ์ต๋์ ๋ถ์ ์ฑ๋ก ๋จ์ต๋๋ค.
2-1์ ์ถ๋ ฅ์ ์ ๋ฅ ์ต๋ ๊ฐ์ ์ฒ์ฅ์ผ๋ก ๋ณด๊ณ , ๊ทธ 60% ์ ๋์์ ์์ํ์ธ์.
๋ค์ ์คํฌ๋ฆฝํธ๋ฅผ shell์ ๋ถ์ฌ๋ฃ์ผ์ธ์.
python - <<'EOF'
# โโโ ์ด ๋ ์ค๋ง ํ์ธํ์ธ์ โโโ
MOTOR_ID = 16
GOAL_CURRENT = 100 # watch_motor.py ๋ก ์ฐ ์ ๋ฅ ์ฒ์ฅ์ 60% ์ ๋
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
import glob
from dynamixel_sdk import PortHandler, PacketHandler, COMM_SUCCESS
ph = PacketHandler(2.0)
p = None
for dev in sorted(glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')):
q = PortHandler(dev)
if not q.openPort():
continue
q.setBaudRate(1_000_000)
if ph.ping(q, MOTOR_ID)[1] == COMM_SUCCESS:
p = q
print(f'{dev} ์์ id {MOTOR_ID} ๋ฐ๊ฒฌ\n')
break
q.closePort()
if p is None:
raise SystemExit(f'id {MOTOR_ID} ๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค.')
r, e = ph.write2ByteTxRx(p, MOTOR_ID, 102, GOAL_CURRENT)
back, _, _ = ph.read2ByteTxRx(p, MOTOR_ID, 102)
limit, _, _ = ph.read2ByteTxRx(p, MOTOR_ID, 38)
print(f'Goal_Current(102) <- {GOAL_CURRENT}: '
f'{"OK" if back == GOAL_CURRENT else "์คํจ"} (์ฝ๊ธฐ {back}) '
f'{ph.getTxRxResult(r)} {ph.getRxPacketError(e)}')
print(f'์ฐธ๊ณ : Current_Limit(38) = {limit}')
p.closePort()
EOF
์คํจ๋ฉด ๊ทธ ๋ชจ๋ธ์ ํ์ฉ ๋ฒ์๋ฅผ ๋ฒ์ด๋ ๊ฐ์
๋๋ค. ๋ฒ์ ๋ฐ์ ๊ฐ์ ์์ธ ์์ด ๊ฑฐ๋ถ๋๊ณ ์ด์ ๊ฐ์ด ๋จ์ผ๋ฏ๋ก ์ถ๋ ฅ์ผ๋ก ํ์ธํ์ธ์.
๋ฌผ์ฒด๋ฅผ ๋ชป ์ฅ ๋งํผ ์ฝํ๋ฉด ์กฐ๊ธ์ฉ ์ฌ๋ฆฌ๋, ์ฒ์ฅ์ ๊ฐ๊น์์ง๋ฉด ๋ค์ ํฌํํฉ๋๋ค.
2-4. ํน์ ์์น์์๋ง ์ ์์ง์ผ ๋
PWM์ ์ฌ์ ๊ฐ ์๊ณ ๊ฒ์ธ๋ ๊ณ ์ณค๋๋ฐ ํน์ ๊ตฌ๊ฐ์์๋ง ๋ฉ์ถ๋ ๊ฒฝ์ฐ๋ ๊ธฐ๊ตฌ ๋ฌธ์ ์ ๋๋ค. ์๋ ์ธ ๊ฐ์ง๊ฐ ํจ๊ป ๋ํ๋๋ฉด ๊ฑฐ์ ํ์คํฉ๋๋ค.
- ์์น ์์กด์ฑ: ํน์ ๊ตฌ๊ฐ์์๋ง ๊ฑธ๋ฆฌ๊ณ ๋ค๋ฅธ ๊ตฌ๊ฐ์ ์ ์
- ๋ฐฉํฅ ์์กด์ฑ: ๋ซํ๋ ๊ฑด ๋๋๋ฐ ์ด๋ฆฌ์ง ์๋ ์์ผ๋ก ํ ๋ฐฉํฅ๋ง ๊ฑธ๋ฆผ
- ์ ๋ฅ ์ฐจ์ด 10๋ฐฐ ์ด์: ์์ ๋กญ๊ฒ ์์ง์ผ ๋์ ๊ฑธ๋ ธ์ ๋์ ์ ๋ฅ๊ฐ ํ ์๋ฆฌ ์์ ์ธ ์๋ฆฌ ์๋ก ๋ฒ์ด์ง
๋์ฌ์ ์ฐ๊ฒฐ๋ถ๋ฅผ ์ ๋ถ ์ ๊ฒํ๊ณ ์กฐ์ด์ธ์. 100 mA ์์ค์ ์ ํญ์ ์ฌ๋ ์์ผ๋ก ๊ตฌ๋ถ๋์ง ์์ผ๋ฏ๋ก ์์ผ๋ก ๋ง์ ธ์ ์ฐพ์ผ๋ ค ํ์ง ๋ง๊ณ ์ธก์ ๊ฐ์ ๋ฏฟ์ผ์ธ์.
๋์ฌ๋ฅผ ์กฐ์ด๋ฉด ๊ฐ๋ ๋ฒ์๊ฐ ๋ฌ๋ผ์ง๋ฏ๋ก 3์ฅ์ผ๋ก ๊ฐ์ ๋ค์ ์ธก์ ํ์ธ์.
2-5. ์ค์ ์ด ์ ์ง๋์ง ์์ ๋ (LeRobot)
Position ๊ฒ์ธ๊ณผ Goal_Current๋ RAM ์์ญ์ด๊ณ LeRobot์ด ์ฐ๊ฒฐํ ๋๋ง๋ค ์์ค์ ๊ฐ์ ๋ค์ ์ฐ๋ฏ๋ก, LeRobot ์์ค์ ์ง์ ๋ฃ์ด์ผ ์ ์ง๋ฉ๋๋ค. src/lerobot/robots/omx_follower/omx_follower.py์ configure()๋ฅผ ๊ณ ์น๊ธฐ ์ํด ์๋ ์คํฌ๋ฆฝํธ๋ฅผ shell์์ ์คํํ์ธ์.
python - <<'EOF'
# โโโ ์ด ์ธ ์ค๋ง ํ์ธํ์ธ์ โโโ
P, I, D = 800, 0, 0 # 2-2์ ์์ ์ ํ ๊ฒ์ธ
GOAL_CURRENT = 100 # 2-3์ ์์ ์ ํ ๊ฐ
CURRENT_LIMIT = 600 # ๋ฐ๊ฟ ์ด์ ๊ฐ ์์ผ๋ฉด ๊ทธ๋๋ก
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
import importlib.util, pathlib
MARK = '# omx-guide'
spec = importlib.util.find_spec('lerobot.robots.omx_follower.omx_follower')
if spec is None or not spec.origin:
raise SystemExit('omx_follower ๋ชจ๋์ ์ฐพ์ง ๋ชปํ์ต๋๋ค. '
'lerobot ์ด ์ค์น๋ ํ๊ฒฝ์์ ์คํํ์ธ์.')
src = pathlib.Path(spec.origin)
text = src.read_text()
if MARK in text:
raise SystemExit(f'์ด๋ฏธ ์ ์ฉ๋์ด ์์ต๋๋ค: {src}')
lines = text.splitlines(keepends=True)
start = next((i for i, ln in enumerate(lines)
if ln.lstrip().startswith('with ') and 'torque_disabled()' in ln), None)
if start is None:
raise SystemExit('torque_disabled ๋ธ๋ก์ ์ฐพ์ง ๋ชปํ์ต๋๋ค. ์์ค๋ฅผ ์ง์ ๊ณ ์น์ธ์.')
wi = len(lines[start]) - len(lines[start].lstrip()) # with ์ค์ ๋ค์ฌ์ฐ๊ธฐ
bi = ' ' * (wi + 4) # ๋ธ๋ก ์
oi = ' ' * wi # ๋ธ๋ก ๋ฐ
end = len(lines) # ๋ธ๋ก์ด ๋๋๋ ์ค
for j in range(start + 1, len(lines)):
if lines[j].strip() and len(lines[j]) - len(lines[j].lstrip()) <= wi:
end = j
break
inside = [
f'{bi}self.bus.write("Operating_Mode", "gripper", '
f'OperatingMode.CURRENT_POSITION.value) {MARK}\n',
f'{bi}self.bus.write("Current_Limit", "gripper", {CURRENT_LIMIT}) {MARK}\n',
f'{bi}self.bus.write("Position_P_Gain", "gripper", {P}) {MARK}\n',
f'{bi}self.bus.write("Position_I_Gain", "gripper", {I}) {MARK}\n',
f'{bi}self.bus.write("Position_D_Gain", "gripper", {D}) {MARK}\n',
]
outside = [
f'{oi}self.bus.write("Goal_Current", "gripper", {GOAL_CURRENT}) {MARK}\n',
]
src.write_text(''.join(lines[:end] + inside + outside + lines[end:]))
if 'OperatingMode' not in text:
print('๊ฒฝ๊ณ : ์ด ํ์ผ์ OperatingMode import ๊ฐ ์์ต๋๋ค.')
print(' Operating_Mode ์ค์ ์ง์ฐ๊ฑฐ๋ import ๋ฅผ ์ถ๊ฐํ์ธ์.\n')
print(f'์์ : {src}\n')
print(f'torque_disabled ๋ธ๋ก ์ {len(inside)}์ค, ๋ธ๋ก ๋ฐ {len(outside)}์ค ์ถ๊ฐ:')
for ln in inside + outside:
print(' +', ln.rstrip())
EOF
์์ ์ด ์ ๋๋ก ๋๋์ง ํ์ธํ์ธ์. ๋ค์ ๋ช ๋ น์ shell์์ ์คํํ์ธ์.
python -c "import lerobot.robots.omx_follower.omx_follower"
์๋ฌด ์ถ๋ ฅ๋ ์์ผ๋ฉด ๋ฌธ๋ฒ์ด ์ ์์
๋๋ค. ์๋ฌ๊ฐ ๋๋ฉด ์ฝ์
์์น๊ฐ ์ด๊ธ๋ ๊ฒ์ด๋ ํ์ผ์ ์ด์ด # omx-guide ๊ฐ ๋ถ์ ์ค์ ํ์ธํ์ธ์. ํด๋น ์ค์ ์ญ์ ํ๋ฉด ์ด์ ์ผ๋ก ๋ณต๊ตฌํ ์ ์์ต๋๋ค.
์
์คํธ๋ฆผ ์์ค๋ elbow_flex์ ๊ฒ์ธ๋ง ์ค์ ํ๊ณ gripper๋ ์๋์ง ์์ผ๋ฏ๋ก, ๊ทธ๋ฆฝํผ๋ ๊ธฐ๋ณธ ๊ฒ์ธ์ผ๋ก ๋์ํฉ๋๋ค.
2-6. ๊ฒ์ฆ
๋ค์ ๋ช ๋ น์ shell์์ ์คํํ์ธ์.
python watch_motor.py 16 2400 60
์ธ ๊ฐ์ง๋ฅผ ํ์ธํ์ธ์.
err๊ฐ ๋๊น์ง 0- ๋ง์ง๋ง ์ค์ด
PWM ํฌํ ์๋ - ์จ๋๊ฐ ๊ณ์ ์ค๋ฅด์ง ์๊ณ ๋ช ๋ ์์์ ์์
๋ฌผ์ฒด๋ฅผ ์ฅ๋ ์ํฉ๋ ์ํํ์ธ์. ์ฅ๋ ์๊ฐ PWM์ด ์ผ์์ ์ผ๋ก ํฌํ๋๋ ๊ฒ์ ๊ด์ฐฎ๊ณ , 2์ด ๋๊ฒ ์ง์๋๋ฉด ๊ณผ๋ถํ๊ฐ ๋ฉ๋๋ค.
2-7. ์ด๋ ์ค์๋ง ์ ๋ฅ๋ฅผ ๋์ด๊ธฐ (์ ํ)
2-4์ ์ ๊ธฐ๊ตฌ ๊ฑธ๋ฆผ์ด ์์ ํ ํด๊ฒฐ๋๋์ง ํ์ ํ ์ ์์ ๋ ์ฐ๋ ์์ ์ฅ์น์ ๋๋ค. ๋ฌผ์ฒด๋ฅผ ์ฅ๋ ๊ฒ์ ์๊ตฌ ์คํจ์ด์ง๋ง ์ด๋์ ๋๋ฌ ๊ฐ๋ฅํ ๊ณผ๋ ๋์์ด๋ฏ๋ก, ๋ฐฉํฅ์ด ๋ฐ๋ ๋ ํ ๋ฒ๋ง ์ ๋ฅ๋ฅผ ์ฌ๋ฆฌ๊ณ 1์ด๊ฐ ์ง์ฒ์ด ์์ผ๋ฉด ๋๋๋ฆฝ๋๋ค.
*์คํจ: ๋ชจํฐ๊ฐ ํ์ ๋ช ๋ น์ ๋ฐ์์ง๋ง ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋งํ ํ์ ํ์ง ๋ชปํ๊ณ ํ๋ง ๊ณ์ ์ฐ๋ ์ํ
omx_follower.py์ ๋ฉ์๋๋ก ์ถ๊ฐํ๊ณ , send_action()์์ ๋ชจํฐ์ ๋ชฉํ๋ฅผ ์ฐ๊ธฐ ์ง์ ์ ํธ์ถํ์ธ์. configure()์ self._gstate = None์ ๋ฃ์ด ์ด๊ธฐํํฉ๋๋ค.
def _gripper_current_policy(self, goal_pos) -> None:
if "gripper" not in goal_pos:
return
GC_HOLD, GC_BOOST = 100, 200
MARGIN, PROGRESS, TIMEOUT = 2.0, 1.0, 1.0
now = time.perf_counter()
st = getattr(self, "_gstate", None)
if st is None:
st = self._gstate = {"mode": "hold", "dir": 0, "armed": True,
"since": 0.0, "ref": 0.0}
g = float(goal_pos["gripper"])
cur = float(self.bus.read("Present_Position", "gripper"))
err = g - cur
if abs(err) <= MARGIN:
if st["mode"] == "boost":
self.bus.write("Goal_Current", "gripper", GC_HOLD)
st.update(mode="hold", dir=0, armed=True)
return
d = 1 if err > 0 else -1
if d != st["dir"]:
st.update(dir=d, since=now, ref=cur)
st["armed"] = st["mode"] == "hold"
if st["mode"] == "hold":
if st["armed"]:
self.bus.write("Goal_Current", "gripper", GC_BOOST)
st.update(mode="boost", armed=False, since=now, ref=cur)
elif (cur - st["ref"]) * d > PROGRESS:
st.update(since=now, ref=cur)
elif now - st["since"] > TIMEOUT:
self.bus.write("Goal_Current", "gripper", GC_HOLD)
st["mode"] = "hold"
๋งค ํฑ serial read๊ฐ ์ถ๊ฐ๋์ด ์ ์ด ๋ฃจํ ๋น์ฉ์ด ๋์ด๋๋ฏ๋ก, ๋ฃจํ๊ฐ ์ด๋ฏธ ๋ชฉํ FPS์ ๋นก๋นกํ๋ฉด ๋ฃ์ง ๋ง์ธ์.
3. ๊ฐ๋ ๋ฒ์๋ฅผ ์ธก์ ํด ๋ฐ์ํ๊ธฐ
๋์ฌ๋ฅผ ์กฐ์ด๊ฑฐ๋ ๋ชจํฐ ๊ด์ ์ ๋ค์ ์กฐ๋ฆฝํ ๋ค์๋ ๊ฐ๋ ๋ฒ์๊ฐ ๋ฌ๋ผ์ง๋ฏ๋ก ์ธก์ ๋ถํฐ ๋ค์ ํ์ธ์.
3-1. ํ ํฌ๋ฅผ ๋๊ณ ์์ผ๋ก ์ธก์
๋ชจํฐ ํ์ผ๋ก ์ฌ๋ฉด ๋ชจํฐ๊ฐ ๋ฐ์ด๋ธ ์์น๊ฐ ์์ฌ ์ค์ ๊ธฐ๊ตฌ ํ๊ณ๋ณด๋ค ๋๊ฒ ๋์ต๋๋ค.
ํ ํฌ๊ฐ ๊บผ์ง๋ฉด ์์ด ์ค๋ ฅ์ผ๋ก ๋ด๋ ค์์ ์ ์์ผ๋ ์์ผ๋ก ๋ฐ์น ์ํ์์ ์คํํ์ธ์.
๋ค์ ์คํฌ๋ฆฝํธ๋ฅผ shell์ ๋ถ์ฌ๋ฃ์ผ์ธ์.
python - <<'EOF'
# โโโ ์ด ์ค๋ง ํ์ธํ์ธ์ โโโ
MOTOR_ID = 16
# โโโโโโโโโโโโโโโโโโโโโโโโโโ
import glob, time
from dynamixel_sdk import PortHandler, PacketHandler, COMM_SUCCESS
ph = PacketHandler(2.0)
p = None
for dev in sorted(glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')):
q = PortHandler(dev)
if not q.openPort():
continue
q.setBaudRate(1_000_000)
if ph.ping(q, MOTOR_ID)[1] == COMM_SUCCESS:
p = q
break
q.closePort()
if p is None:
raise SystemExit(f'id {MOTOR_ID} ๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค.')
ph.write1ByteTxRx(p, MOTOR_ID, 64, 0) # Torque off
print('ํ ํฌ OFF. ์์ผ๋ก ๋๊น์ง ๋ซ๊ธฐ โ ๋๊น์ง ์ด๊ธฐ๋ฅผ ์ฒ์ฒํ 3๋ฒ ํ๊ณ Ctrl+C')
lo = hi = None
try:
while True:
v, r, _ = ph.read4ByteTxRx(p, MOTOR_ID, 132)
if r == COMM_SUCCESS:
if v >= 2 ** 31: v -= 2 ** 32
lo = v if lo is None else min(lo, v)
hi = v if hi is None else max(hi, v)
print(f'\r ํ์ฌ {v:5d} ์ต์ {lo:5d} ์ต๋ {hi:5d} ', end='', flush=True)
time.sleep(0.05)
except KeyboardInterrupt:
print(f'\n\n๋ฒ์ {lo} ~ {hi} (ํญ {hi - lo})' if lo is not None else '\n์ธก์ ๊ฐ ์์')
p.closePort()
EOF
๋น ๋ฅด๊ฒ ์์ง์ด๋ฉด ๋์ ์ ๋์นฉ๋๋ค. 3๋ฒ ๋ฐ๋ณตํด์ ์ต์์ ์ต๋๊ฐ ์๋ ดํ๋์ง ๋ณด์ธ์.
3-2. EEPROM์ ์ฐ๊ธฐ
EEPROM ์ฐ๊ธฐ๋ Torque_Enable=0์ด๊ณ Hardware_Error=0์ผ ๋๋ง ๋๊ณ , ์กฐ๊ฑด์ด ์ด๊ธ๋๊ฑฐ๋ ๋ฒ์๋ฅผ ๋ฒ์ด๋ ๊ฐ์ ์์ธ ์์ด ์กฐ์ฉํ ๊ฑฐ๋ถ๋๋ฏ๋ก ๋ฐํ๊ฐ๊ณผ ์ฝ๊ธฐ๊ฐ์ ๋ ๋ค ํ์ธํฉ๋๋ค.
LeRobot์ ์ฐ๋ฉด ์ด ๋ธ๋ก์ ๊ฑด๋๋ฐ๊ณ 3-3์ ๋ก ๋ฐ๋ก ๊ฐ์ธ์. EEPROM๋ง ๊ณ ์น๋ฉด ๋ค์ ์คํ์์ ๋๋์๊ฐ๋๋ค.
๋ค์ ์คํฌ๋ฆฝํธ๋ฅผ shell์ ๋ถ์ฌ๋ฃ์ผ์ธ์.
python - <<'EOF'
# โโโ ์ด ์ธ ์ค๋ง ํ์ธํ์ธ์ โโโ
MOTOR_ID = 16
MIN_POS, MAX_POS = 2123, 2873 # 3-1์ ์์ ์ฐ ๊ฐ
HOMING_OFFSET = 0 # ์์ ๋ณด์ ์ด ํ์ ์์ผ๋ฉด 0
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
import glob, time
from dynamixel_sdk import PortHandler, PacketHandler, COMM_SUCCESS
ph = PacketHandler(2.0)
p = None
for dev in sorted(glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')):
q = PortHandler(dev)
if not q.openPort():
continue
q.setBaudRate(1_000_000)
if ph.ping(q, MOTOR_ID)[1] == COMM_SUCCESS:
p = q
print(f'{dev} ์์ id {MOTOR_ID} ๋ฐ๊ฒฌ\n')
break
q.closePort()
if p is None:
raise SystemExit(f'id {MOTOR_ID} ๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค.')
ph.reboot(p, MOTOR_ID); time.sleep(1.5) # Hardware_Error ํด์
ph.write1ByteTxRx(p, MOTOR_ID, 64, 0) # Torque off
for addr, val, name in [(20, HOMING_OFFSET, 'Homing_Offset'),
(52, MIN_POS, 'Min_Position_Limit'),
(48, MAX_POS, 'Max_Position_Limit')]:
r, e = ph.write4ByteTxRx(p, MOTOR_ID, addr, val)
back, _, _ = ph.read4ByteTxRx(p, MOTOR_ID, addr)
if back >= 2 ** 31: back -= 2 ** 32
print(f'{name}({addr}) <- {val}: {"OK" if back == val else "์คํจ"} '
f'(์ฝ๊ธฐ {back}) {ph.getTxRxResult(r)} {ph.getRxPacketError(e)}')
p.closePort()
EOF
์ธ ์ค ๋ชจ๋ OK์ฌ์ผ ํฉ๋๋ค. ์คํจ๊ฐ ์์ผ๋ฉด 1-3์ ์ ๋จผ์ ์คํํ์ธ์.
์ฃผ์ 48์ด Max, 52๊ฐ Min์
๋๋ค. ์ค์ ์ ๋ฐ๊พธ์ง ์์ ๋ค๋ฅธ ๋ชจํฐ๋ฅผ ์ฝ์ด addr48=4095 addr52=0์ด ๋์ค๋ ๊ฒ์ผ๋ก ํ์ธํ ์ ์์ต๋๋ค.
3-3. ์บ๋ฆฌ๋ธ๋ ์ด์ ํ์ผ๋ ํจ๊ป ์ฐ๊ธฐ (LeRobot)
LeRobot์ ์ฐ๊ฒฐํ ๋ ์บ๋ฆฌ๋ธ๋ ์ด์
JSON๊ณผ ๋ชจํฐ EEPROM์ ๊ฐ์ ๋น๊ตํ๊ณ , ์ด๊ธ๋๋ฉด calibrate()๋ฅผ ํธ์ถํด ํ๋์ฝ๋ฉ๋ 0๊ณผ 4095๋ฅผ ์์ชฝ์ ๋ค์ ์๋๋ค. ๊ทธ๋์ ํ์ชฝ๋ง ๊ณ ์น๋ฉด ๋ค์ ์คํ์์ ๋๋์๊ฐ๋๋ค.
๋์ ํจ๊ป ์ฐ๋ฉด ์ผ์น ํ์ ์ด ๋์ด calibrate()๊ฐ ํธ์ถ๋์ง ์์ต๋๋ค.
์บ๋ฆฌ๋ธ๋ ์ด์ ํ์ผ์ ๊ด์ ์ด๋ฆ๊ณผ ๋ชจํฐ ID๋ก ์ฐพ์๋ด๋ฏ๋ก ๊ฒฝ๋ก๋ฅผ ์ ๋ ฅํ ํ์๊ฐ ์์ต๋๋ค.
๋ค์ ์คํฌ๋ฆฝํธ๋ฅผ shell์ ๋ถ์ฌ๋ฃ์ผ์ธ์.
python - <<'EOF'
# โโโ ์ด ๋ค ์ค๋ง ํ์ธํ์ธ์ โโโ
MOTOR_ID = 16
JOINT = 'gripper' # ์บ๋ฆฌ๋ธ๋ ์ด์
JSON ์์ ๊ด์ ์ด๋ฆ
MIN_POS, MAX_POS = 2123, 2873 # 3-1์ ์์ ์ฐ ๊ฐ
HOMING_OFFSET = 0
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
import glob, json, pathlib, time
from dynamixel_sdk import PortHandler, PacketHandler, COMM_SUCCESS
# 1. ์บ๋ฆฌ๋ธ๋ ์ด์
ํ์ผ ์ฐพ๊ธฐ (๊ด์ ์ด๋ฆ + ๋ชจํฐ ID๋ก ํน์ )
root = pathlib.Path.home() / '.cache/huggingface/lerobot/calibration'
hits = []
for f in root.rglob('*.json'):
try:
d = json.loads(f.read_text())
except Exception:
continue
if isinstance(d, dict) and JOINT in d and d[JOINT].get('id') == MOTOR_ID:
hits.append(f)
if len(hits) != 1:
raise SystemExit(f'์บ๋ฆฌ๋ธ๋ ์ด์
ํ์ผ์ ํน์ ํ ์ ์์ต๋๋ค. ํ๋ณด {len(hits)}๊ฐ: '
f'{[str(h) for h in hits]}')
cal = hits[0]
data = json.loads(cal.read_text())
print(f'์บ๋ฆฌ๋ธ๋ ์ด์
ํ์ผ: {cal}')
print(f'ํ์ฌ ๊ฐ: {data[JOINT]}\n')
# 2. ๋ชจํฐ EEPROM
ph = PacketHandler(2.0)
p = None
for dev in sorted(glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')):
q = PortHandler(dev)
if not q.openPort():
continue
q.setBaudRate(1_000_000)
if ph.ping(q, MOTOR_ID)[1] == COMM_SUCCESS:
p = q
print(f'{dev} ์์ id {MOTOR_ID} ๋ฐ๊ฒฌ')
break
q.closePort()
if p is None:
raise SystemExit(f'id {MOTOR_ID} ๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค.')
ph.reboot(p, MOTOR_ID); time.sleep(1.5)
ph.write1ByteTxRx(p, MOTOR_ID, 64, 0)
fail = False
for addr, val, name in [(20, HOMING_OFFSET, 'Homing_Offset'),
(52, MIN_POS, 'Min_Position_Limit'),
(48, MAX_POS, 'Max_Position_Limit')]:
ph.write4ByteTxRx(p, MOTOR_ID, addr, val)
back, _, _ = ph.read4ByteTxRx(p, MOTOR_ID, addr)
if back >= 2 ** 31: back -= 2 ** 32
fail = fail or back != val
print(f'{name}({addr}) <- {val}: {"OK" if back == val else "์คํจ"} (์ฝ๊ธฐ {back})')
p.closePort()
if fail:
raise SystemExit('\nEEPROM ์ฐ๊ธฐ ์คํจ. JSON์ ๊ฑด๋๋ฆฌ์ง ์์์ต๋๋ค. '
'1-3์ ์ ๋จผ์ ์คํํ์ธ์.')
# 3. JSON (๋ฐฑ์
๋จผ์ )
cal.with_name(cal.name + '.bak').write_text(cal.read_text())
data[JOINT].update(homing_offset=HOMING_OFFSET, range_min=MIN_POS, range_max=MAX_POS)
cal.write_text(json.dumps(data, indent=4))
print(f'\n๋ฐฑ์
: {cal}.bak')
print(f'JSON: {data[JOINT]}')
EOF
EEPROM ์ฐ๊ธฐ๊ฐ ์คํจํ๋ฉด JSON์ ๊ฑด๋๋ฆฌ์ง ์๊ณ ๋ฉ์ถฅ๋๋ค. ํ์ชฝ๋ง ๋ฐ๋์ด ๋ถ์ผ์น๊ฐ ๋๋ ๊ฒ์ ๋ง๊ธฐ ์ํ ๊ฒ์ ๋๋ค.
ํ๋ณด๊ฐ 0๊ฐ๋ก ๋์ค๋ฉด ๊ทธ ๊ด์ ์ด๋ฆ์ด๋ ID๊ฐ ์บ๋ฆฌ๋ธ๋ ์ด์
ํ์ผ์ ์๋ ๊ฒ์
๋๋ค. ํ์ผ์ ์ง์ ์ด์ด ์ด๋ฆ์ ํ์ธํ์ธ์. ๋ฆฌ๋ ์์ teleoperators/ ์๋, ํ๋ก์ ์์ robots/ ์๋์ ์์ต๋๋ค.
find ~/.cache/huggingface/lerobot -path "*calibration*" -name "*.json"
3-4. ๊ฒ์ฆ
teleop์ ํ ๋ฒ ์คํํ์ธ์. ์นด๋ฉ๋ผ ์ต์ ์ ๋นผ๊ณ ๊ทธ๋ฆฝํผ๋ง ํ์ธํ๋ฉด ๋ฉ๋๋ค.
lerobot-teleoperate \
--robot.type=omx_follower \
--robot.port=/dev/ttyUSB0 \
--robot.id=omx_follower_arm \
--teleop.type=omx_leader \
--teleop.port=/dev/ttyUSB1 \
--teleop.id=omx_leader_arm
์๋ ๋ฌธ์ฅ์ด ๋จ์ง ์์ผ๋ฉด ์ฑ๊ณต์ ๋๋ค.
Mismatch between calibration values in the motor and the calibration file
Using factory default calibration values
๋จ๋ฉด EEPROM๊ณผ JSON์ด ์ฌ์ ํ ์ด๊ธ๋ ๊ฒ์
๋๋ค. 3-3์ ์ถ๋ ฅ์ ์คํจ๊ฐ ์์๋์ง, ๊ด์ ์ด๋ฆ์ด ๋ง๋์ง ํ์ธํ์ธ์.
์ข
๋ฃํ ๋ค motor_status.py๋ก EEPROM ๊ฐ์ด ์ ์ง๋๋์ง๋ ํ์ธํ์ธ์.
4. ๋ฆฌ๋์๊ณผ ํ๋ก์์์ด ์ ๋ง์ ๋ (LeRobot)
4-1. ์์น ๋น๊ต
๋ ์์ ๊ฐ์ ์์ธ๋ก ๋๊ณ ์ฌ์ธ์. ๊ทธ๋ฌ์ง ์์ผ๋ฉด ์ ๋ถ ์ด๊ธ๋ ๋ณด์ ๋๋ค. ๊ธฐ์ค ์์ธ๋ฅผ ์ก๊ธฐ ์ด๋ ค์ฐ๋ฉด ์์ชฝ ํ ํฌ๋ฅผ ๋๊ณ ์์ผ๋ก ์ต๋ํ ๋น์ทํ๊ฒ ๋ง์ถ ๋ค ์ฌ์ธ์.
์กฐ์ ํ ๋๋ง๋ค ๋ค์ ์ฌ์ผ ํ๋ฏ๋ก ํ์ผ๋ก ๋ง๋ค์ด ๋์ธ์.
# compare_pos.py
import sys, glob
from dynamixel_sdk import PortHandler, PacketHandler, COMM_SUCCESS
USAGE = """์ฌ์ฉ๋ฒ: python compare_pos.py [id_offset]
์: python compare_pos.py (ํ๋ก์ ID = ๋ฆฌ๋ ID + 10)
python compare_pos.py 10
ํฌํธ๋ ์๋์ผ๋ก ์ฐพ์ต๋๋ค. ๋ฆฌ๋๋ ID 1, ํ๋ก์๋ ID 1+offset ์ผ๋ก ํ๋ณํฉ๋๋ค."""
OFFSET = 10
if len(sys.argv) == 2:
try:
OFFSET = int(sys.argv[1])
except ValueError:
sys.exit(USAGE)
elif len(sys.argv) > 2:
sys.exit(USAGE)
JOINTS = 6
ph = PacketHandler(2.0)
def scan():
found = {}
for dev in sorted(glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')):
p = PortHandler(dev)
if not p.openPort():
continue
p.setBaudRate(1_000_000)
ids = {mid for mid in range(1, 31) if ph.ping(p, mid)[1] == COMM_SUCCESS}
p.closePort()
if ids:
found[dev] = ids
return found
def read_positions(dev, ids):
p = PortHandler(dev)
p.openPort()
p.setBaudRate(1_000_000)
out = {}
for mid in ids:
v, r, _ = ph.read4ByteTxRx(p, mid, 132)
if r == COMM_SUCCESS:
out[mid] = v - 2 ** 32 if v >= 2 ** 31 else v
p.closePort()
return out
ports = scan()
leader = next((d for d, ids in ports.items() if 1 in ids), None)
follower = next((d for d, ids in ports.items() if 1 + OFFSET in ids), None)
if leader is None or follower is None:
sys.exit(f'๋ฆฌ๋(id 1) ๋๋ ํ๋ก์(id {1 + OFFSET}) ๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค.\n'
f'๋ฐ๊ฒฌ๋ ํฌํธ: { {d: sorted(i) for d, i in ports.items()} }')
print(f'๋ฆฌ๋ {leader} / ํ๋ก์ {follower}\n')
L = read_positions(leader, [n for n in range(1, 1 + JOINTS) if n in ports[leader]])
F = read_positions(follower, [n for n in range(1 + OFFSET, 1 + OFFSET + JOINTS)
if n in ports[follower]])
print(f'{"joint":>5} {"leader":>8} {"follower":>9} {"diff":>7} ํ์ ')
for n in range(1, 1 + JOINTS):
lv, fv = L.get(n), F.get(n + OFFSET)
if lv is None or fv is None:
miss = 'leader' if lv is None else 'follower'
print(f'{n:>5} {"-" if lv is None else lv:>8} '
f'{"-" if fv is None else fv:>9} {"":>7} {miss} ์๋ต ์์')
continue
d = fv - lv
if abs(d) <= 10:
verdict = '์ ์'
elif abs(d) >= 1000:
verdict = '๋ชจํฐ ๊ด์ ์กฐ๋ฆฝ ๊ฐ๋ (4-3์ )'
else:
verdict = '์์ ์ฐจ์ด (4-2์ ์ฐธ๊ณ )'
print(f'{n:>5} {lv:>8} {fv:>9} {d:>7} {verdict}')
print('\n๊ทธ๋ฆฝํผ๋ ๋ชจ๋ธ์ด ๋ฌ๋ผ ๊ฐ๋ ํญ ์์ฒด๊ฐ ๋ค๋ฅผ ์ ์์ต๋๋ค. ๊ทธ ๊ฒฝ์ฐ 3์ฅ์ผ๋ก ๊ฐ์ธ์.')
๋ค์ ๋ช ๋ น์ shell์์ ์คํํ์ธ์.
python compare_pos.py
์ถ๋ ฅ ์์:
๋ฆฌ๋ /dev/ttyUSB1 / ํ๋ก์ /dev/ttyUSB0
joint leader follower diff ํ์
1 2048 2051 3 ์ ์
2 1024 1030 6 ์ ์
3 3072 3068 -4 ์ ์
4 1033 1125 92 ์์ ์ฐจ์ด (4-2์ ์ฐธ๊ณ )
5 2048 2044 -4 ์ ์
6 1187 2338 1151 ๋ชจํฐ ๊ด์ ์กฐ๋ฆฝ ๊ฐ๋ (4-3์ )
4-2. ์ฐจ์ด ํด์
| diff ํฌ๊ธฐ | ์์ธ | ๋์ |
|---|---|---|
| 10 ์ดํ | ์ ์ | ์กฐ์น ๋ถํ์ |
| ์์ญ ~ ์๋ฐฑ | ์์ ์ฐจ์ด | ์กฐ์นํ์ง ์์๋ ๋ฉ๋๋ค |
| 1000 ์ด์ | ๋ชจํฐ ๊ด์ ์กฐ๋ฆฝ ๊ฐ๋๊ฐ ๋ค๋ฆ | 4-3์ |
| ๊ทธ๋ฆฝํผ๋ง ํฐ ์ฐจ์ด | ๊ฐ๋ ํญ ์์ฒด๊ฐ ๋ค๋ฆ | 3์ฅ |
์์ญ์์ ์๋ฐฑ ์นด์ดํธ ์ฐจ์ด๋ ๋ณด์ ํ์ง ์๊ณ ์งํํ๊ณ ๋ฐ์ดํฐ ์์ง์ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
92 ์นด์ดํธ๊ฐ ์ฝ 8๋์ ๋๋ค. EEPROM ์ Homing_Offset ์ผ๋ก ์์ํ ์๋ ์์ง๋ง ์ํํด๋ณด์ง ์์์ผ๋ฏ๋ก ์ ์ฐจ๋ฅผ ์ฃ์ง ์์ต๋๋ค.
๊ทธ๋ฆฝํผ๋ ์ด ์ฅ์ผ๋ก ํด๊ฒฐ๋์ง ์์ต๋๋ค. ๋ฆฌ๋์ ํ๋ก์์ ๋ชจ๋ธ์ด ๋ฌ๋ผ ๊ฐ๋ ํญ ์์ฒด๊ฐ ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ ๋๋ค. ํญ์ด ๋ค๋ฅด๋ฉด ์์ ๋ณด์ ์ผ๋ก๋ ๋ง์ถ ์ ์๊ณ , ์์ชฝ์ ์ค์ ๊ฐ๋ ๋ฒ์๋ฅผ ์ฌ์ ์บ๋ฆฌ๋ธ๋ ์ด์ ์ ๋ฑ๋กํด์ผ ํฉ๋๋ค.
๊ทธ๋ฆฝํผ ๋ฌธ์ ๋ผ๋ฉด 3์ฅ์ผ๋ก ๊ฐ์ธ์.
4-3. ๋ชจํฐ ๊ด์ ๋ค์ ์กฐ๋ฆฝํ๊ธฐ
diff๊ฐ 1000์ ๋์ผ๋ฉด ๋ชจํฐ ๊ด์ ์ ์กฐ๋ฆฝ ๊ฐ๋๊ฐ ๋ค๋ฅธ ๊ฒ์ด๋ ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ค์ ๋ง์ถฐ์ผ ํฉ๋๋ค.
์์ชฝ ํ ํฌ๋ฅผ ๋๊ณ , ํ๋ก์์ ํด๋น ๋ชจํฐ์์ ๊ณ ์ ๋์ฌ๋ฅผ ํ๊ณ , ๊ด์ ๋ถํ์ ๋นผ์ ๋๋ ค ๋ค์ ๋ผ์ฐ๊ณ , ๋์ฌ๋ฅผ ์กฐ์ด๊ณ , compare_pos.py๋ก ๋ค์ ์ฌ์ธ์. diff๊ฐ ์ค์์ผ๋ฉด ๋ง๋ ๋ฐฉํฅ์ด๊ณ , ๋์์ผ๋ฉด ๋ฐ๋ ๋ฐฉํฅ์
๋๋ค. ํ ๋ฒ์ ๋ง์ถ๋ ค ํ์ง ๋ง๊ณ ์ฌ๋ฉด์ ์ ๊ทผํ์ธ์.
๋ฐฉํฅ๊ณผ ๊ฐ๋๋ ์ด๋ ๊ฒ ์ด๋ฆผ์ก์ ์ ์์ต๋๋ค. diff = follower - leader๊ฐ ์์๋ฉด ํ๋ก์๊ฐ ๋ฆฌ๋๋ณด๋ค ํฐ ๊ฐ์ ์์ผ๋ฏ๋ก ๊ฐ์ด ์์์ง๋ ๋ฐฉํฅ์ผ๋ก ๋๋ฆฝ๋๋ค. ์ผ๋ฐ์ ์ผ๋ก + ๋ฐฉํฅ์ด ์ค๋ฅธ์ชฝ์ด๋ ์์ชฝ, - ๋ฐฉํฅ์ด ์ผ์ชฝ์ด๋ ๋ค์ชฝ์
๋๋ค. ๊ฐ๋๋ diff ร 360 / 4096๋์ด๊ณ , diff๊ฐ 2048์ด๋ฉด 180๋์
๋๋ค. ๊ณ์ฐ์ด ํ๋ค๋ฉด AI์๊ฒ ๋ฌผ์ด๋ณด๋ ๊ฒ์ด ํธํฉ๋๋ค!!
๋ชจํฐ ์ถ๋ ฅ์ถ๊ณผ ๊ด์ ๋ถํ์ ๋์ฌ ๊ตฌ๋ฉ ์์น๊ฐ ์ ํด์ ธ ์์ด ์์์ ๊ฐ๋๋ก ์กฐ๋ฆฝํ ์ ์์ต๋๋ค. ๊ฐ์ฅ ๊ฐ๊น์ด ์์น๋ก ๋ผ์ฐ๋ฉด ์์ญ์์ ์๋ฐฑ ์นด์ดํธ๊ฐ ๋จ๋๋ฐ, ๊ทธ ์ ๋๋ 4-2์ ๋๋ก ๊ทธ๋ฅ ๋๋ฉด ๋ฉ๋๋ค.
4-4. ํ๋ก์ ๊ทธ๋ฆฝํผ๊ฐ ์ถฉ๋ถํ ์ด๋ฆฌ์ง ์์ ๋
teleop ๋ก๊ทธ์์ ๋ฆฌ๋์ ์ ๊ทํ ์ถ๋ ฅ๊ฐ์ด 0~100 ๋ฒ์์ธ๋ฐ 62 ๊ทผ์ฒ์์ ์ฒ์ฅ์ด๋ฉด ์ด ๊ฒฝ์ฐ์ ๋๋ค.
t=44.59 lead 62.4 goal 2338
LeRobot์ด ๋ฆฌ๋ ๊ทธ๋ฆฝํผ์ ํ ํฌ๋ฅผ ๊ฑธ์ด ์ ๊ทํ ์์น gripper_open_pos(๊ธฐ๋ณธ๊ฐ 60)๋ก ๋๊ณ ๊ฐ๊ธฐ ๋๋ฌธ์ ํ๋ก์์ ์ ๋ฌ๋๋ ์ต๋ ๋ชฉํ๊ฐ ์ ํ๋ฉ๋๋ค. ๋ฆฌ๋ ๊ทธ๋ฆฝํผ๊ฐ ์์ํ ๋ ํ ๋ฒ์ด์ง๋ ๊ฒ๋ ์คํ๋ง์ด ์๋๋ผ ์ด ํ ํฌ ๋๋ฌธ์
๋๋ค.
range_max๋ฅผ ๋ํ๋ ํด๊ฒฐ๋์ง ์์ผ๋ฏ๋ก, teleop๊ณผ record ๋ช
๋ น์ ์๋ ์ต์
์ ์ถ๊ฐํ์ธ์.
--teleop.gripper_open_pos=95
์์ชฝ ๋ชจ๋์ ๋ฃ์ด์ผ ํฉ๋๋ค. ํ์ชฝ์๋ง ๋ฃ์ผ๋ฉด ๋ค๋ฅธ ์ชฝ์์ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ๋์๊ฐ๋๋ค.
5. ๊ทธ ๋ฐ์ ์์ฃผ ๊ฑธ๋ฆฌ๋ ๊ฒ
์ต์ ์ด ์กฐ์ฉํ ๋ฌด์๋ ๋
์ญ์ฌ๋์ ๋ค์ #์ด ์ค๋ฉด ๊ทธ ์ค์ ๋๋จธ์ง์ ์ค ์ด์ด์ฃผ๊ธฐ๊น์ง ์ฃผ์์ผ๋ก ๋ฒ๋ ค์ง๋ฏ๋ก, ๋ช
๋ น์ด ๊ทธ ์ง์ ์์ ๋๋๊ณ ๋ค์ ์ต์
๋ค์ ๊ธฐ๋ณธ๊ฐ์ด ์ฐ์
๋๋ค. ์ญ์ฌ๋์ ๋ค์ ๊ณต๋ฐฑ๋ ๊ฐ์ ๋ฌธ์ ๋ฅผ ์ผ์ผํต๋๋ค.
์๋๋ ์๋ชป๋ ์์์
๋๋ค. ์ด๋ ๊ฒ ์ฐ๋ฉด reset_time_s๋ถํฐ ์ ๋ถ ๋ฌด์๋ฉ๋๋ค.
--dataset.episode_time_s=60 \ # ํ ๋์์ ์ด ์๊ฐ
--dataset.reset_time_s=10 \
--dataset.push_to_hub=false
์ฃผ์์ ๋ช ๋ น ๋ธ๋ก ์๋ก ๋ถ๋ฆฌํ๊ณ , ๋ณต์ฌํด ๋ถ์ผ ๋ ์ค ๋ ๊ณต๋ฐฑ์ ํ์ธํ์ธ์. ์ต์ ์ด ์ ๋ฌ๋๋์ง๋ ์คํ ์งํ์ ์ค์ ๋คํ๋ก ํ์ธํฉ๋๋ค.
์ ์ด ๋ฃจํ๊ฐ ๋ชฉํ FPS๋ฅผ ๋ชป ๋ง์ถ ๋ (LeRobot)
Control loop is running slower (22.0 Hz) than the target FPS (30 Hz).
์ํผ์๋ ์ข
๋ฃ ์ ์ถ๋ ฅ๋๋ cadence summary์์ telemetry๊ฐ ์์
์๊ฐ์ ํฐ ๋น์ค์ ์ฐจ์งํ๋ฉด --display_data=true์ rerun ์๊ฐํ๊ฐ ์์ธ์
๋๋ค. GPU๊ฐ ์๋ ํ๊ฒฝ์์๋ ์ํํธ์จ์ด ๋ ๋๋ง์ผ๋ก ๋์ ๋น์ฉ์ด ํฝ๋๋ค.
๋ณธ ๋ นํ์์๋ ์๋ ์ต์ ์ผ๋ก ๋์ธ์.
--display_data=false
์นด๋ฉ๋ผ๊ฐ MJPG๋ก ์ค์ ๋์ง ์์ ๋ (LeRobot)
failed to set fourcc=mjpg (actual=YUYV, success=True). Continuing with default format.
์๋ฌธ์๋ ๊ฑฐ๋ถ๋๋ฏ๋ก ๋๋ฌธ์ MJPG์ backend: V4L2๋ฅผ ํจ๊ป ์ง์ ํ์ธ์.
๊ทธ ์ ์ ์นด๋ฉ๋ผ๊ฐ ํด๋น ํด์๋๋ฅผ MJPG๋ก ์ง์ํ๋์ง ํ์ธํฉ๋๋ค. ๋ค์ ๋ช ๋ น์ shell์์ ์คํํ์ธ์.
v4l2-ctl --device=/dev/video0 --list-formats-ext
์ ์ฉ ํ์ธ์ ์ค์ ๋คํ์ 'fourcc': 'MJPG'์ 'backend': <Cv2Backends.V4L2: 200>๋ก ํฉ๋๋ค.
YUYV๋ ๋น์์ถ์ด๋ผ 640x480 30fps์ ์ฝ 18 MB/s๋ฅผ ์๊ตฌํ๋ฏ๋ก, USB2์์ ์นด๋ฉ๋ผ ๋ ๋๋ฅผ ๋๋ฆฌ๋ฉด ํ๋ ์์ด ๋จ์ด์ง๋๋ค.
6. ์ฐธ๊ณ : ์ด ๋ก๋ด์์ ์ธก์ ๋ ๊ฐ
๊ทธ๋๋ก ์ฐ์ง ๋ง์ธ์. ์กฐ๋ฆฝ ๊ณต์ฐจ์ ๋์ฌ ์กฐ๋ฆฝ ์ํ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋๋ค. ๊ฐ์ ์ ์ฐจ๋ก ๋ณธ์ธ ๋ก๋ด์ ์ฌ์ธ์. ์ด๋ค ๊ฐ์ ์ฌ์ผ ํ๊ณ ์ ์ ๋ฒ์๊ฐ ์ด๋ ์ ๋์ธ์ง ์ฐธ๊ณ ํ๊ธฐ ์ํ ์๋ฃ์ ๋๋ค.
๊ตฌ์ฑ
๋ฆฌ๋ ์ ID 16, ํ๋ก์ ์ ID 1116์
๋๋ค. ๋ชจ๋ธ์ ๋ฆฌ๋ 15๊ฐ 1200, ๋ฆฌ๋ 6์ด 1190, ํ๋ก์ 1113์ด 1060, ํ๋ก์ 14~16์ด 1200์
๋๋ค.
์ต์ข ์ค์ ๊ฐ
ํ๋ก์ ๊ทธ๋ฆฝํผ ID 16, Operating Mode 5(Current-Based Position Control)์ ๋๋ค.
| ๋ ์ง์คํฐ | ์ฃผ์ | ๊ฐ | ์์ญ |
|---|---|---|---|
| Current_Limit | 38 | 600 | EEPROM |
| Min_Position_Limit | 52 | 2123 | EEPROM |
| Max_Position_Limit | 48 | 2873 | EEPROM |
| Homing_Offset | 20 | 0 | EEPROM |
| Goal_Current (์ ์ง) | 102 | 100 | RAM |
| Goal_Current (์ด๋ ๋ถ์คํธ) | 102 | 200 | RAM |
| Position_P_Gain | 84 | 800 | RAM |
| Position_I_Gain | 82 | 0 | RAM |
| Position_D_Gain | 80 | 0 | RAM |
๋ฆฌ๋ ๊ทธ๋ฆฝํผ ID 6์ Homing_Offset 100, ๋ฒ์ 917 ~ 2577์ ๋๋ค.
๋ช
๋ น์ค ์ต์
์ --teleop.gripper_open_pos=95๋ฅผ teleop๊ณผ record ์์ชฝ์, ๋ณธ ๋
นํ์์ --display_data=false๋ฅผ ์ผ์ต๋๋ค.
Overload๊ฐ ์ ๋ฅ ์ด๊ณผ๊ฐ ์๋๋ผ๋ ๊ทผ๊ฑฐ (2์ฅ)
Current_Limit 600, Goal_Current 250 ์ค์ ์์ ์ ๋ฅ๊ฐ 162์ผ ๋ Overload๊ฐ ๋ฐ์ํ์ต๋๋ค. PWM์ด ์ํ 885์ ๋ถ์ ์ฑ 2.5์ด ์ง์๋ ์งํ์ ๋๋ค.
t=12.19 goal 2336 pos 2340 cur -8 pwm -6 ์ ์
t=12.79 goal 2295 pos 2338 cur -105 pwm -363 ๋ฌผ์ฒด์ ๋ฟ์
t=13.36 goal 1999 pos 2338 cur -162 pwm -885 PWM ์ํ ๊ณ ์
(2.5์ด๊ฐ pwm -885, cur -162, pos 2338 ์ ๋ถ ๊ณ ์ )
t=15.82 err 32
Goal_Current๋ฅผ 100์ผ๋ก ๋ด๋ฆฐ ๋ค 72์ด๊ฐ ์๋ฌ ์์ด ์ ์ง๋์ต๋๋ค. ์ ๋ฅ๊ฐ 101์์ ๊ท์ ๋๊ณ PWM์ด 285์ ๋จธ๋ฌผ๋ ์ต๋๋ค.
t=43.99 goal 2333 pos 2300 cur 101 pwm 285
์ธก์ ๋ ์ ๋ฅ ์ฒ์ฅ์ ์ฝ 160, PWM_Limit์ 885์์ต๋๋ค. Goal_Current 150์ ์ฒ์ฅ์ ๋๋ฌด ๊ฐ๊น์ ๋ช ์ด ๋ง์ ๊ณผ๋ถํ๊ฐ ๋ฌ๊ณ , 100์ ์์ ์ ์ด์์ต๋๋ค. ์จ๋๋ 72์ด ๋์ 29๋์์ 32๋๋ก ์ฌ๋์ต๋๋ค.
Position ๊ฒ์ธ (2-2์ )
๊ธฐ๋ณธ๊ฐ์ด P 250, I 0, D 600์ด์์ต๋๋ค. D๊ฐ P์ 2.4๋ฐฐ์ธ ์ํ์์ Goal_Current๋ฅผ 300์์ 500์ผ๋ก ์ฌ๋ ค๋ ์ ๋ฅ๊ฐ 300์ ๋ชป ๋ฏธ์น๊ณ 2100๋์์ ๋งํ์ต๋๋ค.
| Goal_Current | ์ ๋ฅ ์ต๋ | ๊ฒฐ๊ณผ |
|---|---|---|
| 300 | 161 | 2188์์ ๋งํ |
| 500 | 160 | 2110์์ ๋งํ |
P 800, I 0, D 0์ผ๋ก ๋ฐ๊พผ ๋ค ์์น ์ค์ฐจ๊ฐ ๋ช ์นด์ดํธ ์ด๋ด๋ก ์ค๊ณ ์ ๋ฅ๊ฐ 2~60์ ๋จธ๋ฌผ๋ ์ต๋๋ค. P 1500์ ์คํ๋ ค ๋๋น ์ ธ 2450 ๋ชฉํ์์ 2344์ ๋ฉ์ถ๊ณ ์ ๋ฅ๊ฐ 160๊น์ง ์ฌ๋์ต๋๋ค.
๊ธฐ๊ตฌ ๊ฑธ๋ฆผ (2-4์ )
์์ ๋กญ๊ฒ ์์ง์ผ ๋ 3~18 mA, ๊ฑธ๋ ธ์ ๋ 150 mA ์ด์์ด์์ต๋๋ค. ์์ผ๋ก๋ ์ ํ ๋นก๋นกํ๊ฒ ๋๊ปด์ง์ง ์์์ต๋๋ค. 100 mA๊ฐ ์ด ๋ชจํฐ ์ต๋ ํ ํฌ์ 7% ์์ค์ด๊ธฐ ๋๋ฌธ์ ๋๋ค.
๊ทธ๋ฆฝํผ ์ธก ๋์ฌ ํ๋๊ฐ ์ด๋ฆผ์ ๋ง๊ณ ์์๊ณ , ์กฐ์ธ ๋ค ์ด๋ฆผ ๋์ด 2573์์ 2902๋ก 330 ์นด์ดํธ ๋์ด์ก์ต๋๋ค.
๋ฆฌ๋์ ํ๋ก์ ๋ถ์ผ์น (4์ฅ)
14๋ฒ ๋ชจํฐ๊ฐ ๋์๋๋ 4๋ฒ๊ณผ ์ด๊ธ๋ ์์์ต๋๋ค. ๋ชจํฐ ๊ด์ ์ 180๋ ๋๋ ค ๋ค์ ์กฐ๋ฆฝํด ์ฐจ์ด๋ฅผ 92 ์นด์ดํธ๊น์ง ์ค์๊ณ , ๋จ์ 92๋ ๋ณด์ ํ์ง ์๊ณ ์งํํ์ต๋๋ค. ๊ทธ ์ํ๋ก 50 ์ํผ์๋๋ฅผ ๋ นํํ์ผ๋ฉฐ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
๊ทธ๋ฆฝํผ๋ ๋ชจ๋ธ์ด ๋ฌ๋ผ ๊ฐ๋ ํญ ์์ฒด๊ฐ ๋ฌ๋์ต๋๋ค.
| ๋ชจํฐ | ๋ชจ๋ธ | ๋ฒ์ | ํญ |
|---|---|---|---|
| ๋ฆฌ๋ 6 | 1190 | 897 ~ 2597 | 1700 |
| ํ๋ก์ 16 | 1200 | 2083 ~ 2602 | 519 |
์ ์ด ๋ฃจํ (5์ฅ)
--display_data=true์ผ ๋ ์์
์๊ฐ 13.9 ms, false์ผ ๋ 5.2 ms์์ต๋๋ค. ๋ ๋ค ํฑ๋น ํ๊ท 28.4 ms์ ์ฌ์ ๊ฐ ์๊ฒผ์ต๋๋ค.
์ ์ฅ์์ ๋ค๋ฅธ ํ์ผ
์ฐ๋ฆฌ ์ค์ ์ ๊ทธ๋๋ก ๊ฐ์ ธ๋ค ์ฐ๋ ค๋ฉด ์๋ ๋ ํ์ผ์ ์ธ ์ ์์ต๋๋ค. ๋ก๋ด์ด ๋ค๋ฅด๋ฉด ๊ฐ์ด ๋ง์ง ์์ผ๋ ์ฐธ๊ณ ์ฉ์ผ๋ก๋ง ๋ณด์ธ์.
omx_patches.diff ๋ LeRobot ์์ค ์์ ๋ด์ญ์
๋๋ค. 2-5์ ๊ณผ 2-7์ ์ ๋ด์ฉ์ด ๋ค์ด ์์ต๋๋ค.
cd ~/lerobot
git apply /๋ด๋ ค๋ฐ์/๊ฒฝ๋ก/omx_patches.diff
git diff --stat
omx_calibration.tar.gz ๋ ์บ๋ฆฌ๋ธ๋ ์ด์
๋๋ ํฐ๋ฆฌ ์ ์ฒด์
๋๋ค. ์์นด์ด๋ธ ๋ฃจํธ๊ฐ calibration/ ์ด๋ฏ๋ก ๊ทธ ์์ ๊ฒฝ๋ก์ ํ์ด์ผ ํฉ๋๋ค.
tar xzf omx_calibration.tar.gz -C ~/.cache/huggingface/lerobot
omx_calibration/ ์๋ ๊ฐ์ ๋ด์ฉ์ด ํผ์ณ์ ธ ์์ด ์น์์ ๋ฐ๋ก ์ฝ์ ์ ์์ต๋๋ค. .bak ๊ณผ .bak2 ๋ ๊ณ ์น๊ธฐ ์ ์ํ์
๋๋ค.
7. ์ฐธ๊ณ ๋ฉ๋ชจ
Dynamixel์ RS-485 ๋ฐ์ด์ค ํต์ ์ด๋ผ ํ ์ ์ผ๋ก ์ก์์ ์ ๋ฒ๊ฐ์ ํ๋ฏ๋ก ๋ง์คํฐ๋ ํ๋์ฌ์ผ ํฉ๋๋ค. ์ ์ด ํ๋ก๊ทธ๋จ์ด ๋์๊ฐ๋ ์ค์ ๋ณ๋ ํฐ๋ฏธ๋์์ ๊ฐ์ ํฌํธ๋ฅผ ์ฝ์ผ๋ฉด ์์ชฝ ํจํท์ด ์์ฌ ์ฒดํฌ์ฌ ์ค๋ฅ์ ์ฐ๋ ๊ธฐ๊ฐ์ด ๋์ต๋๋ค. ๋ฆฌ๋ ์ค๋ tty๋ฅผ ์ฌ๋ฌ ํ๋ก์ธ์ค๊ฐ ์ด๋๋ก ํ์ฉํ๋ฏ๋ก ํฌํธ ์ด๊ธฐ๋ ์ฑ๊ณตํ๊ณ ๊ทธ ๋ค์์ ๊นจ์ง๋๋ค. ์ค์๊ฐ ๋ก๊ทธ๊ฐ ํ์ํ๋ฉด ์ด๋ฏธ ๋ฒ์ค๋ฅผ ์ฅ ํ๋ก์ธ์ค ์์์ ์ฝ์ผ์ธ์.
๋ ์ง์คํฐ๋ RAM๊ณผ EEPROM์ผ๋ก ๋๋ฉ๋๋ค. RAM ๊ฐ์ ์ ์์ ๋๊ฑฐ๋ rebootํ๋ฉด ์ด๊ธฐํ๋๊ณ , EEPROM ๊ฐ์ ์ ์ง๋ฉ๋๋ค. EEPROM์ ์ฐ๋ ค๋ฉด Torque_Enable=0์ด๊ณ Hardware Error๊ฐ 0์ด์ด์ผ ํฉ๋๋ค.
| ๋ ์ง์คํฐ | ์ฃผ์ | ์์ญ |
|---|---|---|
| Position_P/I/D_Gain | 84 / 82 / 80 | RAM |
| Goal_Current | 102 | RAM |
| Current_Limit | 38 | EEPROM |
| Min/Max_Position_Limit | 52 / 48 | EEPROM |
| Homing_Offset | 20 | EEPROM |
๋ ์ง์คํฐ ์ฐ๊ธฐ๋ ๋ฐํ๊ฐ๊ณผ ์ฝ๊ธฐ๊ฐ์ ๋ ๋ค ํ์ธํ์ธ์. ํ์ฉ ๋ฒ์๋ฅผ ๋ฒ์ด๋ ๊ฐ์ ์์ธ ์์ด ๊ฑฐ๋ถ๋๊ณ ์ด์ ๊ฐ์ด ๋จ์ผ๋ฏ๋ก, ์ฝ๊ธฐ๋ง์ผ๋ก๋ "์ํ์ผ๋ก ์๋ ธ๋ค"์ "์ฐ๊ธฐ๊ฐ ๊ฑฐ๋ถ๋๋ค"๋ฅผ ๊ตฌ๋ถํ ์ ์์ต๋๋ค.
Hardware_Error_Status(์ฃผ์ 70)๋ ํ ๋ฒ ๋จ๋ฉด reboot์ด๋ ์ ์์ ๋ ๋๊น์ง ์ ์ง๋๋ฏ๋ก, ๋งค ํ ์คํธ ์์ ์ ์ ๋ช ์์ ์ผ๋ก ์ง์ฐ๊ณ ์ง์์ก๋์ง ํ์ธํ์ธ์. ์จ๋๊ฐ ์ ์์ธ๋ฐ Overload๊ฐ ๋ ์์ผ๋ฉด ๊ทธ ์๋ฌ๋ ๋ฐฉ๊ธ ์๊ธด ๊ฒ์ด ์๋๋๋ค.
์๊ณ๊ฐ์ ํ์ ํ ๋๋ ํ๊ท ์ด ์๋๋ผ ํผํฌ๊ฐ ํ์ํ๊ณ , ์ํ๋ง ๊ฐ๊ฒฉ์ด ํ์์ ์๊ฐ ๊ท๋ชจ๋ณด๋ค ์งง์์ผ ํฉ๋๋ค. 1.2์ด ๊ฐ๊ฒฉ์์๋ ๋ณด์ด์ง ์๋ PWM ํฌํ๊ฐ 40 ms ๊ฐ๊ฒฉ์์ ๋๋ฌ๋ฌ์ต๋๋ค.