제6부 하드웨어 인터페이스
6.3 Resource Manager·Controller Manager 내부
6.3.1 이 장에서 배우는 것 / 선수 지식
6.1에서 구조를, 6.2에서 하드웨어 쪽을 봤다. 이 장은 그 사이를 잇는 관리 층이다. 컨트롤러를 로드하고, 인터페이스 점유를 판정하고, 매 주기 read → update → write를 실행하는 주체.
이 층을 아는 것이 실무에 필요한 이유는 명확하다. "컨트롤러가 활성화되지 않는다"·"왜 이 순서로 실행되나"·"제어 주기가 지연된다" 의 답이 여기 있고, 이것들은 ros2_control을 쓰는 팀이 반드시 만나는 문제다.
배우는 것:
- Resource Manager — 인터페이스 등록·점유·충돌 판정.
- Controller Manager — 이중 정체(ROS 노드 + 제어 루프).
- 업데이트 순서 — 컨트롤러가 어떤 순서로 실행되는가.
- 전환(switch) — 컨트롤러 교체가 원자적으로 일어나는 방식.
- CLI와 spawner — 실무에서 쓰는 도구.
ros2 control서브커맨드 전수(6.3.11) — 열둘의 이름과 쓰임.- 제어 스레드 구성 — 주기·우선순위·측정.
- 진단 — 무엇이 잘못됐는지 알아내는 법.
선수 지식: 6.1(아키텍처), 6.2(하드웨어 컴포넌트), 1.12(라이프사이클), 2.3(executor), 2.5(타이머 정밀도).
정직: 내부 구현은 배포판마다 상당히 변해 왔다 — 특히 비동기 컴포넌트, 체이닝 해석, 업데이트 순서 결정 방식. 아래는 개념 모델이며 정확한 동작은
controller_manager소스가 정본이다(확인 필요). CLI 서브커맨드 이름도 버전 의존이다 — Jazzy의 전수는 6.3.11에서 실행 확인했다. 내부 동작 서술은 이 환경에서 실행 검증하지 않았다(목차 3).검증 상태(추가): 이 장의 서술 가운데 1곳은 이후 회차에서 Jazzy 실물과 대조해 확인했다 — 본문의 (실행 확인) 표기가 그것이다. 표기가 없는 서술은 위 고지가 그대로 적용된다(목차 3의 검증 범위 표).
6.3.2 Resource Manager — 인터페이스의 등록소
하는 일:
| 역할 | 설명 |
|---|---|
| 인터페이스 등록 | 모든 하드웨어 컴포넌트의 인터페이스를 하나의 이름 공간으로 |
| 점유 관리 | 어느 컨트롤러가 어느 명령 인터페이스를 쓰는지 추적 |
| 충돌 검출 | 배타적 점유 위반을 거부(6.1.5) |
| 컴포넌트 라이프사이클 | 하드웨어 컴포넌트의 configure·activate 관리(6.2.6) |
read()·write() 호출 | 모든 컴포넌트에 대해 순회 |
핵심 통찰: 컨트롤러는 어느 하드웨어에서 온 인터페이스인지 모른다. joint1/position이 시리얼 팔에서 왔는지 CAN 그리퍼에서 왔는지, 심지어 mock에서 왔는지(6.2.8) 구분하지 않는다. 이것이 컨트롤러 재사용의 기반이다.
점유 판정:
실패 메시지 해석:
| 메시지 유형 | 원인 |
|---|---|
| 인터페이스를 찾을 수 없음 | URDF의 <command_interface> 누락·이름 오타(6.1.7) |
| 이미 점유됨 | 다른 컨트롤러가 활성 상태 |
| 하드웨어가 비활성 | 컴포넌트가 Active가 아님(6.2.6) |
# 무엇이 있고 누가 쓰는지
ros2 control list_hardware_interfaces
출력 예(개념):
| 인터페이스 | 상태 |
|---|---|
joint1/position [available] [claimed] | 누군가 점유 중 |
joint1/velocity [available] [unclaimed] | 사용 가능 |
joint2/effort [unavailable] | 하드웨어가 비활성 |
견해:
list_hardware_interfaces가 ros2_control 진단의 출발점이다. "컨트롤러가 활성화 안 된다"의 원인 대부분이 이 출력에서 보인다 — 인터페이스가 없거나, 이미 점유됐거나, 하드웨어가 활성화되지 않았거나.
6.3.3 Controller Manager — 이중 정체
controller_manager는 ROS 노드이면서 제어 루프의 주인이다(6.1.6).
노드로서 제공하는 서비스:
| 서비스 | 역할 |
|---|---|
~/list_controllers | 목록·상태 조회 |
~/load_controller | 플러그인 로드 |
~/configure_controller | Inactive로 전이 |
~/switch_controller | 활성화·비활성화를 원자적으로 |
~/unload_controller | 언로드 |
~/list_hardware_components | 하드웨어 상태 |
~/set_hardware_component_state | 하드웨어 라이프사이클 조작 |
제어 루프로서:
// 개념적 루프
while (running) {
auto now = clock->now();
auto period = now - last;
resource_manager->read(now, period); // 모든 하드웨어 컴포넌트
process_pending_switches(); // 전환 요청 반영 (6.3.5)
update_controllers(now, period); // 활성 컨트롤러 순서대로
resource_manager->write(now, period);
last = now;
sleep_until_next_period(); // 절대 시각 기준 (2.5.8)
}
정직: 실제 구현은 이보다 복잡하며(비동기 컴포넌트·체이닝·오류 처리), 순서와 세부는 버전에 따라 다르다
(확인 필요). 여기서는 관찰 가능한 동작을 설명한다.
6.3.4 업데이트 순서
"내 컨트롤러가 다른 것보다 먼저 실행되는가"는 실무에서 중요한 질문이다.
특히 체이닝(6.4)에서는 순서가 정확성에 직결된다 — 상위 컨트롤러가 하위의 참조값을 써야 하는데 순서가 반대면 한 주기 늦은 값을 쓰게 된다.
정직: 비체인 컨트롤러 간의 순서는 명시적으로 보장되는 계약이 아니다
(확인 필요). 2.2.11에서 executor 실행 순서에 의존하지 말라고 한 것과 같은 원칙이 적용된다 — 순서에 의존하는 로직을 짜지 말라.
순서가 문제가 되는 경우와 대처:
| 상황 | 대처 |
|---|---|
| 두 컨트롤러가 같은 상태를 읽고 서로 영향 | 하나로 합치거나 체이닝으로 명시 |
| 안전 감시가 먼저 실행돼야 함 | 하드웨어 컴포넌트 안에서 검사(가장 확실) |
| 순서 의존 계산 | 체이닝(6.4.7)으로 명시적 의존 선언 |
견해: 안전 관련 검사(관절 한계·속도 제한·비상 정지)는 컨트롤러가 아니라 하드웨어 컴포넌트의
write()에 두는 것이 안전하다. 컨트롤러 순서·활성 여부와 무관하게 항상 적용되기 때문이다. 어떤 컨트롤러가 활성화되든 그 검사를 통과해야 하드웨어에 명령이 나간다 — 이것은 안전 논증에서 중요한 차이다(14.10).
// 하드웨어 컴포넌트의 write() 안 — 항상 적용되는 안전 계층
hardware_interface::return_type MyRobotHardware::write(...) {
for (size_t i = 0; i < hw_commands_.size(); ++i) {
// 관절 한계 클램프 (URDF에서 읽은 값)
double cmd = std::clamp(hw_commands_[i], joint_min_[i], joint_max_[i]);
// 속도 제한 (이전 명령과의 차이)
double max_delta = joint_vel_limit_[i] * period.seconds();
cmd = std::clamp(cmd, last_cmd_[i] - max_delta, last_cmd_[i] + max_delta);
tx_commands_[i] = cmd;
last_cmd_[i] = cmd;
}
// ...
}
6.3.5 전환 — 원자적 컨트롤러 교체
6.1.5에서 "동시에 일어난다"고 한 것의 실체다.
핵심 성질:
| 성질 | 의미 |
|---|---|
| 원자성 | 전부 성공하거나 전부 실패 — 부분 적용 없음 |
| 제어 루프 안에서 | 주기 경계에서 일어나므로 update() 중간에 바뀌지 않는다 |
| 순서 | 비활성화가 먼저, 활성화가 나중 → 점유 충돌 회피 |
# 원자적 교체
ros2 control switch_controllers \
--deactivate position_controller \
--activate velocity_controller
# 엄격 모드 — 하나라도 실패하면 전체 실패 (확인 필요: 옵션 이름)
ros2 control switch_controllers --strict \
--activate controller_a controller_b
전환 전략 파라미터(확인 필요: Jazzy에서의 형태):
| 전략 | 의미 |
|---|---|
STRICT | 하나라도 실패하면 전체 실패 |
BEST_EFFORT | 가능한 것만 적용 |
함정:
BEST_EFFORT는 예상 밖의 상태를 만들 수 있다. 두 컨트롤러를 함께 켜려 했는데 하나만 켜지면, 로봇이 절반만 제어되는 상태가 된다. 안전이 걸린 전환에는STRICT를 쓰라.
함정: 전환 순간의 명령 연속성은 프레임워크가 보장하지 않는다. 위치 컨트롤러에서 속도 컨트롤러로 바꾸면, 새 컨트롤러의 초기 명령이 무엇인지는 그 컨트롤러의 구현에 달렸다. 컨트롤러가
on_activate에서 현재 상태를 읽어 초기화해야 하며(6.2.6의 하드웨어 쪽과 같은 논리), 이것을 안 하는 컨트롤러를 쓰면 전환 순간 로봇이 급격히 움직인다.
6.3.6 CLI와 spawner
# 상태 조회
ros2 control list_controllers
ros2 control list_controller_types
ros2 control list_hardware_components
ros2 control list_hardware_interfaces
# 로드·설정·활성화 (한 번에)
ros2 run controller_manager spawner joint_state_broadcaster
ros2 run controller_manager spawner joint_trajectory_controller
# 개별 조작
ros2 control load_controller my_controller
ros2 control set_controller_state my_controller active
ros2 control unload_controller my_controller
# 하드웨어 컴포넌트 조작
ros2 control set_hardware_component_state ArmSystem active
실행 확인(2026-08-16): 위 서브커맨드 9종은 전부 실재한다(ros-jazzy-ros2controlcli 설치 후 ros2 control --help와 기계 대조). 다만 책이 빠뜨린 것이 셋 있다.
| 빠져 있던 서브커맨드 | 쓸모 |
|---|---|
view_controller_chains | 체이닝된 컨트롤러의 연결 관계를 본다(6.4.7) — 체이닝을 쓰는 시스템의 진단 도구인데 이 책이 언급하지 않았다 |
cleanup_controller | 컨트롤러를 unconfigured로 되돌린다(라이프사이클 — 1.12) |
reload_controller_libraries | 컨트롤러 라이브러리를 다시 로드 — 개발 중 재빌드 반영 |
ros2 control view_controller_chains # 체이닝 관계 확인
ros2 control cleanup_controller my_controller
ros2 control reload_controller_libraries
정직: 서브커맨드 이름·집합은 배포판·패키지 버전에 따라 달라진다. 위 12종은 2026-06 빌드의 Jazzy 패치에서 확인한 것이며,
ros2 control --help가 언제나 정본이다.
launch에서 spawner 사용:
from launch.actions import RegisterEventHandler
from launch.event_handlers import OnProcessExit
# 하드웨어와 controller_manager 시작
control_node = Node(
package='controller_manager', executable='ros2_control_node',
parameters=[robot_description, controllers_yaml],
output='screen',
)
# 브로드캐스터 먼저
jsb_spawner = Node(
package='controller_manager', executable='spawner',
arguments=['joint_state_broadcaster', '--controller-manager', '/controller_manager'],
)
# 그 다음 제어 컨트롤러 (순서 보장)
jtc_spawner = Node(
package='controller_manager', executable='spawner',
arguments=['joint_trajectory_controller'],
)
delayed_jtc = RegisterEventHandler(
OnProcessExit(target_action=jsb_spawner, on_exit=[jtc_spawner])
)
견해: spawner를
OnProcessExit로 연쇄시키는 것이 관용구다. spawner는 작업을 마치면 종료하므로, 그 종료를 신호로 다음 spawner를 실행한다. 이렇게 하지 않으면 여러 spawner가 동시에 연결돼 경쟁하거나,controller_manager가 준비되기 전에 요청해 실패한다. launch 심층은 14.3·14.4에서 다룬다.
컨트롤러 설정 YAML:
controller_manager:
ros__parameters:
update_rate: 500 # Hz
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
joint_trajectory_controller:
type: joint_trajectory_controller/JointTrajectoryController
joint_trajectory_controller:
ros__parameters:
joints:
- shoulder_pan
- shoulder_lift
- elbow
command_interfaces:
- position
state_interfaces:
- position
- velocity
두 단계 구조에 유의하라 — controller_manager 아래에 타입을 선언하고, 별도 최상위 키로 각 컨트롤러의 파라미터를 준다. 이것을 혼동해 한 곳에 다 넣으면 파라미터가 적용되지 않는다(1.9.7의 YAML 규칙).
6.3.7 제어 스레드 구성
controller_manager:
ros__parameters:
update_rate: 1000
주기 선택:
| 요인 | 영향 |
|---|---|
| 하드웨어 통신 시간 | read()+write()가 주기 안에 끝나야(6.1.4) |
| 제어 대역폭 요구 | 빠른 동역학일수록 높은 주기 |
| CPU 여유 | 주기가 높을수록 부하 |
| 컨트롤러 계산량 | update() 시간 |
$$T_{\text{주기}} > t_{\text{read}} + t_{\text{update}} + t_{\text{write}} + t_{\text{여유}}$$
견해: 먼저
read()+write()시간을 측정하고 그 3~5배로 주기를 설정하라. 시리얼 통신이 2 ms 걸리면 100~200 Hz가 현실적이고, 1 kHz는 불가능하다. 주기를 먼저 정하고 통신을 맞추려 하면 실패한다.
실시간 우선순위:
# ros2_control_node에 RT 우선순위 부여 (12.2)
# 방법은 배포판·설정에 따라 다르다 (확인 필요)
sudo chrt -f 90 ros2 run controller_manager ros2_control_node ...
정직:
ros2_control_node가 자체적으로 RT 우선순위를 설정하는 기능이 있을 수 있다(확인 필요: 파라미터·빌드 옵션). 없다면 외부에서 설정하거나, 커스텀 실행 파일을 만들어sched_setscheduler를 호출한다. RT 우선순위만으로는 부족하며 RT 커널·CPU 격리·메모리 잠금이 함께 필요하다(12.2·12.3).
주기 측정:
# 실제 update 주기 확인 — 진단 토픽 또는 컨트롤러 상태
ros2 topic hz /joint_states # 브로드캐스터 주기 ≈ 제어 주기
# 더 정확히는 controller_manager의 진단·통계 (확인 필요: 제공 여부)
ros2 topic echo /diagnostics
견해: 제어 주기의 실제 분포를 측정하라(2.5.6). 평균이 1000 Hz여도 p99.9가 800 Hz면 가끔 1.25 ms씩 지연된다는 뜻이고, 제어 성능에 영향을 준다.
update_rate설정값이 아니라 실측 분포가 진실이다.
6.3.8 진단 절차
단계별 명령:
# 1. 하드웨어 상태
ros2 control list_hardware_components
# 2. 인터페이스 가용성·점유
ros2 control list_hardware_interfaces
# 3. 컨트롤러 상태
ros2 control list_controllers
# 4. 제어 루프가 실행되는가
ros2 topic hz /joint_states
# 5. 로그 (하드웨어 컴포넌트의 오류가 여기 나온다)
ros2 run controller_manager ros2_control_node --ros-args --log-level debug
# 또는 실행 중 (1.11.4)
ros2 service call /controller_manager/set_logger_levels \
rcl_interfaces/srv/SetLoggerLevels "{levels: [{name: 'controller_manager', level: 10}]}"
증상별 원인:
| 증상 | 우선 의심 |
|---|---|
컨트롤러가 unconfigured에서 안 넘어감 | YAML 파라미터 구조·이름(6.3.6) |
| 활성화 실패 "resource conflict" | 다른 컨트롤러가 점유 |
| 활성화 실패 "interface not found" | URDF 인터페이스 선언·관절 이름 |
하드웨어가 unconfigured | on_configure 실패 — 장치·권한 |
/joint_states가 안 나옴 | joint_state_broadcaster 미활성 |
| 제어 주기가 낮음 | read()·write() 소요 시간 |
| 명령이 안 먹음 | 컨트롤러 활성 여부·토픽 이름 |
함정: 장치 권한 문제가 흔하다.
/dev/ttyUSB0에 접근하려면 사용자가dialout그룹에 있어야 한다.on_configure가 실패하는데 원인이 불명확하면 권한을 먼저 확인한다.
ls -l /dev/ttyUSB0
groups $USER
sudo usermod -aG dialout $USER # 재로그인 필요
6.3.11 ros2 control 서브커맨드 전수 — 실측
이 장과 6.1·6.2가 세 곳에서 (확인 필요: 서브커맨드 이름)을 달아 두었다. ros-jazzy-ros2controlcli를 설치해 확인했다(실행 확인 — Jazzy · ros:jazzy-ros-base). 열둘이다.
| 서브커맨드 | 하는 일 |
|---|---|
list_controllers | 적재된 컨트롤러와 타입·상태 |
list_controller_types | 쓸 수 있는 컨트롤러 타입과 기반 클래스 |
load_controller | 컨트롤러 적재 |
unload_controller | 컨트롤러 내리기 |
cleanup_controller | 컨트롤러 정리(cleanup 전이) |
set_controller_state | 컨트롤러 상태 조정 — 이 책이 쓰던 이름이 맞다 |
switch_controllers | 컨트롤러 교체(원자적 전환 — 6.3.5) |
reload_controller_libraries | 컨트롤러 라이브러리 다시 읽기 |
list_hardware_components | 하드웨어 컴포넌트 목록 — 6.2가 쓰던 이름이 맞다 |
list_hardware_interfaces | 명령·상태 인터페이스 목록 |
set_hardware_component_state | 하드웨어 컴포넌트 상태 조정 |
view_controller_chains | 체인 컨트롤러 구성도를 /tmp/controller_diagram.gv.pdf로 |
책이 쓰던 이름은 전부 맞았다. 다만 몇 가지가 새로 보인다.
view_controller_chains가 눈에 띈다 — 체이닝은 이 장에서 "구성을 파악하기 어렵다"고 적은 부분인데(6.3.4), 그림을 생성해 주는 명령이 이미 있다. 진단할 때 먼저 칠 만하다.
cleanup_controller와set_controller_state가 따로 있다 — 라이프사이클 전이(1.12)를 CLI로 직접 몰 수 있다는 뜻이다.set_controller_state로inactive·active를 오가고,cleanup_controller로unconfigured까지 되돌린다.
ros-base에는ros2controlcli가 없다 —ros2 control이 전혀 출력되지 않으면 명령이 없는 것이 아니라 패키지가 없는 것이다(ros-jazzy-ros2controlcli).
6.3.9 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | YAML 두 단계 구조 혼동 | 파라미터 미적용 | controller_manager + 별도 최상위 키 |
| 2 | spawner 동시 실행 | 경쟁·실패 | OnProcessExit 연쇄 |
| 3 | joint_state_broadcaster 미활성 | /joint_states 없음 → tf 없음(5.5) | 먼저 spawn |
| 4 | 컨트롤러 순서에 의존 | 버전·구성 따라 깨짐 | 체이닝으로 명시 |
| 5 | 안전 검사를 컨트롤러에만 | 컨트롤러 교체 시 무력화 | 하드웨어 write()에 |
| 6 | BEST_EFFORT 전환 | 부분 제어 상태 | 안전 관련은 STRICT |
| 7 | 컨트롤러가 활성화 시 상태 미확인 | 전환 순간 로봇이 튐 | on_activate에서 현재값 |
| 8 | 주기를 먼저 정하고 통신을 맞춤 | 루프 밀림 | 통신 측정 후 주기 |
| 9 | 설정값을 실제 주기로 신뢰 | 성능 오판 | 실측 분포(2.5.6) |
| 10 | RT 우선순위만으로 실시간 기대 | 지터 잔존 | RT 커널·격리·무할당(12.2) |
| 11 | 장치 권한 미확인 | on_configure 실패 | dialout 그룹 |
| 12 | 진단 없이 추측 | 시간 낭비 | list_* 3종부터 |
5번을 강조한다. 안전 검사를 하드웨어 컴포넌트에 두는 것은 이 장의 실무적으로 가장 중요한 조언이다. 컨트롤러는 교체되고 비활성화되지만, 하드웨어 컴포넌트의 write()는 모든 명령이 반드시 통과하는 지점이다.
6.3.10 체크포인트
- Resource Manager의 역할 다섯 가지를 말할 수 있는가. "컨트롤러는 어느 하드웨어인지 모른다"가 무엇을 가능하게 하는가.
- 점유 판정 흐름과 세 가지 실패 원인을 말할 수 있는가. 진단의 출발점이 되는 명령은?
controller_manager의 이중 정체를 설명할 수 있는가. 전환 요청이 어느 스레드에서 처리되는가.- 비체인 컨트롤러 간 실행 순서를 신뢰해도 되는가. 순서가 필요하면 어떻게 하는가.
- 안전 검사를 하드웨어 컴포넌트에 두라는 조언의 근거는 무엇인가.
- 컨트롤러 전환의 세 가지 핵심 성질을 말할 수 있는가.
BEST_EFFORT의 위험은? - 전환 순간 명령 연속성은 누가 책임지는가.
- 컨트롤러 설정 YAML의 두 단계 구조를 설명할 수 있는가.
- spawner를
OnProcessExit로 연쇄시키는 이유는? - 제어 주기를 정하는 순서와 부등식을 말할 수 있는가. 설정값과 실측 분포 중 무엇이 진실인가.
- ros2_control 진단의 첫 세 명령과, 각 단계에서 무엇을 확인하는지 말할 수 있는가.
- 체인 컨트롤러 구성을 그림으로 확인하는 명령이 있는가(6.3.11).
다음 장 6.4은 이 프레임워크 위에서 컨트롤러를 직접 작성하는 법이다. update()의 실시간 제약, ROS 통신과의 안전한 연결, 그리고 체이닝.