AEGLOSLAB ROS 2 Jazzy 대전

제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 — 인터페이스의 등록소

하드웨어 컴포넌트 A 시리얼 팔 Resource Manager 인터페이스 단일 이름 공간 점유 관리 · 충돌 검출 하드웨어 컴포넌트 B CAN 그리퍼 mock 컴포넌트 6.2.8 컨트롤러 출처를 구분하지 않는다

하는 일:

역할설명
인터페이스 등록모든 하드웨어 컴포넌트의 인터페이스를 하나의 이름 공간으로
점유 관리어느 컨트롤러가 어느 명령 인터페이스를 쓰는지 추적
충돌 검출배타적 점유 위반을 거부(6.1.5)
컴포넌트 라이프사이클하드웨어 컴포넌트의 configure·activate 관리(6.2.6)
read()·write() 호출모든 컴포넌트에 대해 순회

핵심 통찰: 컨트롤러는 어느 하드웨어에서 온 인터페이스인지 모른다. joint1/position이 시리얼 팔에서 왔는지 CAN 그리퍼에서 왔는지, 심지어 mock에서 왔는지(6.2.8) 구분하지 않는다. 이것이 컨트롤러 재사용의 기반이다.

점유 판정:

예 예 아니오 아니오 컨트롤러 활성화 요청 command_interface_configuration() 호출 → 필요 목록 획득 목록의 인터페이스가 존재하는가? ❌ 실패: 인터페이스 없음 이미 점유 중인가? ❌ 실패: 충돌 점유 표시 → 활성화

실패 메시지 해석:

메시지 유형원인
인터페이스를 찾을 수 없음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).

ROS 노드 측면 제어 루프 측면 전환 요청 큐 상태 서비스: load · configure · switch 파라미터: update_rate 등 executor 스레드 고정 주기 스레드 read → update → write

노드로서 제공하는 서비스:

서비스역할
~/list_controllers목록·상태 조회
~/load_controller플러그인 로드
~/configure_controllerInactive로 전이
~/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)에서는 순서가 정확성에 직결된다 — 상위 컨트롤러가 하위의 참조값을 써야 하는데 순서가 반대면 한 주기 늦은 값을 쓰게 된다.

예 아니오 update() 호출 순서 결정 체인 관계가 있나? 의존 순서대로 (상위 → 하위 또는 그 반대) 로드·활성화 순서 등 구현 정의 순서

정직: 비체인 컨트롤러 간의 순서는 명시적으로 보장되는 계약이 아니다(확인 필요). 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에서 "동시에 일어난다"고 한 것의 실체다.

충돌 OK switch_controller 서비스 호출 요청을 큐에 넣음 (executor 스레드) 제어 루프가 다음 주기에 처리 점유 충돌 검사 ❌ 전체 거부 (부분 적용 없음) 비활성화 대상 deactivate 점유 해제 활성화 대상 activate 점유 획득 다음 update()부터 새 구성

핵심 성질:

성질의미
원자성전부 성공하거나 전부 실패 — 부분 적용 없음
제어 루프 안에서주기 경계에서 일어나므로 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 진단 절차

아니오 예 없음 ros2_control 문제 ros2 control list_hardware_components 하드웨어가 active인가? 하드웨어 컴포넌트 오류 6.2.6 · 로그 확인 list_hardware_interfaces 인터페이스가 available인가? URDF ros2_control 태그 확인 · 6.1.7
있음 unconfigured inactive active 안 돎 돎 list_hardware_interfaces 인터페이스가 available인가? list_controllers 컨트롤러 상태는? 설정 실패 파라미터 확인 활성화 실패 점유 충돌? update 실행 중? topic hz /joint_states 제어 루프 문제 read() 실패 주기 이탈 컨트롤러 로직 명령 경로
(계속 — 인터페이스 available 이후)

단계별 명령:

# 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 인터페이스 선언·관절 이름
하드웨어가 unconfiguredon_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 함정 카탈로그

#함정증상대처
1YAML 두 단계 구조 혼동파라미터 미적용controller_manager + 별도 최상위 키
2spawner 동시 실행경쟁·실패OnProcessExit 연쇄
3joint_state_broadcaster 미활성/joint_states 없음 → tf 없음(5.5)먼저 spawn
4컨트롤러 순서에 의존버전·구성 따라 깨짐체이닝으로 명시
5안전 검사를 컨트롤러에만컨트롤러 교체 시 무력화하드웨어 write()에
6BEST_EFFORT 전환부분 제어 상태안전 관련은 STRICT
7컨트롤러가 활성화 시 상태 미확인전환 순간 로봇이 튐on_activate에서 현재값
8주기를 먼저 정하고 통신을 맞춤루프 밀림통신 측정 후 주기
9설정값을 실제 주기로 신뢰성능 오판실측 분포(2.5.6)
10RT 우선순위만으로 실시간 기대지터 잔존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 통신과의 안전한 연결, 그리고 체이닝.