본문으로 건너뛰기

내 서보 드라이브 붙이기

로봇팔 완제품 없이 CiA 402 서보 드라이브를 EtherCAT 버스에 붙여 돌리는 방법입니다. 드라이브는 YAML 파일 하나(드라이브 모델 파일)로 정의합니다. C++ 코드를 쓰지 않고, UI 도 없습니다. 명령줄과 ROS 2 인터페이스로 다룹니다.

대상은 CoE(CANopen over EtherCAT)로 CiA 402 를 말하는 드라이브입니다. 축마다 파일의 mode 로 운전 모드를 하나 고르고, 한 버스에 섞어도 됩니다.

mode운전 모드명령 단위명령을 보내는 곳
csp위치rad궤적 컨트롤러 (Bus.move_to, FollowJointTrajectory)
csv속도rad/s스트림 컨트롤러 (Bus.stream, 토픽)
cst토크Nm스트림 컨트롤러 (Bus.stream, 토픽)

이 페이지에서 csv·cst 축을 스트림 축이라고 부릅니다. 파일의 키는 드라이브 모델 파일에 있습니다. Python API, ROS 인터페이스, 오류 조건은 드라이브 버스 API에 있습니다. 실기로 확인한 범위는 맨 아래에 있습니다.

명령은 plem drive <동사> 입니다. 동사는 check 와 run 두 개이고, plem drive --help 가 목록을 보여 줍니다. plem 은 이 명령을 plem-drive 도구에 그대로 넘깁니다. 드라이브 패키지(plem-drive)만 설치한 기기에도 plem drive 가 있고(plem 은 함께 설치되는 plem-cli 패키지가 줍니다), plem-drive check …, plem-drive run … 으로 직접 불러도 같습니다. 소스 빌드도 워크스페이스를 source 하면(source install/setup.bash) 같은 이름으로 씁니다. 도구는 패키지 설치 경로의 bin/ 에 설치됩니다.

처음 여는 축은 중력 하중이 없는 축으로

아래 절차는 중립 명령(토크 축 0 Nm)으로 브레이크를 해제합니다. 드라이브 계층에는 중력 보상이 없습니다. 하중이 걸린 토크 축은 브레이크가 풀리는 즉시 떨어집니다. 위치·속도 축은 드라이브 안의 루프가 버티지만, 그 거동은 드라이브마다 다르고 실기로 확인하지 않았습니다. 첫 시험은 중력 하중이 없는 축에서, 비상정지에 손이 닿는 자리에서 하세요.

1. 예제를 복사합니다​

설치에 들어 있는 예제(CiA 402 서보 드라이브 한 축)를 복사합니다.

mkdir -p "$HOME/drives"
cp "$(ros2 pkg prefix --share plem_drive_nodes)/examples/example_drive.yaml" "$HOME/drives/my_drive.yaml"

예제의 값은 지어낸 합성 값입니다. 어떤 드라이브에서도 그대로 쓰면 안 됩니다. 모양만 유지하고 값은 전부 자기 드라이브의 것으로 바꿉니다.

코딩 에이전트에게 맡길 수도 있습니다. 패키지에 에이전트 스킬 add-drive 가 함께 설치됩니다. 이 스킬은 복사한 파일을 아래 2절까지 이어 갑니다: 읽기 전용 ethercat 명령만 쓰도록 지시받고 드라이브를 읽으며(에이전트가 실행하려는 명령은 직접 확인합니다), 드라이브·모터·감속기를 물어 그 문서를 읽고, 값마다 출처를 옆 주석으로 달아 쓰고, 결정할 값(한계·정지 감속·주기)은 모두 사용자에게 묻고, 스키마의 전제(축마다 드라이브가 직접 여닫는 유지 브레이크, 절대 엔코더 — 스키마 §4.4)가 맞지 않으면 그 자리에서 멈추고, 근거가 없는 값은 키를 빼고 # OPEN 으로 표시합니다 — 필수 키가 빠진 파일은 거부됩니다. 파일이 통과하거나 열린 키에서만 거부될 때까지 plem drive check 를 돌리고(나머지는 임시 사본으로 검사), 벤치(8절)는 사용자 몫으로 남깁니다. Claude Code 에서는 스킬 폴더에 링크합니다.

mkdir -p ~/.claude/skills
ln -s "$(ros2 pkg prefix plem_drive_nodes)/share/skills/add-drive" ~/.claude/skills/

plem code 는 plem-code 와 plem-drive 가 함께 설치된 기기에서 설정 없이 이 스킬을 찾습니다(bare·safe 모드 제외). 스킬 목록은 plem code 가 시작할 때 읽으므로, 패키지를 설치하거나 갱신한 뒤에는 plem code 를 다시 시작합니다. 다른 에이전트에는 같은 SKILL.md 를 지시문으로 줍니다. 쓴 값은 직접 확인합니다 — 주석이 출처를 알려 주고, 틀린 값도 로드는 됩니다.

A6B 두 축 예제​

실제 드라이브로 쓴 예제도 같은 자리에 있습니다. Panasonic MINAS A6B 두 대(MDDLN55BE, 모터 MSMF102L1H5)의 위치(csp) 축 두 개를 정의한 a6b_pair 폴더입니다. 이 문서에서 「A6B 예제」는 이 폴더의 drive.yaml 을 말합니다. 폴더에는 버스 이름이 a6b_pair 인 드라이브 모델 파일 drive.yaml, 두 축을 거울처럼 맞물려 움직이는 demo.py, PlotJuggler 레이아웃 plotjuggler.xml, 순서와 안전 수칙을 적은 README.md 가 있습니다. 승인을 파일에 기록하므로 쓸 수 있는 곳으로 폴더째 복사합니다.

cp -r "$(ros2 pkg prefix --share plem_drive_nodes)/examples/a6b_pair" "$HOME/drives/"
cd "$HOME/drives/a6b_pair"

파일에 든 설정과 근거는 키마다 파일 주석에 있습니다.

  • DC 동기: dc_sync0, sync0_shift_ns: 300000, write_cycle_time: true.
  • 21:1 감속기: gear_ratio: 21.0. 축 값은 감속기 출력축 기준입니다.
  • 정지 방식 SDO: 0x605A = 2, 0x605C = 1, 0x605E = 2. 정지 감속 0x6084, 0x6085.
  • 축마다의 정지 감속 limits.acceleration_rad_s2.
  • 드라이브 저장값 대조 stored_sdo.

정지 감속 3200 rad/s²(모터축)는 감속기 출력축을 풀어 둔 무부하 벤치에서 고른 값입니다. 하중이 있으면 정지 토크와 회생 에너지가 커집니다. 설비에 맞게 정하고, 첫 정지는 하중 없이 해 보세요. 이 파일은 축 1 의 아래 위치 한계만 바꾼 사본으로 벤치, 승인, 운전했습니다. 0x605A = 2 가 실제로 작동하는 Quick stop 단계는 확인하지 않았습니다.

두 드라이브는 같은 드라이브라서 링 위치 목록(ring_position: [0, 1])으로 한 항목에 적었고, 축 이름·flip·zero_offset_count 만 드라이브마다 하나씩 적습니다. 다른 A6B 모델이나 모터면 product_code 와 motor_rated_torque_nm 이 바뀝니다. 다른 감속기면 gear_ratio 와 모든 축 값이 바뀝니다. mode_display 와 zero_offset_count 는 자기 드라이브에서 잽니다.

두 축은 4절로 버스를 띄운 뒤 5절처럼 움직입니다. 처음에는 velocity_scale, acceleration_scale 을 낮춰 시작하고, --monitor 와 PlotJuggler 를 보면서 올립니다.

두 출력축을 거울처럼 맞물린 장비라면 plem drive run drive.yaml --plot --monitor 로 버스와 그래프, 타이밍 요약을 띄우고, 다른 터미널에서 python3 demo.py 를 실행합니다. demo.py 는 쉬고 있을 때의 두 위치 합 S 를 지키도록 axis1 = S − axis0 으로 두 축을 함께 왕복시킵니다. 쉬던 자리에서 양쪽으로 5 rad 씩 10번, 저크를 제한한 SCurve 로(기본 사다리꼴과 달리 가속이 계단으로 바뀌지 않고 0.1 s 에 걸쳐 오른다) 파일 속도·가속 한계의 40 %(출력에서 약 8 rad/s)로 오간 뒤 제자리로 돌아와 멈춥니다. 두 축이 맞게 도는지는 그래프의 position_mismatch·opposing_torque 로 봅니다. 쉬던 자리에서 양쪽 위치 한계까지 5 rad 이상 여유가 있어야 하며, 없으면 move_to 가 움직이기 전에 거부합니다. demo.py 의 SCALE 은 0.85 쯤(속도 한계 19.9 rad/s 중 17.4 rad/s)을 넘기지 않습니다.

벤치는 모든 위치 축을 한 이동으로 함께 움직이고, 축마다 한계까지 여유가 더 큰 쪽으로 출력축 기준 최대 0.5 rad 갑니다. 맞물린 두 축에서 이 이동이 서로 반대로 가는 것은 한계와 쉬는 위치가 그렇게 만들 때뿐이고, 아니면 두 드라이브가 맞물림을 통해 서로 밉니다. 그러니 맞물림을 풀고 벤치하거나, 한 축의 한계를 좁혀 두 이동이 거울처럼 가게 합니다. 맞물린 장비에서는 axis1 의 아래 위치 한계를 −46 rad 로 좁힌 사본으로 벤치했습니다(실기 근거).

2. 값을 채웁니다​

키는 전부 필수입니다. 빠뜨리면 파일이 거부되고, 값이 없다는 것도 null 이나 [] 로 적습니다. 빼도 되는 곳(cia402_profile, 스위치로 끈 기능의 하위 블록, stored_sdo, approval, dc.write_cycle_time, 값이 하나뿐인 위치·속도 unit, 매핑에 0x603F 가 있을 때의 error_code_pdo)은 드라이브 모델 파일 §1 에 있습니다. 똑같은 드라이브 여러 대는 링 위치를 목록으로 적어 한 항목으로 씁니다(§4.1).

채울 것어디서 얻나
슬레이브 식별과 링 위치 (vendor_id, product_code, ring_position)ethercat slaves -v
PDO 매핑 (pdo_mapping)scanned 는 드라이브가 켜질 때의 매핑을 그대로 씁니다(예제의 형태). standard 는 PLEM 이 쓰는 CiA 402 객체만으로 매핑을 만들어 PREOP 에 씁니다 — 켜질 때 매핑에 필요한 항목이 없고 매핑 변경을 받는 드라이브용이며, 실기에서는 아직 돌려 보지 않았습니다. 특정 PDO 가 필요한 드라이브는 매핑을 선언하고, 코어가 PREOP 에 씁니다. 선언은 ESI 파일이나 ethercat pdos 덤프에서 옮기거나, scanned 로 한 번 기동한 로그의 매핑 텍스트를 붙입니다
버스 (bus)버스 이름 name(영문, 숫자, _, 숫자로 시작하지 않음. 버스는 /<name> 으로 뜹니다), 제어 주기 cycle_time_ns(ns, 1000 의 배수), 동기 방식 sync_mode(sm 또는 dc_sync0)
축 이름 (name)직접 정합니다. 소문자, 숫자, 밑줄로 31바이트 이하, 파일 안에서 겹치지 않게
채널 (channel)드라이브 안의 몇 번째 CiA 402 축인지 (1~8)
운전 모드 (mode)cst, csp, csv 중 하나. 드라이브 매뉴얼로 지원을 확인합니다. 매핑에 그 모드의 목표 객체(0x6071, 0x607A, 0x60FF)가 있어야 기동합니다
0x6061 반영 시점 (mode_display)자기 드라이브에서 잽니다. 기본값이 없습니다. 예제의 pre_enable 은 지어낸 값입니다
기계 상수 (factors)드라이브 매뉴얼과 모터 명판. 감속비 gear_ratio 와 엔코더 분해능 encoder_resolution 은 축마다 한 번 적습니다. 매뉴얼이 0x6071, 0x6077 을 정격 토크의 ‰ 로 적으면 토크 단위는 rated_per_mille(정격 토크 motor_rated_torque_nm, Nm), 전류 카운트로 적으면 scaled_current(counts_per_amp, 토크 상수)
속도 단위 배율 (velocity_factor)드라이브 매뉴얼의 0x606C 단위. 틀려도 파일은 통과하고, 속도 한계와 joint_states 속도가 조용히 어긋납니다
방향과 영점 (flip, zero_offset_count)실측합니다. 링 위치를 목록으로 적은 항목이면 드라이브마다 하나씩 적습니다. 영점은 절대값 엔코더가 전원을 넣을 때마다 같은 위치를 보고할 때만 뜻이 있습니다(지원하지 않는 것)
안전 한계 (limits)선언하는 사람의 책임입니다. 기본값이 없습니다. 끝 멈춤 없이 계속 도는 축은 위치 한계를 null 로 적습니다(limits). 위치·속도 축은 정지 감속 acceleration_rad_s2 도 적습니다. 요청 정지가 축을 세우는 감속(6절)이고, 축이 실제로 낼 수 있으면서 드라이브의 0x6085 를 넘지 않는 값이어야 합니다
기동 시 쓸 SDO (startup_sdo)드라이브 매뉴얼. 쓸 것이 없으면 []
fault reset (fault_reset)자동 reset 여부(policy)는 직접 정합니다. bit 7 을 켜 두는 시간 pulse_ms 는 드라이브 매뉴얼을 따르고, 요구가 없으면 0(한 제어 주기)
전이표 (cia402_profile)표준대로 전이하는 드라이브는 뺍니다. 전원 투입 뒤 not_ready_to_switch_on 에서 Shutdown 을 받는 드라이브는 그 행을 넣은 표를 적습니다(cia402_profile). 예제는 표준 표를 쓰므로 키가 없습니다
승인 (approval)적지 않습니다. 없으면 미승인이고, status: unapproved / record: null 로 적어도 같습니다. 승인 기록은 벤치가 이 파일 안에 씁니다(8절)

3. 파일을 검사합니다​

로봇 없이 파일만 봅니다.

plem drive check "$HOME/drives/my_drive.yaml"

거부되면 무엇이 틀렸고 어떻게 고치는지 알려 줍니다. 오류 문장의 schema §4.1 같은 번호는 드라이브 모델 파일 의 같은 번호 절입니다. 드라이브가 버스에 붙을 때 거부될 파일(PDO 배치에 그 모드의 필수 객체가 없음, 한 채널을 두 축이 씀 등)도 드라이브 없이 같은 문장으로 거부합니다.

통과하면 JSON 문서 하나를 찍습니다. 필드의 뜻과 오류 문장 읽는 법은 드라이브 버스 API 에 있습니다. warnings 는 경고일 뿐 거부가 아니고 종료 코드도 바꾸지 않습니다.

종료 코드뜻
0파일이 주장한 승인 상태가 그대로 성립합니다
1파일이 거부됐거나, 주장한 승인이 성립하지 않습니다(이유는 stderr, 또는 JSON 의 reasons)
2명령 사용법이 틀렸습니다

check 통과가 기동 통과는 아닙니다. 다음은 버스가 기동할 때 판정합니다. JSON 의 not_judged 가 이 목록을 적습니다.

  • 드라이브에서 읽어 오는 매핑(pdo_mapping: scanned)
  • 실물 드라이브 식별(시리얼, revision number, 0x1009, 0x100A, 스캔한 매핑)
  • 제어 주기 대비 fault reset 간격
  • DC 규칙

4. 버스를 띄웁니다​

plem drive run "$HOME/drives/my_drive.yaml"

버스는 띄운 터미널에서 돕니다. Ctrl-C 를 누르면 모든 축을 세운 뒤 버스를 놓습니다(6절).

버스는 파일의 bus.name 을 이름으로 /<bus.name> 네임스페이스에서 뜹니다. 예제 파일은 my_drive 라서 /my_drive 입니다. 이름은 승인에 들어가지 않으므로 바꿔도 승인이 유지됩니다. 이름이 없는 파일은 plem drive check 와 run 이 무엇을 적을지와 함께 거부합니다.

옵션뜻
--master NEtherCAT 마스터 번호. 기본 0
--rt-cpu N버스 루프를 CPU N 에 붙입니다. 기본은 마지막 격리 CPU
--monitor버스 타이밍을 같은 터미널에 1초에 한 줄로 찍습니다(아래)
--plot매 주기 그래프를 띄웁니다. 이 버스의 중계 노드와 PlotJuggler 를 함께 띄우고, 버스가 끝나면 함께 끝납니다(아래)
--unapproved미승인 파일을 승인 없이 「BENCH MODE」로 띄웁니다. 드라이브 앞에 사람이 있을 때만 씁니다
--verbose모든 노드의 로그를 터미널에 그대로 보여 줍니다. 기본은 경고·오류, 버스 자신의 줄, 스포너의 줄(컨트롤러 활성화)만 보여 주고, PlotJuggler 출력은 파일로 보냅니다

run 은 이렇게 진행합니다. 단계별 세부는 드라이브 버스 API 에 있습니다.

  1. 파일을 check 로 봅니다. 같은 이름의 버스가 이 기기에서 이미 떠 있으면 거부합니다. 내 계정의 plem drive run 만 보고, 다른 계정이나 같은 ROS 도메인의 다른 기기에서 뜬 버스는 보지 못합니다. 모션 승인이 없는 파일이면 터미널에서 벤치를 제안합니다(8절). 승인되면 그대로 띄웁니다.
  2. 커널이 PREEMPT_RT 가 아니거나, 격리 CPU 가 없거나, Jetson 의 nvpps PTP-TSC 잠금이 켜져 있으면 고칠 방법과 함께 경고만 하고 띄웁니다.
  3. 로봇 설명(축마다 관절 하나)과 컨트롤러 매니저 파라미터를 쓰고, robot_state_publisher 와 컨트롤러를 띄운 뒤 자신이 컨트롤러 매니저가 됩니다.
  4. 모든 슬레이브가 OP 에 들어갈 때까지(도메인의 working counter 가 다 찰 때까지) 기다린 뒤 every slave in OP (working counter complete) … s after the loop started 한 줄을 찍습니다. 이 줄이 나온 뒤에 움직입니다. 걸리는 시간은 마스터가 정합니다. IgH 는 슬레이브를 하나씩 설정하고, DC 슬레이브는 시계가 기준 클록과 10 µs 안으로 맞을 때까지(최대 5초) OP 에 올리지 않습니다. 그래서 각 슬레이브 시계가 기준 클록에서 얼마나 떨어진 채 시작하느냐에 따라 달라집니다(잰 기동 한 번: 42 µs 에서 초당 약 20 µs 로 수렴. 기준 드라이브 두 대에서 전체 2~11초). 기다리는 중의 Ctrl-C 는 버스를 바로 세웁니다. 연 브레이크가 없으니 확인을 기다리지 않습니다. 60초 안에 OP 에 들지 않은 슬레이브가 있으면 버스를 세우고 기동에 실패하며, ethercat slaves(슬레이브별 상태)와 커널 로그를 가리킵니다.

축의 mode 가 컨트롤러를 정합니다. csp 축은 joint_trajectory_controller 가, 스트림 축은 stream_controller 가 명령합니다. 인터페이스 이름은 ROS 인터페이스에 있습니다.

버스가 둘이면 마스터마다 plem drive run 을 하나씩 띄우고 bus.name, --master, --rt-cpu 를 서로 다르게 줍니다. 격리 CPU 가 하나뿐이면 둘째 루프는 격리되지 않은 CPU 에서 돌아 지터가 늘 수 있습니다. 로봇 스택이나 다른 프로그램이 같은 마스터를 쓰고 있으면 기동에 실패합니다.

매 주기 기록을 PlotJuggler 로 보기​

rt_raw 는 40 주기를 한 메시지로 묶어 냅니다. PlotJuggler 에 바로 걸면 메시지마다 한 점밖에 그리지 못합니다. 같은 기기에서 중계 노드를 띄우면 주기마다 한 메시지가 나옵니다. --plot 이 중계 노드와 PlotJuggler 를 함께 띄웁니다. PlotJuggler 는 PLEM 을 설치·업데이트할 때 ROS apt 저장소(packages.ros.org)에 닿으면 함께 설치됩니다. 소스 빌드, --no-install-recommends 설치, 그 저장소에 닿지 못한 설치처럼 PlotJuggler 나 그 ROS 2 스트리머 plotjuggler_ros 가 없으면 버스를 띄우기 전에 설치 명령(sudo apt install ros-$ROS_DISTRO-plotjuggler-ros)과 함께 거부합니다.

plem drive run "$HOME/drives/my_drive.yaml" --plot
  1. 중계 노드는 /my_drive/rt_raw 를 받아 /my_drive/rt_raw/samples 로 냅니다. 같은 RtRawBatch 형식에 메시지마다 한 주기(batch_size 1)입니다. 로봇의 기록은 -r __ns:=/<robot_id> 로 봅니다. 중계 노드는 보통 스케줄링으로 돌고 RT 전용 코어(isolcpus)에 올라가지 않으며, 제어 루프와 rt_raw 발행을 바꾸지 않습니다.
  2. --plot 은 A6B 예제의 레이아웃 examples/a6b_pair/plotjuggler.xml 을 이 버스의 이름으로 바꾼 사본을 임시 폴더에 만들어 엽니다. 설치된 파일은 고치지 않습니다. 레이아웃에는 /my_drive/rt_raw/samples 같은 이 버스의 토픽과 다음 단계의 시각 기준이 들어 있고, 축 0 과 축 1 의 위치, 속도, 토크, 두 축 동기(위치 어긋남, 속도 합, 서로 미는 토크), 타이밍, DC 편차 그래프가 놓입니다. 축이 더 있으면 actual_pos[2] 처럼 축 번호가 붙은 신호를 PlotJuggler 에서 더합니다. 동기 탭은 축 0 과 축 1 이 거울처럼 맞물린 한 쌍이라고 가정합니다. 다른 버스에서는 그 그래프가 비어 있거나 뜻이 없습니다. 위치 어긋남은 스트리밍이 시작된 뒤 첫 샘플을 기준으로 하고, PlotJuggler 의 버퍼를 지우면 기준도 다시 잡힙니다. 처음 띄울 때 PlotJuggler 가 묻는 것은 이렇게 답합니다. 이 대화 상자들은 실제 버스에서 확인하지 않았습니다.
    • 지난번 스트리밍 플러그인을 다시 시작할지 물으면 Yes 를 고릅니다.
    • ROS 2 구독 창에서 /<bus.name>/rt_raw/samples 를 고르고 header stamp 사용을 켠 채로 둡니다.
    • 없는 곡선을 알리면 빈 자리표시자를 만드는 쪽(create empty placeholders)을 고릅니다.
  3. 시각 기준을 메시지의 header.stamp 로 고릅니다. 중계 노드는 메시지마다 header.stamp 에 그 주기의 RT 시각을 넣습니다. 끄면 받은 시각으로 그려지고, 40 주기가 한꺼번에 도착하므로 점이 몰립니다. 구독 창의 선택지 이름(예: 「use header stamp」)은 PlotJuggler 판마다 다를 수 있고, 이 안내는 GUI 에서 아직 확인하지 않았습니다.
  4. CSV 는 PlotJuggler 의 CSV 내보내기로 뽑습니다.

bus.sync_mode: dc_sync0 버스는 dc_sync_monitor_ns 신호로 주기마다의 DC 동기 편차(ns)를 봅니다. 4294967295 로 튀는 점은 그 주기에 값을 얻지 못했다는 표시이니 편차로 읽지 않습니다. 기동 직후 수십 초는 편차가 크고, 가라앉은 뒤에도 가끔 튑니다. PLEM 은 이 정착을 기다리지 않습니다. A6B 에서 잰 값은 실기 근거에 있습니다.

실측 위치, 속도, 토크만 보려면 중계 없이 /my_drive/joint_states 를 구독해도 됩니다.

버스 상태 요약 보기​

plem drive run <파일> --monitor 는 그래프 없이 루프 타이밍과 드라이브 상태를 버스 곁에 1초에 한 줄로, 칸 이름 아래 맞춰 찍습니다. 상태가 바뀌면(해제, 정지, 보호 정지, fault) 그 시각과 새 상태를 한 줄로 따로 찍고, Ctrl-C 를 누르면 전체 합계를 찍습니다.

칸의 뜻은 --monitor 출력에 있습니다. 이 도구는 짧은 실행으로만 확인했습니다.

5. 해제하고 움직입니다​

해제는 브레이크를 풀고 모든 축을 Operation enabled 로 만드는 일입니다(서비스의 release_brake, Python 의 release()). 해제된 축은 명령이 올 때까지 자리를 지킵니다.

Python​

plem.drive 의 Bus 가 떠 있는 버스에 붙습니다. 이름은 파일의 bus.name 입니다. 그 이름의 버스가 없으면 timeout 뒤에 버스 이름을 밝히고 plem drive run 이 떠 있는지 묻는 DriveError 를 냅니다. with 블록이 예외로 끝나면(Ctrl-C 포함) 축을 먼저 세운 뒤 닫습니다.

from plem.drive import Bus, SCurve

with Bus("my_drive") as bus: # 파일의 bus.name
print(bus.axes, bus.positions) # 축 이름, 위치(rad)
bus.reset() # 버스가 지난번 멈출 때 래치된 fault 해제
bus.release() # 해제: 축은 지금 자리를 지킵니다
start = bus.positions
bus.move_to({name: start[name] + 0.5 for name in bus.axes},
velocity_scale=0.2, acceleration_scale=0.2) # 도착하면 돌아옵니다
bus.move_to(start, profile=SCurve())
bus.stop() # 감속해 세운 뒤 브레이크

스트림 축은 내 제어 루프가 매 주기 stream 을 부릅니다(예: 축 j0 하나).

import threading, time
from plem.drive import Bus

with Bus("my_drive") as bus:
command = {"j0": 0.0} # 스트림 축마다 rad/s(csv) 또는 Nm(cst)
running = True

def control(): # 내 제어 루프: 한 번 부를 때마다 한 주기의 명령
while running:
bus.stream(command)
time.sleep(0.005)

loop = threading.Thread(target=control)
loop.start() # 토크 축이 있는 버스는 스트림이 살아 있어야 해제됩니다
try:
bus.reset() # 스트림이 살아 있을 때(끊긴 스트림의 정지는 남습니다)
bus.release()
command["j0"] = 0.3
time.sleep(2.0)
command["j0"] = 0.0
bus.stop()
finally:
running = False # 위에서 예외가 나도 루프를 끝냅니다
loop.join()

알아 둘 것:

  • release() 전에 reset() 을 부릅니다. 파일의 fault_reset.policy 가 none 이면 fault 상태인 축이 있을 때 release() 가 바로 실패합니다.
  • 스트림을 한 번 보냈다면 release() 와 reset() 동안에도 stream() 을 스레드에서 계속 부릅니다. 이 클라이언트의 스트림이 끊긴 채 보호정지가 서 있으면 둘 다 바로 실패합니다.
  • stream() 을 부르기를 멈추면 마지막 호출 뒤 약 100 ms 에 버스가 보호정지합니다. 스트림 루프는 가볍게 둡니다.
  • with 블록을 정상으로 나가면 클라이언트만 닫힙니다. 해제된 축은 해제된 채 자리를 지킵니다. 예외로 나가면(Ctrl-C 포함) 축을 세운 뒤 닫습니다.

메서드마다 기다리는 것과 내는 오류는 Python plem.drive 에 있습니다.

명령줄​

csp 축은 표준 FollowJointTrajectory 클라이언트(MoveIt 등)로도 움직입니다. 토크(cst) 축이 있는 버스는 순서가 있습니다. 스트림을 먼저 흘리고, 그다음에 해제합니다. 스트림 축의 중립은 토크 축 0 Nm, 속도 축 0 rad/s 입니다. 위치 축은 궤적 컨트롤러가 해제 때의 실측 위치를 지킵니다.

# ① 스트림 축에 중립을 계속 보냅니다 (스트림 축 수만큼. 토크 축 하나라면 0 Nm)
ros2 topic pub -r 200 /my_drive/stream_controller/commands \
std_msgs/msg/Float64MultiArray '{data: [0.0]}'

# ② 다른 터미널에서 해제합니다 (Python 이면 bus.release())
ros2 service call /my_drive/plem/control_command \
plem_drive_msgs/srv/SetControlCommand '{release_brake: true}'
  • 토크 축이 있는 버스는 살아 있는 명령 없이는 해제를 거부합니다. 거부 사유는 서비스 응답에 나옵니다. 길이가 축 수와 다르거나 유한하지 않은 값이 든 메시지는 받지 않으므로, 그것만 보내면 끊긴 것과 같습니다.
  • 위치·속도 축만 있는 버스는 해제가 명령을 기다리지 않습니다. 해제 뒤 첫 명령까지 위치 축은 해제 때의 위치를 지키고(처지는 실측을 따라가지 않습니다) 속도 축은 0 rad/s 를 명령합니다. 해제 전에 쓴 명령은 적용하지 않습니다. 50 ms 명령 감시는 그 첫 명령부터 셉니다.
  • 정지(6절)가 아직 도는 동안의 해제는 거부됩니다.
  • 응답이 success: true 여도 해제가 끝난 것은 아닙니다. 한 주기 뒤 준비 게이트에서 거부되면 브레이크는 물린 채입니다. 해제는 status 의 operation_enabled 와 brake_engaged 로 확인하고, 원인은 rt_events 에서 봅니다(rt_events).

명령 값은 자르지 않습니다. 위치 한계 밖이거나 한 주기 이동량(velocity_rad_s × 주기)보다 멀리 뛰는 위치 명령, 크기가 velocity_rad_s 를 넘는 속도 명령은 적용하지 않고 보호정지를 겁니다. 해제 직후 첫 위치 명령은 그 순간의 실측 위치와 비교합니다. 한 주기 이동량 안이면 그대로 적용되어 축이 움직입니다. 반올림과 기준 위치의 세부는 명령 검사에 있습니다.

ros2 topic pub 은 값을 YAML 로 읽습니다. 1e-05 처럼 소수점 없는 지수 표기는 숫자로 받지 않으니 1.0e-05 로 적습니다.

6. 멈춥니다​

상황무엇이 일어나나누가 하나
스트림이 끊김, 또는 컨트롤러 매니저가 쓰기를 멈춤명령이 오래됨, 보호정지, 전 축 중립, 브레이크 강제 체결. 스트림은 마지막 메시지 뒤 약 100 ms(컨트롤러 50 ms + 버스 50 ms). csp 축은 계획이 곧 명령이라 끊김이 없습니다PLEM
위치·속도 명령이 한계를 넘음그 주기에 보호정지(위치 축은 직전 목표 유지, 나머지 축은 중립). 다음 주기부터 전 축 중립과 브레이크 강제 체결PLEM
run 터미널의 Ctrl-C (컨트롤러 매니저에 SIGINT)컨트롤러를 끄고 하드웨어를 deactivate, 정지(토크 축이 없는 버스는 아래 요청 정지), 브레이크 체결 확인, 버스를 놓음. 확인하지 못하면 ERROR 로그PLEM
컨트롤러 매니저가 강제 종료됨 (SIGKILL)통신이 끊겨 드라이브의 출력 SM watchdog 이 동작드라이브
그냥 멈추고 싶음protective_stop 서비스(Python bus.stop()). 토크 축이 없는 버스는 요청 정지, 있으면 곧바로 disable. 어느 쪽도 보호정지는 걸리지 않습니다(요청 정지가 시간 안에 서지 않으면 예외, 아래 3단계)PLEM

각 행을 실기로 얼마나 확인했는지는 실기 근거에 있습니다.

SIGKILL 은 보장으로 읽지 마세요. 이 정지는 드라이브 설정이며 PLEM 밖입니다. 매핑을 선언한 슬레이브는 출력 SM 에 watchdog 이 켜져 있어야 파일이 통과하지만, 시간은 드라이브 기본값입니다. scanned 슬레이브는 그 확인조차 없습니다.

FollowJointTrajectory goal 취소는 정지가 아닙니다. MoveIt 이나 CLI 로 goal 을 취소하면 궤적 컨트롤러가 그 순간의 실측 위치를 바로 붙듭니다. 움직이는 버스는 protective_stop 으로 세웁니다.

요청 정지​

위치·속도 축만 있는 버스의 protective_stop 과 런의 끝(Ctrl-C)은 요청 정지입니다.

  1. PLEM 이 각 명령을 지금 속도에서 limits.acceleration_rad_s2 로 줄여 0 에 세웁니다. 0 을 지나 되돌아가지 않습니다. 그동안 보내는 명령은 적용하지 않습니다.
  2. 명령이 0 이 되고 모든 축의 실측 속도가 속도 한계의 1 % 안이면 브레이크를 물립니다. 보호정지는 걸리지 않으므로 reset 없이 다시 해제할 수 있습니다.
  3. 계획한 감속 시간 + 0.5 s 안에 그렇게 확인되지 않으면, Operation enabled 인 드라이브에 Quick stop 을 명령합니다. 드라이브는 자기 0x6085 로 감속해 0x605A 가 정한 상태(2 = Switch on disabled)로 갑니다. 이때 보호정지가 걸리고 rt_events 에 HALT_TIMEOUT(0x4F, 값 = 요청 뒤 초)이 남습니다. 다시 해제하려면 reset 이 필요합니다.
  4. 그 뒤 2 s 가 지나도 Quick stop active 인 드라이브에는 Disable voltage 를 보냅니다. 0x6085 가 그 안에 축을 세우지 못하면 움직이는 축의 전원이 끊깁니다.

그러니 0x6085 와 0x605A 도 startup_sdo 로 주고, acceleration_rad_s2 는 0x6085 를 넘지 않게 적습니다(드라이브 모델 파일 §4.10). 서비스는 곧바로 응답하므로 끝은 status 의 브레이크로 확인합니다. Quick stop 단계와 0x605A = 2 는 아직 실기로 확인하지 않았습니다.

곧바로 끄는 정지​

한계, 명령 끊김, 통신 상실의 보호정지와, 토크 축이 있는 버스의 모든 정지는 PLEM 이 표준 CiA 402 disable 순서로 거는 정지입니다. Quick Stop 이 아닙니다.

  • 이 정지에서 PLEM 은 끄기 전에 움직이는 축을 감속하지 않습니다. 멈추는 방식은 드라이브의 disable operation 옵션(0x605C)과 감속(대개 0x6084)이 정합니다. 통신이 끊기면 fault reaction(0x605E, 대개 0x6085)이 정합니다. 파일의 startup_sdo 로 드라이브에 맞게 줍니다.
  • 공장 감속값이 작으면 빠르게 돌던 축이 정지 뒤에도 오래 돕니다. A6B 공장값은 약 0.75 rad/s² 로 실측했습니다.
  • 감속 방식, 브레이크가 물리는 시각, 중력을 받는 축의 거동은 드라이브마다 다릅니다.
  • 위치 축의 중립은 매 주기 그 주기의 실측입니다. 고정 위치를 지킨다는 약속이 아닙니다. 속도 축의 0 rad/s 는 0 토크나 브레이크 체결의 증거가 아닙니다.
  • limits.torque_nm 은 토크 축의 명령에만 걸립니다. 위치·속도 축에서 드라이브가 내는 토크는 제한하지 않습니다.

한계 때문에 걸린 보호정지는 rt_events 에 LIMIT_STOP(0x4E)을 축마다 한 건 남깁니다. 사유 번호는 rt_events에 있습니다.

브레이크 체결 확인​

컨트롤러 매니저의 종료 코드는 브레이크 체결 여부를 가르지 않습니다. 체결을 확인하지 못했으면 ERROR 로그가 남고, 그 줄에 축별 statusword 가 붙습니다. 정지가 끝날 때까지 Fault 였던 축은 그 줄이 따로 적습니다. PLEM 이 disable 순서를 밟게 하지 못해 체결을 확인할 수 없었다는 뜻입니다. 드라이브가 스스로 브레이크를 물렸는지는 드라이브의 서보 오프·알람 동작이고 PLEM 은 보지 않습니다. 다음에는 끄기 전에 reset_error 를 보내 status 가 Fault 를 벗어날 때까지 기다린 뒤 멈추세요.

7. 보호정지에서 돌아오기​

스트림이 끊겼거나 한계를 넘어 보호정지가 걸렸다면 이 순서로 돌아옵니다.

  1. 토크 축이 있는 버스는 스트림을 다시 흘립니다. 스트림 없이 reset 하면 곧바로 다시 걸립니다. 한계를 넘은 명령을 그대로 보내도 다시 걸립니다(위치·속도 축만 있는 버스는 다음 해제 뒤 그 명령이 적용될 때).
  2. reset_error 를 부릅니다.
  3. status 의 protective_stop 이 0 이 될 때까지 기다립니다. 요청은 비동기입니다.
  4. release_brake 를 부릅니다(Python 이면 bus.release()).

정지가 시간 안에 확인되지 않아 보호정지가 걸린 채 끝났을 때도 이 순서를 따릅니다. 해제 거부 문장 읽는 법은 해제 응답과 거부 문장에 있습니다.

위치 한계 밖에서 멈춘 축은 이 순서로 풀리지 않습니다

한계 안에 있어야 해제되므로 reset 이 거부됩니다. 드라이브 계층에는 축을 범위 안으로 되돌리는 수단이 없습니다. 복구는 PLEM 밖의 조치입니다.

8. 벤치와 승인​

모션을 여는 것은 파일의 approval 블록 하나이고, 제품이 그 기록을 판정합니다. 블록이 없는 파일은 미승인입니다. 승인 기록은 벤치가 씁니다.

미승인 파일을 터미널에서 plem drive run 하면 <파일> is not approved for motion — the drive bench follows. 한 줄 뒤 벤치가 이어집니다. 벤치는 같은 --master 로 그 자리에서 돌고, 승인되면 run 이 그대로 버스를 띄웁니다. 거절하거나, 승인으로 끝나지 않거나, 터미널이 없으면 이유와 --unapproved 대안을 알리며 거부합니다.

  1. 확인. 벤치는 한 번 묻습니다([y/N]). 해제 횟수와 함께, 드라이브 앞에 있고 비상정지에 손이 닿는지, 중력이 걸리는 축을 받쳤는지, 파일에 없는 축이 제 브레이크나 기구로 고정됐는지, 한 브레이크를 여러 축이 공유하지 않는지를 묻습니다. 위치·속도 축은 어느 쪽으로든 최대 0.5 rad 움직이니 양쪽 작업 공간을 비웁니다. 거절하면 아무것도 건드리지 않습니다.
  2. 벤치. --unapproved 와 같은 사슬을 띄우고 브레이크를 풀었다 물립니다. 위치·속도 축이 있는 버스는 축을 한 번 움직여 정지 감속을 잽니다(stop-decel). 풀린 채 서 있는 축이 0.005 rad 넘게 움직이면 곧바로 중단하고 브레이크를 물립니다.
  3. 결과. 전부 통과하면 승인 기록을 모델 파일 안에 씁니다(approval recorded in <파일>). 보관할 파일은 이것 하나입니다. 통과하지 못하면 파일을 건드리지 않고 my_drive.bench.log 만 남기며 그 경로를 알려 줍니다. 고친 뒤 다시 벤치합니다.
  4. 기동. 승인된 파일은 plem drive run 이 묻지 않고 띄웁니다. 드라이브 실물(시리얼, revision number, 0x1009, 0x100A, 스캔한 매핑)이 기록과 다르면 버스가 활성화 전에 거부합니다.

벤치는 버스 하나씩 돕니다. 드라이브 버스나 로봇 스택이 그 마스터를 쓰고 있거나 마스터가 Idle 이 아니면 거부합니다. 시작할 때 모든 축이 파일의 위치 한계 안에 있어야 하고, 모델 파일은 실행하는 사용자 소유이면서 그 파일과 디렉터리에 쓸 수 있어야 합니다.

벤치의 필수 항목은 축 모드로 정해집니다(plem drive check JSON 의 required_bench_items). 항목표는 드라이브 모델 파일 §6.1, 벤치가 확인하고 경고하는 것은 벤치 세부에 있습니다.

승인이 판정하는 것은 기록끼리 맞는지입니다. 파일, 그 안의 벤치 증거, 실물 드라이브를 대조할 뿐 측정이 참인지는 판정하지 않습니다. 시리얼을 0 으로 보고하는 드라이브(Neuromeka 등)의 승인은 개체가 아니라 모델 단위로 열립니다. 대조 범위는 §6 approval 에 있습니다.

승인이 풀리는 경우

승인은 소프트웨어 업데이트 뒤에도 유지됩니다. 다만 파일의 값을 고치거나, 드라이브를 바꾸거나, 업데이트가 벤치 기준을 올리면(CHANGELOG 의 「드라이브 재벤치 필요」) 승인이 풀립니다. 같은 파일을 다시 벤치하면 기록이 바뀝니다.

지원하지 않는 것​

  • 모드는 cst, csp, csv 셋뿐이고 기동 때 정해집니다. 실행 중 모드 전환, 버스 루프 안의 궤적 보간, 피드포워드(0x60B1, 0x60B2), 홈잉은 없습니다. Quick Stop 은 요청 정지가 시간 안에 확인되지 않을 때의 대비로만 씁니다(6절). 궤적은 호스트가 만듭니다. 위치 축은 궤적 컨트롤러와 plem.drive 가, 스트림 축은 스트림을 보내는 쪽이 만듭니다. cst 는 위치를 유지하지 않습니다.
  • 버스 루프 안의 궤적 보간(예전 csp 축의 ramp 블록)은 은퇴했습니다. 축에 ramp 키가 있는 파일은 로더가 거부하고 블록을 지우라고 알려 줍니다.

다음 세 가지는 파일에 적을 키가 없어 제품이 거부하지 못합니다. 맞지 않는 드라이브는 쓰지 마세요.

  • 정지 유지는 전원 결합 브레이크만입니다. 0x60FE 디지털 출력으로 여는 브레이크, 외부 브레이크, 브레이크가 없는 축은 지원하지 않습니다.
  • 한 브레이크가 여러 축을 함께 푸는 드라이브는 쓰지 마세요. 제품은 브레이크를 축마다 독립으로 보고 축별로 여닫습니다. 공유를 적을 키도 감지할 수단도 없습니다. 그런 드라이브에서는 한 축의 브레이크 동작이 다른 축의 브레이크까지 풀 수 있고, 그 축은 제품이 모르는 사이에 떨어질 수 있습니다. 승인 절차의 대상도 아닙니다.
  • 엔코더는 절대값 엔코더만입니다. PLEM 에는 홈잉이 없습니다. zero_offset_count 와 위치 한계는 드라이브가 전원을 넣을 때마다 같은 위치를 보고할 때만 축의 자리를 가리킵니다. 증분 엔코더는 켤 때마다 카운트가 새로 시작해 위치 한계가 아무것도 지키지 못하는데, 엔코더 종류를 적을 키가 없어 아무것도 거부하지 않습니다. 쓰지 마세요. 끝 멈춤 없는 축(position: null)은 지킬 위치 한계가 없고, 재기동 뒤의 위치도 어차피 상대 좌표입니다.

그 밖의 범위입니다.

  • 한 드라이브(슬레이브)에 축이 여럿인 것은 지원합니다. 8개까지이고 실기로 확인하지 않았습니다. 위의 공유 브레이크와는 다른 이야기입니다. 축마다 channel 을 적고, 두 번째 채널의 객체는 0x800 간격(예: 0x6841)으로 매핑에 적습니다.
  • 드라이브 계층에는 중력 보상이 없습니다. 핸드 가이딩(free_drive)도 없습니다.
  • IO 슬레이브는 붙이지 못합니다. 버스는 파일에 적은 드라이브 슬레이브만 구성하고, 링의 나머지 슬레이브는 구성하지 않은 채(PREOP) 둡니다.

실기 근거 범위​

지금의 plem drive run 경로는 같은 장비에서 실기 세션 여러 번 돌렸습니다. 감속기 출력축을 조로 묶은 A6B 위치(csp) 축 두 개로 두 프로파일의 이동, 정지, 벤치와 승인을 확인했고, 같은 파일의 csv 사본으로 스트림과 끊김을, cst 사본으로 토크 전달(1.5 N·m)과 끊김 정지를 확인했습니다. 토크로 결합한 두 축을 함께 돌리는 것, --rt-cpu, 두 버스, 스트림 축이 있는 버스의 벤치(stale-engage)는 아직 돌리지 않았습니다. 은퇴한 이전 경로(drive_node)에서는 위치 축을 A6B 두 대로 돌렸고 속도·토크 축은 짧은 저속 런만 돌렸습니다. 결과, 조건, 확인하지 않은 항목의 목록은 실기 근거에 있습니다.