내 서보 드라이브 붙이기
로봇팔 완제품 없이 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 N | EtherCAT 마스터 번호. 기본 0 |
--rt-cpu N | 버스 루프를 CPU N 에 붙입니다. 기본은 마지막 격리 CPU |
--monitor | 버스 타이밍을 같은 터미널에 1초에 한 줄로 찍습니다(아래) |
--plot | 매 주기 그래프를 띄웁니다. 이 버스의 중계 노드와 PlotJuggler 를 함께 띄우고, 버스가 끝나면 함께 끝납니다(아래) |
--unapproved | 미승인 파일을 승인 없이 「BENCH MODE」로 띄웁니다. 드라이브 앞에 사람이 있을 때만 씁니다 |
--verbose | 모든 노드의 로그를 터미널에 그대로 보여 줍니다. 기본은 경고·오류, 버스 자신의 줄, 스포너의 줄(컨트롤러 활성화)만 보여 주고, PlotJuggler 출력은 파일로 보냅니다 |
run 은 이렇게 진행합니다. 단계별 세부는 드라이브 버스 API 에 있습니다.
- 파일을
check로 봅니다. 같은 이름의 버스가 이 기기에서 이미 떠 있으면 거부합니다. 내 계정의plem drive run만 보고, 다른 계정이나 같은 ROS 도메인의 다른 기기에서 뜬 버스는 보지 못합니다. 모션 승인이 없는 파일이면 터미널에서 벤치를 제안합니다(8절). 승인되면 그대로 띄웁니다. - 커널이 PREEMPT_RT 가 아니거나, 격리 CPU 가 없거나, Jetson 의 nvpps PTP-TSC 잠금이 켜져 있으면 고칠 방법과 함께 경고만 하고 띄웁니다.
- 로봇 설명(축마다 관절 하나)과 컨트롤러 매니저 파라미터를 쓰고,
robot_state_publisher와 컨트롤러를 띄운 뒤 자신이 컨트롤러 매니저가 됩니다. - 모든 슬레이브가 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
- 중계 노드는
/my_drive/rt_raw를 받아/my_drive/rt_raw/samples로 냅니다. 같은RtRawBatch형식에 메시지마다 한 주기(batch_size1)입니다. 로봇의 기록은-r __ns:=/<robot_id>로 봅니다. 중계 노드는 보통 스케줄링으로 돌고 RT 전용 코어(isolcpus)에 올라가지 않으며, 제어 루프와rt_raw발행을 바꾸지 않습니다. --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)을 고릅니다.
- 시각 기준을 메시지의
header.stamp로 고릅니다. 중계 노드는 메시지마다header.stamp에 그 주기의 RT 시각을 넣습니다. 끄면 받은 시각으로 그려지고, 40 주기가 한꺼번에 도착하므로 점이 몰립니다. 구독 창의 선택지 이름(예: 「use header stamp」)은 PlotJuggler 판마다 다를 수 있고, 이 안내는 GUI 에서 아직 확인하지 않았습니다. - 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)은 요청 정지입니다.
- PLEM 이 각 명령을 지금 속도에서
limits.acceleration_rad_s2로 줄여 0 에 세웁니다. 0 을 지나 되돌아가지 않습니다. 그동안 보내는 명령은 적용하지 않습니다. - 명령이 0 이 되고 모든 축의 실측 속도가 속도 한계의 1 % 안이면 브레이크를 물립니다. 보호정지는 걸리지 않으므로 reset 없이 다시 해제할 수 있습니다.
- 계획한 감속 시간 + 0.5 s 안에 그렇게 확인되지 않으면, Operation enabled 인 드라이브에 Quick stop 을 명령합니다. 드라이브는 자기 0x6085 로 감속해 0x605A 가 정한 상태(2 = Switch on disabled)로 갑니다. 이때 보호정지가 걸리고
rt_events에HALT_TIMEOUT(0x4F, 값 = 요청 뒤 초)이 남습니다. 다시 해제하려면 reset 이 필요합니다. - 그 뒤 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. 보호정지에서 돌아오기
스트림이 끊겼거나 한계를 넘어 보호정지가 걸렸다면 이 순서로 돌아옵니다.
- 토크 축이 있는 버스는 스트림을 다시 흘립니다. 스트림 없이 reset 하면 곧바로 다시 걸립니다. 한계를 넘은 명령을 그대로 보내도 다시 걸립니다(위치·속도 축만 있는 버스는 다음 해제 뒤 그 명령이 적용될 때).
reset_error를 부릅니다.status의protective_stop이 0 이 될 때까지 기다립니다. 요청은 비동기입니다.release_brake를 부릅니다(Python 이면bus.release()).
정지가 시간 안에 확인되지 않아 보호정지가 걸린 채 끝났을 때도 이 순서를 따릅니다. 해제 거부 문장 읽는 법은 해제 응답과 거부 문장에 있습니다.
한계 안에 있어야 해제되므로 reset 이 거부됩니다. 드라이브 계층에는 축을 범위 안으로 되돌리는 수단이 없습니다. 복구는 PLEM 밖의 조치입니다.
8. 벤치와 승인
모션을 여는 것은 파일의 approval 블록 하나이고, 제품이 그 기록을 판정합니다. 블록이 없는 파일은 미승인입니다. 승인 기록은 벤치가 씁니다.
미승인 파일을 터미널에서 plem drive run 하면 <파일> is not approved for motion — the drive bench follows. 한 줄 뒤 벤치가 이어집니다. 벤치는 같은 --master 로 그 자리에서 돌고, 승인되면 run 이 그대로 버스를 띄웁니다. 거절하거나, 승인으로 끝나지 않거나, 터미널이 없으면 이유와 --unapproved 대안을 알리며 거부합니다.
- 확인. 벤치는 한 번 묻습니다(
[y/N]). 해제 횟수와 함께, 드라이브 앞에 있고 비상정지에 손이 닿는지, 중력이 걸리는 축을 받쳤는지, 파일에 없는 축이 제 브레이크나 기구로 고정됐는지, 한 브레이크를 여러 축이 공유하지 않는지를 묻습니다. 위치·속도 축은 어느 쪽으로든 최대 0.5 rad 움직이니 양쪽 작업 공간을 비웁니다. 거절하면 아무것도 건드리지 않습니다. - 벤치.
--unapproved와 같은 사슬을 띄우고 브레이크를 풀었다 물립니다. 위치·속도 축이 있는 버스는 축을 한 번 움직여 정지 감속을 잽니다(stop-decel). 풀린 채 서 있는 축이 0.005 rad 넘게 움직이면 곧바로 중단하고 브레이크를 물립니다. - 결과. 전부 통과하면 승인 기록을 모델 파일 안에 씁니다(
approval recorded in <파일>). 보관할 파일은 이것 하나입니다. 통과하지 못하면 파일을 건드리지 않고my_drive.bench.log만 남기며 그 경로를 알려 줍니다. 고친 뒤 다시 벤치합니다. - 기동. 승인된 파일은
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 두 대로 돌렸고 속도·토크 축은 짧은 저속 런만 돌렸습니다. 결과, 조건, 확인하지 않은 항목의 목록은 실기 근거에 있습니다.