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์žฅ

์žฅ ๊ตฌ์„ฑ

  1. ์ค€๋น„ (๋ชจํ„ฐ ์ƒํƒœ ์ฝ๊ธฐ, ์–ด๋””๋กœ ๊ฐˆ์ง€ ํŒ์ •, ์—๋Ÿฌ ์ง€์šฐ๊ณ  ํ† ํฌ ๋„๊ธฐ)
  2. ๊ทธ๋ฆฝํผ์— Overload(์—๋Ÿฌ 32)๊ฐ€ ๋ฐ˜๋ณต๋  ๋•Œ
  3. ๊ฐ€๋™ ๋ฒ”์œ„๋ฅผ ์ธก์ •ํ•ด ๋ฐ˜์˜ํ•˜๊ธฐ
  4. ๋ฆฌ๋”์•”๊ณผ ํŒ”๋กœ์›Œ์•”์ด ์•ˆ ๋งž์„ ๋•Œ (LeRobot)
  5. ๊ทธ ๋ฐ–์— ์ž์ฃผ ๊ฑธ๋ฆฌ๋Š” ๊ฒƒ
  6. ์ฐธ๊ณ : ์ด ๋กœ๋ด‡์—์„œ ์ธก์ •๋œ ๊ฐ’
  7. ์ฐธ๊ณ  ๋ฉ”๋ชจ

์ด ์ €์žฅ์†Œ

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 ๊ฐ„๊ฒฉ์—์„œ ๋“œ๋Ÿฌ๋‚ฌ์Šต๋‹ˆ๋‹ค.

Downloads last month

-

Downloads are not tracked for this model. How to track
Video Preview
loading